乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - .NET 7正式发布,看看ASP.NET Core 7.0和EF Core 7新增哪些功能

2022年11月8日.NET 7正式发布

.NET仍然是最快、最受欢迎、最值得信赖的平台之一,其庞大的.NET软件包生态系统包括33万多个软件包。

image

.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项贡献!

image

.NET 7的发布是我们.NET统一之旅的第三个主要版本(自2016年的.NET 5以来)。

有了.NET 7,你可以通过一个SDK、一个运行时、一套基础库来构建多种类型的应用程序(云、网络、桌面、移动、游戏、物联网和人工智能),一次学习并重复使用你的技能。

.NET 7支持周期

image

.NET 7得到了微软的正式支持。它被标记为一个标准期限支持(STS)版本,将被支持18个月。奇数的.NET版本是STS版本,在随后的STS或LTS版本之后,可以获得6个月的免费支持和补丁。

获取

从官网获取

https://dotnet.microsoft.com/zh-cn/download/dotnet/7.0

image

安装SDK

安装桌面运行时

安装ASP.NET Core运行时

安装.NET运行时

通过Nuget获取

安装SDK

winget install Microsoft.DotNet.SDK.7

image

安装桌面运行时

winget install Microsoft.DotNet.DesktopRuntime.7

image

安装ASP.NET Core运行时

winget install Microsoft.DotNet.AspNetCore.7

image

安装.NET运行时

winget install Microsoft.DotNet.Runtime.7

image

在.NET 7中ASP.NET Core更新有哪些?

ASP.NET Core 7.0 的新增功能

服务与运行时(Servers and runtime)

最小API(Minimal APIs)

远程调用(gRPC)

实时应用(SignalR)

MVC

客户端Web应用(Blazor)

在.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_VALUEJSON_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通过新的ExecuteUpdateAsyncExecuteDeleteAsync方法实现了这一点。这些方法被应用于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)

更新或删除单行

虽然ExecuteUpdateAsyncExecuteDeleteAsync通常用于同时更新或删除许多行(即"批量"更改),但它们对于高效的单行更改也很有用

例如,考虑在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();

这既是更少的代码,也是明显的速度,因为它只执行了一次数据库往返

何时使用批量更新

ExecuteUpdateAsyncExecuteDeleteAsync是简单、明确的更新和删除的最佳选择

然而,请记住,

  • 必须明确指定要做的具体变化;EFCore不会自动检测到这些变化。
  • 任何被跟踪的实体都不会保持同步。
  • 可能需要多个命令,而且这些命令的顺序要正确,以免违反数据库约束。例如,在删除委托人之前,必须先删除依赖者。
  • ExecuteUpdateAsyncExecuteDeleteAsync的多次调用不会被自动包裹在一个事务中。

所有这些意味着ExecuteUpdateAsyncExecuteDeleteAsync是对现有SaveChanges机制的补充,而不是取代。

更多关于批量更新的信息

保存变更更快

在EF 7中,SaveChangesSaveChangesAsync的性能得到了明显的改善。在某些情况下,现在保存变化的速度比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上的速度快四倍,这取决于环境因素,如应用程序和数据库之间的延迟。

这消除了两个数据库往返,这会使整体性能产生巨大影响,尤其是在对数据库的调用延迟较高时。在典型的生产系统中,数据库不与应用程序位于同一台计算机上。这意味着延迟通常相对较高,使得这种优化在实际生产系统中特别有效

更多关于高性能保存变更的信息

每个具体类型的表(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)的信息

定制数据库逆向工程模板

EF 7支持T4模板,用于在从数据库逆向工程EF模型时定制支架代码。默认的模板是通过dotnet命令添加到项目中的。

dotnet new --install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates

然后,这些模板可以被定制,并将自动被dotnet ef dbcontext scaffoldScaffold-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 #>>();

由于ObservableCollectionSystem.Collections.ObjectModel命名空间中,我们还需要在脚手架的代码中添加一个using指令。

var usings = new List<string>
{
    "System",
    "System.Collections.Generic",
    "System.Collections.ObjectModel"
};

更多关于反向工程T4模板的信息

定制建模转换

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

更多关于建模转换的信息

插入/更新/删除的存储过程映射

默认情况下,EF Core生成的插入、更新和删除命令是直接与表或可更新的视图一起工作。

EF 7引入了对这些命令到存储过程的映射的支持

OnModelCreating中使用InsertUsingStoredProcedureUpdateUsingStoredProcedureDeleteUsingStoredProcedure来映射存储过程。

例如,要为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上的连接字符串将被使用,而无需查找新的连接字符串。

更多关于拦截器和事件的信息

参考

posted @ 2022-11-09 15:10  TaylorShi  阅读(2693)  评论(0编辑  收藏  举报