InfoPool

私人信息记录

0%

创建并配置模型

Entity Framework Core 使用一组约定来根据实体类的形状生成模型。 可指定其他配置以补充和/或替代约定的内容。

使用 fluent API 配置模型

Fluent API 配置具有最高优先级,并将替代约定和数据注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }

#region Required
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
#endregion
}

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

分组配置

为了减小 OnModelCreating 方法的大小,可以将实体类型的所有配置提取到实现 IEntityTypeConfiguration<TEntity> 的单独类中。

1
2
3
4
5
6
7
8
9
public class BlogEntityTypeConfiguration : IEntityTypeConfiguration<Blog>
{
public void Configure(EntityTypeBuilder<Blog> builder)
{
builder
.Property(b => b.Url)
.IsRequired();
}
}

然后,只需从 OnModelCreating 调用 Configure 方法。

1
new BlogEntityTypeConfiguration().Configure(modelBuilder.Entity<Blog>());

可以在给定程序集中应用实现 IEntityTypeConfiguration的类型中指定的所有配置。

1
modelBuilder.ApplyConfigurationsFromAssembly(typeof(BlogEntityTypeConfiguration).Assembly);

使用数据注释来配置模型

也可将特性(称为数据注释)应用于类和属性。 数据注释会替代约定,但会被 Fluent API 配置替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
}

[Table("Blogs")]
public class Blog
{
public int BlogId { get; set; }

[Required]
public string Url { get; set; }
}

实体类型

在上下文中包含一种类型的 DbSet 意味着它包含在 EF Core 的模型中;我们通常将此类类型称为实体。

在模型中包含类型

按照约定,上下文的DbSet属性中公开的类型作为实体包含在模型中。 还包括在 OnModelCreating方法中指定的实体类型,以及通过递归探索其他发现的实体类型的导航属性找到的任何类型。

*** 下面的代码示例中包含了所有类型:***

包含Blog,因为它在上下文的DbSet属性中公开。
包含Post,因为它是通过Blog.Posts 导航属性发现的。
包含AuditEntry因为它是 OnModelCreating中指定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AuditEntry>();
}
}

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

public Blog Blog { get; set; }
}

public class AuditEntry
{
public int AuditEntryId { get; set; }
public string Username { get; set; }
public string Action { get; set; }
}

从模型中排除类型

如果不希望在模型中包含某一类型,则可以排除它:

1
2
3
4
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Ignore<BlogMetadata>();
}

从迁移中排除

有时,将相同的实体类型映射到多个DbContext类型中非常有用。 在使用绑定上下文时尤其如此,对于每段绑定上下文,使用不同DbContext类型的情况很常见

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdentityUser>()
.ToTable("AspNetUsers", t => t.ExcludeFromMigrations());
}

此配置迁移不会创建AspNetUsers该表,但IdentityUser 仍包含在模型中,并且可正常使用。

如果需要再次使用迁移来管理表,则应创建不包括 AspNetUsers的新迁移。 下一次迁移将包含对表所做的任何更改。

表名称

每个实体类型都将设置为映射到与公开实体的 DbSet 属性名称相同的数据库表。 如果给定实体不存在 DbSet,则使用类名称,可以手动配置表名

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.ToTable("blogs");
}

表架构

使用关系数据库时,表按约定在数据库的默认架构中创建,你可以配置要在特定架构中创建的表,如下所示

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.ToTable("blogs", schema: "blogging");
}

还可以在模型级别使用 Fluent API 定义默认架构,而不是为每个表指定架构:

1
2
3
4
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("blogging");
}

设置默认架构也会影响其他数据库对象,例如序列。

视图映射

可以使用 Fluent API 将实体类型映射到数据库视图,EF 假定数据库中已存在引用的视图,它不会在迁移中自动创建它。

1
2
modelBuilder.Entity<Blog>()
.ToView("blogsView", schema: "blogging");

映射到视图将删除默认表映射,但从 EF 5.0 开始,实体类型也可以显式映射到表。 在这种情况下,查询映射将用于查询,表映射将用于更新。

表值函数映射

若要将实体映射到表值函数,函数必须是无参数的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BlogWithMultiplePosts
{
public string Url { get; set; }
public int PostCount { get; set; }
}

CREATE FUNCTION dbo.BlogsWithMultiplePosts()
RETURNS TABLE
AS
RETURN
(
SELECT b.Url, COUNT(p.BlogId) AS PostCount
FROM Blogs AS b
JOIN Posts AS p ON b.BlogId = p.BlogId
GROUP BY b.BlogId, b.Url
HAVING COUNT(p.BlogId) > 1
)

modelBuilder.Entity<BlogWithMultiplePosts>().HasNoKey().ToFunction("BlogsWithMultiplePosts");

通常情况下,实体属性将映射到 TVF 返回的匹配列。 如果 TVF 返回的列名称与实体属性的名称不同,则可以使用 HasColumnName 方法配置实体的列,就像映射到常规表一样。

表注释

可以对数据库表设置任意文本注释,从而在数据库中记录架构

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().ToTable(
tableBuilder => tableBuilder.HasComment("Blogs managed on the website"));
}

共享类型实体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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");
});
}
}

实体属性

模型中的每个实体类型都有一组属性,EF Core 将从数据库中读取和写入这些属性。 如果使用的是关系数据库,实体属性将映射到表列。

已包含和已排除的属性

按照约定,所有具有 Getter 和 Setter 的公共属性都将包含在模型中。

可以按如下所示排除特定属性:

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Ignore(b => b.LoadedFromDatabase);
}

列名

按照约定,使用关系数据库时,实体属性将映射到与属性同名的表列。

如果希望配置具有不同名称的列,可以按以下代码片段进行操作:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.BlogId)
.HasColumnName("blog_id");
}

列数据类型

还可以配置列以指定列的确切数据类型

1
2
3
4
5
6
7
8
9
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>(
eb =>
{
eb.Property(b => b.Url).HasColumnType("varchar(200)");
eb.Property(b => b.Rating).HasColumnType("decimal(5, 2)");
});
}

最大长度

在向提供程序传递数据之前,实体框架不会执行任何最大长度的验证。 而是由提供程序或数据存储根据情况进行验证。

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.HasMaxLength(500);
}

精度和小数位数

哪些数据类型支持精度和小数位数取决于数据库,但在大多数数据库中,decimal 和 DateTime 类型支持这些 Facet

1
2
3
4
5
6
7
8
9
10
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Score)
.HasPrecision(14, 2);

modelBuilder.Entity<Blog>()
.Property(b => b.LastUpdated)
.HasPrecision(3);
}

如果不先定义精度,则永远不会定义小数位数,因此用于定义小数位数的 Fluent API 为 HasPrecision(precision, scale)。

Unicode

在某些关系数据库中,存在不同的类型来表示 Unicode 和非 Unicode 文本数据。 例如,在 SQL Server 中,nvarchar(x)用于表示 UTF-16 中的 Unicode 数据,而varchar(x)用于表示非 Unicode 数据
默认情况下,文本属性配置为 Unicode。 可以将列配置为非 Unicode,如下所示:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.Property(b => b.Isbn)
.IsUnicode(false);
}

必需和可选属性

如果属性包含null是有效的,则该属性被视为可选属性。 如果null不是要分配给属性的有效值,则它被视为必需属性。 映射到关系数据库架构时,必需属性创建为不可为null的列,可选属性创建为可为null的列。

约定

按照约定,其.NET类型可包含null的属性将配置为可选属性,而.NET类型不能包含null的属性将配置为必需属性。 例如,所有具有.NET值类型(intdecimal、bool等)的属性都配置为必需,并且具有可为null.NET值类型的所有属性(int?decimal?、bool?等)配置为可选。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; } // Required by convention
public string LastName { get; set; } // Required by convention
public string? MiddleName { get; set; } // Optional by convention

