阴影和索引器属性

阴影属性不是在 .NET 实体类中定义的,但在 EF Core 模型中是为该实体类型定义的。 这些属性的值和状态全部在更改跟踪器中维护。 当数据库中存在不应在映射的实体类型上公开的数据时,阴影属性非常有用。

索引器属性是实体类型属性,由 .NET 实体类中的 索引器器提供支持。 可以使用 .NET 类实例上的索引器访问它们。 它还允许向实体类型添加其他属性,而无需更改 CLR 类。

外键阴影属性

阴影属性最常用于外键属性,在这种用法中,当约定未找到外键属性或未显式配置外键属性时,约定会将其添加到模型中。 关系由导航属性表示,但在数据库中由外键约束强制执行,外键列的值存储在相应的阴影属性中。

属性将命名为 <navigation property name><principal key property name>(指向主体实体的依赖实体上的导航用于命名)。 如果主体键属性名称以导航属性的名称开始,则该名称即为 <principal key property name>。 如果具有依赖性的实体上没有导航属性,则会改用与主键或备用键属性名称串接的主体实体类型名称 <principal type name><principal key property name>

例如,以下代码列表将导致 BlogId 阴影属性引入 Post 实体:

internal class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    // Since there is no CLR property which holds the foreign
    // key for this relationship, a shadow property is created.
    public Blog Blog { get; set; }
}

配置阴影属性

可以使用 Fluent API 来配置阴影属性。 调用 Property<TProperty>(String) 的字符串重载后,可以链接针对其他属性的任何配置调用。 在下面的示例中,由于 Blog 没有名为 LastUpdated 的 CLR 属性,因此将创建阴影属性:

internal class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property<DateTime>("LastUpdated");
    }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

如果提供给 Property 方法的名称与现有属性(阴影属性或实体类上定义的属性)的名称匹配,则代码将配置该现有属性,而不是引入新的阴影属性。

访问阴影属性

可以通过 ChangeTracker API 获取和更改阴影属性值:

context.Entry(myBlog).Property("LastUpdated").CurrentValue = DateTime.Now;

可以通过 EF.Property 静态方法在 LINQ 查询中引用阴影属性:

var blogs = context.Blogs
    .OrderBy(b => EF.Property<DateTime>(b, "LastUpdated"));

在无跟踪查询后无法访问阴影属性,因为更改跟踪器不会跟踪返回的实体。

配置索引器属性

可以使用 Fluent API 来配置索引器属性。 调用 IndexerProperty 方法后,可以链接针对其他属性的任何配置调用。 在下面的示例中,Blog 定义了一个索引器,该索引器将用于创建索引器属性。

internal class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>().IndexerProperty<DateTime>("LastUpdated");
    }
}

public class Blog
{
    private readonly Dictionary<string, object> _data = new Dictionary<string, object>();
    public int BlogId { get; set; }

    public object this[string key]
    {
        get => _data[key];
        set => _data[key] = value;
    }
}

如果提供给 IndexerProperty 方法的名称与现有索引器属性的名称匹配,则代码将配置该现有属性。 如果实体类型具有属性,该属性由实体类上的属性提供支持,则会引发异常,因为只能通过索引器访问索引器属性。

可以通过 EF.Property 静态方法(如上所示)或通过使用 CLR 索引器属性在 LINQ 查询中引用索引器属性。

属性包实体类型

仅包含索引器属性的实体类型称为属性包实体类型。 这些实体类型没有阴影属性,EF 会改为创建索引器属性。 目前仅支持将 Dictionary<string, object> 作为属性包实体类型。 必须配置为具有唯一名称的共享类型实体 类型,并且必须使用 Set 调用实现相应的 DbSet 属性。

internal class MyContext : DbContext
{
    public DbSet<Dictionary<string, object>> Blogs => Set<Dictionary<string, object>>("Blog");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.SharedTypeEntity<Dictionary<string, object>>(
            "Blog", bb =>
            {
                bb.Property<int>("BlogId");
                bb.Property<string>("Url");
                bb.Property<DateTime>("LastUpdated");
            });
    }
}

无论使用哪种普通实体类型(包括从属实体类型),都可以使用属性包实体类型。 但是,它们有一些限制: