导航到一般定义的实体的正确方法

osc*_*tin 10 .net abstract-class entity-framework interface entity-framework-6

新赏金2017/10/31

遗憾的是,由于TPC限制,自动接受的答案不适用于我当前的实体模型.我迫切需要找到一种方法来通过接口或抽象类来促进双向导航,所以我开始了另一个赏金.

请注意,我必须使用现有的模型设计,因此重构不是一种选择.

原始问题如下

我有一个父实体,它与多个可能的表有一对一的关系(FK在子表上).因为子节点的导航属性是由接口定义的,所以我没有导航到关系的另一端.

我知道这是一个自然的限制,但仍然寻求一种在使用抽象类型或泛型时实现双向导航的方法.我遇到了一些类似于我想做的问题,但它们要么已经很老了,要么我认为它们与我想要实现的完全匹配.我寻求一个特定于我的困境的更新的答案.

这是我的代码,可以轻松地复制/粘贴到测试应用程序中:

编辑(响应Ivan Stoev的回答):当我尝试实施您的解决方案时,我在尝试创建迁移时遇到此错误:

The association 'SoftwareApplicationData_CreatedBy' between entity types 'SoftwareApplicationData' and 'AppUser' is invalid. In a TPC hierarchy independent associations are only allowed on the most derived types.

所以看来我需要编辑我的原始代码以反映我最初为简洁而省略的更复杂的模型.我道歉,因为我认为其他代码到目前为止并不相关.

请注意,我现在所有实体都继承自MyEntity.

结束编辑

public abstract class MyEntity
{
    public int Id { get; set; }

    public AppUser CreatedBy { get; set; }
}

public class AppUser : MyEntity { }

public interface ISoftwareApplicationData
{
    SoftwareApplicationBase Application { get; set; }
}

//Parent entity representing a system installation and the software installed on it.
//The collection property is *not* the generic entity I mentioned earlier.
public class SystemConfiguration : MyEntity
{
    public ICollection<SoftwareApplicationBase> Applications { get; set; }
}

//Represents the software itself. Has other generic attributes that I've ommitted for brevity.
//The Data property represents additional, application-specific attributes. I need to be able
//to navigate from SoftwareApplicationBase to whatever may be on the other end
public class SoftwareApplicationBase : MyEntity
{
    public SystemConfiguration Configuration { get; set; }

    public string ApplicationName { get; set; }

    public ISoftwareApplicationData Data { get; set; }
}

//This is a generic, catch-all application class that follows a basic Application/Version
//convention. Most software will use this class
public class SoftwareApplication : MyEntity, ISoftwareApplicationData
{
    public SoftwareApplicationBase Application { get; set; }

    public string Version { get; set; }
}

//Operating systems have special attributes, so they get their own class.
public class OperatingSystem : MyEntity, ISoftwareApplicationData
{
    public SoftwareApplicationBase Application { get; set; }

    public string Version { get; set; }

    public string ServicePack { get; set; }
}

//Yet another type of software with its own distinct attributes
public class VideoGame : MyEntity, ISoftwareApplicationData
{
    public SoftwareApplicationBase Application { get; set; }

    public string Publisher { get; set; }