// Note the following use of constructor binding, which avoids compiled warnings
// for uninitialized non-nullable properties.
public Customer(string firstName, string lastName, string? middleName = null)
{
FirstName = firstName;
LastName = lastName;
MiddleName = middleName;
}
}

显式配置

按约定为可选属性的属性可以配置为必需属性,如下所示:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}

列排序规则

可以定义文本列的排序规则,以确定如何比较和排序

1
2
modelBuilder.Entity<Customer>().Property(c => c.Name)
.UseCollation("SQL_Latin1_General_CP1_CI_AS");

如果数据库中的所有列都需要使用特定的排序规则,请改为在数据库级别定义排序规则。

列注释

可以对数据库列设置任意文本注释,从而在数据库中记录架构:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.HasComment("The URL of the blog");
}

列顺序

默认情况下,在使用迁移创建表时,EF Core 首先为主键列排序,然后为实体类型和从属类型的属性排序,最后为基类型中的属性排序。 但是,你可以指定不同的列顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>(x =>
{
x.Property(b => b.Id)
.HasColumnOrder(0);

x.Property(b => b.FirstName)
.HasColumnOrder(1);

x.Property(b => b.LastName)
.HasColumnOrder(2);
});
}

请注意,在一般情况下,大多数数据库仅支持在创建表时对列进行排序。 这意味着不能使用列顺序特性对现有表中的列进行重新排序

键用作每个实体实例的唯一标识符

配置主键

根据约定,名为 Id 或 Id 的属性将被配置为实体的主键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
internal class Car
{
public string Id { get; set; }

public string Make { get; set; }
public string Model { get; set; }
}

internal class Truck
{
public string TruckId { get; set; }

public string Make { get; set; }
public string Model { get; set; }
}

可将单个属性配置为实体的主键,如下所示:

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasKey(c => c.LicensePlate);
}

还可将多个属性配置为实体的键 - 这称为组合键。 约定仅在特定情况下设置组合键 - 例如,对于拥有的类型集合。

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasKey(c => new { c.State, c.LicensePlate });
}

创造价值

对于非复合数字和GUID主键,EF Core根据约定设置值生成

主键名称

根据约定,在关系数据库上,主键使用名称PK_<type name> 进行创建。 可按如下方式配置主键约束的名称:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasKey(b => b.BlogId)
.HasName("PrimaryKey_BlogId");
}

键类型和值

向上下文添加新实体时,键属性必须始终具有非默认值,但某些类型将由数据库生成。 在这种情况下,当添加实体以用于跟踪时,EF 将尝试生成一个临时值。 调用 SaveChanges 后,临时值将替换为数据库生成的值。

如果键属性的值由数据库生成,并且在添加实体时指定了非默认值,则 EF 将假定该实体已存在于数据库中,并尝试更新它,而不是插入新的实体。

备用键

除主键外,备选键还充当每个实体实例的备用唯一标识符;它可以用作关系的目标
如果只想对列强制执行唯一性,请定义唯一索引而不是备选键

备选建通常根据需要引入,无需手动配置。 根据约定,当你将不是主键的属性标识为关系的目标时,会引入备选键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogUrl)
.HasPrincipalKey(b => b.Url);
}
}

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

public string BlogUrl { get; set; }
public Blog Blog { get; set; }
}

还可将单个属性配置为备选键:

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasAlternateKey(c => c.LicensePlate);
}

还可将多个属性配置为备选键(即复合备选键):

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasAlternateKey(c => new { c.State, c.LicensePlate });
}

可配置备选键的索引和唯一约束的名称:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasAlternateKey(c => c.LicensePlate)
.HasName("AlternateKey_LicensePlate");
}

生成的值

数据库列的值可以通过多种方式生成:主键列通常是自动递增的整数,其他列具有默认值或计算值等。

默认值

在关系数据库中,可以为列配置默认值;如果插入的行没有该列的值,则将使用默认值。

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Rating)
.HasDefaultValue(3);
}

还可以指定用于计算默认值的 SQL 片段:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Created)
.HasDefaultValueSql("getdate()");
}

计算列

1
2
3
modelBuilder.Entity<Person>()
.Property(p => p.DisplayName)
.HasComputedColumnSql("[LastName] + ', ' + [FirstName]");

以上命令将创建一个虚拟计算列,每次从数据库中提取时都会计算其值。 你也可以将计算列指定为存储(有时称为持久化)计算列,这意味着系统会在每次更新行时计算该列,并将其与常规列一起存储在磁盘上:

1
2
3
modelBuilder.Entity<Person>()
.Property(p => p.NameLength)
.HasComputedColumnSql("LEN([LastName]) + LEN([FirstName])", stored: true);

主键

显式配置值生成

EF Core 会自动为主键设置值生成 - 但我们可能希望对非键属性执行相同的操作。 你可以将任何属性配置为针对插入的实体生成其值,如下所示:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Inserted)
.ValueGeneratedOnAdd();
}

同样,可以将属性配置为在添加或更新时生成其值:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.LastUpdated)
.ValueGeneratedOnAddOrUpdate();
}

与默认值或计算列不同,我们没有指定值的生成方式;这取决于所使用的数据库提供程序。 数据库提供程序可能会自动为某些属性类型设置值生成,但其他属性类型可能需要你手动设置值的生成方式。

日期/时间值生成

EF Core 提供程序通常不会为日期/时间列自动设置值生成 - 你必须自行配置。

创建时间戳

若要将日期/时间列配置为包含行的创建时间戳,通常需要使用适当的 SQL 函数来配置默认值。

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Created)
.HasDefaultValueSql("getdate()");
}

更新时间戳

尽管存储计算列看起来非常适合管理上次更新时间戳,但数据库通常不允许在计算列中指定诸如 GETDATE() 之类的函数。 作为替代方法,你可以设置一个数据库触发器来达到同样的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE TRIGGER [dbo].[Blogs_UPDATE] ON [dbo].[Blogs]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;

IF ((SELECT TRIGGER_NESTLEVEL()) > 1) RETURN;

DECLARE @Id INT

SELECT @Id = INSERTED.BlogId
FROM INSERTED

UPDATE dbo.Blogs
SET LastUpdated = GETDATE()
WHERE BlogId = @Id
END

替代值生成

尽管为属性配置了值生成,但在许多情况下,你仍然可以为其显式指定一个值。 此操作能否真正起作用取决于已配置的特定值生成机制;虽然你可以指定显式值而不是使用列的默认值,但不能对计算列执行相同的操作。

若要使用显式值替代值生成,只需将属性设置为该属性类型的 CLR 默认值(string 为 null,int 为 0,Guid 为 Guid.Empty,等等)以外的任意值。

若要为已配置为在添加或更新时生成值的属性提供显式值,还必须按以下方式配置该属性:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().Property(b => b.LastUpdated)
.ValueGeneratedOnAddOrUpdate()
.Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Save);
}

无值生成

除了上述特定方案外,属性通常不会配置值生成;这意味着,始终由应用程序提供要保存到数据库的值。 必须先将此值分配给新实体,然后才能将新实体添加到上下文中。

但是,在某些情况下,你可能希望禁用按约定设置的值生成。

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.BlogId)
.ValueGeneratedNever();
}

阴影和索引器属性

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

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

外键阴影属性

阴影属性通常用于外键属性,其中两个实体之间的关系由数据库中的外键值表示,但这种关系是通过实体类型之间的导航属性来管理的。 根据约定,当发现关系,但在依赖实体类中找不到外键属性时,EF 将引入阴影属性。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 的字符串重载后,可以链接针对其他属性的任何配置调用。 在下面的示例中,由于 Blog 没有名为 LastUpdated 的 CLR 属性,因此将创建阴影属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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; }
}

访问阴影属性

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

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

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

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

配置索引器属性

在下面的示例中,Blog 定义了一个索引器,该索引器将用于创建索引器属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}
}

属性包实体类型

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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");
});
}
}

关系

关系定义两个实体之间的相关性。 在关系数据库中,关系由外键约束表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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; }

