乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - .NET 7正式发布,看看ASP.NET Core 7.0和EF Core 7新增哪些功能
2022年11月8日.NET 7正式发布
.NET仍然是最快、最受欢迎、最值得信赖的平台之一,其庞大的.NET软件包生态系统包括33万多个软件包。
.NET 7为您的应用程序带来了更高的性能和C# 11/F# 7、.NET MAUI、ASP.NET Core/Blazor、Web APIs、WinForms、WPF等的新功能。有了.NET 7,你还可以轻松地将你的.NET 7项目容器化,在GitHub行动中设置CI/CD工作流程,并实现云原生的可观察性。
感谢开源的.NET社区为帮助塑造这个.NET 7版本所作的大量贡献。在整个.NET 7版本中,有超过8900名贡献者做出了28000项贡献!
.NET 7的发布是我们.NET统一之旅的第三个主要版本(自2016年的.NET 5以来)。
有了.NET 7,你可以通过一个SDK、一个运行时、一套基础库来构建多种类型的应用程序(云、网络、桌面、移动、游戏、物联网和人工智能),一次学习并重复使用你的技能。
.NET 7支持周期
.NET 7得到了微软的正式支持。它被标记为一个标准期限支持(STS)版本,将被支持18个月。奇数的.NET版本是STS版本,在随后的STS或LTS版本之后,可以获得6个月的免费支持和补丁。
获取
从官网获取
安装SDK
- sdk-7.0.100-windows-x64-installer
- sdk-7.0.100-windows-x86-installer
- sdk-7.0.100-windows-arm64-installer
安装桌面运行时
- runtime-desktop-7.0.0-windows-x64-installer
- runtime-desktop-7.0.0-windows-x86-installer
- runtime-desktop-7.0.0-windows-arm64-installer
安装ASP.NET Core运行时
- runtime-aspnetcore-7.0.0-windows-x64-installer
- runtime-aspnetcore-7.0.0-windows-x86-installer
- runtime-aspnetcore-7.0.0-windows-hosting-bundle-installer
安装.NET运行时
- runtime-7.0.0-windows-x64-installer
- runtime-7.0.0-windows-x86-installer
- runtime-7.0.0-windows-arm64-installer
通过Nuget获取
安装SDK
winget install Microsoft.DotNet.SDK.7
安装桌面运行时
winget install Microsoft.DotNet.DesktopRuntime.7
安装ASP.NET Core运行时
winget install Microsoft.DotNet.AspNetCore.7
安装.NET运行时
winget install Microsoft.DotNet.Runtime.7
在.NET 7中ASP.NET Core更新有哪些?
服务与运行时(Servers and runtime)
- 速率限制(Rating limiting):使用灵活的端点配置和策略限制处理请求的速率。
- 输出缓存(Output caching):为响应配置缓存以更有效地处理请求。
- 请求解压缩(Request decompression):接受具有压缩内容的请求。
- HTTP/3:内置支持HTTP/3,这是基于新的QUIC复用传输协议的最新HTTP版本。
- Http/2 WebSockets支持: 使用WebSockets over HTTP/2连接。
- WebTransport(实验性):通过对WebTransport的实验性支持,在HTTP/3上创建流和数据克。
最小API(Minimal APIs)
- 终结点筛选器(Endpoint filters):使用端点过滤器在路由处理程序之前或之后运行横切代码。
- 类型化结果(Typed results):从最小的API返回强类型的结果。
- 路由组(Route groups):用一个共同的前缀来组织端点组。
远程调用(gRPC)
- JSON转码(JSON transcoding):通过将你的gRPC服务暴露为基于JSON的API来扩大它们的覆盖范围
- JSON转码文档与Swagger/OpenAPI(实验性的):使用实验性支持,为你的gRPC JSON转码服务生成OpenAPI规范。
- gRPC运行状况检查:报告和检查gRPC服务器应用程序的健康状况。
- gRPC客户端添加持有者令牌:创建使用承载令牌发送授权请求的客户端。
实时应用(SignalR)
- 客户端结果(Client results):响应服务器的请求,向服务器返回客户结果。
MVC
- MVC视图和Razor页面中的可为空模型(Nullable view and page models):现在支持Nullable页面和视图模型,以改善使用空状态检查时的体验。
客户端Web应用(Blazor)
- 自定义元素(Custom elements):用Blazor构建标准的HTML自定义元素,将Blazor组件与任何基于JavaScript的应用程序整合起来。
- 处理位置更改事件(Handle location changing events):拦截位置变化事件,以创建导航时的自定义用户体验。
- 数据绑定(之后/获取/设置)修改器(Bind after/get/set modifiers):在数据绑定后运行异步逻辑,并独立控制数据绑定如何获取和设置数据。
- 动态认证请求(Dynamic authentication requests):在运行时用自定义参数创建动态认证请求,以处理Blazor WebAssembly应用程序中的高级认证场景。
- 在WebAssembly中改进的JavaScript互操作(Improved JavaScript interop on WebAssembly):使用新的
[JSImport]
/[JSExport]
支持,在WebAssembly上运行时优化JavaScript互操作调用。 - WebAssembly的SIMD和异常处理(WebAssembly SIMD & exception handling):使用WebAssembly SIMD和异常处理支持,提高.NET WebAssembly超时(AOT)编译的性能。
在.NET 7中Entity Framework Core 7更新有哪些?
JSON列
大多数关系数据库都支持包含JSON文档的列,这些列中的JSON可以通过查询进行钻取。例如,这允许按文档内的属性进行筛选和排序,以及将文档中的属性投影到结果中。JSON列允许关系数据库具有文档数据库的某些特征,从而在两者之间创建有用的混合;它们还可用于消除查询中的联接,从而提高性能。
EF7包含对JSON列的提供程序无关支持,以及SQLServer的实现。此支持允许将从.NET类型生成的聚合映射到JSON文档。可以在聚合上使用普通的LINQ查询,这些查询将转换为钻取到JSON所需的相应查询构造。EF7还支持保存对JSON文档所做的更改。
使用Linq来查询Json
使用示例
var postsWithViews = await context.Posts.Where(post => post.Metadata!.Views > 3000)
.AsNoTracking()
.Select(
post => new
{
post.Author!.Name,
post.Metadata!.Views,
Searches = post.Metadata.TopSearches,
Commits = post.Metadata.Updates
})
.ToListAsync();
EFCore 7翻译后的SQL语句为
SELECT [a].[Name],
CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int),
JSON_QUERY([p].[Metadata],'$.TopSearches'),
[p].[Id],
JSON_QUERY([p].[Metadata],'$.Updates')
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int) > 3000
注意的是,JSON_VALUE
、JSON_QUERY
被用来查询Json文档的部分。
在保存时更新Json
EF7的更改跟踪查找需要更新的JSON文档中最小的单个部分,并发送SQL命令以有效地相应地更新列。例如,考虑修改嵌入在JSON文档中的单个属性的代码:
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
arthur.Contact.Address.Country = "United Kingdom";
await context.SaveChangesAsync();
EFCore 7仅为修改的值生成的一个SQL参数
@p0='["United Kingdom"]' (Nullable = false) (Size = 18)
然后使用这个参数,结合JSON_MODIFY
命令进行修改
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;
更多关于Json列的信息
批量更新和删除
EFCore跟踪实体的变化,然后在SaveChangesAsync
被调用时将更新发送到数据库。只有那些实际发生了变化的属性和关系才会被发送。同时,被跟踪的实体与发送到数据库的变化保持同步。这种机制是一种高效、便捷的方式,可以向数据库发送通用的插入、更新和删除。这些变化也是分批进行的,以减少数据库的往返次数。
然而,有时在数据库上执行更新或删除命令而不加载实体或涉及到变化跟踪器是很有用的。EF7通过新的ExecuteUpdateAsync
和ExecuteDeleteAsync
方法实现了这一点。这些方法被应用于LINQ查询,并根据查询的结果立即更新或删除数据库中的实体。许多实体可以用一个命令来更新,而且这些实体不会被载入内存。
批量删除
使用了ExecuteDeleteAsync
的示例代码
var priorToDateTime = new DateTime(priorToYear, 1, 1);
await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn < priorToDateTime)).ExecuteDeleteAsync();
这将生成“立即从数据库中删除所有在给定年份之前发表的帖子的标签”的SQL。
DELETE FROM [t]
FROM [Tags] AS [t]
WHERE NOT EXISTS (
SELECT 1
FROM [PostTag] AS [p]
INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
WHERE [t].[Id] = [p].[TagsId] AND [p0].[PublishedOn] < @__priorToDateTime_1)
批量更新
使用ExecuteUpdateAsync
与使用ExecuteDeleteAsync
非常相似,只是它需要额外的参数来指定对每条记录的修改。
例如,考虑下面这个以调用ExecuteUpdateAsync
结束的LINQ查询。
var priorToDateTime = new DateTime(priorToYear, 1, 1);
await context.Tags
.Where(t => t.Posts.All(e => e.PublishedOn < priorToDateTime))
.ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));
这将生成“立即更新给定年份之前发布的帖子的所有标签的"文本"列”的SQL。
UPDATE [t]
SET [t].[Text] = [t].[Text] + N' (old)'
FROM [Tags] AS [t]
WHERE NOT EXISTS (
SELECT 1
FROM [PostTag] AS [p]
INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
WHERE [t].[Id] = [p].[TagsId] AND [p0].[PublishedOn] < @__priorToDateTime_1)
更新或删除单行
虽然ExecuteUpdateAsync
和ExecuteDeleteAsync
通常用于同时更新或删除许多行(即"批量"更改),但它们对于高效的单行更改也很有用。
例如,考虑在ASP.NETCore应用程序中删除一个实体的常见模式。
public async Task<ActionResult> DeletePost(int id)
{
var post = await _context.Posts.FirstOrDefaultAsync(p => p.Id == id);
if (post == null)
{
return NotFound();
}
_context.Posts.Remove(post);
await _context.SaveChangesAsync();
return Ok();
}
如果使用EF 7将可以写成
public async Task<ActionResult> DeletePost(int id)
=> await _context.Posts.Where(p => p.Id == id).ExecuteDeleteAsync() == 0
? NotFound()
: Ok();
这既是更少的代码,也是明显的速度,因为它只执行了一次数据库往返。
何时使用批量更新
ExecuteUpdateAsync
和ExecuteDeleteAsync
是简单、明确的更新和删除的最佳选择。
然而,请记住,
- 必须明确指定要做的具体变化;EFCore不会自动检测到这些变化。
- 任何被跟踪的实体都不会保持同步。
- 可能需要多个命令,而且这些命令的顺序要正确,以免违反数据库约束。例如,在删除委托人之前,必须先删除依赖者。
- 对
ExecuteUpdateAsync
和ExecuteDeleteAsync
的多次调用不会被自动包裹在一个事务中。
所有这些意味着ExecuteUpdateAsync
和ExecuteDeleteAsync
是对现有SaveChanges
机制的补充,而不是取代。
更多关于批量更新的信息
- What’s New: ExecuteUpdate and ExecuteDelete (Bulk updates)
- .NET Data Community Standup Video: Bulk updates
- Sample code: ExecuteUpdateAsync
- Sample code: ExecuteDeleteAsync
保存变更更快
在EF 7中,SaveChanges
和SaveChangesAsync
的性能得到了明显的改善。在某些情况下,现在保存变化的速度比EF Core 6快四倍。这些改进来自于执行更少的往返于数据库和生成更有效的SQL。
避免非必要的事务
所有现代关系型数据库都保证了(大多数)单一SQL语句的事务性。也就是说,即使发生错误,该语句也不会只完成一部分。
EF 7避免在这些情况下启动显式事务。
例如,考虑以下对SaveChangesAsync
的调用,它插入了一个单一实体。
await context.AddAsync(new Blog { Name = "MyBlog" });
await context.SaveChangesAsync();
在EF Core 6中,INSERT命令被开始和提交事务的命令所包裹。
dbug: 9/29/2022 11:43:09.196 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
Began transaction with isolation level 'ReadCommitted'.
info: 9/29/2022 11:43:09.265 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (27ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Blogs] ([Name])
VALUES (@p0);
SELECT [Id]
FROM [Blogs]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
dbug: 9/29/2022 11:43:09.297 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
Committed transaction.
EF 7检测到这里不需要事务,所以删除了这些调用。
info: 9/29/2022 11:42:34.776 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (25ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
INSERT INTO [Blogs] ([Name])
OUTPUT INSERTED.[Id]
VALUES (@p0);
插入多行
在EF Core 6中,插入多条记录的默认方法是由SQLServer对带有触发器的表的支持的限制所驱动的。这意味着EF Core 6不能使用一个简单的OUTPUT子句。相反,当插入多个实体时,EF Core 6产生了一些相当复杂的涉及到临时表的SQL。
例如,考虑对SaveChangesAsync
的调用。
for (var i = 0; i < 4; i++)
{
await context.AddAsync(new Blog { Name = "Foo" + i });
}
await context.SaveChangesAsync();
由EF Core 6生成的SQL如下
dbug: 9/30/2022 17:19:51.919 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
Began transaction with isolation level 'ReadCommitted'.
info: 9/30/2022 17:19:51.993 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (27ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p0, 0),
(@p1, 1),
(@p2, 2),
(@p3, 3)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position
INTO @inserted0;
SELECT [i].[Id] FROM @inserted0 i
ORDER BY [i].[_Position];
dbug: 9/30/2022 17:19:52.023 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
Committed transaction.
相比之下,EF 7在针对一个没有触发器的表时,会生成一条更简单的命令。
info: 9/30/2022 17:40:37.612 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (4ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
MERGE [Blogs] USING (
VALUES (@p0, 0),
(@p1, 1),
(@p2, 2),
(@p3, 3)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position;
事务没有了,就像单次插入的情况一样,因为MERGE是一个受隐式事务保护的单一语句。另外,临时表也没有了,OUTPUT子句现在直接把生成的ID发回给客户端。这可能比EF Core 6上的速度快四倍,这取决于环境因素,如应用程序和数据库之间的延迟。
这消除了两个数据库往返,这会使整体性能产生巨大影响,尤其是在对数据库的调用延迟较高时。在典型的生产系统中,数据库不与应用程序位于同一台计算机上。这意味着延迟通常相对较高,使得这种优化在实际生产系统中特别有效。
更多关于高性能保存变更的信息
- What’s New: Faster SaveChanges
- .NET Blog: Announcing Entity Framework Core 7 Preview 6–Performance Edition
- .NET Data Community Standup Video: Performance Improvements to the EF7 Update Pipeline
- Sample code: SaveChanges performance
每个具体类型的表(TPC)的继承映射
默认情况下,EF Core将一个.NET类型的继承层次映射到一个数据库表,这被称为"每层表"(TPH)的映射策略。EF Core 5引入了table-per-type
(TPT)策略,它支持将每个.NET类型映射到一个不同的数据库表中。EF 7引入了table-per-concrete-type
(TPC)策略。TPC也是将.NET类型映射到不同的表,但其方式是解决TPT策略的一些常见的性能问题。
TPC策略与TPT策略类似,只是为层次结构中的每个具体类型创建不同的表,但不为抽象类型创建表——因此被称为"每具体类型表"。与TPT一样,表本身表明了保存对象的类型。然而,与TPT映射不同,每个表都包含了具体类型及其基础类型中每个属性的列。因此,TPC数据库模式是非规范化的。
每具体类型表(TPC)
思考以下这样一个示例
public abstract class Animal
{
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
{
public string? Vet { get; set; }
public ICollection<Human> Humans { get; } = new List<Human>();
}
public class FarmAnimal : Animal
{
public override string Species { get; }
public decimal Value { get; set; }
}
public class Cat : Pet
{
public string EducationLevel { get; set; }
public override string Species => "Felis catus";
}
public class Dog : Pet
{
public string FavoriteToy { get; set; }
public override string Species => "Canis familiaris";
}
public class Human : Animal
{
public override string Species => "Homo sapiens";
public Animal? FavoriteAnimal { get; set; }
public ICollection<Pet> Pets { get; } = new List<Pet>();
}
这是在OnModelCreating
中使用UseTpcMappingStrategy
将其映射到TPC
表的案例:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
}
当使用SQL Server时,为这个层次结构创建的表是:
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]));
每具体类型表(TPC)的查询
TPC生成的查询比TPT更有效,需要从更少的表中获取数据,并且利用UNION ALL
代替JOIN
。
例如,对于查询整个层次结构,EF7会产生:
SELECT [f].[Id], [f].[FoodId], [f].[Name], [f].[Species], [f].[Value], NULL AS [FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator]
FROM [FarmAnimals] AS [f]
UNION ALL
SELECT [h].[Id], [h].[FoodId], [h].[Name], NULL AS [Species], NULL AS [Value], [h].[FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Human' AS [Discriminator]
FROM [Humans] AS [h]
UNION ALL
SELECT [c].[Id], [c].[FoodId], [c].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
FROM [Cats] AS [c]
UNION ALL
SELECT [d].[Id], [d].[FoodId], [d].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
FROM [Dogs] AS [d]
当查询一个类型的子集时,情况会变得更好:
SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
FROM [Cats] AS [c]
UNION ALL
SELECT [d].[Id], [d].[FoodId], [d].[Name], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
FROM [Dogs] AS [d]
TPC查询在对单一叶子类型进行查询时真的很有优势:
SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel]
FROM [Cats] AS [c]
更多关于每具体类型表(TPC)的信息
- What’s New: Table-per-concrete-type (TPC) inheritance mapping
- .NET Data Community Standup Video: TPH, TPT, and TPC Inheritance mapping with EF Core
- Sample code: TPC inheritance
定制数据库逆向工程模板
EF 7支持T4模板,用于在从数据库逆向工程EF模型时定制支架代码。默认的模板是通过dotnet
命令添加到项目中的。
dotnet new --install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates
然后,这些模板可以被定制,并将自动被dotnet ef dbcontext scaffold
和Scaffold-DbContext
所使用。
自定义生成后的实体类型
让我们来看看定制模板是什么样子的。
默认情况下,EF Core为集合导航属性生成了以下代码。
public virtual ICollection<Album> Albums { get; } = new List<Album>();
对于大多数应用程序来说,使用List<T>
是一个很好的默认值。然而,如果你使用一个基于XAML的框架,如WPF、WinUI或.NET MAUI,你经常想使用ObservableCollection<T>
来实现数据绑定。
EntityType.t4
模板可以被编辑来做这个改变。
例如,下面的代码包含在默认模板中。
if (navigation.IsCollection)
{
#>
public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; } = new List<<#= targetType #>>();
<#
}
这可以很容易地改成使用ObservableCollection
public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; } = new ObservableCollection<<#= targetType #>>();
由于ObservableCollection
在System.Collections.ObjectModel
命名空间中,我们还需要在脚手架的代码中添加一个using
指令。
var usings = new List<string>
{
"System",
"System.Collections.Generic",
"System.Collections.ObjectModel"
};
更多关于反向工程T4模板的信息
- What’s New: Custom Reverse Engineering Templates
- .NET Data Community Standup Video: Database-first with T4 Templates in EF7–An early look
定制建模转换
EF Core使用一个元数据"模型"来描述应用程序的实体类型是如何映射到底层数据库的。这个模型是通过一组大约60个"约定"建立的。然后可以使用映射属性(又称"数据注释")和/或在OnModelCreating
中调用ModelBuilder API
来定制由惯例构建的模型。
从EF 7开始,应用程序可以删除或替换这些约定,也可以添加新的约定。
模型构建约定是控制模型配置的一个强有力的方法。
移除转换约定
EF 7允许删除EF Core使用的默认约定。
例如,为外键(FK)列创建索引通常是有意义的,因此,有一个内置的约定用于此。
外键索引公约(ForeignKeyIndexConvention
)。然而,在更新行的时候,索引会增加开销,而且为所有的FK列创建索引可能并不总是合适。
为了达到这个目的,在建立模型时可以删除ForeignKeyIndexConvention
。
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
增加转换约定
EF Core用户的一个共同要求是为所有字符串属性设置一个默认长度。这在EF 7中可以通过编写一个公约来实现。
public class MaxStringLengthConvention : IModelFinalizingConvention
{
private readonly int _maxLength;
public MaxStringLengthConvention(int maxLength)
{
_maxLength = maxLength;
}
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
{
foreach (var property in modelBuilder.Metadata.GetEntityTypes()
.SelectMany(
e => e.GetDeclaredProperties()
.Where(p => p.ClrType == typeof(string))))
{
property.Builder.HasMaxLength(_maxLength);
}
}
}
这个约定是非常简单的。
它找到模型中的每个字符串属性,并将其最大长度设置为指定值。
然而,使用这样的约定的关键之处在于,那些使用[MaxLength]
或[StringLength]
属性或OnModelCreating
中的HasMaxLength
明确设置其最大长度的属性将保留其明确值。
换句话说,只有在没有指定其他长度的情况下,才会使用该约定所设置的默认值。
这个新约定可以通过在ConfigureConventions
中添加它来使用。
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new MaxStringLengthConvention(256));
}
更多关于建模转换的信息
- What’s New: Model building conventions
- .NET Data Community Standup Video: EF7 Custom Model Conventions
- Sample code: Model building conventions
插入/更新/删除的存储过程映射
默认情况下,EF Core生成的插入、更新和删除命令是直接与表或可更新的视图一起工作。
EF 7引入了对这些命令到存储过程的映射的支持。
在OnModelCreating
中使用InsertUsingStoredProcedure
、UpdateUsingStoredProcedure
和DeleteUsingStoredProcedure
来映射存储过程。
例如,要为Person
实体类型映射存储过程。
modelBuilder.Entity<Person>()
.InsertUsingStoredProcedure(
"People_Insert",
storedProcedureBuilder =>
{
storedProcedureBuilder.HasParameter(a => a.Name);
storedProcedureBuilder.HasResultColumn(a => a.Id);
})
.UpdateUsingStoredProcedure(
"People_Update",
storedProcedureBuilder =>
{
storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
storedProcedureBuilder.HasParameter(person => person.Name);
storedProcedureBuilder.HasRowsAffectedResultColumn();
})
.DeleteUsingStoredProcedure(
"People_Delete",
storedProcedureBuilder =>
{
storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
storedProcedureBuilder.HasRowsAffectedResultColumn();
});
在使用SQL Server时,该配置映射到以下存储过程。
针对插入场景
CREATE PROCEDURE [dbo].[People_Insert]
@Name [nvarchar](max)
AS
BEGIN
INSERT INTO [People] ([Name])
OUTPUT INSERTED.[Id]
VALUES (@Name);
END
针对更新场景
CREATE PROCEDURE [dbo].[People_Update]
@Id [int],
@Name_Original [nvarchar](max),
@Name [nvarchar](max)
AS
BEGIN
UPDATE [People] SET [Name] = @Name
WHERE [Id] = @Id AND [Name] = @Name_Original
SELECT @@ROWCOUNT
END
针对删除场景
CREATE PROCEDURE [dbo].[People_Delete]
@Id [int],
@Name_Original [nvarchar](max)
AS
BEGIN
DELETE FROM [People]
OUTPUT 1
WHERE [Id] = @Id AND [Name] = @Name_Original;
END
然后在调用SaveChangesAsync
时使用这些存储程序。
SET NOCOUNT ON;
EXEC [People_Update] @p1, @p2, @p3;
EXEC [People_Update] @p4, @p5, @p6;
EXEC [People_Delete] @p7, @p8;
EXEC [People_Delete] @p9, @p10;
更多关于存储过程映射的信息
全新的改进后的拦截器和事件
EF Core拦截器能够拦截、修改和/或抑制EFCore操作。EF Core还包括传统的.NET事件和日志记录。
EF 7包括以下拦截器的增强功能
- 拦截创建和填充新的实体实例(又称"实体化")
- 拦截器可以在编译查询之前修改LINQ表达式树
- 拦截乐观的并发性处理(
DbUpdateConcurrencyException
) - 在检查连接字符串是否已被设置之前,对连接进行拦截
- 当EF Core消耗完一个结果集后,在该结果集被关闭之前进行拦截
- 对EF Core创建
DbConnection
的拦截 - 对
DbCommand
初始化后的拦截
此外,EF7还包括新的传统的.NET事件,用于:
- 当一个实体即将被追踪或改变状态时,但在它实际被追踪或改变状态之前
- 在EF Core检测到实体和属性的变化之前和之后(又称
DetectChanges
拦截)
Materialization拦截器
新的IMaterializationInterceptor
支持在实体实例被创建前后以及该实例的属性被初始化前后进行拦截。
拦截器可以在每个点上改变或替换实体实例。这允许:
- 设置未映射的属性或调用验证、计算值或标志所需的方法。
- 使用一个工厂来创建实例。
- 创建与EF通常创建的不同的实体实例,如来自缓存的实例,或代理类型的实例。
- 将服务注入到实体实例中。
例如,想象一下,我们想跟踪一个实体从数据库中检索的时间,也许这样就可以显示给编辑数据的用户。为此可以创建一个物化拦截器。
public class SetRetrievedInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
{
if (instance is IHasRetrieved hasRetrieved)
{
hasRetrieved.Retrieved = DateTime.UtcNow;
}
return instance;
}
}
在配置DbContext
时,这个拦截器的一个实例被注册。
public class CustomerContext : DbContext
{
private static readonly SetRetrievedInterceptor _setRetrievedInterceptor = new();
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.AddInterceptors(_setRetrievedInterceptor)
.UseSqlite("Data Source = customers.db");
}
现在,每当从数据库查询一个客户时,Retrieved
属性将被自动设置,比如说。
await using (var context = new CustomerContext())
{
var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
Console.WriteLine($"Customer '{customer.Name}' was retrieved at '{customer.Retrieved.ToLocalTime()}'");
}
进程的输出为
Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'
连接字符串的延迟初始化
连接字符串通常是从配置文件中读取的静态资产。在配置DbContext
时,这些可以很容易地传递给UseSqlServer
或类似的东西。然而,连接字符串有时可以为每个上下文实例而改变。
例如,在一个多租户系统中,每个租户可能有不同的连接字符串。
EF 7通过对IDbConnectionInterceptor
的改进,使其更容易处理动态连接和连接字符串。
这首先是在没有任何连接字符串的情况下配置DbContext
的能力。比如说:
services.AddDbContext<CustomerContext>(
b => b.UseSqlServer();
然后,可以实现IDbConnectionInterceptor
方法之一,在使用连接之前对其进行配置。
ConnectionOpeningAsync
是一个很好的选择,因为它可以执行一个异步操作来获取连接字符串,找到一个访问令牌,等等。
public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
private readonly IClientConnectionStringFactory _connectionStringFactory;
public ConnectionStringInitializationInterceptor(IClientConnectionStringFactory connectionStringFactory)
{
_connectionStringFactory = connectionStringFactory;
}
public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
CancellationToken cancellationToken = new())
{
if (string.IsNullOrEmpty(connection.ConnectionString))
{
connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
}
return result;
}
}
请注意,只有在第一次使用连接时才会获得连接字符串。在那之后,存储在
DbConnection
上的连接字符串将被使用,而无需查找新的连接字符串。
更多关于拦截器和事件的信息
- What’s New: New and improved interceptors and events
- .NET Data Community Standup Video: Intercept this EF7 Preview 6 Event
- Sample code: Materialization interception
- Sample code: Connection string interception
参考
- ASP.NET Core 7.0 的新增功能
- 从ASP.NET Core 6.0迁移到7.0
- .NET Live TV
- .NET 7 is Available Today
- Announcing ASP.NET Core in .NET 7
- Announcing .NET MAUI for .NET 7 General Availability
- What’s new for WPF in .NET 7
- What’s new in Windows Forms in .NET 7.0
- Entity Framework Core 7 (EF7) is available today
- Welcome to C# 11
- Announcing F# 7
- What’s new in Orleans 7.0
- 使用Windows包管理器 (winget) 进行安装
- 乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 获取.Net 7并查看.Net 7中的性能提升(简中译文)
- 乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 伴随.Net6/7与时俱进的.Net CLI命令行接口
- 乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - .NET 7预览和RC1,内置MAUI、帮助.NET应用程序现代化升级
- 乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core远程过程调用,HttpClientFactory和gRPC最佳实践
- 统一的开发平台.NET 7正式发布
- 微软发布.NET 7,Visual Studio 2022 17.4原生支持Arm64架构