    public string Genre { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我想到的一个解决方案是创建一个方法,将GetById委托传递给实现的实体的repos集合ISoftwareApplicationData.我不喜欢在迭代中做一个GetById的想法,但是我可能只有五种类型我需要这样做,所以这是一个成功的解决方案,所有其他方面都失败了.

Iva*_*oev 3

由于子级的导航属性是由接口定义的,因此我无法导航到关系的另一端。

我知道这是一个自然的限制,但仍然寻求一种在使用抽象类型或泛型时实现导航的方法。

此设计中的主要问题是接口,因为 EF 仅适用于类。但是,如果您可以将其替换为抽象类,并且子表中的FK也是PK(即遵循共享主键关联模式来表示一对一关系),那么您可以使用EF Table per Concrete Type( TPC)继承策略来映射现有的子表,这反过来又允许 EF 自动为您提供所需的导航。

这是修改后的模型示例(排除ISoftwareApplicationBase和SystemConfiguration不相关的部分):

public class SoftwareApplicationBase
{
    public int Id { get; set; }
    public string ApplicationName { get; set; }
    public SoftwareApplicationData Data { get; set; }
}

public abstract class SoftwareApplicationData
{
    public int ApplicationId { get; set; }
    public SoftwareApplicationBase Application { get; set; }
}

public class SoftwareApplication : SoftwareApplicationData
{
    public string Version { get; set; }
}

public class OperatingSystem : SoftwareApplicationData
{
    public string Version { get; set; }
    public string ServicePack { get; set; }
}

public class VideoGame : SoftwareApplicationData
{
    public string Publisher { get; set; }
    public string Genre { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

配置:

modelBuilder.Entity<SoftwareApplicationBase>()
    .HasOptional(e => e.Data)
    .WithRequired(e => e.Application);

modelBuilder.Entity<SoftwareApplicationData>()
    .HasKey(e => e.ApplicationId);

modelBuilder.Entity<SoftwareApplication>()
    .Map(m => m.MapInheritedProperties().ToTable("SoftwareApplication"));

modelBuilder.Entity<OperatingSystem>()
    .Map(m => m.MapInheritedProperties().ToTable("OperatingSystem"));

modelBuilder.Entity<VideoGame>()
    .Map(m => m.MapInheritedProperties().ToTable("VideoGame"));
Run Code Online (Sandbox Code Playgroud)

生成的表和关系:

CreateTable(
    "dbo.SoftwareApplicationBase",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            ApplicationName = c.String(),
        })
    .PrimaryKey(t => t.Id);

CreateTable(
    "dbo.SoftwareApplication",
    c => new
        {
            ApplicationId = c.Int(nullable: false),
            Version = c.String(),
        })
    .PrimaryKey(t => t.ApplicationId)
    .ForeignKey("dbo.SoftwareApplicationBase", t => t.ApplicationId)
    .Index(t => t.ApplicationId);

CreateTable(
    "dbo.OperatingSystem",
    c => new
        {
            ApplicationId = c.Int(nullable: false),
            Version = c.String(),
            ServicePack = c.String(),
        })
    .PrimaryKey(t => t.ApplicationId)
    .ForeignKey("dbo.SoftwareApplicationBase", t => t.ApplicationId)
    .Index(t => t.ApplicationId);

CreateTable(
    "dbo.VideoGame",
    c => new
        {
            ApplicationId = c.Int(nullable: false),
            Publisher = c.String(),
            Genre = c.String(),
        })
    .PrimaryKey(t => t.ApplicationId)
    .ForeignKey("dbo.SoftwareApplicationBase", t => t.ApplicationId)
    .Index(t => t.ApplicationId);
Run Code Online (Sandbox Code Playgroud)

导航测试:

var test = db.Set<SoftwareApplicationBase>()
    .Include(e => e.Data)
    .ToList();
Run Code Online (Sandbox Code Playgroud)

EF 根据上面生成的 SQL 查询:

modelBuilder.Entity<SoftwareApplicationBase>()
    .HasOptional(e => e.Data)
    .WithRequired(e => e.Application);

modelBuilder.Entity<SoftwareApplicationData>()
    .HasKey(e => e.ApplicationId);

modelBuilder.Entity<SoftwareApplication>()
    .Map(m => m.MapInheritedProperties().ToTable("SoftwareApplication"));

modelBuilder.Entity<OperatingSystem>()
    .Map(m => m.MapInheritedProperties().ToTable("OperatingSystem"));

modelBuilder.Entity<VideoGame>()
    .Map(m => m.MapInheritedProperties().ToTable("VideoGame"));
Run Code Online (Sandbox Code Playgroud)

不是最好看的,但可以为你做肮脏的工作:)

编辑: MyEntity基类以及每个实体类必须继承它的要求极大地限制了选项。由于基类内部定义导航属性的关系(另一个 EF 限制),TPC 不再适用。因此,唯一可行的自动 EF 选项是使用其他两种 EF 继承策略中的一些,但它们需要更改数据库结构。

如果您有能力引入包含公共SoftwareApplicationData属性和关系的中间表,则可以使用按类型表 (TPT)策略,如下所示:

模型:

public class SoftwareApplicationBase : MyEntity
{
    public string ApplicationName { get; set; }
    public SoftwareApplicationData Data { get; set; }
}

public abstract class SoftwareApplicationData : MyEntity
{
    public SoftwareApplicationBase Application { get; set; }
}

public class SoftwareApplication : SoftwareApplicationData
{
    public string Version { get; set; }
}

public class OperatingSystem : SoftwareApplicationData
{
    public string Version { get; set; }
    public string ServicePack { get; set; }
}

public class VideoGame : SoftwareApplicationData
{
    public string Publisher { get; set; }
    public string Genre { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

配置:

modelBuilder.Entity<SoftwareApplicationBase>()
    .HasOptional(e => e.Data)
    .WithRequired(e => e.Application);

modelBuilder.Entity<SoftwareApplicationData>()
    .ToTable("SoftwareApplicationData");

modelBuilder.Entity<SoftwareApplication>()
    .ToTable("SoftwareApplication");

modelBuilder.Entity<OperatingSystem>()
    .ToTable("OperatingSystem");

modelBuilder.Entity<VideoGame>()
    .ToTable("VideoGame");
Run Code Online (Sandbox Code Playgroud)

相关表格:

CreateTable(
    "dbo.SoftwareApplicationData",
    c => new
        {
            Id = c.Int(nullable: false),
            CreatedBy_Id = c.Int(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.AppUser", t => t.CreatedBy_Id)
    .ForeignKey("dbo.SoftwareApplicationBase", t => t.Id)
    .Index(t => t.Id)
    .Index(t => t.CreatedBy_Id);

CreateTable(
    "dbo.SoftwareApplication",
    c => new
        {
            Id = c.Int(nullable: false),
            Version = c.String(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.SoftwareApplicationData", t => t.Id)
    .Index(t => t.Id);

CreateTable(
    "dbo.OperatingSystem",
    c => new
        {
            Id = c.Int(nullable: false),
            Version = c.String(),
            ServicePack = c.String(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.SoftwareApplicationData", t => t.Id)
    .Index(t => t.Id);

CreateTable(
    "dbo.VideoGame",
    c => new
        {
            Id = c.Int(nullable: false),
            Publisher = c.String(),
            Genre = c.String(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.SoftwareApplicationData", t => t.Id)
    .Index(t => t.Id);
Run Code Online (Sandbox Code Playgroud)

所需的导航与以前一样,但额外允许立即加载基本导航属性:

var test = db.Set<SoftwareApplicationBase>()
    .Include(e => e.Data)
    .Include(e => e.Data.CreatedBy)
    .ToList();
Run Code Online (Sandbox Code Playgroud)

回顾一下,在 EF 中实现自动导航的唯一方法是使用抽象类和 EF 继承,并带有相应的约束。如果它们都不适用于您的场景,您必须求助于类似于问题末尾提到的自定义代码处理选项。