public int BlogId { get; set; }
public Blog Blog { get; set; }
}

Post 是依赖实体

Blog 是主体实体

Blog.BlogId 是主体键(在此示例中,它是主键,而不是备选键)

Post.BlogId 是外键

Post.Blog 是引用导航属性

Blog.Posts 是集合导航属性

Post.BlogBlog.Posts 的反向导航属性(反之亦然)

约定

按约定发现的关系将始终以主体实体的主键为目标。 若要以备选键为目标,必须使用 Fluent API 执行配置。

完全定义的关系

无外键属性

虽然建议在依赖实体类中定义外键属性,但这不是必需的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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; }

public Blog Blog { get; set; }
}

此示例中,影子外键为 BlogId,因为预先输入导航名称是多余的。

如果已存在同名的属性,则影子属性名称将带有数字后缀(如果属性是主键或属性的类型与主体键不兼容,则不会将其配置为外键。)

单个导航属性

仅包含一个导航属性(没有反向导航,也没有外键属性)就足以按约定定义关系。 还可包含一个导航属性和一个外键属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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; }
}

限制

如果在两个类型之间定义了多个导航属性(即不止一对指向彼此的导航),则由导航属性表示的关系是不明确的。 需要对它们进行手动配置才能解决这种不明确的关系。

级联删除

根据约定,对于必需关系,级联删除将设置为Cascade,对于可选关系,它将设置为ClientSetNullCascade表示也会删除依赖实体。ClientSetNull表示未加载到内存中的依赖实体将保持不变,必须手动删除或更新以指向有效的主体实体。 对于加载到内存中的实体,EF Core 将尝试将外键属性设置为 null

手动配置

若要在Fluent API中配置关系,首先应标识构成关系的导航属性。HasOneHasMany标识要开始配置的实体类型的导航属性。然后,将调用链接到WithOneWithMany以标识反向导航。HasOne/WithOne用于引用导航属性,HasMany/WithMany用于集合导航属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts);
}
}

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

public Blog Blog { get; set; }
}

单个导航属性

如果只有一个导航属性,则WithOneWithMany会发生无参数重载。 这表示在关系的另一端,存在概念上的引用或集合,但实体类中不包含导航属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne();
}
}

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

配置导航属性

创建导航属性后,可能需要进一步对其进行配置。

1
2
3
4
5
6
7
8
9
10
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne();

modelBuilder.Entity<Blog>()
.Navigation(b => b.Posts)
.UsePropertyAccessMode(PropertyAccessMode.Property);
}

外键

可使用 Fluent API 来配置哪个属性应用作给定关系的外键属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogForeignKey);
}
}

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

public int BlogForeignKey { get; set; }
public Blog Blog { get; set; }
}

复合主键

internal class MyContext : DbContext
{
public DbSet<Car> Cars { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasKey(c => new { c.State, c.LicensePlate });

modelBuilder.Entity<RecordOfSale>()
.HasOne(s => s.Car)
.WithMany(c => c.SaleHistory)
.HasForeignKey(s => new { s.CarState, s.CarLicensePlate });
}
}

public class Car
{
public string State { get; set; }
public string LicensePlate { get; set; }
public string Make { get; set; }
public string Model { get; set; }

public List<RecordOfSale> SaleHistory { get; set; }
}

public class RecordOfSale
{
public int RecordOfSaleId { get; set; }
public DateTime DateSold { get; set; }
public decimal Price { get; set; }

public string CarState { get; set; }
public string CarLicensePlate { get; set; }
public Car Car { get; set; }
}

影子外键

可以使用 的字符串重载 HasForeignKey(…) 将阴影属性配置为外键,建议在将影子属性用作外键之前将其显式添加到模型中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Add the shadow property to the model
modelBuilder.Entity<Post>()
.Property<int>("BlogForeignKey");

// Use the shadow property as a foreign key
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey("BlogForeignKey");
}
}

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

public Blog Blog { get; set; }
}

外键约束名称

根据约定,当以关系数据库作为目标时,外键约束将命名为 FK__<依赖类型名称><主体类型名称><外键属性名称>。 对于复合外键,<外键属性名称> 将成为外键属性名称的下划线分隔列表。

1
2
3
4
5
6
7
8
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogId)
.HasConstraintName("ForeignKey_Post_Blog");
}

没有导航属性

无需提供导航属性。 只需在关系的一侧提供外键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne<Blog>()
.WithMany()
.HasForeignKey(p => p.BlogId);
}
}

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

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

public int BlogId { get; set; }
}

主体键

如果想要外键引用主键外的属性,可使用 Fluent API 为关系配置主体键属性。 配置为主体键的属性将自动设置为备选键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
internal class MyContext : DbContext
{
public DbSet<Car> Cars { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<RecordOfSale>()
.HasOne(s => s.Car)
.WithMany(c => c.SaleHistory)
.HasForeignKey(s => new { s.CarState, s.CarLicensePlate })
.HasPrincipalKey(c => new { c.State, c.LicensePlate });
}
}

public class Car
{
public int CarId { get; set; }
public string State { get; set; }
public string LicensePlate { get; set; }
public string Make { get; set; }
public string Model { get; set; }

public List<RecordOfSale> SaleHistory { get; set; }
}

public class RecordOfSale
{
public int RecordOfSaleId { get; set; }
public DateTime DateSold { get; set; }
public decimal Price { get; set; }

public string CarState { get; set; }
public string CarLicensePlate { get; set; }
public Car Car { get; set; }
}

必需关系和可选关系

如果实体类中具有外键属性,则关系的必需性取决于外键属性是必需还是可选,外键属性位于依赖实体类型上,因此如果按要求配置它们,则意味着每个依赖实体都需要具有相应的主体实体。

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.IsRequired();
}

级联删除

可使用 Fluent API 显式配置给定关系的级联删除行为。

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.OnDelete(DeleteBehavior.Cascade);
}

其他关系模式

一对一

一对一关系在两侧都有引用导航属性。 它们遵循与一对多关系相同的约定,但在外键属性上引入了一个唯一索引,以确保只有一个依赖项与每个主体相关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }

public BlogImage BlogImage { get; set; }
}

public class BlogImage
{
public int BlogImageId { get; set; }
public byte[] Image { get; set; }
public string Caption { get; set; }

public int BlogId { get; set; }
public Blog Blog { get; set; }
}

在配置与 Fluent API 的关系时,可使用 HasOne 和 WithOne 方法。

显而易见,在一对多关系中,具有引用导航的实体是依赖项,而具有集合的实体是主体。 但在一对一的关系中并非如此,因此需要对其进行显式定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<BlogImage> BlogImages { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasOne(b => b.BlogImage)
.WithOne(i => i.Blog)
.HasForeignKey<BlogImage>(b => b.BlogForeignKey);
}
}

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

public BlogImage BlogImage { get; set; }
}

public class BlogImage
{
public int BlogImageId { get; set; }
public byte[] Image { get; set; }
public string Caption { get; set; }

public int BlogForeignKey { get; set; }
public Blog Blog { get; set; }
}

默认情况下依赖端为可选,但可视需要进行配置。 但是,EF 不会验证是否提供了依赖实体,因此此配置仅在数据库映射允许强制执行时才会产生影响。 此种情况的一个常见场景是默认使用表拆分的引用从属类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
modelBuilder.Entity<Order>(
ob =>
{
ob.OwnsOne(
o => o.ShippingAddress,
sa =>
{
sa.Property(p => p.Street).IsRequired();
sa.Property(p => p.City).IsRequired();
});

ob.Navigation(o => o.ShippingAddress)
.IsRequired();
});

通过此配置,与 ShippingAddress 对应的列将在数据库中标记为不可为 null。

多对多

多对多关系需要两端的集合导航属性。 与其他类型的关系一样,它们也可通过约定发现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }

public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
public string TagId { get; set; }

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

