EF Core 6.0 中的新增功能

EF Core 6.0 已 寄送到 NuGet。 此页面包含此版本中引入的有趣更改的概述。

小窍门

可以通过 从 GitHub 下载示例代码来运行并调试如下所示的示例。

SQL Server 时态表

GitHub 问题: #4693

即使数据已更新或删除,SQL Server 临时表也会自动跟踪表中存储的所有数据。 这是通过创建并行的“历史记录表”来实现的,每当对主表进行更改时,将存储带时间戳的历史数据。 这样就可以查询历史数据,例如进行审核或还原,例如在意外突变或删除后进行恢复。

EF Core 现在支持:

  • 使用迁移创建时间表
  • 使用迁移再次将现有表转换为临时表
  • 查询历史数据
  • 从过去某个时间点恢复数据以重置到原始状态

配置时态表

模型生成器可用于将表配置为临时表。 例如:

modelBuilder
    .Entity<Employee>()
    .ToTable("Employees", b => b.IsTemporal());

使用 EF Core 创建数据库时,新表将配置为具有时间戳和历史记录表的 SQL Server 默认值的临时表。 例如,请考虑实体 Employee 类型:

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
    public string Department { get; set; }
    public string Address { get; set; }
    public decimal AnnualSalary { get; set; }
}

创建的临时表如下所示:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistory]))');

请注意,SQL Server 会创建两个隐藏列,分别名为PeriodEndPeriodStart。 这些“时间段列”表示行中的数据存在的时间范围。 这些列映射到 EF Core 模型中的 阴影属性 ,允许它们在查询中使用,如下所示。

重要

这些列中的时间始终是 SQL Server 生成的 UTC 时间。 UTC 时间用于涉及时间表的所有操作,例如在如下所示的查询中。

另请注意,一个名为EmployeeHistory的关联历史记录表会自动创建。 可以通过对模型构建器进行额外配置来更改时间段列和历史记录表的名称。 例如:

modelBuilder
    .Entity<Employee>()
    .ToTable(
        "Employees",
        b => b.IsTemporal(
            b =>
            {
                b.HasPeriodStart("ValidFrom");
                b.HasPeriodEnd("ValidTo");
                b.UseHistoryTable("EmployeeHistoricalData");
            }));

这反映在 SQL Server 创建的表中:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [ValidFrom] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    [ValidTo] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([ValidFrom], [ValidTo])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistoricalData]))');

使用时态表

大多数情况下,临时表的使用方式与任何其他表一样。 也就是说,SQL Server 会透明地处理期间列和历史数据,使应用程序可以忽略它们。 例如,可以正常方式将新实体保存到数据库:

context.AddRange(
    new Employee
    {
        Name = "Pinky Pie",
        Address = "Sugarcube Corner, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Party Organizer",
        AnnualSalary = 100.0m
    },
    new Employee
    {
        Name = "Rainbow Dash",
        Address = "Cloudominium, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Ponyville weather patrol",
        AnnualSalary = 900.0m
    },
    new Employee
    {
        Name = "Fluttershy",
        Address = "Everfree Forest, Equestria",
        Department = "DevDiv",
        Position = "Animal caretaker",
        AnnualSalary = 30.0m
    });

await context.SaveChangesAsync();

然后,可以按正常方式查询、更新和删除此数据。 例如:

var employee = await context.Employees.SingleAsync(e => e.Name == "Rainbow Dash");
context.Remove(employee);
await context.SaveChangesAsync();

此外,进行正常的 跟踪查询 后,可以从 跟踪实体访问当前数据中的时间列的值。 例如:

var employees = await context.Employees.ToListAsync();
foreach (var employee in employees)
{
    var employeeEntry = context.Entry(employee);
    var validFrom = employeeEntry.Property<DateTime>("ValidFrom").CurrentValue;
    var validTo = employeeEntry.Property<DateTime>("ValidTo").CurrentValue;

    Console.WriteLine($"  Employee {employee.Name} valid from {validFrom} to {validTo}");
}

这将打印:

Starting data:
  Employee Pinky Pie valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Rainbow Dash valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Fluttershy valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM

请注意, ValidTo 列(默认情况下调用 PeriodEnd)包含 datetime2 最大值。 对于表格中的当前行,情况始终如此。 默认情况下称为PeriodStartValidFrom列包含插入该行的 UTC 时间。

查询历史数据

EF Core 支持通过多个新查询运算符包含历史数据的查询:

  • TemporalAsOf:返回给定 UTC 时间处于活动状态(当前)的行。 这是给定主键的当前表或历史记录表中的单个行。
  • TemporalAll:返回历史数据中的所有行。 这通常是历史记录表和/或给定主键的当前表中的许多行。
  • TemporalFromTo:返回在两个给定 UTC 时间之间处于活动状态的所有行。 这可能是历史记录表和/或给定主键的当前表中的许多行。
  • TemporalBetween:与 TemporalFromTo 相同,除了包括在上边界上变为活动状态的行。
  • TemporalContainedIn:返回开始处于活动状态且在两个给定 UTC 时间之间结束处于活动状态的所有行。 这可能是历史记录表和/或给定主键的当前表中的许多行。

注释

有关每个运算符包含哪些行的详细信息,请参阅 SQL Server 临时表文档

例如,在对数据进行一些更新和删除后,可以使用查询 TemporalAll 来查看历史数据:

var history = await context
    .Employees
    .TemporalAll()
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToListAsync();

foreach (var pointInTime in history)
{
    Console.WriteLine(
        $"  Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Position}' from {pointInTime.ValidFrom} to {pointInTime.ValidTo}");
}

请注意如何使用 EF.Property 方法 来访问时间列中的值。 OrderBy 条款用于对数据进行排序,然后在映射中将这些值包含在返回的数据中。

此查询返回以下数据:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM

请注意,返回的最后一行在 2021/8/26 下午 4:44:59 停止处于活动状态。 这是因为当时从主表中删除了彩虹小马的行。 稍后我们将了解如何还原此数据。

可以编写类似的查询,使用TemporalFromToTemporalBetweenTemporalContainedIn。 例如:

var history = await context
    .Employees
    .TemporalBetween(timeStamp2, timeStamp3)
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToListAsync();

此查询返回以下行:

Historical data for Rainbow Dash between 8/26/2021 4:41:14 PM and 8/26/2021 4:42:44 PM:
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM

还原历史数据

如上所述,Rainbow Dash 已从 Employees 表中删除。 这显然是一个错误,所以让我们回到某个时间点,并从该时间点还原缺少的行。

var employee = await context
    .Employees
    .TemporalAsOf(timeStamp2)
    .SingleAsync(e => e.Name == "Rainbow Dash");

context.Add(employee);
await context.SaveChangesAsync();

此查询返回在给定 UTC 时间的 Rainbow Dash 的单行。 默认情况下,使用时态运算符的所有查询都不会跟踪,因此不会跟踪此处返回的实体。 这很有意义,因为它当前不存在于主表中。 若要将实体重新插入主表中,只需将其 Added 标记为然后调用 SaveChanges

重新插入行Rainbow Dash后,查询历史数据显示该行在给定的 UTC 时间被还原:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:44:59 PM to 12/31/9999 11:59:59 PM

迁移套件

GitHub 问题: #19693

EF Core 迁移用于基于对 EF 模型的更改生成数据库架构更新。 这些架构更新应在应用程序部署时应用,通常是持续集成/持续部署(C.I./C.D.)系统的一部分。

EF Core 现在包含应用这些架构更新的新方法:迁移捆绑包。 迁移捆绑包是一个小可执行文件,其中包含迁移以及将这些迁移应用到数据库所需的代码。

注释

有关迁移、捆绑包和部署的更深入讨论,请参阅 .NET 博客上的 DevOps 友好 EF Core 迁移捆绑包介绍

迁移捆绑包是使用 dotnet ef 命令行工具创建的。 在继续之前,请确保已安装了该工具的最新版本

捆绑包需要包含迁移。 这些项目是使用dotnet ef migrations add,按照迁移文档中所述创建的。 当您准备好迁移以部署时,使用 dotnet ef migrations bundle 创建一个捆绑包。 例如:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

输出是适用于目标操作系统的可执行文件。 在我这种情况下,这是 Windows x64,因此我在本地文件夹中获得了一个 efbundle.exe。 运行此可执行文件会应用其中包含的迁移:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903083845_MyMigration'.
Done.
PS C:\local\AllTogetherNow\SixOh>

仅当尚未应用迁移时,才会将迁移应用到数据库。 例如,再次运行同一套件不会进行任何操作,因为没有要应用的新迁移。

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
No migrations were applied. The database is already up to date.
Done.
PS C:\local\AllTogetherNow\SixOh>

但是,如果对模型进行了更改并生成了更多迁移,则可以将这些迁移打包成新的可执行文件以准备应用。 例如:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add SecondMigration
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add Number3
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle --force
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

请注意, --force 此选项可用于用新捆绑包覆盖现有捆绑包。

执行此新捆绑包会将这两个新迁移应用到数据库:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

默认情况下,捆绑包使用应用程序配置中的数据库连接字符串。 但是,可以通过在命令行上传递连接字符串来迁移其他数据库。 例如:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe --connection "Data Source=(LocalDb)\MSSQLLocalDB;Database=SixOhProduction"
Applying migration '20210903083845_MyMigration'.
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

请注意,这一次应用了所有三个迁移,因为它们尚未应用于生产数据库。

其他选项可以传递给命令行。 一些常见选项包括:

  • --output 用于指定要创建的可执行文件的路径。
  • --context 指定项目包含多个上下文类型时要使用的 DbContext 类型。
  • --project 指定要使用的项目。 默认为当前工作目录。
  • --startup-project 指定要使用的启动项目。 默认为当前工作目录。
  • --no-build 以防止在运行命令之前生成项目。 仅当已知项目为 up-to-date 时,才应使用此方法。
  • --verbose 查看有关命令正在做什么的详细信息。 在 bug 报告中包括信息时使用此选项。

使用 dotnet ef migrations bundle --help 查看所有可用选项。

请注意,默认情况下,每个迁移都会在其自己的事务中应用。 有关此领域的可能增强功能的讨论,请参阅 GitHub 问题 #22616

约定前模型配置

GitHub 问题: #12229

旧版 EF Core 要求当映射与默认值不同时,必须显式配置特定类型的每个属性的映射。 这包括“方面”,例如字符串的最大长度和小数精度,以及属性类型的值转换。

这需要以下任一项:

  • 每个属性的模型生成器配置
  • 每个属性上的映射属性
  • 对所有实体类型的所有属性进行显式迭代,并在生成模型时使用低级别元数据 API。

请注意,显式迭代容易出错且难以可靠执行,因为发生此迭代时,实体类型和映射属性的列表可能不是最终的。

