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 的隐藏 datetime2 列。 这些“时间段列”表示行中的数据存在的时间范围。 这些列会映射到 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
    });

context.SaveChanges();

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

var employee = context.Employees.Single(e => e.Name == "Rainbow Dash");
context.Remove(employee);
context.SaveChanges();

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

var employees = context.Employees.ToList();
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 最大值。 表中的当前行始终是这种情况。 ValidFrom 列(默认情况下称为 PeriodStart)包含插入行的 UTC 时间。

查询历史数据

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

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

注意

请参阅 SQL Server 时态表文档,详细了解每个运算符包含了哪些行。

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

var history = 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")
        })
    .ToList();

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 停止活动。 这是因为在该时间从主表中删除了 Rainbow Dash 行。 稍后我们将介绍如何还原此数据。

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

var history = 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")
        })
    .ToList();

此查询会返回以下行:

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

还原历史数据

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

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

context.Add(employee);
context.SaveChanges();

此查询会返回指定 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>

但是,如果对模型进行更改,并且使用 dotnet ef migrations add 生成了更多的迁移,则可以将这些迁移捆绑到新的可执行文件中,准备应用。 例如:

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,禁止在运行命令之前生成项目。 仅当已知项目为最新状态时,才应使用此选项。
  • --verbose,查看有关命令正在执行的操作的详细信息。 在 Bug 报告中包括信息时,请使用此选项。

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

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

预先约定模型配置

GitHub 问题:#12229

EF Core 的早期版本要求在给定类型的每个属性的映射不同于默认值时显式配置该映射。 这包括“Facet”,如字符串的最大长度和小数精度,以及属性类型的值转换。

这需要:

  • 每个属性的模型生成器配置
  • 每个属性上的一个映射属性
  • 生成模型时,对所有实体类型的所有属性进行显式迭代,并使用低级别元数据 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);

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

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

使用任一内置值转换器可以将所有 bool 属性转换为整数 01

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 的字符串列指定其他 Facet。 在这种情况下,列的最大长度为 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 预览版 6:配置约定

已编译的模型

GitHub 问题:#1906

已编译的模型可以加快具有大型模型的应用程序的 EF Core 启动时间。 大型模型通常是指数百到数千种实体类型和关系。

启动时间是指在应用程序中首次使用 DbContext 类型时对 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 倍的改进。

Compiled model performance improvement

注意

有关 EF Core 启动性能和已编译的模型的更深入讨论,请参阅 .NET 博客上的宣布推出 Entity Framework Core 6.0 预览版 5:已编译的模型

提高了 TechEmpower Fortunes 的性能

GitHub 问题:#23611

我们对 EF Core 6.0 的查询性能进行了重大改进。 具体而言:

  • 在行业标准的 TechEmpower Fortunes 基准中,EF Core 6.0 的性能现在比 5.0 快 70%。
    • 这是全堆栈性能改进,包括对基准代码、.NET 运行时等的改进。
  • EF Core 6.0 本身执行未跟踪查询的速度提高了 31%。
  • 执行查询时,堆分配时间减少了 43%。

经过这些改进后,TechEmpower Fortunes 基准中常见的“微型 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

EF Core 6.0 在使用 Azure Cosmos DB 数据库提供程序时,会本机映射基元类型的集合。 例如,请考虑以下实体类型:

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 提供。 非常感谢!

现在可以显式设置 Azure Cosmos DB 提供程序使用的 HttpClientFactory。 这在测试期间特别有用,例如,在 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
                    }));
        });

注意

有关将 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; }
}

并为 Tag 生成一个类:

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# 可为空引用类型

GitHub 问题:#15520

