I am having problems to create an Entity Framework Code-First mapping for the following sample database schema (in SQL Server):
Every table contains a TenantId
which is part of all (composite) primary and foreign keys (Multi-Tenancy).
A Company
is either a Customer
or a Supplier
and I try to model this via Table-Per-Type (TPT) inheritance mapping:
public abstract class Company
{
public int TenantId { get; set; }
public int CompanyId { get; set; }
public int AddressId { get; set; }
public Address Address { get; set; }
}
public class Customer : Company
{
public string CustomerName { get; set; }
public int SalesPersonId { get; set; }
public Person SalesPerson { get; set; }
}
public class Supplier : Company
{
public string SupplierName { get; set; }
}
Mapping with Fluent API:
modelBuilder.Entity<Company>()
.HasKey(c => new { c.TenantId, c.CompanyId });
modelBuilder.Entity<Customer>()
.ToTable("Customers");
modelBuilder.Entity<Supplier>()
.ToTable("Suppliers");
The base table Companies
has a one-to-many relationship to an Address
(every company has an address, no matter if customer or supplier) and I can create a mapping for this association:
modelBuilder.Entity<Company>()
.HasRequired(c => c.Address)
.WithMany()
.HasForeignKey(c => new { c.TenantId, c.AddressId });
The foreign key is composed of one part of the primary key - the TenantId
- and a separate column - the AddressId
. This works.
As you can see in the database schema, from database perspective the relationship between Customer
and Person
is basically the same kind of one-to-many relationship as between Company
and Address
- the foreign key is composed again of the TenantId
(part of the primary key) and the column SalesPersonId
. (Only a customer has a sales person, not a Supplier
, therefore the relationship is in the derived class this time, not in the base class.)
I try to create a mapping for this relationship with Fluent API the same way as before:
modelBuilder.Entity<Customer>()
.HasRequired(c => c.SalesPerson)
.WithMany()
.HasForeignKey(c => new { c.TenantId, c.SalesPersonId });
But when EF tries to compile the model an InvalidOperationException
is thrown:
The foreign key component 'TenantId' is not a declared property on type 'Customer'. Verify that it has not been explicitly excluded from the model and that it is a valid primitive property.
Apparently I cannot compose a foreign key from a property in the base class and from another property in the derived class (although in the database schema the foreign key is composed of columns both in the derived type's table Customer
).
I tried two modifications to get it working perhaps:
Changed the foreign key association between Customer
and Person
to an independent association, i.e. removed the property SalesPersonId
, and then tried the mapping:
modelBuilder.Entity<Customer>()
.HasRequired(c => c.SalesPerson)
.WithMany()
.Map(m => m.MapKey("TenantId", "SalesPersonId"));
It doesn't help (I didn't really hope, it would) and the exception is:
Schema specified is not valid. ... Each property name in a type must be unique. Property name 'TenantId' was already defined.
Changed TPT to TPH mapping, i.e. removed the two ToTable
calls. But it throws the same exception.
I see two workarounds:
Introduce a SalesPersonTenantId
into the Customer
class:
public class Customer : Company
{
public string CustomerName { get; set; }
public int SalesPersonTenantId { get; set; }
public int SalesPersonId { get; set; }
public Person SalesPerson { get; set; }
}
and the mapping:
modelBuilder.Entity<Customer>()
.HasRequired(c => c.SalesPerson)
.WithMany()
.HasForeignKey(c => new { c.SalesPersonTenantId, c.SalesPersonId });
I tested this and it works. But I will have a new column SalesPersonTenantId
in the Customers
table in addition to the TenantId
. This column is redundant because both columns always must have the same value from business perspective.
Abandon inheritance mapping and create one-to-one mappings between Company
and Customer
and between Company
and Supplier
. Company
must become a concrete type then, not abstract and I would have two navigation properties in Company
. But this model wouldn't express correctly that a company is either a customer or a supplier and cannot be both at the same time. I didn't test it but I believe it would work.
I paste the full example I tested with (console application, reference to EF 4.3.1 assembly, downloaded via NuGet) in here if someone likes to experiment with it:
using System;
using System.Data.Entity;
namespace EFTPTCompositeKeys
{
public abstract class Company
{
public int TenantId { get; set; }
public int CompanyId { get; set; }
public int AddressId { get; set; }
public Address Address { get; set; }
}
public class Customer : Company
{
public string CustomerName { get; set; }
public int SalesPersonId { get; set; }
public Person SalesPerson { get; set; }
}
public class Supplier : Company
{
public string SupplierName { get; set; }
}
public class Address
{
public int TenantId { get; set; }
public int AddressId { get; set; }
public string City { get; set; }
}
public class Person
{
public int TenantId { get; set; }
public int PersonId { get; set; }
public string Name { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Company> Companies { get; set; }
public DbSet<Address> Addresses { get; set; }
public DbSet<Person> Persons { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Company>()
.HasKey(c => new { c.TenantId, c.CompanyId });
modelBuilder.Entity<Company>()
.HasRequired(c => c.Address)
.WithMany()
.HasForeignKey(c => new { c.TenantId, c.AddressId });
modelBuilder.Entity<Customer>()
.ToTable("Customers");
// the following mapping doesn't work and causes an exception
modelBuilder.Entity<Customer>()
.HasRequired(c => c.SalesPerson)
.WithMany()
.HasForeignKey(c => new { c.TenantId, c.SalesPersonId });
modelBuilder.Entity<Supplier>()
.ToTable("Suppliers");
modelBuilder.Entity<Address>()
.HasKey(a => new { a.TenantId, a.AddressId });
modelBuilder.Entity<Person>()
.HasKey(p => new { p.TenantId, p.PersonId });
}
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
using (var ctx = new MyContext())
{
try
{
ctx.Database.Initialize(true);
}
catch (Exception e)
{
throw;
}
}
}
}
}
Question: Is there any way to map the database schema above to a class model with Entity Framework?
Is it possible to use one of the attributes of a composite primary key as a foreign key? Yes, this is quite common and perfectly valid. The best example comes from the classic many-to-many relationship.
Composite key is a Candidate key that consists of more than one attribute. Foreign key is an attribute which is a Primary key in its parent table but is included as an attribute in the host table. Foreign keys may accept non-unique and null values.
A composite foreign key is a foreign key that consists of two or more columns. It is important to note that all the columns in a single foreign key must point to the same table. In other words, it is not possible to have a foreign key that references to a column in Table 1 and a column in Table 2.
Well, I can't seem to comment on anything so I'm adding this as an answer.
I created an Issue on CodePlex for this problem so hopefully they will look into it soon. Stay tuned!
http://entityframework.codeplex.com/workitem/865
Result of the Issue at CodePlex (which has been closed in the meantime) is that the scenario in the question is not supported and there are currently no plans to support it in the near future.
Quote from the Entity Framework team at CodePlex:
This is part of a more fundamental limitation where EF doesn't support having a property defined in a base type and then using it as a foreign key in a derived type. Unfortunately this is a limitation that would be very hard to remove from our code base. Given that we haven't seen a lot of requests for it, it's not something we are planning to address at this stage so we are closing this issue.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With