问题
I am working with Entity Framework 7 and code-first, and I have a model which involves parent-child relations on 3 levels :
Corporations
havecompanies
Companies
belong to acorporation
and havefactories
Factories
belong to acompany
Since these 3 entities share a lot in common, they all inherit from an abstract BaseOrganization
entity.
When I try to list all the factories, including their mother companies, and then including their mother corporations, I have these two different scenarios :
- Without including
BaseOrganization
into the context, code-first creates three tables (which corresponds to the Table-Per-Concrete-Type or TPC pattern).Include()
andThenInclude()
work fine, and I can list factories and traverse relations as expected. - Including
BaseOrganization
into the context, code-first creates one table with a discriminator field (which corresponds to the Table-Per-Hierarchy or TPH pattern).Include()
andThenInclude()
throw aSequence contains more than one matching element
exception.
This issue (without the inheritance and abstract base class pattern) was already adressed in EF7 Github repo, and had been cleared (see https://github.com/aspnet/EntityFramework/issues/1460).
So I currently don't know if my approach has something wrong, or if this is clearly an issue with EF7 RC1 ? Note that I'd really prefer keeping inheritance so that my SQL model gets far more readable.
Below is the full reproduction code :
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Data.Entity;
namespace MultiLevelTest
{
// All places share name and Id
public abstract class BaseOrganization
{
public int Id { get; set; }
public string Name { get; set; }
}
// a corporation (eg : Airbus Group)
public class Corporation : BaseOrganization
{
public virtual ICollection<Company> Companies { get; set; } = new List<Company>();
}
// a company (eg : Airbus, Airbus Helicopters, Arianespace)
public class Company : BaseOrganization
{
public virtual Corporation Corporation { get; set; }
public virtual ICollection<Factory> Factories { get; set; } = new List<Factory>();
}
// a factory of a company (Airbus Toulouse, Airbus US...)
public class Factory : BaseOrganization
{
public virtual Company Company { get; set; }
}
// setup DbContext
public class MyContext : DbContext
{
// if this line is commented, then code first creates 3 tables instead of one, and everything works fine.
public DbSet<BaseOrganization> BaseOrganizationCollection { get; set; }
public DbSet<Corporation> Corporations { get; set; }
public DbSet<Company> Companies { get; set; }
public DbSet<Factory> Factories { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=MultiLevelTest;Trusted_Connection=True;MultipleActiveResultSets=true");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Corporation>().HasMany(c => c.Companies).WithOne(c => c.Corporation);
modelBuilder.Entity<Company>().HasMany(c => c.Factories).WithOne(c => c.Company);
modelBuilder.Entity<Factory>().HasOne(f => f.Company);
}
}
public class Program
{
public static void Main(string[] args)
{
using (var ctx = new MyContext())
{
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();
// Add a corporation with companies then factories (this works fine)
if (!ctx.Corporations.Any()) CreateOrganizationGraph(ctx);
// Get all the factories without including anything (this is still working fine)
var simpleFactories = ctx.Factories.ToList();
foreach(var f in simpleFactories) Console.WriteLine(f.Name);
// Get all the factories including their mother company, then their mother corporation
var fullFactories = ctx.Factories
.Include(f => f.Company)
.ThenInclude(c => c.Corporation)
.ToList();
foreach (var f in fullFactories) Console.WriteLine($"{f.Company.Corporation.Name} > {f.Company.Name} > {f.Name}");
}
}
public static void CreateOrganizationGraph(MyContext ctx)
{
var airbusCorp = new Corporation()
{
Name = "Airbus Group",
Companies = new List<Company>()
{
new Company
{
Name = "Airbus",
Factories = new List<Factory>()
{
new Factory {Name = "Airbus Toulouse (FR)"},
new Factory {Name = "Airbus Hambourg (DE)"}
}
},
new Company
{
Name = "Airbus Helicopters",
Factories = new List<Factory>()
{
new Factory {Name = "Eurocopter Marignane (FR)"},
new Factory {Name = "Eurocopter Deutschland (DE)"}
}
}
}
};
ctx.Corporations.Add(airbusCorp);
ctx.SaveChanges();
}
}
}
You will want to include the following NuGet packages :
"EntityFramework.Commands": "7.0.0-rc1-final",
"EntityFramework.Core": "7.0.0-rc1-final",
"EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-final"
UPDATE
As said in my own comments, my first workaround would be to avoid including the base type in the DbContext, so that code-first generates schema with TPC pattern (the bug throws only when in TPH strategy).
Problem is that the example above is simpler as my actual implementation, which involves many-to-many relationships defined at base type level.
Since EF7 does not (yet?) support many-to-many relationships, we have to define a link entity which maps two one-to-many relationships on its own.
That mapping entity being defined and used at base type level, code-first still opts for TPH strategy, and then the bug still throws.
In other words, I'm stuck, or I will have to duplicate some logic three times, which is almost like breaking my own leg on purpose !
回答1:
I think you should not try to use a base class in your case.
Organizations, companies, factories represents different objects and from what I see here, you are trying to refactor code not abstracting object:
If you create a database that store authors and books, both authors and books will have a name and an id but does it make sense to have a base class ?
For sure you are going to save few lines of code but it will make you code less readable.
I think you should use a base class when there is a real inheritance:
For example, you can have a base class Person
and a Manager
and Employee
class that inherits from the Person class because both employees and managers are persons.
For me you just have to remove your base class and It should work as expected :
public class Corporation
{
public int Id { get; set; }
public string Name { get; set; }
public List<Company> Companies { get; set; } = new List<Company>();
}
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public Corporation Corporation { get; set; }
public List<Factory> Factories { get; set; } = new List<Factory>();
}
public class Factory
{
public int Id { get; set; }
public string Name { get; set; }
public Company Company { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Corporation> Corporations { get; set; }
public DbSet<Company> Companies { get; set; }
public DbSet<Factory> Factories { get; set; }
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Corporation>().HasMany(c => c.Companies).WithOne(c => c.Corporation);
modelBuilder.Entity<Company>().HasMany(c => c.Factories).WithOne(c => c.Company);
modelBuilder.Entity<Factory>().HasOne(f => f.Company);
}
}
来源:https://stackoverflow.com/questions/36554296/include-theninclude-throws-sequence-contains-more-than-one-matching-element