在数据库中实现此关系的方式是使用联接表,其中包含 Post 和 Tag 的外键。 例如,以下就是 EF 将在关系数据库中为上述模型创建的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE TABLE [Posts] (
[PostId] int NOT NULL IDENTITY,
[Title] nvarchar(max) NULL,
[Content] nvarchar(max) NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([PostId])
);

CREATE TABLE [Tags] (
[TagId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_Tags] PRIMARY KEY ([TagId])
);

CREATE TABLE [PostTag] (
[PostsId] int NOT NULL,
[TagsId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
CONSTRAINT [FK_PostTag_Posts_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([PostId]) ON DELETE CASCADE,
CONSTRAINT [FK_PostTag_Tags_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([TagId]) ON DELETE CASCADE
);

在内部,EF 创建一个实体类型Dictionary<string, object> 来表示将称为联接实体类型的联接表。 Dictionary<string, object> 当前用于处理外键属性的任意组合,有关详细信息,请参阅属性包实体类型。 模型中可以存在多个多对多关系,因此必须为联接实体类型指定唯一的名称,在本例中为 PostTag。 允许此操作的功能称为共享类型实体类型。
按照约定,用于联接实体类型的 CLR 类型可能会在未来版本中更改以提高性能。 除非已显式配置,否则不要依赖于联接类型 Dictionary<string, object>,如下一节所述。

多对多导航称为跳过导航,因为它们有效地跳过联接实体类型。 如果使用的是批量配置,则可以从 GetSkipNavigations获取所有跳过导航。

1
2
3
4
5
6
7
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var skipNavigation in entityType.GetSkipNavigations())
{
Console.WriteLine(entityType.DisplayName() + "." + skipNavigation.Name);
}
}

联接实体类型配置

将配置应用于联接实体类型是很常见的。 此操作可通过 UsingEntity 完成。

1
2
3
4
5
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity(j => j.ToTable("PostTags"));

其他数据可存储在联接实体类型中,但为此最好创建一个定制的 CLR 类型。 使用自定义联接实体类型配置关系时,需要显式指定这两个外键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
internal class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}

public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId),
j => j
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId),
j =>
{
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}
}

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

public ICollection<Tag> Tags { get; set; }
public List<PostTag> PostTags { get; set; }
}

public class Tag
{
public string TagId { get; set; }

public ICollection<Post> Posts { get; set; }
public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
public DateTime PublicationDate { get; set; }

public int PostId { get; set; }
public Post Post { get; set; }

public string TagId { get; set; }
public Tag Tag { get; set; }
}

联接关系配置

EF 对联接实体类型使用两个一对多关系来表示多对多关系。 可在 UsingEntity 参数中配置这些关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, object>>(
"PostTag",
j => j
.HasOne<Tag>()
.WithMany()
.HasForeignKey("TagsId")
.HasConstraintName("FK_PostTag_Tags_TagId")
.OnDelete(DeleteBehavior.Cascade),
j => j
.HasOne<Post>()
.WithMany()
.HasForeignKey("PostsId")
.HasConstraintName("FK_PostTag_Posts_PostId")
.OnDelete(DeleteBehavior.ClientCascade));

间接多对多关系

还可表示多对多关系,只需添加联接实体类型并映射两个单独的一对多关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}

public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PostTag>()
.HasKey(t => new { t.PostId, t.TagId });

modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId);

modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId);
}
}

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

public List<PostTag> PostTags { get; set; }
}

public class Tag
{
public string TagId { get; set; }

public List<PostTag> PostTags { get; set; }
}

public class PostTag
{
public DateTime PublicationDate { get; set; }

public int PostId { get; set; }
public Post Post { get; set; }

public string TagId { get; set; }
public Tag Tag { get; set; }
}

索引

索引是许多数据存储中的常见概念。 尽管它们在数据存储中的实现可能会有所不同,但它们可用于使基于列(或一组列)的查找更加高效。
可对列指定索引,如下所示:

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url);
}

根据约定,会在用作外键的每个属性(或属性集)中创建索引

复合索引

索引还可以跨多个列:

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>()
.HasIndex(p => new { p.FirstName, p.LastName });
}

索引唯一性

默认情况下,索引不唯一:对于索引的列集,允许多行具有相同的值。
可使索引唯一,如下所示:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url)
.IsUnique();
}

尝试为索引的列集插入多个具有相同值的实体将导致引发异常。

索引排序顺序

在大多数数据库中,索引涵盖的每个列可以是升序或降序。 对于仅包含一列的索引,这通常并不重要:数据库可以根据需要以相反的顺序遍历索引。 但是,对于复合索引,排序对于良好的性能至关重要,并且可以表示查询是否使用索引之间的差异。 通常,索引列的排序顺序应对应于查询的 ORDER BY 子句中指定的排序顺序。

索引排序顺序默认为升序。 可以按如下所示使所有列按降序排列:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => new { b.Url, b.Rating })
.IsDescending();
}

还可以按列指定排序顺序,如下所示:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => new { b.Url, b.Rating })
.IsDescending(false, true);
}

索引名称

根据约定,在关系数据库中创建的索引被命名为IX_<type name>_<property name>。 对于复合索引,<property name> 将成为以下划线分隔的属性名称列表。

可设置在数据库中创建的索引的名称:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url)
.HasDatabaseName("Index_Url");
}

索引筛选器

通过某些关系数据库,可指定筛选索引或部分索引。 这使你可以仅索引列值的子集,从而减少索引的大小并改善性能和磁盘空间的使用情况。
可使用 Fluent API 在索引上指定筛选器(以 SQL 表达式的形式提供):

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url)
.HasFilter("[Url] IS NOT NULL");
}

EF 为作为唯一索引一部分的所有可为 null 列添加 ‘IS NOT NULL’ 筛选器。 若要替代此约定,可提供一个 null 值。

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasIndex(b => b.Url)
.IsUnique()
.HasFilter(null);
}

包含列

通过某些关系数据库,可配置一组列,这些列包含在索引中,但不是其“键”的一部分。 当查询中的所有列都作为键列或非键列包含在索引中时,这可以显著提高查询性能,因为无需访问表本身。 在以下示例中,Url 列是索引键的一部分,因此对该列的任何查询筛选都可以使用索引。 但除此之外,仅访问 Title 和 PublishedOn 列的查询将不需要访问表,并且会更高效地运行:

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasIndex(p => p.Url)
.IncludeProperties(
p => new { p.Title, p.PublishedOn });
}

检查约束

检查约束是一项标准关系功能,让你可以定义一个条件,该条件必须适用于表中的所有行;任何违反约束的插入或修改数据的尝试都将失败。 检查约束类似于非 null 约束(禁止列中的空值)或唯一约束(禁止重复),但允许定义任意 SQL 表达式。

可使用 Fluent API 指定表的检查约束(以 SQL 表达式的形式提供):

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Product>()
.ToTable(b => b.HasCheckConstraint("CK_Prices", "[Price] > [DiscountedPrice]"));
}

继承

EF 可以将 .NET 类型层次结构映射到数据库。 这允许你像往常一样使用基类型和派生类型在代码中编写 .NET 实体,并让 EF 无缝创建适当的数据库架构、发出查询等。有关如何映射类型层次结构的实际细节取决于提供程序;本页介绍关系数据库上下文中的继

实体类型层次结构映射(TPH)

如果要映射层次结构中的 CLR 类型,就必须在模型上显式指定该类型。 例如,仅指定层次结构的基类型不会导致 EF Core 隐式包含其所有子类型。

以下示例将为 Blog 及其子类 RssBlog 公开 DbSet。 如果 Blog 有任何其他子类,它不会包含在模型中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<RssBlog> RssBlogs { get; set; }
}

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

public class RssBlog : Blog
{
public string RssUrl { get; set; }
}

使用 TPH 映射时,数据库列会根据需要自动设置为可为 null。 例如,RssUrl 列可为 null,因为常规 Blog 实例没有该属性。

