EF Core通过空检查来克服构造函数的目的

时间:2018-06-28 22:41:40

标签: c# entity-framework entity-framework-core

我已经设计了模型,因此应该使用null参数创建模型。例如,如果我要强制每个Post应该有一个对应的Blog,那么我的模型将如下所示:

public class Post
{
    private Post() { }
    public Post(Blog blog)
    {
        Blog = blog ?? throw new ArgumentNullException(nameof(blog));
    }
    public int PostId { get; private set; }
    public Blog Blog { get; private set; }
}
public class Blog
{
    public int BlogId { get; private set; }
}

这样,如果Exceptionblog,它将抛出null

但是我使用EF Core,但该测试未通过。

public class Tests
{
    [Fact]
    public void Test()
    {
        using (var ctx = new Context())
        {
            ctx.Database.EnsureDeleted();
            ctx.Database.EnsureCreated();
            ctx.Add(new Post(new Blog()));
            ctx.SaveChanges();
        }
        using (var ctx = new Context())
        {
            var post = ctx.Post.First();
            Assert.NotNull(post.Blog); //fail
        }
    }
}
public class Context : DbContext
{
    public DbSet<Post> Post { get; set; }
    public DbSet<Blog> Blog { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>(b => b.HasOne(p => p.Blog));
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlite("datasource=db.sqlite");
}

我知道这是因为EF Core调用了不带参数的私有构造函数,并且我需要加载(即急切,显式或惰性)Post.Blog导航属性。

但是我想知道的是我的EF模型设计方法是否不正确,因为EF Core通过空检查无法达到构造函数的目的?

编辑:使用C#8的可为空的引用类型时,EF Core可能会朝着可以使用构造函数设置导航属性的方向发展。请参阅EF核心问题:Support C# nullable references

1 个答案:

答案 0 :(得分:1)

TL; DR

您可以在对象模型中显式定义外键,数据库模型用来表示Post / Blog关系。

public class Post
{
    private Post() { }
    public Post(Blog blog)
    {
        Blog = blog ?? throw new ArgumentNullException(nameof(blog));
    }
    public int PostId { get; private set; }
    public int BlogId {get; private set; } # Foreign Key Property
    public Blog Blog { get; private set; }
}
public class Blog
{
    public int BlogId { get; private set; }
}

然后,您可以重写测试以检查此外键是否为空值。

public class Tests
{
    [Fact]
    public void Test()
    {
        using (var ctx = new Context())
        {
            ctx.Database.EnsureDeleted();
            ctx.Database.EnsureCreated();
            ctx.Add(new Post(new Blog()));
            ctx.SaveChanges();
        }
        using (var ctx = new Context())
        {
            var post = ctx.Post.First();
            Assert.NotEqual(0, post.BlogId); //passes
        }
    }
}
public class Context : DbContext
{
    public DbSet<Post> Post { get; set; }
    public DbSet<Blog> Blog { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>(b => b.HasOne(p => p.Blog));
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlite("datasource=db.sqlite");
}

一种替代方法是,当您选择帖子时,告诉您的实体模型包括导航属性。

public class Tests
{
    [Fact]
    public void Test()
    {
        using (var ctx = new Context())
        {
            ctx.Database.EnsureDeleted();
            ctx.Database.EnsureCreated();
            ctx.Add(new Post(new Blog()));
            ctx.SaveChanges();
        }
        using (var ctx = new Context())
        {
            var post = ctx.Post.Include(p=>p.Blog).First();
            Assert.NotNull(post.Blog); //passes
        }
    }
}
public class Context : DbContext
{
    public DbSet<Post> Post { get; set; }
    public DbSet<Blog> Blog { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>(b => b.HasOne(p => p.Blog));
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlite("datasource=db.sqlite");
}



详细说明

如果您的设计可以保证,您的方法是正确的。错误的是您对职责分离的理解。我们有多个关注领域:

  1. 对象模型和不变式
  2. 实体模型与对象关系映射
  3. 数据库模型,约束和引用完整性

EF Core负责#2。 EF Core需要知道您的对象模型和数据库模型的结构才能成功地在两者之间进行映射。您可以在对象模型或实体模型中指定数据库约束,以帮助您在访问数据库之前捕获违反这些约束的情况,但这不是必需的。

让我们看看两种情况。

场景1

我们希望将新记录插入到表中,使用您的应用代码帮助用户在将记录提交到数据库之前制作记录。我们从关注的领域#1(您的对象模型)开始。

根据对象的建模方式,可能需要强制执行某些不变式。在您的示例中,您具有以下规则:每个Post对象都属于Blog对象。表中的每个Post记录都有一个关联的Blog记录,这纯粹是此不变因素的副作用。

场景#2

您想从表中选择一条记录,使用您的应用程序代码在内存中表示该记录,以便将其显示给用户。我们从关注的领域#3(您的数据库模型)开始。

您的数据库使用外键(即BlogId)强制执行参照完整性。在EF Core中,您不需要在Post对象上定义此外键。 EF Core将根据您帖子的导航属性Blog为您创建一个阴影属性。影子属性只是数据库模型中存在的一种,而对象模型中没有。

在向数据库模型查询记录时,它要求您非常明确地包含要包含的关系。默认情况下,不会将所有外键都连接到它们各自的表,您需要明确地执行此操作。您的实体模型也不会自动为您执行此操作。您需要调用.include到select语句中,以填充对象模型的导航属性。

就数据库模型和您的实体模型而言,引用完整性存在,并已正确映射到您的对象模型。结果,对象模型的不变性不会得到强制,因为您是从数据库模型的数据表示开始的,然后又回到了对象模型的表示形式。