EF Core 6.0 允许为给定类型指定一次此映射配置。 然后,它将应用于模型中该类型的所有属性。 这称为“前约定模型配置”,因为它配置模型的各个方面,供模型构建约定使用。 通过在您的 DbContext 上覆盖 ConfigureConventions 来应用此类配置。

public class SomeDbContext : DbContext
{
    protected override void ConfigureConventions(
        ModelConfigurationBuilder configurationBuilder)
    {
        // Pre-convention model configuration goes here
    }
}

例如,请考虑以下实体类型:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public Money AccountValue { get; set; }

    public Session CurrentSession { get; set; }

    public ICollection<Order> Orders { get; } = new List<Order>();
}

public class Order
{
    public int Id { get; set; }
    public string SpecialInstructions { get; set; }
    public DateTime OrderDate { get; set; }
    public bool IsComplete { get; set; }
    public Money Price { get; set; }
    public Money? Discount { get; set; }

    public Customer Customer { get; set; }
}

所有字符串属性都可以配置为 ANSI(而不是 Unicode),最大长度为 1024:

configurationBuilder
    .Properties<string>()
    .AreUnicode(false)
    .HaveMaxLength(1024);

可以使用从 DateTimes 到 longs 的默认转换,将所有 DateTime 属性转换为数据库中的 64 位整数:

configurationBuilder
    .Properties<DateTime>()
    .HaveConversion<long>();

所有布尔属性都可以转换为整数 0 或使用 1 内置值转换器之一:

configurationBuilder
    .Properties<bool>()
    .HaveConversion<BoolToZeroOneConverter<int>>();

假设 Session 是实体的暂时性属性,不应持久保存,则可以在模型中的任何地方忽略它:

configurationBuilder
    .IgnoreAny<Session>();

使用值对象时,预设模型配置非常有用。 例如,上述模型中的类型 Money 由只读结构表示:

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

然后,使用自定义值转换器将此值序列化到 JSON 和从 JSON 进行序列化:

public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter()
        : base(
            v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
            v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null))
    {
    }
}

可将此值转换器配置为适用于所有 Money 的使用场景。

configurationBuilder
    .Properties<Money>()
    .HaveConversion<MoneyConverter>()
    .HaveMaxLength(64);

另请注意,可以为存储序列化 JSON 的字符串列指定其他方面。 在这种情况下,列的最大长度限制为 64。

使用迁移为 SQL Server 创建的表显示了如何将配置应用于所有映射列:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] varchar(1024) NULL,
    [IsActive] int NOT NULL,
    [AccountValue] nvarchar(64) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [Order] (
    [Id] int NOT NULL IDENTITY,
    [SpecialInstructions] varchar(1024) NULL,
    [OrderDate] bigint NOT NULL,
    [IsComplete] int NOT NULL,
    [Price] nvarchar(64) NOT NULL,
    [Discount] nvarchar(64) NULL,
    [CustomerId] int NULL,
    CONSTRAINT [PK_Order] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Order_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id])
);

还可以为给定类型指定默认类型映射。 例如:

configurationBuilder
    .DefaultTypeMapping<string>()
    .IsUnicode(false);

这很少需要,但如果在查询中使用类型的方式与模型的任何映射属性无关,则这非常有用。

注释

如需有关预约定模型配置的更多讨论和示例,请参阅 .NET 博客中的 宣布 Entity Framework Core 6.0 Preview 6:配置约定

已编译的模型

GitHub 问题: #1906

已编译的模型可以加快具有大型模型的应用程序的 EF Core 启动时间。 大型模型通常表示 100 到 1000 个实体类型和关系。

启动时间是指首次在应用程序中使用这个类型的 DbContext时执行第一个操作的时间。 请注意,仅创建 DbContext 实例不会导致初始化 EF 模型。 相反,会导致模型初始化的典型首次操作包括调用 DbContext.Add 或执行第一个查询。

使用 dotnet ef 命令行工具创建已编译的模型。 在继续之前,请确保已安装了该工具的最新版本

dbcontext optimize 命令用于生成已编译的模型。 例如:

dotnet ef dbcontext optimize

--output-dir--namespace 选项可用于指定将在其中生成已编译的模型的目录和命名空间。 例如:

PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>

运行此命令的输出包括一段代码,用于复制并粘贴到 DbContext 配置中,从而导致 EF Core 使用已编译的模型。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

编译模型启动

通常不需要查看生成的启动代码。 但有时,这样做有助于对模型或其加载方式进行自定义。 启动代码看起来如下所示:

[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
    private static BlogsContextModel _instance;
    public static IModel Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BlogsContextModel();
                _instance.Initialize();
                _instance.Customize();
            }

            return _instance;
        }
    }

    partial void Initialize();

    partial void Customize();
}

这是一个包含分部方法的分部类,可实现这些方法来根据需要自定义模型。

此外,可以为可以使用不同模型的 DbContext 类型生成多个编译的模型,具体取决于某些运行时配置。 它们应置于不同的文件夹和命名空间中,如上所示。 然后,可以检查运行时信息(如连接字符串),并根据需要返回正确的模型。 例如:

public static class RuntimeModelCache
{
    private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
        = new();

    public static IModel GetOrCreateModel(string connectionString)
        => _runtimeModels.GetOrAdd(
            connectionString, cs =>
            {
                if (cs.Contains("X"))
                {
                    return BlogsContextModel1.Instance;
                }

                if (cs.Contains("Y"))
                {
                    return BlogsContextModel2.Instance;
                }

                throw new InvalidOperationException("No appropriate compiled model found.");
            });
}

局限性

已编译的模型有一些限制:

由于这些限制,只应在 EF Core 启动时间太慢时使用已编译的模型。 编译小型模型通常不太值得使用已编译的模型。

如果支持其中的任何功能对你的成功至关重要,那么请为上面链接的相应问题投票。

基准

小窍门

可以通过 从 GitHub 下载示例代码来尝试编译大型模型并运行基准。

上面引用的 GitHub 存储库中的模型包含 449 个实体类型、6390 个属性和 720 个关系。 这是一个中等大的模型。 使用 BenchmarkDotNet 测量,首次查询的平均时间为 1.02 秒(在相当强大的笔记本电脑上)。 使用已编译的模型,在同一硬件上,这可以降低到 117 毫秒。 模型大小增加时,8 倍到 10 倍的改进将保持相对不变。

已编译的模型性能改进

注释

请参阅 宣布 Entity Framework Core 6.0 预览版 5:已编译模型 在 .NET 博客上的文章,以便更深入地讨论 EF Core 启动性能和已编译的模型。

改进了 TechEmpower Fortunes 性能表现

GitHub 问题: #23611

我们对 EF Core 6.0 的查询性能进行了显著改进。 具体说来:

  • 与 5.0 相比,EF Core 6.0 性能在行业标准 TechEmpower 财富基准方面现在快 70%。
    • 这是全堆栈性能改进,包括基准代码、.NET 运行时等改进。
  • EF Core 6.0 本身是 31% 更快地执行未跟踪的查询。
  • 执行查询时,堆分配减少了 43%。

经过这些改进,TechEmpower 财富基准中流行的“微型ORM”Dapper与EF Core之间的差距从55%缩小到略低于5%。

注释

有关 EF Core 6.0 中查询性能改进的详细讨论,请参阅 .NET 博客上的 宣布发布 Entity Framework Core 6.0 预览版 4:性能版

Azure Cosmos DB 提供服务的增强功能

EF Core 6.0 包含对 Azure Cosmos DB 数据库提供程序的许多改进。

小窍门

可以通过 从 GitHub 下载示例代码来运行和调试所有特定于 Cosmos 的示例。

默认为隐式所有权

GitHub 问题: #24803

在构建用于 Azure Cosmos DB 提供程序的模型时,EF Core 6.0 默认将子实体类型标记为由其父实体拥有。 这消除了在 Azure Cosmos DB 模型中进行大量 OwnsManyOwnsOne 调用的需要。 这样,可以更轻松地将子类型嵌入父类型的文档中,这通常是为文档数据库中的父级和子级建模的适当方法。

例如,请考虑以下实体类型:

public class Family
{
    [JsonPropertyName("id")]
    public string Id { get; set; }

    public string LastName { get; set; }
    public bool IsRegistered { get; set; }

    public Address Address { get; set; }

    public IList<Parent> Parents { get; } = new List<Parent>();
    public IList<Child> Children { get; } = new List<Child>();
}

public class Parent
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
}

public class Child
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public string Gender { get; set; }

    public IList<Pet> Pets { get; } = new List<Pet>();
}

在 EF Core 5.0 中,这些类型针对 Azure Cosmos DB 进行了以下配置建模:

modelBuilder.Entity<Family>()
    .HasPartitionKey(e => e.LastName)
    .OwnsMany(f => f.Parents);

modelBuilder.Entity<Family>()
    .OwnsMany(f => f.Children)
    .OwnsMany(c => c.Pets);

modelBuilder.Entity<Family>()
    .OwnsOne(f => f.Address);

在 EF Core 6.0 中,所有权是隐式的,将模型配置减少到:

modelBuilder.Entity<Family>().HasPartitionKey(e => e.LastName);

生成的 Azure Cosmos DB 文档在家庭文档中嵌入了家庭的父母、子女、宠物和地址。 例如:

{
  "Id": "Wakefield.7",
  "LastName": "Wakefield",
  "Discriminator": "Family",
  "IsRegistered": true,
  "id": "Family|Wakefield.7",
  "Address": {
    "City": "NY",
    "County": "Manhattan",
    "State": "NY"
  },
  "Children": [
    {
      "FamilyName": "Merriam",
      "FirstName": "Jesse",
      "Gender": "female",
      "Grade": 8,
      "Pets": [
        {
          "GivenName": "Goofy"
        },
        {
          "GivenName": "Shadow"
        }
      ]
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Lisa",
      "Gender": "female",
      "Grade": 1,
      "Pets": []
    }
  ],
  "Parents": [
    {
      "FamilyName": "Wakefield",
      "FirstName": "Robin"
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Ben"
    }
  ],
  "_rid": "x918AKh6p20CAAAAAAAAAA==",
  "_self": "dbs/x918AA==/colls/x918AKh6p20=/docs/x918AKh6p20CAAAAAAAAAA==/",
  "_etag": "\"00000000-0000-0000-adee-87f30c8c01d7\"",
  "_attachments": "attachments/",
  "_ts": 1632121802
}

注释

请务必记住,如果需要进一步配置这些拥有的类型,则必须使用 OwnsOne/OwnsMany 配置。

基元类型的集合

GitHub 问题: #14762

使用 Azure Cosmos DB 数据库提供程序时,EF Core 6.0 可以自然地映射原始类型的集合。 例如,请考虑以下实体类型:

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

列表和字典都可以以正常方式填充并插入到数据库中:

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
await context.SaveChangesAsync();