如果不依赖约定,则可以使用 HasBaseType 显式指定基类型。 还可以使用 .HasBaseType((Type)null) 从层次结构中删除实体类型。

每个层次结构一张表和鉴别器配置(TPT)

TPH 使用单个表来存储层次结构中所有类型的数据,并使用鉴别器列来标识每行表示的类型。

上面的模型映射到以下数据库架构(注意隐式创建的 Discriminator 列,它标识了每行中存储的 Blog 类型)。

可以配置鉴别器列的名称和类型以及用于标识层次结构中每种类型的值:

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator<string>("blog_type")
.HasValue<Blog>("blog_base")
.HasValue<RssBlog>("blog_rss");
}

在上面的示例中,EF 在层次结构的基本实体上隐式添加了鉴别器作为影子属性。 可以像配置任何其他属性一样配置此属性:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property("Discriminator")
.HasMaxLength(200);
}

最后,鉴别器也可以映射到实体中的常规 .NET 属性:

1
2
3
4
5
6
7
8
9
10
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator(b => b.BlogType);

modelBuilder.Entity<Blog>()
.Property(e => e.BlogType)
.HasMaxLength(200)
.HasColumnName("blog_type");
}

查询使用 TPH 模式的派生实体时,EF Core 会在查询中添加一个基于鉴别器列的谓词。
此筛选器确保对于结果中没有的基类型或同级类型,我们不会获得任何附加行。
对于基本实体类型,将跳过此筛选器谓词,因为查询基本实体将获得层次结构中所有实体的结果。
在具体化查询结果时,如果遇到未映射到模型中任何实体类型的鉴别器值,我们将引发异常,因为我们不知道如何具体化结果。
仅当数据库包含的行具有鉴别器值并且这些值未映射到 EF 模型时,才会发生此错误。
如果你有这样的数据,可以将 EF Core 模型中的鉴别器映射标记为不完整,以指示我们应始终添加筛选器谓词来查询层次结构中的任意类型。 IsComplete(false) 在鉴别器配置上调用会将映射标记为不完整。

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator()
.IsComplete(false);
}

共享列

默认情况下,当层次结构中的两个同级实体类型具有同名的属性时,它们将映射到两个单独的列。 但是,如果它们的类型相同,则可以映射到相同的数据库列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MyContext : DbContext
{
public DbSet<BlogBase> Blogs { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.HasColumnName("Url");

modelBuilder.Entity<RssBlog>()
.Property(b => b.Url)
.HasColumnName("Url");
}
}

public abstract class BlogBase
{
public int BlogId { get; set; }
}

public class Blog : BlogBase
{
public string Url { get; set; }
}

public class RssBlog : BlogBase
{
public string Url { get; set; }
}

使用强制转换查询共享列时,关系数据库提供程序(例如 SQL Server)不会自动使用鉴别器谓词。 查询 Url = (blog as RssBlog).Url 还将返回同级 Blog 行的 Url 值。 若要将查询限制为 RssBlog 实体,你需要在鉴别器上手动添加筛选器,例如 Url = blog is RssBlog ? (blog as RssBlog).Url : null。

每个类型一张表配置

在 TPT 映射模式中,所有类型都分别映射到各自的表。 仅属于某个基类型或派生类型的属性存储在映射到该类型的一个表中。 映射到派生类型的表还存储将派生表与基表联接的外键。

1
2
modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");

ToTable可以针对每个根实体类型调用 ,而不是对每个实体类型调用 modelBuilder.Entity().UseTptMappingStrategy() ,并且表名称将由 EF 生成。

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE [Blogs] (
[BlogId] int NOT NULL IDENTITY,
[Url] nvarchar(max) NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
[BlogId] int NOT NULL,
[RssUrl] nvarchar(max) NULL,
CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId]),
CONSTRAINT [FK_RssBlogs_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([BlogId]) ON DELETE NO ACTION
);

在许多情况下,与 TPH 相比,TPT 性能较差。 有关详细信息,请参阅性能文档。

每个具体表类型(TPC)

1
2
3
4
modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
.ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
.ToTable("RssBlogs");

ToTable将按约定生成表名称,而不是在每个实体类型上调用 ,而只需在每个根实体类型上调用 modelBuilder.Entity().UseTpcMappingStrategy() 。

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE [Blogs] (
[BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
[Url] nvarchar(max) NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
[BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
[Url] nvarchar(max) NULL,
[RssUrl] nvarchar(max) NULL,
CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId])
);

TPC 数据库架构

TPC 策略类似于 TPT 策略,只是为层次结构中的每个 具体 类型创建不同的表,但 不会 为 抽象 类型创建表,因此名称为“table-per-concrete-type”。 与 TPT 一样,表本身指示保存的对象的类型。 但是,与 TPT 映射不同,每个表都包含具体类型及其基类型中每个属性的列。 TPC 数据库架构非规范化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public abstract class Animal
{
protected Animal(string name)
{
Name = name;
}

public int Id { get; set; }
public string Name { get; set; }
public abstract string Species { get; }

public Food? Food { get; set; }
}

public abstract class Pet : Animal
{
protected Pet(string name)
: base(name)
{
}

public string? Vet { get; set; }

public ICollection<Human> Humans { get; } = new List<Human>();
}

public class FarmAnimal : Animal
{
public FarmAnimal(string name, string species)
: base(name)
{
Species = species;
}

public override string Species { get; }

[Precision(18, 2)]
public decimal Value { get; set; }

public override string ToString()
=> $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Cat : Pet
{
public Cat(string name, string educationLevel)
: base(name)
{
EducationLevel = educationLevel;
}

public string EducationLevel { get; set; }
public override string Species => "Felis catus";

public override string ToString()
=> $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Dog : Pet
{
public Dog(string name, string favoriteToy)
: base(name)
{
FavoriteToy = favoriteToy;
}

public string FavoriteToy { get; set; }
public override string Species => "Canis familiaris";

public override string ToString()
=> $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Human : Animal
{
public Human(string name)
: base(name)
{
}

public override string Species => "Homo sapiens";

public Animal? FavoriteAnimal { get; set; }
public ICollection<Pet> Pets { get; } = new List<Pet>();

public override string ToString()
=> $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
$" eats {Food?.ToString() ?? "<Unknown>"}";
}

为此层次结构创建的表为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
CREATE TABLE [Cats] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[EducationLevel] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));

CREATE TABLE [Dogs] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[FavoriteToy] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));

CREATE TABLE [FarmAnimals] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Value] decimal(18,2) NOT NULL,
[Species] nvarchar(max) NOT NULL,
CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));

CREATE TABLE [Humans] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[FavoriteAnimalId] int NULL,
CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));

请注意:

没有 用于 Animal 或 Pet 类型的表,因为这些表位于 abstract 对象模型中。 请记住,C# 不允许抽象类型的实例,因此不存在将抽象类型实例保存到数据库的情况。

对于每个具体类型,将重复基类型中的属性映射。 例如,每个表都有一列 Name ,Cats 和 Dogs 都有一列 Vet 。

密钥生成

选择的继承映射策略会对生成和管理主键值的方式产生影响。 TPH 中的键很简单,因为每个实体实例都由单个表中的一行表示。 可以使用任何类型的键值生成,并且不需要其他约束。

对于 TPT 策略,表中始终有一行映射到层次结构的基类型。 可以在此行上使用任何类型的键生成,其他表的键使用外键约束链接到此表。

对于 TPC 来说,事情会稍微复杂一些。 首先,请务必了解 EF Core 要求层次结构中的所有实体都具有唯一的键值,即使实体具有不同的类型。 例如,使用我们的示例模型,狗不能具有与 Cat 相同的 Id 键值。 其次,与 TPT 不同,没有共同表可以充当键值所在的单个位置,并且可以生成键值。 这意味着无法使用简单 Identity 列。

