Herança e chaves externas compostas - uma parte da chave na classe base, a outra parte na classe derivada

Estou tendo problemas para criar um mapeamento Code-First do Entity Framework para o seguinte esquema de banco de dados de amostra (no SQL Server):

Cada mesa contém umTenantId que faz parte de todas as chaves primárias e estrangeiras (compostas) (Multi-Tenancy).

A Company é umCustomer ou umSupplier e eu tento modelar isso via mapeamento de herança Tabela-Por-Tipo (TPT):

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; }
}

Mapeamento com a API do Fluent:

modelBuilder.Entity<Company>()
    .HasKey(c => new { c.TenantId, c.CompanyId });

modelBuilder.Entity<Customer>()
    .ToTable("Customers");

modelBuilder.Entity<Supplier>()
    .ToTable("Suppliers");

A mesa baseCompanies tem um relacionamento um-para-muitos para umAddress (cada empresa tem um endereço, não importa se cliente ou fornecedor) e eu posso criar um mapeamento para esta associação:

 modelBuilder.Entity<Company>()
     .HasRequired(c => c.Address)
     .WithMany()
     .HasForeignKey(c => new { c.TenantId, c.AddressId });

A chave estrangeira é composta por uma parte da chave primária - aTenantId - e uma coluna separada - oAddressId. Isso funciona.

Como você pode ver no esquema do banco de dados, da perspectiva do banco de dados, a relação entreCustomer ePerson é basicamente o mesmo tipo de relação um-para-muitos entreCompany eAddress - a chave estrangeira é composta novamente doTenantId (parte da chave primária) e a colunaSalesPersonId. (Apenas um cliente tem um vendedor, não umSupplier, portanto, o relacionamento está na classe derivada desta vez, não na classe base.)

Eu tento criar um mapeamento para essa relação com a API do Fluent da mesma maneira que antes:

modelBuilder.Entity<Customer>()
    .HasRequired(c => c.SalesPerson)
    .WithMany()
    .HasForeignKey(c => new { c.TenantId, c.SalesPersonId });

Mas quando a EF tenta compilar o modeloInvalidOperationException é lançado:

O componente de chave estrangeira 'TenantId' não é uma propriedade declarada no tipo 'Cliente'. Verifique se ele não foi explicitamente excluído do modelo e se é uma propriedade primitiva válida.

Aparentemente, não consigo compor uma chave estrangeira de uma propriedade na classe base e de outra propriedade na classe derivada (embora no esquema do banco de dados a chave estrangeira seja composta de colunasambos na tabela do tipo derivadoCustomer).

Eu tentei duas modificações para fazê-lo funcionar, talvez:

Mudou a associação da chave estrangeira entreCustomer ePerson para uma associação independente, ou seja, removeu a propriedadeSalesPersonIde, em seguida, tentei o mapeamento:

modelBuilder.Entity<Customer>()
    .HasRequired(c => c.SalesPerson)
    .WithMany()
    .Map(m => m.MapKey("TenantId", "SalesPersonId"));

Isso não ajuda (eu realmente não esperava, seria) e a exceção é:

Esquema especificado não é válido. ... Cada nome de propriedade em um tipo deve ser exclusivo. O nome da propriedade 'TenantId' já estava definido.

Mudou o TPT para o mapeamento TPH, ou seja, removeu os doisToTable chamadas. Mas lança a mesma exceção.

Eu vejo duas soluções alternativas:

Introduzir umSalesPersonTenantId noCustomer classe:

public class Customer : Company
{
    public string CustomerName { get; set; }

    public int SalesPersonTenantId { get; set; }
    public int SalesPersonId { get; set; }
    public Person SalesPerson { get; set; }
}

e o mapeamento:

modelBuilder.Entity<Customer>()
    .HasRequired(c => c.SalesPerson)
    .WithMany()
    .HasForeignKey(c => new { c.SalesPersonTenantId, c.SalesPersonId });

Eu testei isso e funciona. Mas vou ter uma nova colunaSalesPersonTenantId noCustomers mesa para além doTenantId. Essa coluna é redundante porque as duas colunas sempre devem ter o mesmo valor da perspectiva de negócios.

Abandone o mapeamento de herança e crie mapeamentos um-para-um entreCompany eCustomer e entreCompany eSupplier. Company deve se tornar um tipo concreto, em seguida, não abstrato e eu teria duas propriedades de navegação emCompany. Mas este modelo não expressaria corretamente que uma empresa éou um clienteou um fornecedor e não pode ser ambos ao mesmo tempo. Eu não testei, mas acredito que funcionaria.

Eu colo o exemplo completo com o qual testei (aplicativo de console, referência à montagem do EF 4.3.1, baixado via NuGet) aqui, se alguém gosta de experimentá-lo:

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;
                }
            }
        }
    }
}

Pergunta: Existe alguma maneira de mapear o esquema do banco de dados acima para um modelo de classe com o Entity Framework?

questionAnswers(3)

yourAnswerToTheQuestion