这会导致以下 JSON 文档:

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

然后,可以按正常方式更新这些集合:

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

await context.SaveChangesAsync();

限制:

  • 仅支持包含字符串键的字典
  • 目前不支持查询基元集合的内容。 如果这些功能对你很重要,请投票支持 #16926#25700#25701

与内置函数相关的翻译

GitHub 问题: #16143

Azure Cosmos DB 提供程序现在将更多基类库 (BCL) 方法转换为 Azure Cosmos DB 内置函数。 下表显示了 EF Core 6.0 中的新增翻译。

字符串翻译

BCL 方法 内置函数 注释
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
+ 运算符 CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUALS 仅不区分大小写的调用

以下翻译LOWERLTRIMRTRIMTRIMUPPERSUBSTRING@Marusyk贡献。 非常感谢!

例如:

var stringResults = await context.Triangles.Where(
        e => e.Name.Length > 4
             && e.Name.Trim().ToLower() != "obtuse"
             && e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
    .ToListAsync();

这转换为:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((LENGTH(c["Name"]) > 4) AND (LOWER(TRIM(c["Name"])) != "obtuse")) AND STRINGEQUALS(SUBSTRING(LTRIM(c["Name"]), 2, 2), "uT", true)))

数学翻译

BCL 方法 内置函数
Math.AbsMathF.Abs ABS
Math.AcosMathF.Acos ACOS
Math.AsinMathF.Asin ASIN
Math.AtanMathF.Atan ATAN
Math.Atan2MathF.Atan2 ATN2
Math.CeilingMathF.Ceiling CEILING
Math.CosMathF.Cos COS
Math.ExpMathF.Exp EXP
Math.FloorMathF.Floor FLOOR
Math.LogMathF.Log LOG
Math.Log10MathF.Log10 LOG10
Math.PowMathF.Pow POWER
Math.RoundMathF.Round ROUND
Math.SignMathF.Sign SIGN
Math.SinMathF.Sin SIN
Math.SqrtMathF.Sqrt SQRT
Math.TanMathF.Tan TAN
Math.TruncateMathF.Truncate TRUNC
DbFunctions.Random RAND

这些翻译是由 @Marusyk贡献的。 非常感谢!

例如:

var hypotenuse = 42.42;
var mathResults = await context.Triangles.Where(
        e => (Math.Round(e.Angle1) == 90.0
              || Math.Round(e.Angle2) == 90.0)
             && (hypotenuse * Math.Sin(e.Angle1) > 30.0
                 || hypotenuse * Math.Cos(e.Angle2) > 30.0))
    .ToListAsync();

这转换为:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((ROUND(c["Angle1"]) = 90.0) OR (ROUND(c["Angle2"]) = 90.0)) AND (((@__hypotenuse_0 * SIN(c["Angle1"])) > 30.0) OR ((@__hypotenuse_0 * COS(c["Angle2"])) > 30.0))))

DateTime 翻译

BCL 方法 内置函数
DateTime.UtcNow GetCurrentDateTime

这些翻译是由 @Marusyk贡献的。 非常感谢!

例如:

var timeResults = await context.Triangles.Where(
        e => e.InsertedOn <= DateTime.UtcNow)
    .ToListAsync();

这转换为:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (c["InsertedOn"] <= GetCurrentDateTime()))

使用 FromSql 的原始 SQL 查询

GitHub 问题: #17311

有时需要执行原始 SQL 查询,而不是使用 LINQ。 现在,可以通过 Azure Cosmos DB 提供程序使用 FromSql 方法来支持这一功能。 它的工作方式与关系提供程序一直以来的工作方式相同。 例如:

var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
        @"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
    .ToListAsync();

执行如下:

SELECT c
FROM (
    SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c

不同的查询

GitHub 问题: #16144

现在简单查询使用 Distinct 会被翻译。 例如:

var distinctResults = await context.Triangles
    .Select(e => e.Angle1).OrderBy(e => e).Distinct()
    .ToListAsync();

这转换为:

SELECT DISTINCT c["Angle1"]
FROM root c
WHERE (c["Discriminator"] = "Triangle")
ORDER BY c["Angle1"]

诊断

GitHub 问题: #17298

Azure Cosmos DB 提供程序现在记录更多诊断信息,包括用于插入、查询、更新和删除数据库中的数据的事件。 只要在适当情况下,请求单位(RU)就会包含在这些事件中。

注释

此处显示的日志使用 EnableSensitiveDataLogging() ,以便显示 ID 值。

将项插入 Azure Cosmos DB 数据库将生成事件 CosmosEventId.ExecutedCreateItem 。 例如,此代码:

var triangle = new Triangle
{
    Name = "Impossible",
    PartitionKey = "TrianglesPartition",
    Angle1 = 90,
    Angle2 = 90,
    InsertedOn = DateTime.UtcNow
};
context.Add(triangle);
await context.SaveChangesAsync();

记录以下诊断事件:

info: 8/30/2021 14:41:13.356 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed CreateItem (5 ms, 7.43 RU) ActivityId='417db46f-fcdd-49d9-a7f0-77210cd06f84', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

使用查询从 Azure Cosmos DB 数据库中检索项会生成 CosmosEventId.ExecutingSqlQuery 事件,并且为所读取的项生成一个或多个 CosmosEventId.ExecutedReadNext 事件。 例如,此代码:

var equilateral = await context.Triangles.SingleAsync(e => e.Name == "Equilateral");

记录以下诊断事件:

info: 8/30/2021 14:41:13.475 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing SQL query for container 'Shapes' in partition '(null)' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2
info: 8/30/2021 14:41:13.651 CosmosEventId.ExecutedReadNext[30102] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadNext (169.6126 ms, 2.93 RU) ActivityId='4e465fae-3d49-4c1f-bd04-142bc5d0b0a1', Container='Shapes', Partition='(null)', Parameters=[]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2

使用Find 和分区键从 Azure Cosmos DB 数据库检索单个项会生成CosmosEventId.ExecutingReadItemCosmosEventId.ExecutedReadItem事件。 例如,此代码:

var isosceles = await context.Triangles.FindAsync("Isosceles", "TrianglesPartition");

记录以下诊断事件:

info: 8/30/2021 14:53:39.326 CosmosEventId.ExecutingReadItem[30101] (Microsoft.EntityFrameworkCore.Database.Command)
      Reading resource 'Isosceles' item from container 'Shapes' in partition 'TrianglesPartition'.
info: 8/30/2021 14:53:39.330 CosmosEventId.ExecutedReadItem[30103] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadItem (1 ms, 1 RU) ActivityId='3c278643-4e7f-4bb2-9953-6055b5f1288f', Container='Shapes', Id='Isosceles', Partition='TrianglesPartition'

将更新的项保存到 Azure Cosmos DB 数据库时,会生成 CosmosEventId.ExecutedReplaceItem 事件。 例如,此代码:

triangle.Angle2 = 89;
await context.SaveChangesAsync();

记录以下诊断事件:

info: 8/30/2021 14:53:39.343 CosmosEventId.ExecutedReplaceItem[30105] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReplaceItem (6 ms, 10.67 RU) ActivityId='1525b958-fea1-49e8-89f9-d429d0351fdb', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

从 Azure Cosmos DB 数据库中删除项将生成事件 CosmosEventId.ExecutedDeleteItem 。 例如,此代码:

context.Remove(triangle);
await context.SaveChangesAsync();

记录以下诊断事件:

info: 8/30/2021 14:53:39.359 CosmosEventId.ExecutedDeleteItem[30106] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DeleteItem (6 ms, 7.43 RU) ActivityId='cbc54463-405b-48e7-8c32-2c6502a4138f', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

配置吞吐量

GitHub 问题: #17301

Azure Cosmos DB 模型现在可以配置为手动或自动缩放吞吐量。 这些值为数据库配置吞吐量。 例如:

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

此外,可以将各个实体类型配置为为相应的容器预配吞吐量。 例如:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

配置生存时限

GitHub 问题: #17307

现在,可以为 Azure Cosmos DB 模型中的实体类型配置默认生存时间,以及分析存储的生存时间。 例如:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasDefaultTimeToLive(100);
        entityTypeBuilder.HasAnalyticalStoreTimeToLive(200);
    });

解决 HTTP 客户端工厂问题

GitHub 问题: #21274。 此功能由 @dnperfors提供。 非常感谢!

HttpClientFactory提供程序现在可以显式设置 Azure Cosmos DB 的使用配置。 这在测试期间特别有用,例如,在 Linux 上使用 Azure Cosmos DB 模拟器时绕过证书验证:

optionsBuilder
    .EnableSensitiveDataLogging()
    .UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        "PrimitiveCollections",
        cosmosOptionsBuilder =>
        {
            cosmosOptionsBuilder.HttpClientFactory(
                () => new HttpClient(
                    new HttpClientHandler
                    {
                        ServerCertificateCustomValidationCallback =
                            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
                    }));
        });

注释

有关将 EF Core Azure Cosmos DB 提供程序 应用于现有应用程序的详细示例,请参阅 .NET 博客上的文章《试驾 EF Core Azure Cosmos DB 提供程序》,了解提供程序的改进。

对现有数据库中基架的改进

从现有数据库反向工程 EF 模型时,EF Core 6.0 包含多项改进。

构建多对多关系的框架

GitHub 问题: #22475

EF Core 6.0 能够检测出简单的联接表,并自动为其生成多对多映射。 例如,考虑PostsTags表,以及连接它们的联接表PostTag

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE);

可以从命令行搭建这些表。 例如:

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=BloggingWithNRTs" Microsoft.EntityFrameworkCore.SqlServer

这将生成一个关于 Post 的类:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

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

以及标记的类:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

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

但是对 PostTag 表没有分类。 相反,多对多关系的配置是基架:

entity.HasMany(d => d.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
        r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
        j =>
            {
                j.HasKey("PostsId", "TagsId");
                j.ToTable("PostTag");
                j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
            });

基架 C# 可为 null 的引用类型

GitHub 问题: #15520

EF Core 6.0 现在为 EF 模型和实体类型搭建基架,该模型和实体类型使用 C# 可为 null 的引用类型(NRT)。 在将代码搭建到的 C# 项目中启用 NRT 支持时,会自动搭建 NRT 用法。

例如,下表 Tags 包含可为 null 的字符串列和不可为 null 的字符串列:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

这会导致生成的类中的相应可为 null 和不可为 null 的字符串属性:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

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

同样,下列 Posts 表格包含与 Blogs 表的必需关系:

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

这会导致博客之间不可为 null 且必需的关系结构:

public partial class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;

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

和帖子:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

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

最后,生成的 DbContext 中的 DbSet 属性以 NRT 友好的方式创建。 例如:

public virtual DbSet<Blog> Blogs { get; set; } = null!;
public virtual DbSet<Post> Posts { get; set; } = null!;
public virtual DbSet<Tag> Tags { get; set; } = null!;

