Search code examples
entity-frameworkentity-framework-4.1ef-code-firstfluent-interface

Fluent API Composite Foreign Key with Generic Object Type


I have an Organization class and a User class as follows, & I want to be able to assign an Address to each individually - without having the need for two separate many-to-many tables for AddressUser & AddressOrganization, AddressAnything, etc. The reason being is I may have multiple entities that I want to assign an Address to & don't want an in-between join table for every single entity that needs an Address record.

I also don't want to store a foreign keys like Address.OrganizationId or Address.UserId for each entity that needs an address associated with it.

Is there a way Fluent API or Code-First Data Annotations can accommodate some sort of composite generic foreign key with an object type?

Like a DomainObjectType table?

DomainObjectType:

public class DomainObjectType
{
    [Key]
    public int Id { get; set; } // seeded

    public string ObjectType { get; set; } // User or Organization
}

DomainObjectType would be seeded with:

var objTypes = new List<DomainObjectType>
  { 
      new DomainObjectType() { Id = 1, ObjectType = "User" },
      new DomainObjectType() { Id = 2, ObjectType = "Organization" }
  };
objTypes.ForEach(c => context.DomainObjectTypes.Add(c));
context.SaveChanges();

Organization:

public class Organization
    {    
        // Primary key
        [Key]
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }

        #region Navigation Properties

        public virtual ICollection<User> Users { get; set; }
        public virtual ICollection<Address> Addresses { get; set; }

        #endregion

    }

User

public class User
{
    [Key]
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int OrganizationId { get; set; }

    [ForeignKey("OrganizationId")]
    public virtual Organization Organization { get; set; }
    public virtual ICollection<Address> Addresses { get; set; }
}

Address

public class Address
{
    // Primary Key
    public int Id { get; set; }
    public string Address1 { get; set; }
    public string Address2 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    public string AddressType { get; set; }

    #region Foreign Keys

    // not completely sure about this? (maybe move to many-to-many "join" table)
    public int DomainObjectId { get; set; } // Composite key; would be either an OrganizationId or UserId
    public string DomainObjectTypeId { get; set; } // Composite key

    #endregion

    #region Navigation Properties

    public virtual ICollection<Organization> Organizations { get; set; }
    public virtual ICollection<User> Users { get; set; }

    #endregion

}

Fluent API stuff:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Configure Code First to ignore PluralizingTableName convention
        // If you keep this convention then the generated tables will have pluralized names.
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

        // one-to-many Organization-to-Users
        modelBuilder.Entity<User>()
            .HasRequired(u => u.Organization)
            .WithMany(o => o.Users)
            .HasForeignKey(u => u.OrganizationId);

        // what goes here for generic AddressDomainObjectType?
    }

Solution

  • You can achieve what you want - having only a single many-to-many table between Address and "other entities" - by creating a base class for those "other entities", for example:

    public abstract class DomainObjectWithAddresses
    {
        public int Id { get; set; }
    
        public virtual ICollection<Address> Addresses { get; set; }
    }
    
    public class User : DomainObjectWithAddresses
    {
        // other properties but without Id which is inherited from base class
    }
    
    public class Organization : DomainObjectWithAddresses
    {    
        // other properties but without Id which is inherited from base class
    }
    
    public class Address
    {
        public int Id { get; set; }
        // other properties
    
        public virtual ICollection<DomainObjectWithAddresses> DomainObjects
                                                                     { get; set; }
    }
    

    Mapping with Fluent API:

    modelBuilder.Entity<DomainObjectWithAddresses>()
        .HasMany(d => d.Addresses)
        .WithMany(a => a.DomainObjects)
        .Map(x =>
        {
            x.ToTable("DomainObjectAddresses");
            x.MapLeftKey("DomainObjectId");
            x.MapRightKey("AddressId");
        });
    
    modelBuilder.Entity<User>()
        .ToTable("Users");           // TPT inheritance mapping
    
    modelBuilder.Entity<Organization>()
        .ToTable("Organizations");   // TPT inheritance mapping
    

    Your DbContext has a DbSet for the base type:

    public DbSet<DomainObjectWithAddresses> DomainObjects { get; set; }
    

    I don't like it. I would prefer to introduce multiple join tables for every type that has a many-to-many relationship with addresses. I have doubt that it is a good idea to introduce an abstraction in the domain model only for the sake of saving a few tables in the database. This abstract DomainObjectWithAddresses class is a very technical ("table-savings-helper") artifact that has no valuable meaning in your domain model. For me it sounds much more natural to say that a user has addresses and an organization has addresses instead of saying that a user is an object with addresses and an organization is an object with addresses. On the long run this will only complicate working with your model.