Inheritance and composite foreign keys - one part of the key in base class, the other part in derived class
Asked Answered
T

3

17

I am having problems to create an Entity Framework Code-First mapping for the following sample database schema (in SQL Server):

Database schema with composite foreign keys

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?

Thorbert answered 9/6, 2012 at 14:12 Comment(6)
Why do you need that? Why not just having one single Id column and avoid the headache?Acceptant
@ivowiblo: Because I want to ensure in the database that relationships between rows in different tables always refer between data of the same tenant. If I had a single Id column I could have a customer of tenant 1 refering to a SalesPerson of tenant 2 which would be valid in the DB. I had to ensure that this does not happen on application level. A bug would be desastrous (tenant 1 can access data of tenant 2). But nonetheless your remark is a good point and I have to consider it if there is no other solution.Thorbert
@Slauma: Hi man, did you have found any solution or workaround? I am having the same issue, and dont know how to proceed. I also tryied to eg. include an override version for the int TenantId to the Customer class. But still doesnt work.Overcloud
@luri: No, I have no solution, currently I follow the workaround mentioned in my last point above "Abandon inheritance mapping and create one-to-one mappings..."Thorbert
@Slauma: Thank you. I will addopt the redundant approach workaround. I think it is a low cost to keep this constraint at database level. And if someday we have the bug resolved, I just could remove the redundant attribute.Overcloud
@Slauma: Thank you as well for this.. I'm having a similar issues here "System.InvalidOperationException: The property 'TenantID' cannot be part of a foreign key on 'tbl_CalendarEvent' because it is contained in a key defined on a base entity type". Our tenant tables are derived from a base library we made and then inherited a calendar event table and added some additional relationships and got the error you described. Its ashame that this bug still hasnt been resolvedArmalda
S
7

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.

Stocker answered 14/2, 2013 at 16:59 Comment(1)
I have added the result from CodePlex to the answer.Thorbert
P
0

Not a solution, but a workaround (*): a nice choice is to use single Id columns (as ), usually auto-incremented, and providing database integrity using foreign keys, unique indexes, etc. More complex data integrity could be achieved with triggers, so maybe you could be heading that way, but you might leave that to the application business logic level, unless the application is really data-focused. But since you're using Entity Framework, it's probably safe to assume that this is not your case...?

(*) as suggested by ivowiblo

Printmaker answered 18/6, 2012 at 14:34 Comment(1)
This is a workaround, but not a good practice in my opinion. It will actually end up more complex and spread out your business logic (triggers can be just nasty to develop and debug). I would suggests it is worth the effort to find a data architecture solution to this problem before trying to compromise the relational integrity of the database to make easier programming.Political
B
0

I think it is simpler and reduces complexity to have Table --> TableId (PK) --> other columns including FKs.

So in your example - adding a CustomerId column to the Customers table would solve your problem.

Brady answered 16/9, 2012 at 15:27 Comment(7)
AAAAARRRRGGGHHHHHH! Sorry :( But your answer is like so: I'm asking "I want to build my own home weather station that can measure temperature AND barometric pressure. How can I do that?" and then you answer: "Measuring pressure is too complex. Build a home weather station that can only measure the temperature. That would solve your problem."Thorbert
Really? How does adding CustomerId reduce your functionality?Brady
How does it solve the problem? If you mean a single key column instead of composite key please read the two comments below the question where I described the reason why I want composite keys. But it's well possible that I misunderstand your proposal.Thorbert
I mean a single key column yes, but each key column is unique to the table. Customers has CustomerId, People has PersonId, Tennant has TennantId, etc. It's a common pattern because of its simplicity. That way no need for composite keys, no possibilities of duplication, and your code works :)Brady
No duplication, but the possibility to link a customer and a person of two different tenants which must not happen in the business model. My question is about a case where I cannot use that "simple common pattern" you are recommending (and Luis Ferreira in his answer as well).Thorbert
I agree with you that People should be linked to the base type (Companies) not the derived types. Maybe try creating it model-first with the linkage that way and check the code that is produced?Brady
@Slauma: how about create a trigger to handle this situation? "possibility to link a customer and a person of two different tenants" Business world of applications is a world of workarounds, it is not practical to make it too hard. And remember - you are NOT dealing with insurance, so it could have actually been worse.Chita

© 2022 - 2024 — McMap. All rights reserved.