EF Core 6.0 现在可搭建使用 C# 可空引用类型 (NRT) 的 EF 模型和实体类型。 在要搭建代码的 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; }
}

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

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 = context.People
    .Include(e => e.Shoes)
    .GroupBy(e => e.FirstName)
    .Select(
        g => g.OrderBy(e => e.FirstName)
            .ThenBy(e => e.LastName)
            .FirstOrDefault())
    .ToList();
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 = context.People
    .Select(
        p => new
        {
            p.FirstName,
            FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
        })
    .GroupBy(p => p.FirstName)
    .Select(g => g.First())
    .First();
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 = context.People
    .Where(e => e.MiddleInitial == "Q" && e.Age == 20)
    .GroupBy(e => e.LastName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e.Length)
    .ToList();
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 = (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()
               })
    .ToList();
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 = context.People
    .GroupBy(e => e.FirstName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e)
    .ToList();
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 = context.People.Where(e => e.Age == 20)
    .GroupBy(e => e.Id)
    .Select(g => g.First().MiddleInitial)
    .OrderBy(e => e)
    .ToList();
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
    = 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),
            })
        .ToList();
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 = 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)
        })
    .Count();
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 = context.People
    .GroupBy(n => n.FirstName)
    .Select(g => new
    {
        Feet = g.Key,
        Total = g.Sum(n => n.Feet.Size)
    })
    .ToList();
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 = 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)
    .ToList();
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 = 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()})
    .ToList();
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 = context.People
    .GroupBy(m => new {m.FirstName, m.MiddleInitial })
    .Select(am => new
    {
        Key = am.Key,
        Items = am.ToList()
    })
    .ToList();
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]

Model

这些示例中使用的实体类型为:

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 = context.Shards
    .Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToList();

将转换为以下 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 中,我们充分利用了 IAsyncEnumerable<T> 的 C# 模式匹配,这样公开的 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));

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

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

使用 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 = context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToList();

使用 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 存储库中实现转换。 例如,考虑具有 Popularity 属性的 User 实体类型:

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 = context.Users.Where(u => u.Popularity == (int)(EF.Functions.Random() * 4.0) + 1).ToList();

使用 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 = context.Users.Where(
    e => string.IsNullOrWhiteSpace(e.FirstName)
         || string.IsNullOrWhiteSpace(e.LastName)).ToList();

在 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 属性,同时为其他顶级实体类型定义集合:

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 = context.CustomerDensities.ToList();

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

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

EF Core 6.0 现在使用单个参数转换 string.Substring 的用法。 例如:

var result = context.Customers
    .Select(a => new { Name = a.Name.Substring(3) })
    .FirstOrDefault(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 查询:

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

单个 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 查询:

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

单个 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 查询:

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

单个 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 子句,以确保将给定实体的所有相关实体组合在一起。 但是,最后一个 ORDER BY 子句对于 EF 生成所需的分组不是必需的,并且可能会影响性能。 因此,EF Core 6.0 删除了此子句。

例如,请考虑下面的查询:

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

在 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 = context
    .Customers
    .TagWithCallSite()
    .Where(c => c.Name.StartsWith("A"))
    .ToList();

使用 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 来确保这一点。

现在,查询客户时,如果邮政编码列为 null,则表示客户没有地址,Customer.Address 导航属性保留为 null。 例如,循环访问客户并检查地址是否为 null:

foreach (var customer in context.Customers1)
{
    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; }
}

现在,既可以保存无地址的客户,也可以保存有地址但所有地址属性均为 null 的客户:

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

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

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

这会使情况变得更糟糕,在这种情况下,可选依赖项本身充当另一个可选依赖项的主体,并且它们映射到同一个表。 除了发出警告,EF Core 6.0 还会禁用这种嵌套的可选依赖项。 以下面的模型为例,其中 ContactInfoCustomer 所有,而 AddressContactInfo 所有:

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:Entity type '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 包含多个新属性,这些属性可应用于代码,以更改映射到数据库的方式。

UnicodeAttribute

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

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

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 列。 当数据库系统仅支持 Unicode 类型时,UnicodeAttribute 会被忽略。

PrecisionAttribute

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

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

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

EntityTypeConfigurationAttribute

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 继承映射时非常有用,其中很少使用的子类型的属性将导致表中大多数行的列值为 null。 例如,考虑从 ForumUser 扩展而来的 ForumModerator 类:

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

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

用户数可能以数百万计,而其中只有少数人是版主。 这意味着将 ForumName 映射为稀疏在此处可能会有意义。 现在可以使用 OnModelCreating 中的 IsSparse 对此进行配置。 例如:

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,使其能够使用 HasConversion<string> 将此枚举的值另存为字符串“UsDollars”、“PoundsStirling”和“Euros”。 例如:

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