数据库注释是代码注释的基架

GitHub 问题: #19113。 此功能由 @ErikEJ提供。 非常感谢!

SQL 表和列的注释现在已基架到从现有 SQL Server 数据库 反向工程 EF Core 模型 时创建的实体类型。

/// <summary>
/// The Blog table.
/// </summary>
public partial class Blog
{
    /// <summary>
    /// The primary key.
    /// </summary>
    [Key]
    public int Id { get; set; }
}

LINQ 查询增强功能

EF Core 6.0 在 LINQ 查询的翻译和执行方面进行了多项改进。

改进了 GroupBy 支持

GitHub 问题: #12088#13805#22609

EF Core 6.0 对 GroupBy 查询提供了更好的支持。 具体而言,EF Core 现在:

  • 使用 GroupBy,再加上 FirstOrDefault(或类似)应用于一个组。
  • 支持从组中选择前 N 个结果
  • 应用运算符后展开GroupBy导航

下面是来自客户报表及其在 SQL Server 上的翻译的示例查询。

示例 1:

var people = await context.People
    .Include(e => e.Shoes)
    .GroupBy(e => e.FirstName)
    .Select(
        g => g.OrderBy(e => e.FirstName)
            .ThenBy(e => e.LastName)
            .FirstOrDefault())
    .ToListAsync();
SELECT [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t].[FirstName], [s].[Id], [s].[Age], [s].[PersonId], [s].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName], [p0].[LastName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
LEFT JOIN [Shoes] AS [s] ON [t0].[Id] = [s].[PersonId]
ORDER BY [t].[FirstName], [t0].[FirstName]

示例 2:

var group = await context.People
    .Select(
        p => new
        {
            p.FirstName,
            FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
        })
    .GroupBy(p => p.FirstName)
    .Select(g => g.First())
    .FirstAsync();
SELECT [t0].[FirstName], [t0].[FullName], [t0].[c]
FROM (
    SELECT TOP(1) [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[FirstName], [t1].[FullName], [t1].[c]
    FROM (
        SELECT [p0].[FirstName], (((COALESCE([p0].[FirstName], N'') + N' ') + COALESCE([p0].[MiddleInitial], N'')) + N' ') + COALESCE([p0].[LastName], N'') AS [FullName], 1 AS [c], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]

示例 3:

var people = await context.People
    .Where(e => e.MiddleInitial == "Q" && e.Age == 20)
    .GroupBy(e => e.LastName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e.Length)
    .ToListAsync();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))
FROM [People] AS [p]
WHERE ([p].[MiddleInitial] = N'Q') AND ([p].[Age] = 20)
GROUP BY [p].[LastName]
ORDER BY CAST(LEN((
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))) AS int)

示例 4

var results = await (from person in context.People
               join shoes in context.Shoes on person.Age equals shoes.Age
               group shoes by shoes.Style
               into people
               select new
               {
                   people.Key,
                   Style = people.Select(p => p.Style).FirstOrDefault(),
                   Count = people.Count()
               })
    .ToListAsync();
SELECT [s].[Style] AS [Key], (
    SELECT TOP(1) [s0].[Style]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
    WHERE ([s].[Style] = [s0].[Style]) OR ([s].[Style] IS NULL AND [s0].[Style] IS NULL)) AS [Style], COUNT(*) AS [Count]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [s].[Style]

示例 5:

var results = await context.People
    .GroupBy(e => e.FirstName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e)
    .ToListAsync();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
FROM [People] AS [p]
GROUP BY [p].[FirstName]
ORDER BY (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))

示例 6:

var results = await context.People
    .Where(e => e.Age == 20)
    .GroupBy(e => e.Id)
    .Select(g => g.First().MiddleInitial)
    .OrderBy(e => e)
    .ToListAsync();
SELECT (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
FROM [People] AS [p]
WHERE [p].[Age] = 20
GROUP BY [p].[Id]
ORDER BY (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))

示例 7:

var size = 11;
var results
    = await context.People
        .Where(
            p => p.Feet.Size == size
                 && p.MiddleInitial != null
                 && p.Feet.Id != 1)
        .GroupBy(
            p => new
            {
                p.Feet.Size,
                p.Feet.Person.LastName
            })
        .Select(
            g => new
            {
                g.Key.LastName,
                g.Key.Size,
                Min = g.Min(p => p.Feet.Size),
            })
        .ToListAsync();
Executed DbCommand (12ms) [Parameters=[@__size_0='11'], CommandType='Text', CommandTimeout='30']
SELECT [p0].[LastName], [f].[Size], MIN([f0].[Size]) AS [Min]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
LEFT JOIN [People] AS [p0] ON [f].[Id] = [p0].[Id]
LEFT JOIN [Feet] AS [f0] ON [p].[Id] = [f0].[Id]
WHERE (([f].[Size] = @__size_0) AND [p].[MiddleInitial] IS NOT NULL) AND (([f].[Id] <> 1) OR [f].[Id] IS NULL)
GROUP BY [f].[Size], [p0].[LastName]

示例 8:

var result = await context.People
    .Include(x => x.Shoes)
    .Include(x => x.Feet)
    .GroupBy(
        x => new
        {
            x.Feet.Id,
            x.Feet.Size
        })
    .Select(
        x => new
        {
            Key = x.Key.Id + x.Key.Size,
            Count = x.Count(),
            Sum = x.Sum(el => el.Id),
            SumOver60 = x.Sum(el => el.Id) / (decimal)60,
            TotalCallOutCharges = x.Sum(el => el.Feet.Size == 11 ? 1 : 0)
        })
    .CountAsync();
SELECT COUNT(*)
FROM (
    SELECT [f].[Id], [f].[Size]
    FROM [People] AS [p]
    LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
    GROUP BY [f].[Id], [f].[Size]
) AS [t]

示例 9:

var results = await context.People
    .GroupBy(n => n.FirstName)
    .Select(g => new
    {
        Feet = g.Key,
        Total = g.Sum(n => n.Feet.Size)
    })
    .ToListAsync();
SELECT [p].[FirstName] AS [Feet], COALESCE(SUM([f].[Size]), 0) AS [Total]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [p].[FirstName]

示例 10:

var results = from Person person1
                  in from Person person2
                         in context.People
                     select person2
              join Shoes shoes
                  in context.Shoes
                  on person1.Age equals shoes.Age
              group shoes by
                  new
                  {
                      person1.Id,
                      shoes.Style,
                      shoes.Age
                  }
              into temp
              select
                  new
                  {
                      temp.Key.Id,
                      temp.Key.Age,
                      temp.Key.Style,
                      Values = from t
                                   in temp
                               select
                                   new
                                   {
                                       t.Id,
                                       t.Style,
                                       t.Age
                                   }
                  };
SELECT [t].[Id], [t].[Age], [t].[Style], [t0].[Id], [t0].[Style], [t0].[Age], [t0].[Id0]
FROM (
    SELECT [p].[Id], [s].[Age], [s].[Style]
    FROM [People] AS [p]
    INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
    GROUP BY [p].[Id], [s].[Style], [s].[Age]
) AS [t]
LEFT JOIN (
    SELECT [s0].[Id], [s0].[Style], [s0].[Age], [p0].[Id] AS [Id0]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
) AS [t0] ON (([t].[Id] = [t0].[Id0]) AND (([t].[Style] = [t0].[Style]) OR ([t].[Style] IS NULL AND [t0].[Style] IS NULL))) AND ([t].[Age] = [t0].[Age])
ORDER BY [t].[Id], [t].[Style], [t].[Age], [t0].[Id0]

示例 11:

var grouping = await context.People
    .GroupBy(i => i.LastName)
    .Select(g => new { LastName = g.Key, Count = g.Count() , First = g.FirstOrDefault(), Take = g.Take(2)})
    .OrderByDescending(e => e.LastName)
    .ToListAsync();
SELECT [t].[LastName], [t].[c], [t0].[Id], [t2].[Id], [t2].[Age], [t2].[FirstName], [t2].[LastName], [t2].[MiddleInitial], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial]
FROM (
    SELECT [p].[LastName], COUNT(*) AS [c]
    FROM [People] AS [p]
    GROUP BY [p].[LastName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[LastName] ORDER BY [p0].[Id]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[LastName] = [t0].[LastName]
LEFT JOIN (
    SELECT [t3].[Id], [t3].[Age], [t3].[FirstName], [t3].[LastName], [t3].[MiddleInitial]
    FROM (
        SELECT [p1].[Id], [p1].[Age], [p1].[FirstName], [p1].[LastName], [p1].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p1].[LastName] ORDER BY [p1].[Id]) AS [row]
        FROM [People] AS [p1]
    ) AS [t3]
    WHERE [t3].[row] <= 2
) AS [t2] ON [t].[LastName] = [t2].[LastName]
ORDER BY [t].[LastName] DESC, [t0].[Id], [t2].[LastName], [t2].[Id]

示例 12:

var grouping = await context.People
    .Include(e => e.Shoes)
    .OrderBy(e => e.FirstName)
    .ThenBy(e => e.LastName)
    .GroupBy(e => e.FirstName)
    .Select(g => new { Name = g.Key, People = g.ToList()})
    .ToListAsync();
SELECT [t].[FirstName], [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t0].[Id0], [t0].[Age0], [t0].[PersonId], [t0].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], [s].[Id] AS [Id0], [s].[Age] AS [Age0], [s].[PersonId], [s].[Style]
    FROM [People] AS [p0]
    LEFT JOIN [Shoes] AS [s] ON [p0].[Id] = [s].[PersonId]
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
ORDER BY [t].[FirstName], [t0].[Id]

示例 13:

var grouping = await context.People
    .GroupBy(m => new {m.FirstName, m.MiddleInitial })
    .Select(am => new
    {
        Key = am.Key,
        Items = am.ToList()
    })
    .ToListAsync();
SELECT [t].[FirstName], [t].[MiddleInitial], [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial]
FROM (
    SELECT [p].[FirstName], [p].[MiddleInitial]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName], [p].[MiddleInitial]
) AS [t]
LEFT JOIN [People] AS [p0] ON (([t].[FirstName] = [p0].[FirstName]) OR ([t].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AND (([t].[MiddleInitial] = [p0].[MiddleInitial]) OR ([t].[MiddleInitial] IS NULL AND [p0].[MiddleInitial] IS NULL))
ORDER BY [t].[FirstName], [t].[MiddleInitial]

型号

用于这些示例的实体类型包括:

public class Person
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MiddleInitial { get; set; }
    public Feet Feet { get; set; }
    public ICollection<Shoes> Shoes { get; } = new List<Shoes>();
}

public class Shoes
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string Style { get; set; }
    public Person Person { get; set; }
}

