文章目录

  • 影子属性和索引器属性
  • 1. 外键影子属性
  • 2. 配置影子属性
  • 3. 访问影子属性
  • 4. 配置索引器属性
  • 5. 属性包实体类型


影子属性和索引器属性

影子属性(Shadow properties),
影子属性是一种属性,它不在.NET实体类中定义,而是在EF Core模型中为该实体类型定义的
影子属性的值和状态完全是在更改追踪器(Change Tracker)中维护的。
当数据库中有不应该在映射的实体类上公开的数据时,影子属性是很有用的。

索引器属性(Indexer properties),
索引器属性是实体类型属性,由.NET实体类中索引器支持。可以使用.NET类实例上的索引器访问它们。它还允许你在不更改CLR类的情况下向型添加属性。

1. 外键影子属性

影子属性最常用于外键属性,两个实体之间的关系由数据库中的外键值来表示,但是关系是在实体类型上使用实体类型之间的导航属性进行管理的。按照约定,当在从实体类型中发现关系但没有发现外键属性时EF将引入影子属性

该影子属性会被命名为<navigation property name><principal key property 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; }

    // 因为这里没有保存关系外键的CLR属性,所以影子属性被创建
    public Blog Blog { get; set; }
}

2. 配置影子属性

你可以使用fluent API来配置影子属性。一旦你调用了Property的字符串重载,你就能为其他属性链接任意配置调用。在下例中,因为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方法的名称与已有属性的名称匹配(影子属性或实体类上定义的属性),那么代码将配置已有属性,而不是引入新的影子属性。

3. 访问影子属性

影子属性的值可以通过ChangeTracker API获取和修改:

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

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

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

影子属性无法在无跟踪的查询后被访问,因为返回的实体没有被更改跟踪器跟踪。

4. 配置索引器属性

可以使用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方法的名称与现有的索引器属性名称匹配,那么代码将配置该现有属性。如果实体类型有一个由实体类上的属性支持的属性,则会引发异常,因为索引器属性必须通过索引器来访问。

索引器属性可以在LINQ查询中通过EF.Property静态方法(如上面展示的那样)被引用,或使用CLR索引器属性来引用。

5. 属性包实体类型

注意:
属性包实体类型(Property bag entity types)是在EF Core 5.0引入的。

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

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

属性包实体类型可用在普通的实体类型能用的任何地方,包括作为自身拥有的实体类型。不过,它们也有一定的局限性:

  • 它们无法拥有影子属性。
  • 不支持索引器导航。
  • 无法继承。
  • 一些关系模型构建API缺乏共享历史题类型的重载。
  • 其他类型不能被标记为属性包。