对于支持序列的数据库,可以使用每个表的默认约束中引用的单个序列来生成键值。 这是上面所示的 TPC 表中使用的策略,其中每个表具有以下项:

1
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence 是 EF Core 创建的数据库序列。 将 EF Core 数据库提供程序用于SQL Server时,默认将此策略用于 TPC 层次结构。 支持序列的其他数据库的数据库提供程序应具有类似的默认值。 使用序列的其他密钥生成策略(如 Hi-Lo 模式)也可用于 TPC。

虽然标准标识列不适用于 TPC,但如果每个表都配置了适当的种子和增量,则可以使用标识列,以便为每个表生成的值永远不会发生冲突。 例如:

1
2
3
4
modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));

外键约束

TPC 映射策略创建非规范化的 SQL 架构 - 这是一些数据库纯粹主义者反对它的原因之一。 例如,请考虑外键列 FavoriteAnimalId。 此列中的值必须与某些动物的主键值匹配。 使用 TPH 或 TPT 时,可以使用简单的 FK 约束在数据库中强制实施此操作。

1
CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

但是,使用 TPC 时,任何给定动物的主键都存储在对应于该动物的具体类型的表中。 例如,猫的主键存储在 Cats.Id 列中,而狗的主键存储在 Dogs.Id 列中,依此而行。 这意味着无法为此关系创建 FK 约束。

实际上,只要应用程序不尝试插入无效数据,就不是问题。 例如,如果所有数据都由 EF Core 插入并使用导航来关联实体,则保证 FK 列将始终保持有效的 PK 值。

摘要和指南

总之,TPH 通常适用于大多数应用程序,并且对于各种方案都是很好的默认值,因此,如果不需要 TPC,请不要增加 TPC 的复杂性。 具体而言,如果代码将主要查询许多类型的实体(例如针对基类型编写查询),则倾向于使用 TPH 和 TPC。

话亦然,当代码主要查询单叶类型的实体,并且基准显示与 TPH 相比,TPC 也是一个很好的映射策略。

仅当受外部因素限制时,才使用 TPT。

序列

序列在数据库中生成唯一的顺序数值。 序列不与特定表关联,可以设置多个表以从同一序列提取值。

基本用法

可以在模型中设置序列,然后使用它为属性生成值:

1
2
3
4
5
6
7
8
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasSequence<int>("OrderNumbers");

modelBuilder.Entity<Order>()
.Property(o => o.OrderNo)
.HasDefaultValueSql("NEXT VALUE FOR OrderNumbers");
}

请注意,从序列生成值的特定SQL是特定于数据库的;上面的示例适用于 SQL Server 但在其他数据库上将失败。 有关详细信息,请查阅数据库的文档。

配置序列设置

还可以配置序列的其他方面,例如其架构、起始值、增量等:

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasSequence<int>("OrderNumbers", schema: "shared")
.StartsAt(1000)
.IncrementsBy(5);
}

实现领域驱动设计

写代码非常简单,但是写简单的代码却非常难

随着应用程序的变化,有时候,为了节省开发时间会违反一些本应遵守的规则,使得代码变得复杂且难以维护.短期来看确实节省了开发时间,但是后期可能需要花费更多的时间为之前的偷懒而买单.无法对原有的代码进行维护,导致大量的逻辑都需要进行重写.

什么是领域驱动设计?

领域驱动设计(DDD)是一种将实现与持续进化的模型连接在一起来满足复杂需求的软件开发方法.

DDD适用于复杂领域或较大规模的系统,而不是简单的CRUD程序.它着重于核心领域逻辑,而不是基础架构.这样有助于构建一个灵活,模块化,可维护的代码库.

核心构建组成
DDD的关注点在领域层和应用层上,而展现层和基础设施层则视为细节(这个词原文太抽象,自己体会吧),业务层不应依赖它们.

这并不意味着展现层和基础设施层不重要.它们非常重要,但UI框架 和 数据库提供程序 需要你自己定义规则和总结最佳实践.这些不在DDD的讨论范围中.

本节将介绍领域层和应用层的基本构建组件.

领域层构建组成

  • 实体(Entity): 实体是种领域对象,它有自己的属性(状态,数据)和执行业务逻辑的方法.实体由唯一标识符(Id)表示,不同ID的两个实体被视为不同的实体.
  • 值对象(Value Object): 值对象是另外一种类型的领域对象,使用值对象的属性来判断两个值对象是否相同,而非使用ID判断.如果两个值对象的属性值全部相同就被视为同一对象.值对象通常是不可变的,大多数情况下它比实体简单.
  • 聚合(Aggregate) 和 聚合根(Aggregate Root): 聚合是由聚合根包裹在一起的一组对象(实体和值对象).聚合根是一种具有特定职责的实体.
  • 仓储(Repository) (接口): 仓储是被领域层或应用层调用的数据库持久化接口.它隐藏了DBMS的复杂性,领域层中只定义仓储接口,而非实现.
  • 领域服务(Domain Service): 领域服务是一种无状态的服务,它依赖多个聚合(实体)或外部服务来实现该领域的核心业务逻辑.
    规约(Specification): 规约是一种强命名,可重用,可组合,可测试的实体过滤器.
  • 领域事件(Domain Event): 领域事件是当领域某个事件发生时,通知其它领域服务的方式,为了解耦领域服务间的依赖.

应用层构建组成

  • 应用服务(Application Service): 应用服务是为实现用例的无状态服务.展现层调用应用服务获取DTO.应用服务调用多个领域服务实现用例.用例通常被视为一个工作单元.
  • 数据传输对象(DTO): DTO是一个不含业务逻辑的简单对象,用于应用服务层与展现层间的数据传输.
  • 工作单元(UOW): 工作单元是事务的原子操作.UOW内所有操作,当成功时全部提交,失败时全部回滚.

领域驱动设计

领域驱动设计(DDD) 是一种通过将实现连接到持续进化的模型来满足复杂需求的软件开发方法. 领域驱动设计的前提是:

  • 把项目的主要重点放在核心领域和领域逻辑上
  • 把复杂的设计放在领域模型上
  • 发起技术专家和领域专家之间的创造性协作,以迭代方式完善解决特定领域问题的概念模型

分层

ABP框架遵循DDD原则和模式去实现分层应用程序模型,该模型由四个基本层组成:

  • 表示层: 为用户提供接口. 使用应用层实现与用户交互.
  • 应用层: 表示层与领域层的中介,编排业务对象执行特定的应用程序任务. 使用应用程序逻辑实现用例.
  • 领域层: 包含业务对象以及业务规则. 是应用程序的核心.
  • 基础设施层: 提供通用的技术功能,支持更高的层,主要使用第三方类库.

领域层

实体

实体通常映射到关系型数据库的表中.
实体都继承自Entity类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
public class Book : Entity<Guid>
{
public string Name { get; set; }
public float Price { get; set; }
protected Book()
{
}
public Book(Guid id)
: base(id)
{
}
}

如果你不想继承基类Entity<TKey>,也可以直接实现IEntity<TKey>接口

Entity<TKey>类只是用给定的主 键类型定义了一个Id属性,在上面的示例中是Guid类型.可以是其他类型如string, int, long或其他你需要的类型.

Guid主键的实体

  • 创建一个构造函数,获取ID作为参数传递给基类.如果没有为GUID Id赋值,ABP框架会在保存时设置它,但是在将实体保存到数据库之前最好在实体上有一个有效的Id.
  • 如果使用带参数的构造函数创建实体,那么还要创建一个 privateprotected 构造函数. 当数据库提供程序从数据库读取你的实体时(反序列化时)将使用它.
  • 不要使用 Guid.NewGuid() 来设置Id! 在创建实体的代码中使用IGuidGenerator服务 传递Id参数. IGuidGenerator经过优化可以产生连续的GUID.这对于关系数据库中的聚集索引非常重要.