允许值转换器转换 null 值

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,但实体类型想要为属性使用其他一些默认值时。 请考虑这样一个枚举,其中它的默认值为“Unknown”:

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
    }

品种为“Unknown”的小猫会在数据库中将其 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 });

context.SaveChanges();

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

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 void DoSomething()
    {
        using var context1 = _contextFactory.CreateDbContext();
        using var context2 = _contextFactory.CreateDbContext();

        var results1 = context1.Blogs.ToList();
        var results2 = context2.Blogs.ToList();

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

或:

private class MyController1
{
    private readonly SomeDbContext _context;

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

    public void DoSomething()
    {
        var results = _context.Blogs.ToList();

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

DbContextFactory 忽略 DbContext 无参数构造函数

GitHub 问题:#24124

EF Core 6.0 现在允许无参数 DbContext 构造函数,以及通过 AddDbContextFactory 注册工厂时在同一上下文类型上使用 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

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

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 代码,以在所有 await 异步代码的位置将 Task.ConfigureAwait 设置为 false。 通常,这是使用 EF Core 时的更好选择。 但是,SaveChangesAsync 是一种特殊情况,因为在完成异步数据库操作后,EF Core 会将生成的值设置为跟踪的实体。 然后,这些更改可能会触发通知,例如,通知可能必须在 U.I. 线程上运行。 因此,我们会在 EF Core 6.0 中仅针对 SaveChangesAsync 方法还原此更改。

内存中数据库:验证必需的属性不为 null

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

如果尝试为标记为“必需”的属性保存 null 值,则 EF Core 内存中数据库将引发异常。 例如,考虑具有必需的 Username 属性的 User 类型:

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

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

如果尝试保存一个具有空 Username 的实体,将导致以下异常出现:

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

如果需要,可以禁用此验证。 例如:

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

一旦上下文跟踪 BlogId 键属性就会获得一个临时值。 例如,调用 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

EF Core 为 C# 可为空引用类型进行批注

GitHub 问题:#19007

EF Core 代码库现在全部使用 C# 可为空引用类型 (NRT)。 这意味着,从你自己的代码中使用 6.0 EF Core 时,你将获得正确的编译器指示 null 的用法。

Microsoft.Data.Sqlite 6.0

提示

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

连接池

GitHub 问题:#13837

通常应尽可能减少数据库连接的打开时间。 这有助于防止争用连接资源。 这就是为什么像 EF Core 这样的库在即将执行数据库操作之前才打开连接,然后立即关闭。 例如,请考虑以下 EF Core 代码:

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

var users = context.Users.ToList();

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

        context.SaveChanges();

        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 中新的 DateOnly 类型和 TimeOnly 类型。 还可在 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 = context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToList();

在 SQLite 上转换为以下内容:

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

并且仅在 1900 CE 之前与生日一起使用:

Found 'ajcvickers'
Found 'wendy'

保存点 API

GitHub 问题:#20228

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

使用保存点允许回滚事务的一部分,而不是回滚整个事务。 例如,以下代码可执行以下操作:

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

using var transaction = connection.BeginTransaction();

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

transaction.Save("MySavepoint");

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

transaction.Rollback("MySavepoint");

transaction.Commit();

这将使第一次更新提交到数据库,而第二次更新不会提交,因为在提交事务之前回滚了保存点。

连接字符串中的命令超时

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

ADO.NET 提供程序支持两种不同的超时:

  • 连接超时,这决定了连接到数据库时等待的最长时间。
  • 命令超时,这决定了等待命令完成执行所用的最长时间。

命令超时可以使用 DbCommand.CommandTimeout 从代码进行设置。 许多提供程序现在还在连接字符串中公开此命令超时。 Microsoft.Data.Sqlite 使用 Command Timeout 连接字符串关键字跟随这一趋势。 例如,"Command Timeout=60;DataSource=test.db" 将使用 60 秒作为连接创建的命令的超时默认值。

提示

Sqlite 将 Default Timeout 视为 Command Timeout 的同义词,因此可以改为使用前者(如果愿意)。