.net - 导航到通用定义实体的正确方法

标签 .net entity-framework interface entity-framework-6 abstract-class

新赏金 2017/10/31

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

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

以下原始问题

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

我知道这是一个自然的限制,但仍然寻求一种在使用抽象类型或泛型时实现双向导航的方法。我遇到了许多与我想做的类似的问题,但它们要么很老,要么我认为它们与我想要实现的目标不完全匹配。我寻求一个针对我的困境的更当前的答案。

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

编辑 (回应 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.
所以看来我需要编辑我的原始代码以反射(reflect)我最初为了简洁而省略的更复杂的模型。我很抱歉,因为直到现在我才认为附加代码是相关的。

请注意,我现在让所有实体都继承自 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; }
}

我想到的一个解决方案是创建一个方法,它将 GetById 委托(delegate)传递给实现 ISoftwareApplicationData 的实体的存储库集合。 .我不喜欢在迭代中执行 GetById 的想法,但可能只有五种类型我需要执行此操作,因此这是一个可行的解决方案,其他所有方法都失败了。

最佳答案

Because the navigation property to the child is defined by an interface, I have no navigation to the other end of the relationship.

I understand that this is a natural limitation, but still seek a means to achieve navigation while using abstract types or generics.



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

以下是修改后的示例模型(不相关的 ISoftwareApplicationBaseSystemConfiguration 除外):

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

配置:
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"));

生成的表和关系:
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);

导航测试:
var test = db.Set<SoftwareApplicationBase>()
    .Include(e => e.Data)
    .ToList();

EF 从上面生成的 SQL 查询:

SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[ApplicationName] AS [ApplicationName],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN '2X0X' WHEN ([UnionAll4].[C6] = 1) THEN '2X1X' ELSE '2X2X' END AS [C1],
    [UnionAll4].[ApplicationId] AS [C2],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN [UnionAll4].[C1] WHEN ([UnionAll4].[C6] = 1) THEN CAST(NULL AS varchar(1)) END AS [C3],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN [UnionAll4].[C2] WHEN ([UnionAll4].[C6] = 1) THEN CAST(NULL AS varchar(1)) END AS [C4],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C6] = 1) THEN [UnionAll4].[Version] END AS [C5],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C6] = 1) THEN CAST(NULL AS varchar(1)) ELSE [UnionAll4].[C3] END AS [C6],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C6] = 1) THEN CAST(NULL AS varchar(1)) ELSE [UnionAll4].[C4] END AS [C7]
    FROM   [dbo].[SoftwareApplicationBase] AS [Extent1]
    LEFT OUTER JOIN  (SELECT
        [Extent2].[ApplicationId] AS [ApplicationId]
        FROM [dbo].[SoftwareApplication] AS [Extent2]
    UNION ALL
        SELECT
        [Extent3].[ApplicationId] AS [ApplicationId]
        FROM [dbo].[VideoGame] AS [Extent3]
    UNION ALL
        SELECT
        [Extent4].[ApplicationId] AS [ApplicationId]
        FROM [dbo].[OperatingSystem] AS [Extent4]) AS [UnionAll2] ON [Extent1].[Id] = [UnionAll2].[ApplicationId]
    LEFT OUTER JOIN  (SELECT
        [Extent5].[ApplicationId] AS [ApplicationId],
        CAST(NULL AS varchar(1)) AS [C1],
        CAST(NULL AS varchar(1)) AS [C2],
        [Extent5].[Version] AS [Version],
        CAST(NULL AS varchar(1)) AS [C3],
        CAST(NULL AS varchar(1)) AS [C4],
        cast(0 as bit) AS [C5],
        cast(1 as bit) AS [C6]
        FROM [dbo].[SoftwareApplication] AS [Extent5]
    UNION ALL
        SELECT
        [Extent6].[ApplicationId] AS [ApplicationId],
        CAST(NULL AS varchar(1)) AS [C1],
        CAST(NULL AS varchar(1)) AS [C2],
        CAST(NULL AS varchar(1)) AS [C3],
        [Extent6].[Publisher] AS [Publisher],
        [Extent6].[Genre] AS [Genre],
        cast(0 as bit) AS [C4],
        cast(0 as bit) AS [C5]
        FROM [dbo].[VideoGame] AS [Extent6]
    UNION ALL
        SELECT
        [Extent7].[ApplicationId] AS [ApplicationId],
        [Extent7].[Version] AS [Version],
        [Extent7].[ServicePack] AS [ServicePack],
        CAST(NULL AS varchar(1)) AS [C1],
        CAST(NULL AS varchar(1)) AS [C2],
        CAST(NULL AS varchar(1)) AS [C3],
        cast(1 as bit) AS [C4],
        cast(0 as bit) AS [C5]
        FROM [dbo].[OperatingSystem] AS [Extent7]) AS [UnionAll4] ON [Extent1].[Id] = [UnionAll4].[ApplicationId]

不是最好看,但对你来说脏活:)

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

如果您有能力引入持有通用 SoftwareApplicationData 的中间表属性和关系,您可以使用 Table Per Type (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; }
}

配置:
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");

相关表格:
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);

所需的导航与以前一样,并允许预先加载基本导航属性:
var test = db.Set<SoftwareApplicationBase>()
    .Include(e => e.Data)
    .Include(e => e.Data.CreatedBy)
    .ToList();

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

关于.net - 导航到通用定义实体的正确方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46811017/

相关文章:

entity-framework - 我的 Seed() 方法从未在 Code First EF 5 中调用

java - 声明接口(interface)并在抽象类 Java 的类中实现

c# - 我如何从方法返回 IEnumerable<T>

.net - Servicestack.Redis是否支持启用集群的redis?

C# 相当于 IEEE 754 余数 ()?

.net - Model First with DbContext,无法初始化新的数据库

.net - 如何使用 Microsoft.Data.SQLite 存储包含空字符的 BLOB?

c# - Entity Framework 中 "CASE WHEN THEN"(T-SQL) 的等价物是什么?

c# - Visual Studio for Mac 上出现奇怪的 linq 异常

java - Jax-WS 接口(interface)作为参数