具有复合键的实体
有些实体可能需要 复合键 .在这种情况下,可以从非泛型Entity类派生实体.如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserRole : Entity
{
public Guid UserId { get; set; }

public Guid RoleId { get; set; }

public DateTime CreationTime { get; set; }

public UserRole()
{

}

public override object[] GetKeys()
{
return new object[] { UserId, RoleId };
}
}

上面的例子中,复合键由UserId和RoleId组成.在关系数据库中,它是相关表的复合主键. 具有复合键的实体应当实现上面代码中所示的GetKeys()方法.

需要注意,复合主键实体不可以使用 IRepository<TEntity, TKey> 接口,因为它需要一个唯一的Id属性. 但你可以使用IRepository<TEntity>.

聚合根

AggregateRoot<TKey>类继承自Entity<TKey>类,所以默认有Id这个属性

值得注意的是 ABP 会默认为聚合根创建仓储,当然,ABP也可以为所有的实体创建仓储

聚合根例子
这是一个具有子实体集合的聚合根例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class Order : AggregateRoot<Guid>
{
public virtual string ReferenceNo { get; protected set; }

public virtual int TotalItemCount { get; protected set; }

public virtual DateTime CreationTime { get; protected set; }

public virtual List<OrderLine> OrderLines { get; protected set; }

protected Order()
{

}

public Order(Guid id, string referenceNo)
{
Check.NotNull(referenceNo, nameof(referenceNo));

Id = id;
ReferenceNo = referenceNo;

OrderLines = new List<OrderLine>();
}

public void AddProduct(Guid productId, int count)
{
if (count <= 0)
{
throw new ArgumentException(
"You can not add zero or negative count of products!",
nameof(count)
);
}

var existingLine = OrderLines.FirstOrDefault(ol => ol.ProductId == productId);

if (existingLine == null)
{
OrderLines.Add(new OrderLine(this.Id, productId, count));
}
else
{
existingLine.ChangeCount(existingLine.Count + count);
}

TotalItemCount += count;
}
}

public class OrderLine : Entity
{
public virtual Guid OrderId { get; protected set; }

public virtual Guid ProductId { get; protected set; }

public virtual int Count { get; protected set; }

protected OrderLine()
{

}

internal OrderLine(Guid orderId, Guid productId, int count)
{
OrderId = orderId;
ProductId = productId;
Count = count;
}

internal void ChangeCount(int newCount)
{
Count = newCount;
}

public override object[] GetKeys()
{
return new Object[] {OrderId, ProductId};
}
}

如果你不想你的聚合根继承AggregateRoot<TKey>类,你可以直接实现IAggregateRoot<TKey>接口

Order是一个具有Guid类型Id属性的 聚合根.它有一个OrderLine实体集合.OrderLine是一个具有组合键(OrderId和 ProductId)的实体.

虽然这个示例可能无法实现聚合根的所有最佳实践,但它仍然遵循良好的实践:

  • Order有一个公共的构造函数,它需要 minimal requirements 来构造一个”订单”实例.因此,在没有IdreferenceNo的时候是无法创建订单的.protected/private的构造函数只有从数据库读取对象时 反序列化 才需要.
  • OrderLine的构造函数是internal的,所以它只能由领域层来创建.在Order.AddProduct这个方法的内部被使用.
  • Order.AddProduct实现了业务规则将商品添加到订单中
    所有属性都有protected的set.这是为了防止实体在实体外部任意改变.因此,在没有向订单中添加新产品的情况下设置 TotalItemCount将是危险的.它的值由AddProduct方法维护.

带有组合键的聚合根
虽然这种聚合根并不常见(也不建议使用),但实际上可以按照与上面提到的跟实体相同的方式定义复合键.在这种情况下,要使用非泛型的AggregateRoot基类.

BasicAggregateRoot类
AggregateRoot 类实现了 IHasExtraProperties 和 IHasConcurrencyStamp 接口,这为派生类带来了两个属性. IHasExtraProperties 使实体可扩展(请参见下面的 额外的属性部分) 和 IHasConcurrencyStamp 添加了由ABP框架管理的 ConcurrencyStamp 属性实现乐观并发. 在大多数情况下,这些是聚合根需要的功能.

但是,如果你不需要这些功能,你的聚合根可以继承 BasicAggregateRoot<TKey>(或BasicAggregateRoot).

值对象

值对象类必须实现 GetAtomicValues()方法来返回原始值

仓储

仓储用于领域对象在数据库中的操作, 通常每个 聚合根 或不同的实体创建对应的仓储.

通用(泛型)仓储
ABP为每个聚合根或实体提供了 默认的通用(泛型)仓储 . 你可以在服务中注入 IRepository<TEntity, TKey> 使用标准的CRUD操作.

默认通用仓储用法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class PersonAppService : ApplicationService
{
private readonly IRepository<Person, Guid> _personRepository;

public PersonAppService(IRepository<Person, Guid> personRepository)
{
_personRepository = personRepository;
}

public async Task Create(CreatePersonDto input)
{
var person = new Person { Name = input.Name, Age = input.Age };

await _personRepository.InsertAsync(person);
}

public List<PersonDto> GetList(string nameFilter)
{
var people = _personRepository
.Where(p => p.Name.Contains(nameFilter))
.ToList();

return people
.Select(p => new PersonDto {Id = p.Id, Name = p.Name, Age = p.Age})
.ToList();
}
}

在这个例子中

  • PersonAppService 在它的构造函数中注入了 IRepository<Person, Guid> .
  • Create 方法使用了 InsertAsync 创建并保存新的实体.
  • GetList 方法使用标准LINQ Where 和 ToList 方法在数据源中过滤并获取People集合.

通用仓储提供了一些开箱即用的标准CRUD功能:

  • 提供 Insert 方法用于保存新实体.
  • 提供 Update 和 Delete 方法通过实体或实体id更新或删除实体.
  • 提供 Delete 方法使用条件表达式过滤删除多个实体.
  • 实现了 IQueryable<TEntity>, 所以你可以使用LINQ和扩展方法 FirstOrDefault, Where, OrderBy, ToList 等…
  • 所有方法都具有 sync(同步) 和 async(异步) 版本

只读仓储

对于想要使用只读仓储的开发者,我们提供了IReadOnlyRepository<TEntity, TKey> 与 IReadOnlyBasicRepository<Tentity, TKey>接口.

无主键的通用(泛型)仓储

如果你的实体没有id主键 (例如, 它可能具有复合主键) 那么你不能使用上面定义的 IRepository<TEntity, TKey>, 在这种情况下你可以仅使用实体(类型)注入 IRepository<TEntity>.

IRepository<TEntity> 有一些缺失的方法, 通常与实体的 Id 属性一起使用. 由于实体在这种情况下没有 Id 属性, 因此这些方法不可用. 比如 Get 方法通过id获取具有指定id的实体. 不过, 你仍然可以使用IQueryable<TEntity>的功能通过标准LINQ方法查询实体.

自定义仓储

对于大多数情况, 默认通用仓储就足够了. 但是, 你可能会需要为实体创建自定义仓储类.

自定义仓储接口,首先在领域层定义一个仓储接口:

1
2
3
4
public interface IPersonRepository : IRepository<Person, Guid>
{
Task<Person> FindByNameAsync(string name);
}

此接口扩展了 IRepository<Person, Guid> 以使用已有的通用仓储功能.

自定义存储库依赖于你使用的数据访问工具. 在此示例中, 我们将使用Entity Framework Core:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PersonRepository : EfCoreRepository<MyDbContext, Person, Guid>, IPersonRepository
{
public PersonRepository(IDbContextProvider<TestAppDbContext> dbContextProvider)
: base(dbContextProvider)
{

}

public async Task<Person> FindByNameAsync(string name)
{
var dbContext = await GetDbContextAsync();
return await dbContext.Set<Person>()
.Where(p => p.Name == name)
.FirstOrDefaultAsync();
}
}

你可以直接使用数据库访问提供程序 (本例中是 DbContext ) 来执行操作.