public class Feet
{
    public int Id { get; set; }
    public int Size { get; set; }
    public Person Person { get; set; }
}

使用多个参数转换 String.Concat

GitHub 问题: #23859。 此功能由 @wmeints提供。 非常感谢!

从 EF Core 6.0 开始,对 String.Concat 的多个参数调用已转化为 SQL 语句。 例如,以下查询:

var shards = await context.Shards
    .Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToListAsync();

使用 SQL Server 时,将转换为以下 SQL:

SELECT [s].[Id], [s].[Token1], [s].[Token2], [s].[Token3], [s].[TokensProcessed]
FROM [Shards] AS [s]
WHERE (([s].[Token1] + ([s].[Token2] + [s].[Token3])) <> [s].[TokensProcessed]) OR [s].[TokensProcessed] IS NULL

与 System.Linq.Async 的平滑集成

GitHub 问题: #24041

System.Linq.Async 包添加了客户端异步 LINQ 处理。 由于异步 LINQ 方法的命名空间冲突,将此包与以前版本的 EF Core 一起使用很麻烦。 在 EF Core 6.0 中,我们利用了 C# 的模式匹配来处理 IAsyncEnumerable<T> ,从而使公开的 EF Core DbSet<TEntity> 不需要直接实现该接口。

请注意,大多数应用程序不需要使用 System.Linq.Async,因为 EF Core 查询通常在服务器上完全翻译。

GitHub 问题: #23921

在 EF Core 6.0 中,我们放宽了参数要求 FreeText(DbFunctions, String, String)Contains。 这样,这些函数就可以与二进制列或使用值转换器映射的列一起使用。 例如,假设实体类型具有 Name 定义为值对象的属性:

public class Customer
{
    public int Id { get; set; }

    public Name Name{ get; set; }
}

public class Name
{
    public string First { get; set; }
    public string MiddleInitial { get; set; }
    public string Last { get; set; }
}

在数据库中映射为 JSON:

modelBuilder.Entity<Customer>()
    .Property(e => e.Name)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<Name>(v, (JsonSerializerOptions)null));

现在可以使用ContainsFreeText执行查询,即使属性Name的类型不是string。 例如:

var result = await context.Customers.Where(e => EF.Functions.Contains(e.Name, "Martin")).ToListAsync();

使用 SQL Server 时,这会生成以下 SQL:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE CONTAINS([c].[Name], N'Martin')

在 SQLite 中使用 ToString 转换字符串

GitHub 问题: #17223。 此功能由 @ralmsdeveloper提供。 非常感谢!

现在,在使用 SQLite 数据库提供程序时,对 ToString() 的调用会被翻译为 SQL。 这对于涉及非字符串列的文本搜索非常有用。 例如,请考虑将 User 电话号码存储为数值的实体类型:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public long PhoneNumber { get; set; }
}

ToString 可用于将数字转换为数据库中的字符串。 然后,我们可以将此字符串与函数一起使用,例如 LIKE 查找与模式匹配的数字。 例如,若要查找包含 555 的所有数字,

var users = await context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToListAsync();

这在使用 SQLite 数据库时转换为以下 SQL:

SELECT "u"."Id", "u"."PhoneNumber", "u"."Username"
FROM "Users" AS "u"
WHERE CAST("u"."PhoneNumber" AS TEXT) LIKE '%555%'

请注意,EF Core 5.0 中已支持 SQL Server 的 ToString() 转换,其他数据库提供程序可能也支持转换。

EF.Functions.Random

GitHub 问题: #16141。 此功能由 @RaymondHuy提供。 非常感谢!

EF.Functions.Random 映射到返回介于 0 和 1 之间的伪随机数的数据库函数。 已在适用于 SQL Server、SQLite 和 Azure Cosmos DB 的 EF Core 存储库中实现翻译。 例如,考虑具有 User 属性的 Popularity 实体类型:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public int Popularity { get; set; }
}

Popularity 可以具有 1 到 5 之间的值。 使用 EF.Functions.Random 我们可以编写查询,以返回具有随机选择受欢迎度的所有用户:

var users = await context.Users.Where(u => u.Popularity == (int)(EF.Functions.Random() * 4.0) + 1).ToListAsync();

这在使用 SQL Server 数据库时转换为以下 SQL:

SELECT [u].[Id], [u].[Popularity], [u].[Username]
FROM [Users] AS [u]
WHERE [u].[Popularity] = (CAST((RAND() * 4.0E0) AS int) + 1)

改进了 IsNullOrWhitespace 的 SQL Server 翻译

GitHub 问题: #22916。 此功能由 @Marusyk提供。 非常感谢!

请考虑下列查询:

var users = await context.Users.Where(
    e => string.IsNullOrWhiteSpace(e.FirstName)
         || string.IsNullOrWhiteSpace(e.LastName)).ToListAsync();

在 EF Core 6.0 之前,这已转换为 SQL Server 上的以下内容:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR (LTRIM(RTRIM([u].[FirstName])) = N'')) OR ([u].[LastName] IS NULL OR (LTRIM(RTRIM([u].[LastName])) = N''))

EF Core 6.0 的此翻译已改进为:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR ([u].[FirstName] = N'')) OR ([u].[LastName] IS NULL OR ([u].[LastName] = N''))

定义用于内存提供程序的查询

GitHub 问题: #24600

新方法 ToInMemoryQuery 可用于针对给定实体类型的内存中数据库编写定义查询。 这最适用于在内存中数据库上创建等效视图,尤其是在这些视图返回无键实体类型时。 例如,考虑针对位于英国的客户的客户数据库。 每个客户都有一个地址:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

现在,假设我们想要查看此数据,其中显示了每个邮政编码区域中有多少客户。 我们可以创建一个无键的实体类型来表示这个。

public class CustomerDensity
{
    public string Postcode { get; set; }
    public int CustomerCount { get; set; }
}

并在 DbContext 上为它定义一个 DbSet 属性,并为其他顶级实体类型定义 DbSet 属性:

public DbSet<Customer> Customers { get; set; }
public DbSet<CustomerDensity> CustomerDensities { get; set; }

然后,在 OnModelCreating,我们可以编写一个 LINQ 查询来定义要返回的数据 CustomerDensities

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<CustomerDensity>()
        .HasNoKey()
        .ToInMemoryQuery(
            () => Customers
                .GroupBy(c => c.Address.Postcode.Substring(0, 3))
                .Select(
                    g =>
                        new CustomerDensity
                        {
                            Postcode = g.Key,
                            CustomerCount = g.Count()
                        }));
}

然后,可以像查询任何其他 DbSet 属性一样进行查询:

var results = await context.CustomerDensities.ToListAsync();

使用单个参数转换子字符串

GitHub 问题: #20173。 此功能由 @stevendarby提供。 非常感谢!

EF Core 6.0 现在支持翻译包含单个参数的 string.Substring 用法。 例如:

var result = await context.Customers
    .Select(a => new { Name = a.Name.Substring(3) })
    .FirstOrDefaultAsync(a => a.Name == "hur");

这在使用 SQL Server 时转换为以下 SQL:

SELECT TOP(1) SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) AS [Name]
FROM [Customers] AS [c]
WHERE SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) = N'hur'

非导航集合的拆分查询

GitHub 问题: #21234

EF Core 支持将单个 LINQ 查询拆分为多个 SQL 查询。 在 EF Core 6.0 中,这项支持已被扩展,其中包括查询投影中包含非导航集合的情况。

以下是一些示例查询,展示在 SQL Server 中如何把操作转换为单个查询或多个查询。

示例 1:

LINQ 查询:

await context.Customers
    .Select(
        c => new
        {
            c,
            Orders = c.Orders
                .Where(o => o.Id > 1)
        })
    .ToListAsync();

单个 SQL 查询:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

多个 SQL 查询:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

示例 2:

LINQ 查询:

await context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
        })
    .ToListAsync();

单个 SQL 查询:

SELECT [c].[Id], [t].[OrderDate], [t].[Id]
FROM [Customers] AS [c]
  LEFT JOIN (
  SELECT [o].[OrderDate], [o].[Id], [o].[CustomerId]
  FROM [Order] AS [o]
  WHERE [o].[Id] > 1
  ) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

多个 SQL 查询:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

示例 3:

LINQ 查询:

await context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
                .Distinct()
        })
    .ToListAsync();

单个 SQL 查询:

SELECT [c].[Id], [t].[OrderDate]
FROM [Customers] AS [c]
  OUTER APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

多个 SQL 查询:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
  CROSS APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

在对集合进行联接时删除最后一个 ORDER BY 子句

GitHub 问题: #19828

在加载一对多相关实体时,EF Core 会添加 ORDER BY 子句,以确保某个实体的所有相关实体能够被组合在一起。 不过,EF 在生成所需分组时并不需要最后一个 ORDER BY 子句,这可能会影响性能。 因此,EF Core 6.0 会删除此子句。

例如,请考虑以下查询:

await context.Customers
    .Select(
        e => new
        {
            e.Id,
            FirstOrder = e.Orders.Where(i => i.Id == 1).ToList()
        })
    .ToListAsync();

在 SQL Server 上使用 EF Core 5.0,此查询将转换为:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id], [t].[Id]

使用 EF Core 6.0,改为翻译为:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

使用文件名和行号标记查询

GitHub 问题: #14176。 此功能由 @michalczerwinski提供。 非常感谢!

查询标签允许将文本标签添加到 LINQ 查询,从而将其包含在生成的 SQL 查询中。 在 EF Core 6.0 中,这可用于使用 LINQ 代码的文件名和行号标记查询。 例如:

var results1 = await context
    .Customers
    .TagWithCallSite()
    .Where(c => c.Name.StartsWith("A"))
    .ToListAsync();

这会导致使用 SQL Server 时生成以下 SQL:

-- file: C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\TagWithFileAndLineSample.cs:21

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N'A%')

对拥有的可选依赖处理的更改

GitHub 问题: #24558

当可选依赖实体与其主体实体共享一个表时,要知道该可选依赖实体是否存在就变得很棘手。 这是因为在表中有一行是为依赖项准备的,因为无论依赖项是否存在,主体都需要它。 明确处理此问题的方法是确保依赖者至少有一个必需属性。 由于必需的属性不能为 null,这意味着如果该属性的列中的值为 null,则依赖实体不存在。

例如,考虑一个Customer类,其中每个客户都有一个拥有的Address

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

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

地址是可选的,这意味着可以有效地保存没有地址的客户。

context.Customers1.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

但是,如果客户确实有一个地址,则该地址必须至少具有非 null 邮政编码:

context.Customers1.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
        {
            Postcode = "AN1 1PL",
        }
    });

通过将 Postcode 属性标记为 Required 来确保这一点。

现在,当查询客户时,如果 Postcode 列为 null,则表示客户没有地址,并且 Customer.Address 导航属性也为 null。 例如,遍历客户并检查其地址是否为 null:

await foreach (var customer in context.Customers1.AsAsyncEnumerable())
{
    Console.Write(customer.Name);

    if (customer.Address == null)
    {
        Console.WriteLine(" has no address.");
    }
    else
    {
        Console.WriteLine($" has postcode {customer.Address.Postcode}.");
    }
}

生成以下结果:

Foul Ole Ron has no address.
Havelock Vetinari has postcode AN1 1PL.

请考虑以下情况:不需要与地址相关的任何属性。

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

现在可以保存没有地址的客户,也可以保存所有地址属性均为空的客户。

context.Customers2.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

context.Customers2.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
    });

但是,在数据库中,这两种情况不可区分,因为我们可以通过直接查询数据库列来查看:

Id  Name               House   Street  City    Postcode
1   Foul Ole Ron       NULL    NULL    NULL    NULL
2   Havelock Vetinari  NULL    NULL    NULL    NULL

因此,EF Core 6.0 现在会在保存其所有属性为 null 的可选依赖项时发出警告。 例如:

warn:9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) 类型为“Address”的实体,其主键值为 {CustomerId:-2147482646} 是作为可选依赖项通过表共享使用的。 该实体没有任何具有非默认值的属性,用于标识实体是否存在。 这意味着在查询时,不会创建对象实例,而是用所有属性设置为默认值的实例。 任何嵌套依赖项也将丢失。 不要保存任何仅具有默认值的实例,也不会在模型中根据需要标记传入导航。

这变得更加棘手,因为可选依赖项本身还充当另一个可选依赖项的主要角色,且它们都映射到同一个表中。 EF Core 6.0 不仅仅是警告,而且禁止嵌套的可选依赖项的某些情况。 例如,请考虑以下模型,其中 ContactInfoCustomer 拥有,而 Address 又由 ContactInfo 拥有。

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class ContactInfo
{
    public string Phone { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

现在,如果 ContactInfo.Phone 为 null,并且关系是可选的,那么即使地址本身可能有数据,EF Core 也不会创建 Address 实例。 对于此类模型,EF Core 6.0 将引发以下异常:

System.InvalidOperationException:实体类型“ContactInfo”是可选的依赖类型,使用表共享并包含其他依赖项,无需任何必需的非共享属性来标识实体是否存在。 如果数据库中所有可以为 null 的属性都包含 null 值,则不会在查询中创建对象实例,从而导致嵌套依赖项的值丢失。 添加必需属性以创建具有其他属性的 null 值的实例,或根据需要标记传入导航以始终创建实例。

此处的底线是避免以下情况:可选依赖属性可以包含所有可为 null 的属性值,并与其主体共享表。 有三种简单的方法可以避免这种情况:

  1. 将依赖项设为必需项。 这意味着,即使其所有属性均为 null,依赖实体在查询后始终有一个值。
  2. 请确保依赖属性至少包含一个必需的属性,如上所述。
  3. 将可选依赖项保存到自己的表,而不是与主体共享表。

可以使用依赖项导航上的 Required 属性,将其设置为必需项:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

或者,可以通过在 OnModelCreating 中指定来要求它是必需的。

modelBuilder.Entity<WithRequiredNavigation.Customer>(
    b =>
        {
            b.OwnsOne(e => e.Address);
            b.Navigation(e => e.Address).IsRequired();
        });

可以通过指定用于 OnModelCreating 的表,将依赖项保存到不同的表中。

modelBuilder
    .Entity<WithDifferentTable.Customer>(
        b =>
            {
                b.ToTable("Customers");
                b.OwnsOne(
                    e => e.Address,
                    b => b.ToTable("CustomerAddresses"));
            });

有关可选依赖项的更多示例,请参阅 GitHub 中的 OptionalDependentsSample ,包括嵌套的可选依赖项的情况。

新的映射属性

EF Core 6.0 包含多个可应用于代码的新属性,以更改映射到数据库的方式。

Unicode属性

GitHub 问题: #19794。 此功能由 @RaymondHuy提供。 非常感谢!

从 EF Core 6.0 开始,现在可以使用映射属性将字符串属性映射到非 Unicode 列, 而无需直接指定数据库类型。 例如,假设实体 Book 类型具有“ISBN 978-3-16-148410-0”格式的 国际标准书号(ISBN) 的属性:

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }

    [Unicode(false)]
    [MaxLength(22)]
    public string Isbn { get; set; }
}

由于 ISBN 不能包含任何非 unicode 字符,因此该 Unicode 属性将导致使用非 Unicode 字符串类型。 此外, MaxLength 还用于限制数据库列的大小。 例如,使用 SQL Server 时,这会导致数据库列:varchar(22)

CREATE TABLE [Book] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NULL,
    [Isbn] varchar(22) NULL,
    CONSTRAINT [PK_Book] PRIMARY KEY ([Id]));

注释

默认情况下,EF Core 将字符串属性映射到 Unicode 列。 UnicodeAttribute 当数据库系统仅支持 Unicode 类型时,将忽略 。

PrecisionAttribute

GitHub 问题: #17914。 此功能由 @RaymondHuy提供。 非常感谢!

现在可以使用映射属性配置数据库列的精度和规模,而无需直接指定数据库类型。 例如,请考虑具有 Product 十进制 Price 属性的实体类型:

public class Product
{
    public int Id { get; set; }

    [Precision(precision: 10, scale: 2)]
    public decimal Price { get; set; }
}

EF Core 会将此属性映射到精度为 10 且刻度为 2 的数据库列。 例如,在 SQL Server 上:

CREATE TABLE [Product] (
    [Id] int NOT NULL IDENTITY,
    [Price] decimal(10,2) NOT NULL,
    CONSTRAINT [PK_Product] PRIMARY KEY ([Id]));

实体类型配置属性

GitHub 问题: #23163。 此功能由 @KaloyanIT提供。 非常感谢!

IEntityTypeConfiguration<TEntity> 实例允许 ModelBuilder 每个实体类型的配置包含在其自己的配置类中。 例如:

public class BookConfiguration : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder
            .Property(e => e.Isbn)
            .IsUnicode(false)
            .HasMaxLength(22);
    }
}

通常,必须实例化和调用 DbContext.OnModelCreating此配置类。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    new BookConfiguration().Configure(modelBuilder.Entity<Book>());
}

从 EF Core 6.0 开始,可以将一个 EntityTypeConfigurationAttribute 放置在实体类型上,以便 EF Core 可以找到和使用适当的配置。 例如:

[EntityTypeConfiguration(typeof(BookConfiguration))]
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Isbn { get; set; }
}

此属性意味着每当Book实体类型包含在模型中时,EF Core 都将使用指定的IEntityTypeConfiguration实现。 实体类型通过常规机制之一包含在模型中。 例如,通过为实体类型创建 DbSet<TEntity> 属性:

public class BooksContext : DbContext
{
    public DbSet<Book> Books { get; set; }

    //...

或者通过在 OnModelCreating 注册它。

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

注释

EntityTypeConfigurationAttribute 类型不会在程序集中自动被发现。 实体类型必须添加到模型中,然后才能在该实体类型上发现该属性。

模型生成改进

除了新的映射属性,EF Core 6.0 还包含对模型生成过程的其他一些改进。

支持 SQL Server 稀疏列

GitHub 问题: #8023

SQL Server 稀疏列 是经过优化以存储 null 值的普通列。 当使用 TPH 继承映射时,这可能会很有用,因为很少使用的子类型的属性会使表中大多数行产生空列值。 例如,请考虑一个 ForumModerator 类,它从 ForumUser 继承:

public class ForumUser
{
    public int Id { get; set; }
    public string Username { get; set; }
}

public class ForumModerator : ForumUser
{
    public string ForumName { get; set; }
}

可能有数百万用户,其中只有少数是审查者。 这意味着将 ForumName 映射为稀疏可能在此处是合理的。 现在可以使用IsSparseOnModelCreating中进行配置。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<ForumModerator>()
        .Property(e => e.ForumName)
        .IsSparse();
}

然后,EF Core 迁移会将列标记为稀疏。 例如:

CREATE TABLE [ForumUser] (
    [Id] int NOT NULL IDENTITY,
    [Username] nvarchar(max) NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [ForumName] nvarchar(max) SPARSE NULL,
    CONSTRAINT [PK_ForumUser] PRIMARY KEY ([Id]));

注释

稀疏列具有限制。 请务必阅读 SQL Server 稀疏列文档 ,以确保稀疏列适合你的方案。

HasConversion API 的改进

GitHub 问题: #25468

在 EF Core 6.0 之前,方法的 HasConversion 泛型重载使用泛型参数指定要 转换为的类型。 例如,请考虑枚举 Currency:

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

EF Core 可配置为将此枚举的值另存为字符串“UsDollars”、“PoundsStirling”和“Euros”。HasConversion<string> 例如:

modelBuilder.Entity<TestEntity1>()
    .Property(e => e.Currency)
    .HasConversion<string>();

从 EF Core 6.0 开始,泛型类型可以改为指定 值转换器类型。 这可以是内置值转换器之一。 例如,若要将枚举值存储为数据库中的 16 位数字:

modelBuilder.Entity<TestEntity2>()
    .Property(e => e.Currency)
    .HasConversion<EnumToNumberConverter<Currency, short>>();

也可以是自定义值转换器类型。 例如,请考虑将枚举值存储为其货币符号的转换器:

public class CurrencyToSymbolConverter : ValueConverter<Currency, string>
{
    public CurrencyToSymbolConverter()
        : base(
            v => v == Currency.PoundsSterling ? "£" : v == Currency.Euros ? "€" : "$",
            v => v == "£" ? Currency.PoundsSterling : v == "€" ? Currency.Euros : Currency.UsDollars)
    {
    }
}

现在可以使用泛型 HasConversion 方法配置此配置:

modelBuilder.Entity<TestEntity3>()
    .Property(e => e.Currency)
    .HasConversion<CurrencyToSymbolConverter>();

多对多关系的配置需求减少

GitHub 问题: #21535

通过约定可以识别两种实体类型之间的明确多对多关系。 如有必要或如果需要,可以显式指定导航。 例如:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats);

在这两种情况下,EF Core 会基于 Dictionary<string, object> 创建一个共享实体类型,以作为这两种类型之间的联接实体。 从 EF Core 6.0 开始, UsingEntity 可以添加到配置中以仅更改此类型,而无需进行其他配置。 例如:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>();

此外,可以进一步配置联接实体类型,而无需明确指定左右关系。 例如:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

最后,可以提供完整的配置。 例如:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasOne<Human>().WithMany().HasForeignKey(e => e.CatsId),
        e => e.HasOne<Cat>().WithMany().HasForeignKey(e => e.HumansId),
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

允许值转换器转换空值

GitHub 问题: #13850

重要