IAsyncQueryableExecuter
IAsyncQueryableExecuter 是一个用于异步执行 IQueryable<T>对象的服务,不依赖于实际的数据库提供程序.

示例:
注入并使用IAsyncQueryableExecuter.ToListAsync()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Linq;

namespace AbpDemo
{
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IRepository<Product, Guid> _productRepository;
private readonly IAsyncQueryableExecuter _asyncExecuter;

public ProductAppService(
IRepository<Product, Guid> productRepository,
IAsyncQueryableExecuter asyncExecuter)
{
_productRepository = productRepository;
_asyncExecuter = asyncExecuter;
}

public async Task<ListResultDto<ProductDto>> GetListAsync(string name)
{
//Create the query
var query = _productRepository
.Where(p => p.Name.Contains(name))
.OrderBy(p => p.Name);

//Run the query asynchronously
List<Product> products = await _asyncExecuter.ToListAsync(query);

//...
}
}
}

ABP框架使用实际数据库提供程序的API异步执行查询.虽然这不是执行查询的常见方式,但它是使用异步API而不依赖于数据库提供者的最佳方式.

领域服务

领域驱动设计(DDD) 解决方案中,核心业务逻辑通常在聚合(实体)和领域服务中实现.

在以下情况下特别需要创建领域服务

  • 你实现了依赖于某些服务(如存储库或其他外部服务)的核心域逻辑.
  • 你需要实现的逻辑与多个聚合/实体相关,因此它不适合任何聚合.

领域服务是简单的无状态类. 虽然你不必从任何服务或接口派生,但ABP框架提供了一些有用的基类和约定.

DomainServiceIDomainService
DomainService基类派生领域服务或直接实现 IDomainService接口.

示例:
创建从DomainService基类派生的领域服务.

1
2
3
4
5
6
7
8
using Volo.Abp.Domain.Services;
namespace MyProject.Issues
{
public class IssueManager : DomainService
{

}
}

当你这样做时:ABP框架自动将类注册为瞬态生命周期到依赖注入系统.
你可以直接使用一些常用服务作为基础属性,而无需手动注入 (例如ILogger and IGuidGenerator).

建议使用ManagerService 后缀命名领域服务. 我们通常使用如上面示例中的 Manager 后缀.

示例:
实现将问题分配给用户的领域逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class IssueManager : DomainService
{
private readonly IRepository<Issue, Guid> _issueRepository;
public IssueManager(IRepository<Issue, Guid> issueRepository)
{
_issueRepository = issueRepository;
}

public async Task AssignAsync(Issue issue, AppUser user)
{
var currentIssueCount = await _issueRepository
.CountAsync(i => i.AssignedUserId == user.Id);

//Implementing a core business validation
if (currentIssueCount >= 3)
{
throw new IssueAssignmentException(user.UserName);
}
issue.AssignedUserId = user.Id;
}
}

问题是定义如下所示的 聚合根:

1
2
3
4
5
6
public class Issue : AggregateRoot<Guid>
{
public Guid? AssignedUserId { get; internal set; }

//...
}

使用internalset确保外层调用者不能直接在调用 set,并强制始终使用IssueManagerUser分配 Issue.

应用程序服务与领域服务
虽然应用服务领域服务都实现了业务规则,但存在根本的逻辑和形式差异:

  • 应用程序服务实现应用程序的用例(典型Web应用程序中的用户交互),而领域服务实现核心的、用例独立的领域逻辑.
  • 应用程序服务获取/返回 数据传输对象,领域服务方法通常获取和返回领域对象(实体,值对象).
  • 领域服务通常由应用程序服务或其他领域服务使用,而应用程序服务由表示层或客户端应用程序使用.

生命周期
领域服务的生命周期是瞬态的,它们会自动注册到依赖注入服务.

规约

规约模式用于为实体和其他业务对象定义 命名、可复用、可组合和可测试的过滤器 .
你可以创建一个由Specification<Customer>派生的新规约类.

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
using System.Linq.Expressions;
using Volo.Abp.Specifications;
namespace MyProject
{
public class Age18PlusCustomerSpecification : Specification<Customer>
{
public override Expression<Func<Customer, bool>> ToExpression()
{
return c => c.Age >= 18;
}
}
}

你也可以直接实现ISpecification<T>接口,但是基类Specification<T>做了大量简化.

虽然规约模式通常与C#lambda表达式相比较,算是一种更老的方式.一些开发人员可能认为不再需要它,我们可以直接将表达式传入到仓储或领域服务中,如下所示:

1
var count = await _customerRepository.CountAsync(c => c.Balance > 100000 && c.Age => 18);

自从ABP的仓储支持表达式,这是一个完全有效的用法.你不必在应用程序中定义或使用任何规约,可以直接使用表达式.

所以,规约的意义是什么?为什么或者应该在什么时候考虑去使用它?

何时使用?
使用规约的一些好处:

  • 可复用:假设你在代码库的许多地方都需要用到优质顾客过滤器.如果使用表达式而不创建规约,那么如果以后更改“优质顾客”的定义会发生什么?假设你想将最低余额从100000美元更改为250000美元,并添加另一个条件,成为顾客超过3年.如果使用了规约,只需修改一个类.如果在任何其他地方重复(复制/粘贴)相同的表达式,则需要更改所有的表达式.
  • 可组合:可以组合多个规约来创建新规约.这是另一种可复用性.
    命名:PremiumCustomerSpecification更好地解释了为什么使用规约,而不是复杂的表达式.因此,如果在你的业务中使用了一个有意义的表达式,请考虑使用规约.
  • 可测试:规约是一个单独(且易于)测试的对象.

什么时侯不要使用?

  • 没有业务含义的表达式:不要对与业务无关的表达式和操作使用规约.
  • 报表:如果只是创建报表,不要创建规约,而是直接使用IQueryable 和LINQ表达式.你甚至可以使用普通SQL、视图或其他工具生成报表.DDD不关心报表,因此从性能角度来看,查询底层数据存储的方式可能很重要.

应用服务层

应用服务

应用服务实现应用程序的用例, 将领域层逻辑公开给表示层.从表示层调用应用服务,DTO作为参数. 返回DTO给表示层.

  1. 从github下载nvm仓库到 ~/目录 地址:https://github.com/nvm-sh/nvm.git
    git clone https://github.com/nvm-sh/nvm.git
  2. 进入 nvm目录中执行install.sh 等待执行完成
    sh install.sh
  3. 配置nvm环境变量将下述代码复制到 ~/.bash_profile
    vim ~/.bash_profile
1
2
3
4
5
6
7
8
9
export NVM_DIR="$HOME/.nvm"

[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# This loads nvm

[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

# This loads nvm bash_completion
  1. 执行source ~/.bash_profile
  2. 执行nvm –version是否可以正常输出,若不行则重启终端再次尝试
  3. nvm操作
    ①:使用 nvm install node版本号 也可直接输入nvm install node 最新版本
    ②:使用 nvm list 或 nvm ls 可查看当前安装的node版本
    ③:使用 nvm use node版本 可以切换当前使用的node
    ④:使用 nvm alias default node版本 可以指定默认打开终端时的node版本

问题

每开一次终端,要 source ~/.bash_profile 环境变量才生效。

原因

MacOS Catalina(10.15),macOS的默认终端从bash变成了zsh。
Mac10.15以下版本,默认shell环境是bash,系统环境变量的配置文件是 /etc/profile 文件。
Mac10.15以上版本,默认shell环境是zsh,系统环境变量的配置文件是 /etc/zshrc 文件。

解决方法

编辑个人主目录下的.zshrc 这个文件

1
vim ~/.zshrc

在最后一行少添加一句:source ~/.bash_profile

这样每次打开新窗口或标签页就自动执行了source ~/.bash_profile,环境变量就有了

您浏览到的信息,来源于网络,网站中的内容只为个人研究记录,对于信息真实性本站点概不负责,如有侵权,请留意联系,谢谢!