由于下面所述的问题,为允许 null 转换的构造函数 ValueConverter 已在 EF Core 6.0 版本中用 [EntityFrameworkInternal] 标记。 使用这些构造函数现在将会产生一个编译警告。

值转换器通常不允许将 null 转换为其他值。 这是因为同一个值转换器可以同时用于可为 null 和不可为 null 的类型,这对于 PK/FK 组合非常有用,其中 FK 通常可为 null,而 PK 则不可为 null。

从 EF Core 6.0 开始,可以创建一个用于转换 null 的值转换器。 然而,对此功能的验证表明,它在实践中存在许多问题和陷阱,非常棘手。 例如:

这些不是微不足道的问题,对于查询问题,它们不容易检测。 因此,我们已将此功能标记为 EF Core 6.0 的内部功能。 你仍然可以使用它,但会收到编译器警告。 可以使用#pragma warning disable EF1001禁用警告。

其中一个将null值转换为其他值的例子是,当数据库中包含null值时,实体类型希望对属性使用其他默认值。 例如,考虑一个枚举,其默认值为“未知”:

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

但是,当品种未知时,数据库可能具有 null 值。 在 EF Core 6.0 中,值转换器可用于考虑这一点:

    public class BreedConverter : ValueConverter<Breed, string>
    {
#pragma warning disable EF1001
        public BreedConverter()
            : base(
                v => v == Breed.Unknown ? null : v.ToString(),
                v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
                convertsNulls: true)
        {
        }
#pragma warning restore EF1001
    }

具有“未知”品种的猫在数据库中的 Breed 列设置为 null。 例如:

context.AddRange(
    new Cat { Name = "Mac", Breed = Breed.Unknown },
    new Cat { Name = "Clippy", Breed = Breed.Burmese },
    new Cat { Name = "Sid", Breed = Breed.Tonkinese });

await context.SaveChangesAsync();

这会在 SQL Server 上生成以下 insert 语句:

info: 9/27/2021 19:43:55.966 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (16ms) [Parameters=[@p0=NULL (Size = 4000), @p1='Mac' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Burmese' (Size = 4000), @p1='Clippy' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Tonkinese' (Size = 4000), @p1='Sid' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

DbContext 工厂改进

AddDbContextFactory 还直接注册 DbContext

GitHub 问题: #25164

有时,在应用程序的依赖注入(D.I.)容器中同时注册一个 DbContext 类型和该类型的上下文工厂是非常有用的。 例如,这允许从请求范围解析 DbContext 的作用域实例,而工厂可用于在需要时创建多个独立实例。

为了支持这一点,AddDbContextFactory 现在还将 DbContext 类型注册为作用域服务。 例如,在应用程序的 D.I. 容器中考虑此注册:

var container = services
    .AddDbContextFactory<SomeDbContext>(
        builder => builder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample;ConnectRetryCount=0"))
    .BuildServiceProvider();

通过此注册,可以从根 D.I. 容器中解析出工厂,就像在以前的版本中一样。

var factory = container.GetService<IDbContextFactory<SomeDbContext>>();
using (var context = factory.CreateDbContext())
{
    // Contexts obtained from the factory must be explicitly disposed
}

请注意,必须显式处置由工厂创建的上下文实例。

此外,可以直接从容器范围解析 DbContext 实例:

using (var scope = container.CreateScope())
{
    var context = scope.ServiceProvider.GetService<SomeDbContext>();
    // Context is disposed when the scope is disposed
}

在这种情况下,当容器范围被销毁时,上下文实例会被销毁;上下文不应被显式销毁。

在更高级别,这意味着工厂的 DbContext 可以注入到其他 D.I. 类型。 例如:

private class MyController2
{
    private readonly IDbContextFactory<SomeDbContext> _contextFactory;

    public MyController2(IDbContextFactory<SomeDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public async Task DoSomething()
    {
        using var context1 = _contextFactory.CreateDbContext();
        using var context2 = _contextFactory.CreateDbContext();

        var results1 = await context1.Blogs.ToListAsync();
        var results2 = await context2.Blogs.ToListAsync();

        // Contexts obtained from the factory must be explicitly disposed
    }
}

或者:

private class MyController1
{
    private readonly SomeDbContext _context;

    public MyController1(SomeDbContext context)
    {
        _context = context;
    }

    public async Task DoSomething()
    {
        var results = await _context.Blogs.ToListAsync();

        // Injected context is disposed when the request scope is disposed
    }
}

DbContextFactory 忽略 DbContext 无参数构造函数

GitHub 问题: #24124

EF Core 6.0 现在允许在通过 AddDbContextFactory 注册工厂时,使用无参数的 DbContext 构造函数和用于相同上下文类型的DbContextOptions构造函数。 例如,上述示例中使用的上下文包含两个构造函数:

public class SomeDbContext : DbContext
{
    public SomeDbContext()
    {
    }

    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

可以在不注入依赖项的情况下使用 DbContext 连接池

GitHub 问题: #24137

PooledDbContextFactory 类型已公开,因此它可用作 DbContext 实例的独立池,而无需应用程序具有依赖项注入容器。 池是使用该实例创建的,该实例 DbContextOptions 将用于创建上下文实例:

var options = new DbContextOptionsBuilder<SomeDbContext>()
    .EnableSensitiveDataLogging()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample;ConnectRetryCount=0")
    .Options;

var factory = new PooledDbContextFactory<SomeDbContext>(options);

然后,工厂可用于创建和汇集实例。 例如:

for (var i = 0; i < 2; i++)
{
    using var context1 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context1.ContextId}");

    using var context2 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context2.ContextId}");
}

当实例被释放时,会返回到池中。

其他改进

最后,EF Core 在上面未涵盖的领域包含多项改进。

创建表时使用 [ColumnAttribute.Order]

GitHub 问题: #10059

现在,可以使用 Order 的属性在创建迁移表时对列进行排序。 例如,请考虑以下模型:

public class EntityBase
{
    public int Id { get; set; }
    public DateTime UpdatedOn { get; set; }
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    public string Department { get; set; }
    public decimal AnnualSalary { get; set; }
    public Address Address { get; set; }
}

[Owned]
public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

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

默认情况下,EF Core 首先对主键列进行排序,然后按实体类型和拥有的类型的属性以及基类型中的属性排序。 例如,下表是在 SQL Server 上创建的:

CREATE TABLE [EmployeesWithoutOrdering] (
    [Id] int NOT NULL IDENTITY,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [Address_House] nvarchar(max) NULL,
    [Address_Street] nvarchar(max) NULL,
    [Address_City] nvarchar(max) NULL,
    [Address_Postcode] nvarchar(max) NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    CONSTRAINT [PK_EmployeesWithoutOrdering] PRIMARY KEY ([Id]));

在 EF Core 6.0 中, ColumnAttribute 可用于指定不同的列顺序。 例如:

public class EntityBase
{
    [Column(Order = 1)]
    public int Id { get; set; }

    [Column(Order = 98)]
    public DateTime UpdatedOn { get; set; }

    [Column(Order = 99)]
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    [Column(Order = 2)]
    public string FirstName { get; set; }

    [Column(Order = 3)]
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    [Column(Order = 20)]
    public string Department { get; set; }

    [Column(Order = 21)]
    public decimal AnnualSalary { get; set; }

    public Address Address { get; set; }
}

[Owned]
public class Address
{
    [Column("House", Order = 10)]
    public string House { get; set; }

    [Column("Street", Order = 11)]
    public string Street { get; set; }

    [Column("City", Order = 12)]
    public string City { get; set; }

    [Required]
    [Column("Postcode", Order = 13)]
    public string Postcode { get; set; }
}

在 SQL Server 上,生成的表现在为:

CREATE TABLE [EmployeesWithOrdering] (
    [Id] int NOT NULL IDENTITY,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    [House] nvarchar(max) NULL,
    [Street] nvarchar(max) NULL,
    [City] nvarchar(max) NULL,
    [Postcode] nvarchar(max) NULL,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    CONSTRAINT [PK_EmployeesWithOrdering] PRIMARY KEY ([Id]));

这会将FistNameLastName列移动到顶部,即使它们是在基类型中定义的。 请注意,列序值可能会有间隙,允许使用范围来确保始终将列放置在末尾,即使被多个派生类型使用时,也能保持。

此示例还演示如何通过使用相同的ColumnAttribute同时指定列名和顺序。

可以在 OnModelCreating 中使用 ModelBuilder API 配置列排序。 例如:

modelBuilder.Entity<UsingModelBuilder.Employee>(
    entityBuilder =>
    {
        entityBuilder.Property(e => e.Id).HasColumnOrder(1);
        entityBuilder.Property(e => e.FirstName).HasColumnOrder(2);
        entityBuilder.Property(e => e.LastName).HasColumnOrder(3);

        entityBuilder.OwnsOne(
            e => e.Address,
            ownedBuilder =>
            {
                ownedBuilder.Property(e => e.House).HasColumnName("House").HasColumnOrder(4);
                ownedBuilder.Property(e => e.Street).HasColumnName("Street").HasColumnOrder(5);
                ownedBuilder.Property(e => e.City).HasColumnName("City").HasColumnOrder(6);
                ownedBuilder.Property(e => e.Postcode).HasColumnName("Postcode").HasColumnOrder(7).IsRequired();
            });

        entityBuilder.Property(e => e.Department).HasColumnOrder(8);
        entityBuilder.Property(e => e.AnnualSalary).HasColumnOrder(9);
        entityBuilder.Property(e => e.UpdatedOn).HasColumnOrder(10);
        entityBuilder.Property(e => e.CreatedOn).HasColumnOrder(11);
    });

在模型生成器上进行排序时,HasColumnOrder 优先于使用 ColumnAttribute 指定的任何顺序。 这意味着 HasColumnOrder 可用于覆盖使用属性进行的顺序,包括解决在不同属性中指定了相同排序编号时产生的任何冲突。

重要

请注意,一般情况下,大多数数据库仅支持在创建表时对列进行排序。 这意味着列顺序属性不能用于对现有表中的列重新排序。 这是 SQLite 的一个显著例外,其中迁移将使用新的列顺序重新生成整个表。

EF Core 最小 API

GitHub 问题: #25192

.NET Core 6.0 包括更新的模板,这些模板简化了“最小 API”,从而消除了 .NET 应用程序中传统上所需的大量样板代码。

EF Core 6.0 包含一种新的扩展方法,该方法注册 DbContext 类型,并在单行中为数据库提供程序提供配置。 例如:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlite<MyDbContext>("Data Source=mydatabase.db");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlServer<MyDbContext>(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCosmos<MyDbContext>(
    "https://localhost:8081",
    "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==");

这些内容与以下项完全等效:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlite("Data Source=mydatabase.db"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase;ConnectRetryCount=0"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="));

注释

EF Core 最小 API 仅支持 DbContext 和提供程序的基本注册和配置。 使用 AddDbContextAddDbContextPoolAddDbContextFactory等访问 EF Core 中可用的所有类型的注册和配置。

请查看以下资源,详细了解最小 API:

在 SaveChangesAsync 中保留同步上下文

GitHub 问题: #23971

我们在 5.0 版本中更改了 EF Core 代码,并在所有异步代码的位置将Task.ConfigureAwait 设置为 falseawait。 这通常是用于 EF Core 的更好选择。 但是,SaveChangesAsync 是一种特殊情况,因为异步数据库操作完成后,EF Core 会将生成的值设置为跟踪的实体。 然后,这些更改可能会触发通知,例如,这些通知可能需要在 U.I. 线程上运行。 因此,我们仅在 EF Core 6.0 中针对 SaveChangesAsync 方法还原此更改。

内存数据库:验证必需属性不是 null

GitHub 问题: #10613。 此功能由 @fagnercarvalho提供。 非常感谢!

如果尝试保存标记为必需属性的 null 值,EF Core 内存中数据库现在将引发异常。 例如,请考虑一种 User 类型,其中包含一个必需的 Username 属性。

public class User
{
    public int Id { get; set; }

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

尝试使用 null Username 保存实体将导致以下异常:

Microsoft.EntityFrameworkCore.DbUpdateException:实体类型“User”实例缺少属性“{'Username'}”,其键值为“{Id: 1}”。

如有必要,可以禁用此验证。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .LogTo(Console.WriteLine, new[] { InMemoryEventId.ChangesSaved })
        .UseInMemoryDatabase("UserContextWithNullCheckingDisabled", b => b.EnableNullChecks(false));
}

诊断和拦截器的命令源信息

GitHub 问题: #23719。 此功能由 @Giorgi提供。 非常感谢!

CommandEventData 现在包含一个枚举值,该枚举值提供给诊断源和侦听器,指示 EF 中负责创建命令的部分。 这可用作诊断或拦截器中的筛选器。 例如,我们可能需要一个仅应用于来自 SaveChanges 的命令的拦截器:

public class CommandSourceInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        if (eventData.CommandSource == CommandSource.SaveChanges)
        {
            Console.WriteLine($"Saving changes for {eventData.Context!.GetType().Name}:");
            Console.WriteLine();
            Console.WriteLine(command.CommandText);
        }

        return result;
    }
}

这会将拦截器仅过滤为 SaveChanges 事件,当在生成迁移和查询的应用程序中使用时。 例如:

Saving changes for CustomersContext:

SET NOCOUNT ON;
INSERT INTO [Customers] ([Name])
VALUES (@p0);
SELECT [Id]
FROM [Customers]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

更好的临时值处理

GitHub 问题: #24245

EF Core 不会在实体类型实例上公开临时值。 例如,请考虑具有 Blog 存储生成的密钥的实体类型:

public class Blog
{
    public int Id { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

一旦上下文开始跟踪 Blog,键 Id 属性将获取一个临时值。 例如,调用 DbContext.Add时:

var blog = new Blog();
context.Add(blog);

可以从上下文更改跟踪器获取临时值,但未设置为实体实例。 例如,此代码:

Console.WriteLine($"Blog.Id value on entity instance = {blog.Id}");
Console.WriteLine($"Blog.Id value tracked by EF = {context.Entry(blog).Property(e => e.Id).CurrentValue}");

生成以下输出:

Blog.Id value on entity instance = 0
Blog.Id value tracked by EF = -2147482647

这很好,因为它可以防止临时值泄漏到应用程序代码中,而应用程序代码中可能会意外将其视为非临时值。 但是,有时直接处理临时值很有用。 例如,应用程序可能需要在跟踪实体图之前为实体图生成自己的临时值,以便它们可用于使用外键形成关系。 这可以通过将值显式标记为临时来完成。 例如:

var blog = new Blog { Id = -1 };
var post1 = new Post { Id = -1, BlogId = -1 };
var post2 = new Post { Id = -2, BlogId = -1 };

context.Add(blog).Property(e => e.Id).IsTemporary = true;
context.Add(post1).Property(e => e.Id).IsTemporary = true;
context.Add(post2).Property(e => e.Id).IsTemporary = true;

Console.WriteLine($"Blog has explicit temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has explicit temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has explicit temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

在 EF Core 6.0 中,即使它现在标记为临时,该值也会保留在实体实例上。 例如,上述代码生成以下输出:

Blog has explicit temporary ID = -1
Post 1 has explicit temporary ID = -1 and FK to Blog = -1
Post 2 has explicit temporary ID = -2 and FK to Blog = -1

同样,EF Core 生成的临时值可以显式设置为实体实例,并标记为临时值。 这可用于使用新实体的临时键值显式设置关系。 例如:

var post1 = new Post();
var post2 = new Post();

var blogIdEntry = context.Entry(blog).Property(e => e.Id);
blog.Id = blogIdEntry.CurrentValue;
blogIdEntry.IsTemporary = true;

var post1IdEntry = context.Add(post1).Property(e => e.Id);
post1.Id = post1IdEntry.CurrentValue;
post1IdEntry.IsTemporary = true;
post1.BlogId = blog.Id;

var post2IdEntry = context.Add(post2).Property(e => e.Id);
post2.Id = post2IdEntry.CurrentValue;
post2IdEntry.IsTemporary = true;
post2.BlogId = blog.Id;

Console.WriteLine($"Blog has generated temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has generated temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has generated temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

结果:

Blog has generated temporary ID = -2147482647
Post 1 has generated temporary ID = -2147482647 and FK to Blog = -2147482647
Post 2 has generated temporary ID = -2147482646 and FK to Blog = -2147482647

针对 C# 可空引用类型更新的 EF Core

GitHub 问题: #19007

EF Core 代码库现在在整个过程中使用 C# 可为 null 的引用类型(NRT)。 这意味着,在从您自己的代码使用 EF Core 6.0 时,您将获得正确的编译器提示,以正确使用 null。

Microsoft.Data.Sqlite 6.0

小窍门

可以通过 从 GitHub 下载示例代码来运行和调试下面显示的所有示例。

连接池

GitHub 问题: #13837

通常的做法是尽可能少地使数据库连接保持打开状态。 这有助于防止对连接资源进行争用。 这就是为什么 EF Core 等库在执行数据库作之前立即打开连接,并在之后立即将其关闭。 例如,请考虑以下 EF Core 代码:

Console.WriteLine("Starting query...");
Console.WriteLine();

var users = await context.Users.ToListAsync();

Console.WriteLine();
Console.WriteLine("Query finished.");
Console.WriteLine();

foreach (var user in users)
{
    if (user.Username.Contains("microsoft"))
    {
        user.Username = "msft:" + user.Username;

        Console.WriteLine("Starting SaveChanges...");
        Console.WriteLine();

        await context.SaveChangesAsync();

        Console.WriteLine();
        Console.WriteLine("SaveChanges finished.");
    }
}

在打开连接日志记录的情况下,此代码的输出为:

Starting query...

dbug: 8/27/2021 09:26:57.810 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

Query finished.

Starting SaveChanges...

dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.814 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

SaveChanges finished.

请注意,每个操作都会快速打开和关闭连接。

但是,对于大多数数据库系统,打开与数据库的物理连接是一项昂贵的作。 因此,大多数 ADO.NET 提供商都会创建物理连接的池,并根据需要将其出租给 DbConnection 实例。

SQLite 稍有不同,因为数据库访问通常只是访问文件。 这意味着打开与 SQLite 数据库的连接通常非常快。 但是,情况并非总是如此。 例如,打开与加密数据库的连接可能非常慢。 因此,使用 Microsoft.Data.Sqlite 6.0 时,SQLite 连接现在会共用。

支持 DateOnly 和 TimeOnly

GitHub 问题: #24506

Microsoft.Data.Sqlite 6.0 支持 .NET 6 中的DateOnlyTimeOnly新类型。 还可以在 EF Core 6.0 中与 SQLite 提供程序一起使用。 与 SQLite 一样,其本机类型系统意味着这些类型的值需要存储为四种受支持的类型之一。 Microsoft.Data.Sqlite 将其存储为 TEXT. 例如,使用这些类型的实体:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }

    public DateOnly Birthday { get; set; }
    public TimeOnly TokensRenewed { get; set; }
}

SQLite 数据库中与以下表对应的映射:

CREATE TABLE "Users" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
    "Username" TEXT NULL,
    "Birthday" TEXT NOT NULL,
    "TokensRenewed" TEXT NOT NULL);

然后,可以按正常方式保存、查询和更新值。 例如,此 EF Core LINQ 查询:

var users = await context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToListAsync();

在 SQLite 上转换为以下内容:

SELECT "u"."Id", "u"."Birthday", "u"."TokensRenewed", "u"."Username"
FROM "Users" AS "u"
WHERE "u"."Birthday" < '1900-01-01'

返回仅使用生日在公元1900年之前的记录:

Found 'ajcvickers'
Found 'wendy'

Savepoints API

GitHub 问题: #20228

我们在 ADO.NET 提供程序中对保存点的通用 API 进行了标准化。 Microsoft.Data.Sqlite 现在支持此 API,包括:

使用保存点可以回滚部分事务,而不需要回滚整个事务。 例如,下面的代码:

  • 创建交易
  • 将更新发送到数据库
  • 创建保存点
  • 向数据库发送另一次更新
  • 回滚到之前创建的保存点
  • 提交事务
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
await connection.OpenAsync();

await using var transaction = await connection.BeginTransactionAsync();

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'ajcvickers' WHERE Id = 1";
    await command.ExecuteNonQueryAsync();
}

await transaction.SaveAsync("MySavepoint");

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'wfvickers' WHERE Id = 2";
    await command.ExecuteNonQueryAsync();
}

await transaction.RollbackAsync("MySavepoint");

await transaction.CommitAsync();

这将导致第一个更新被提交到数据库,而第二个更新未提交,因为在准备提交事务之前,保存点已被回滚。

连接字符串中的命令超时

GitHub 问题: #22505。 此功能由 @nmichels提供。 非常感谢!

ADO.NET 供应商支持两个不同的超时:

  • 连接超时,用于确定与数据库建立连接时要等待的最长时间。
  • 命令超时,确定等待命令完成执行的最长时间。

可以从代码中使用 DbCommand.CommandTimeout 设置命令超时。 现在,许多供应商还会在连接字符串中展示此命令超时。 Microsoft.Data.Sqlite 正在通过 Command Timeout 连接字符串关键字 来遵循这一趋势。 例如, "Command Timeout=60;DataSource=test.db" 将使用 60 秒作为连接创建的命令的默认超时。

小窍门

Sqlite 将 Default Timeout 视为 Command Timeout 的同义词,因此可以改用(如果首选)。