Partilhar via


Novidades no EF Core 6.0

O EF Core 6.0 foi enviado para o NuGet. Esta página contém uma visão geral das alterações interessantes introduzidas nesta versão.

Dica

Você pode executar e depurar as amostras indicadas a seguir baixando o código de exemplo do GitHub.

Tabelas temporais do SQL Server

Problema do GitHub: nº 4693.

As tabelas temporais do SQL Server controlam automaticamente todos os dados armazenados em uma tabela, mesmo depois de esses dados serem atualizados ou excluídos. Isso é feito com a criação de uma "tabela de histórico" paralela na qual os dados históricos com carimbo de data/hora são armazenados sempre que uma alteração é feita na tabela principal. Com isso, é possível consultar dados históricos, como para auditoria, ou restaurá-los, como para recuperação após mutação ou exclusão acidental.

O EF Core já dá suporte ao seguinte:

  • Criação de tabelas temporais usando as Migrações
  • Transformação de tabelas existentes em tabelas temporais, novamente usando as Migrações
  • Consulta de dados históricos
  • Restauração de dados de um ponto no passado

Configuração de uma tabela temporal

O construtor de modelos pode ser usado para configurar uma tabela como temporal. Por exemplo:

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

Quando o EF Core for usado para criar o banco de dados, a nova tabela será configurada como uma tabela temporal com os padrões do SQL Server para os carimbos de data/hora e a tabela de histórico. Por exemplo, considere um tipo de entidade 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; }
}

A tabela temporal criada terá esta aparência:

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

Observe que o SQL Server cria duas colunas datetime2 ocultas chamadas PeriodEnd e PeriodStart. Essas "colunas de período" representam o intervalo de tempo durante o qual os dados na linha existiam. Essas colunas são mapeadas para propriedades de sombra no modelo do EF Core, permitindo o uso delas em consultas, conforme mostrado mais adiante.

Importante

As horas nas colunas são sempre a hora UTC gerada pelo SQL Server. As horas UTC são usadas para todas as operações que envolvem tabelas temporais, como nas consultas mostradas abaixo.

Observe também que uma tabela de histórico associada chamada EmployeeHistory é criada automaticamente. Os nomes das colunas de período e da tabela de histórico podem ser alterados com uma configuração adicional para o construtor de modelos. Por exemplo:

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

Isso é refletido na tabela criada pelo 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]))');

Como usar tabelas temporais

Na maioria das vezes, as tabelas temporais são usadas como qualquer outra tabela. Ou seja, as colunas de período e os dados históricos são tratados de maneira transparente pelo SQL Server, de modo que o aplicativo pode ignorá-los. Por exemplo, as novas entidades podem ser salvas no banco de dados da maneira habitual:

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

Em seguida, esses dados podem ser consultados, atualizados e excluídos da maneira habitual. Por exemplo:

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

Além disso, após uma consulta de acompanhamento normal, os valores das colunas de período dos dados atuais podem ser acessados por meio das entidades rastreadas. Por exemplo:

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

Isso imprime:

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

Observe que a coluna ValidTo (por padrão, chamada PeriodEnd) contém o valor máximo de datetime2. Esse é sempre o caso das linhas atuais na tabela. As colunas ValidFrom (por padrão, chamadas PeriodStart) contêm a hora UTC em que a linha foi inserida.

Como consultar dados históricos

O EF Core dá suporte a consultas que incluem dados históricos por meio de vários novos operadores de consulta:

  • TemporalAsOf: retorna as linhas que estavam ativas (atuais) na hora UTC especificada. Essa é uma linha individual da tabela atual ou da tabela de histórico de determinada chave primária.
  • TemporalAll: retorna todas as linhas nos dados históricos. Normalmente, são muitas linhas da tabela de histórico e/ou da tabela atual de determinada chave primária.
  • TemporalFromTo: retorna todas as linhas que estavam ativas entre duas horas UTC especificadas. Pode ser muitas linhas da tabela de histórico e/ou da tabela atual de determinada chave primária.
  • TemporalBetween: o mesmo que TemporalFromTo, com exceção de que são incluídas as linhas que se tornaram ativas no limite superior.
  • TemporalContainedIn: retorna todas as linhas que começaram a ficar ativas e deixaram de estar ativas entre duas horas UTC especificadas. Pode ser muitas linhas da tabela de histórico e/ou da tabela atual de determinada chave primária.

Observação

Confira a documentação de tabelas temporais do SQL Server para obter mais informações sobre exatamente quais linhas estão incluídas para cada um desses operadores.

Por exemplo, depois de fazer algumas atualizações e exclusões nos dados, podemos executar uma consulta usando TemporalAll para ver os dados históricos:

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

Observe como o método EF.Property pode ser usado para acessar valores das colunas de período. Isso é usado na cláusula OrderBy para classificar os dados e, em seguida, em uma projeção para incluir esses valores nos dados retornados.

Essa consulta retorna os seguintes dados:

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

Observe que a última linha retornada deixou de estar ativa em 26/8/2021, às 16:44:59. Isso ocorre porque a linha de Rainbow Dash foi excluída da tabela principal naquele momento. Veremos posteriormente como esses dados podem ser restaurados.

Consultas semelhantes podem ser escritas por meio de TemporalFromTo, TemporalBetween ou TemporalContainedIn. Por exemplo:

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

Essa consulta retorna as seguintes linhas:

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

Como restaurar dados históricos

Conforme mencionado acima, Rainbow Dash foi excluído da tabela Employees. Isso foi claramente um erro, então, vamos voltar a um ponto no tempo e restaurar a linha ausente a partir desse momento.

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

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

Essa consulta retorna uma linha individual para Rainbow Dash como era na hora UTC especificada. Todas as consultas que usam operadores temporais não têm acompanhamento por padrão, ou seja, a entidade retornada aqui não é rastreada. Isso faz sentido, pois ela não existe atualmente na tabela principal. Para inserir novamente a entidade na tabela principal, simplesmente a marcamos como Added e chamamos SaveChanges.

Após a nova inserção da linha Rainbow Dash, uma consulta dos dados históricos mostra que a linha foi restaurada como era na hora UTC especificada:

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

Pacotes de migração

Problema do GitHub: nº 19693.

As migrações do EF Core são usadas para gerar atualizações de esquema de banco de dados com base nas alterações no modelo do EF. Essas atualizações de esquema devem ser aplicadas no momento da implantação do aplicativo, geralmente, como parte de um sistema de CI/CD (integração contínua/implantação contínua).

O EF Core já inclui uma nova maneira de aplicar essas atualizações de esquema: pacotes de migração. Um pacote de migração é um pequeno executável que contém as migrações e o código necessário para aplicar essas migrações ao banco de dados.

Observação

Confira Introdução aos pacotes de migração do EF Core para DevOps no blog do .NET para ver uma discussão mais detalhada sobre migrações, pacotes e implantação.

Os pacotes de migração são criados por meio da ferramenta de linha de comando dotnet ef. Verifique se você instalou a última versão da ferramenta antes de continuar.

Um pacote precisa de migrações para incluir. Elas são criadas por meio de dotnet ef migrations add, conforme descrito na documentação sobre migrações. Depois que você tiver migrações prontas para implantar, crie um pacote usando o dotnet ef migrations bundle. Por exemplo:

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>

A saída é um executável adequado para o sistema operacional de destino. No meu caso, este é o Windows x64, portanto, recebo um efbundle.exe solto na pasta local. A execução desse executável aplica as migrações contidas nele:

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

As migrações só são aplicadas ao banco de dados se ainda não foram aplicadas. Por exemplo, a execução do mesmo pacote novamente não tem nenhum efeito, pois não há novas migrações a serem aplicadas:

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

No entanto, se forem feitas alterações no modelo e mais migrações forem geradas com dotnet ef migrations add, elas poderão ser agrupadas em um novo executável pronto para ser aplicado. Por exemplo:

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>

Observe que a opção --force pode ser usada para substituir o pacote existente por um novo.

A execução desse novo pacote aplica estas duas novas migrações ao banco de dados:

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

Por padrão, o pacote usa a cadeia de conexão de banco de dados da configuração do aplicativo. No entanto, um banco de dados diferente pode ser migrado pela transmissão da cadeia de conexão na linha de comando. Por exemplo:

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>

Observe que, desta vez, as três migrações foram aplicadas, pois nenhuma delas ainda havia sido aplicada ao banco de dados de produção.

Outras opções podem ser transmitidas para a linha de comando. Algumas opções comuns são:

  • --output para especificar o caminho do arquivo executável a ser criado.
  • --context para especificar o tipo DbContext a ser usado quando o projeto contiver vários tipos de contexto.
  • --project para especificar o projeto a ser usado. Usa o diretório de trabalho atual por padrão.
  • --startup-project para especificar o projeto de inicialização a ser usado. Usa o diretório de trabalho atual por padrão.
  • --no-build para impedir que o projeto seja criado antes da execução do comando. Isso só deve ser usado se o projeto é considerado atualizado.
  • --verbose para ver informações detalhadas sobre o que o comando está fazendo. Use essa opção ao incluir informações em relatórios de bugs.

Use dotnet ef migrations bundle --help para ver todas as opções disponíveis.

Observe que, por padrão, cada migração é aplicada em uma transação própria. Confira Problema do GitHub nº 22616 para ver uma discussão sobre os possíveis aprimoramentos futuros nessa área.

Configuração do modelo de pré-convenção

Problema do GitHub: nº 12229.

As versões anteriores do EF Core exigem que o mapeamento para cada propriedade de determinado tipo seja configurado explicitamente quando esse mapeamento for diferente do padrão. Isso inclui "facetas", como o comprimento máximo das cadeias de caracteres e a precisão decimal, bem como a conversão de valor para o tipo de propriedade.

Para isso, foi preciso o seguinte:

  • Configuração do construtor de modelos para cada propriedade
  • Um atributo de mapeamento em cada propriedade
  • Iteração explícita em todas as propriedades de todos os tipos de entidades e uso das APIs de metadados de baixo nível ao compilar o modelo.

Observe que a iteração explícita é propensa a erros e difícil de ser feita de maneira robusta, porque a lista de tipos de entidades e propriedades mapeadas pode não ser definitiva no momento em que essa iteração acontece.

O EF Core 6.0 permite que essa configuração de mapeamento seja especificada uma vez para determinado tipo. Em seguida, ela será aplicada a todas as propriedades desse tipo no modelo. Isso é chamado de "configuração do modelo de pré-convenção", pois configura aspectos do modelo que serão usados pelas convenções de criação do modelo. Essa configuração é aplicada pela substituição de ConfigureConventions no DbContext:

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

Por exemplo, considere os seguintes tipos de entidades:

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

Todas as propriedades de cadeia de caracteres podem ser configuradas como ANSI (em vez de Unicode) e têm um comprimento máximo de 1.024:

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

Todas as propriedades DateTime podem ser convertidas em inteiros de 64 bits no banco de dados por meio da conversão padrão de DateTimes para longos:

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

Todas as propriedades bool podem ser convertidas nos inteiros 0 ou 1 por meio de um dos conversores de valor internos:

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

Supondo que Session seja uma propriedade transitória da entidade e não deva ser persistida, ela pode ser ignorada em todos os lugares do modelo:

configurationBuilder
    .IgnoreAny<Session>();

A configuração do modelo de pré-convenção é muito útil ao trabalhar com objetos de valor. Por exemplo, o tipo Money no modelo acima é representado pelo struct somente leitura:

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
}

Em seguida, isso é serializado bidirecionalmente em JSON por meio de um conversor de valor personalizado:

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

Esse conversor de valor pode ser configurado uma vez para todos os usos de Money:

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

Observe também que é possível especificar facetas adicionais para a coluna de cadeia de caracteres na qual o JSON serializado é armazenado. Nesse caso, a coluna é limitada a um comprimento máximo de 64.

As tabelas criadas para o SQL Server por meio de migrações mostram como a configuração foi aplicada a todas as colunas mapeadas:

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

Também é possível especificar um mapeamento de tipo padrão para um tipo especificado. Por exemplo:

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

Isso raramente é necessário, mas poderá ser útil se um tipo for usado na consulta de uma forma que não esteja relacionada com nenhuma propriedade mapeada do modelo.

Observação

Confira Comunicado sobre o Entity Framework Core 6.0 Versão Prévia 6: Configurar convenções no blog do .NET para ver mais discussões e exemplos de configuração do modelo de pré-convenção.

Modelos compilados

Problema do GitHub: nº 1906.

Os modelos compilados podem aprimorar o tempo de inicialização do EF Core em aplicativos com modelos grandes. Um modelo grande costuma significar centenas a milhares de tipos de entidades e relações.

Tempo de inicialização significa a hora de execução da primeira operação em um DbContext quando esse tipo DbContext é usado pela primeira vez no aplicativo. Observe que apenas a criação de uma instância DbContext não faz com que o modelo do EF seja inicializado. Em vez disso, as primeiras operações típicas que fazem com que o modelo seja inicializado incluem uma chamada a DbContext.Add ou a execução da primeira consulta.

Os modelos compilados são criados por meio da ferramenta de linha de comando dotnet ef. Verifique se você instalou a última versão da ferramenta antes de continuar.

Um novo comando dbcontext optimize é usado para gerar o modelo compilado. Por exemplo:

dotnet ef dbcontext optimize

As opções --output-dir e --namespace podem ser usadas para especificar o diretório e o namespace no qual o modelo compilado será gerado. Por exemplo:

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>

A saída da execução desse comando inclui uma parte do código para copiar e colar na configuração DbContext a fim de instruir o EF Core a usar o modelo compilado. Por exemplo:

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

Inicialização do modelo compilado

Normalmente, não é necessário examinar o código de inicialização gerado. No entanto, às vezes, talvez seja útil personalizar o modelo ou o carregamento dele. O código de inicialização é parecido com este:

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

Essa é uma classe parcial com métodos parciais que podem ser implementados para personalizar o modelo conforme necessário.

Além disso, vários modelos compilados podem ser gerados para tipos DbContext que podem usar modelos diferentes, dependendo de uma configuração de runtime. Eles devem ser colocados em pastas e namespaces diferentes, conforme mostrado acima. Em seguida, as informações de runtime, como a cadeia de conexão, podem ser examinadas e o modelo correto retornado conforme necessário. Por exemplo:

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

Limitações

Os modelos compilados têm algumas limitações:

Devido a essas limitações, você só deverá usar modelos compilados se o tempo de inicialização do EF Core for muito lento. Normalmente, não vale a pena compilar modelos pequenos.

Se o suporte a um desses recursos for crítico para seu sucesso, vote nos problemas apropriados vinculados acima.

Parâmetros de comparação

Dica

Você pode tentar compilar um modelo grande e executar um parâmetro de comparação nele baixando o código de exemplo do GitHub.

O modelo do repositório GitHub referenciado acima contém 449 tipos de entidades, 6.390 propriedades e 720 relações. Esse é um modelo moderadamente grande. Usando BenchmarkDotNet para a medição, o tempo médio para a primeira consulta é de 1,02 segundo em um laptop razoavelmente avançado. O uso de modelos compilados reduz esse número para 117 milissegundos no mesmo hardware. Um aprimoramento de 8x a 10x como esse permanece relativamente constante à medida que o tamanho do modelo aumenta.

Compiled model performance improvement

Observação

Confira Comunicado sobre o Entity Framework Core 6.0 Versão Prévia 5: Modelos compilados no blog do .NET para ver uma discussão mais detalhada sobre o desempenho da inicialização do EF Core e os modelos compilados.

Desempenho aprimorado no TechEmpower Fortunes

Problema do GitHub: nº 23611.

Fizemos aprimoramentos significativos no desempenho de consulta do EF Core 6.0. Especificamente:

  • O desempenho do EF Core 6.0 agora é 70% mais rápido no parâmetro de comparação TechEmpower Fortunes padrão do setor, em comparação com a versão 5.0.
    • Esse é o aprimoramento de desempenho de pilha completa, incluindo aprimoramentos no código do parâmetro de comparação, no runtime do .NET etc.
  • O EF Core 6.0 em si é 31% mais rápido executando consultas não rastreadas.
  • As alocações de heap foram reduzidas em 43% ao executar consultas.

Após esses aprimoramentos, a diferença entre o popular Dapper "micro ORM" e o EF Core no parâmetro de comparação TechEmpower Fortunes diminuiu de 55% para cerca de pouco menos de 5%.

Observação

Confira Comunicado sobre o Entity Framework Core 6.0 Versão Prévia 4: Edição de desempenho no blog do .NET para ver uma discussão detalhada sobre os aprimoramentos de desempenho de consulta do EF Core 6.0.

Aprimoramentos do provedor do Azure Cosmos DB

O EF Core 6.0 contém vários aprimoramentos no provedor de banco de dados do Azure Cosmos DB.

Dica

Você pode executar e depurar todas as amostras específicas do Cosmos baixando o código de exemplo do GitHub.

Padrão de propriedade implícita

Problema do GitHub: nº 24803.

Durante a criação de um modelo para o provedor do Azure Cosmos DB, o EF Core 6.0 marcará os tipos de entidades filho como propriedade da respectiva entidade pai por padrão. Isso elimina a necessidade de grande parte das chamadas OwnsMany e OwnsOne no modelo do Azure Cosmos DB. Facilita também a inserção de tipos filho no documento do tipo pai, que geralmente é a maneira apropriada de modelar pais e filhos em um banco de dados de documentos.

Por exemplo, considere estes tipos de entidades:

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

No EF Core 5.0, esses tipos teriam sido modelados para o Azure Cosmos DB com a seguinte configuração:

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

No EF Core 6.0, a propriedade é implícita, reduzindo a configuração do modelo para:

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

Os documentos resultantes do Azure Cosmos DB têm os pais, os filhos, os animais de estimação e o endereço da família inseridos no documento da família. Por exemplo:

{
  "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
}

Observação

É importante lembrar que a configuração OwnsOne/OwnsMany precisa ser usada caso você necessite fazer outras configurações nesses tipos de propriedades.

Coleções de tipos primitivos

Problema do GitHub: nº 14762.

O EF Core 6.0 mapeia nativamente as coleções de tipos primitivos durante o uso do provedor de banco de dados do Azure Cosmos DB. Por exemplo, considere este tipo de entidade:

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

A lista e o dicionário podem ser preenchidos e inseridos no banco de dados da maneira normal:

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

Isso resulta no seguinte documento 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
}

Depois, essas coleções podem ser atualizadas novamente da maneira normal:

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

await context.SaveChangesAsync();

Limitações:

  • Há suporte apenas para dicionários com chaves de cadeia de caracteres
  • Atualmente, não há suporte para consultar o conteúdo de coleções primitivas. Vote em #16926, #25700 e # 25701 se esses recursos forem importantes para você.

Conversões em funções internas

Problema do GitHub: nº 16143.

O provedor do Azure Cosmos DB já converte mais métodos BCL (biblioteca de classes base) para funções internas do Azure Cosmos DB. As tabelas a seguir mostram as novas conversões do EF Core 6.0.

Conversões de cadeia de caracteres

Método BCL Função integrada Observações
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
Operador + CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUALS Somente chamadas que não diferenciam maiúsculas de minúsculas

As conversões de LOWER, LTRIM, RTRIM, TRIM, UPPER e SUBSTRING foram uma contribuição de @Marusyk. Muito obrigado!

Por exemplo:

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

Que se traduz em:

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

Conversões matemáticas

Método BCL Função integrada
Math.Abs ou MathF.Abs ABS
Math.Acos ou MathF.Acos ACOS
Math.Asin ou MathF.Asin ASIN
Math.Atan ou MathF.Atan ATAN
Math.Atan2 ou MathF.Atan2 ATN2
Math.Ceiling ou MathF.Ceiling CEILING
Math.Cos ou MathF.Cos COS
Math.Exp ou MathF.Exp EXP
Math.Floor ou MathF.Floor FLOOR
Math.Log ou MathF.Log LOG
Math.Log10 ou MathF.Log10 LOG10
Math.Pow ou MathF.Pow POWER
Math.Round ou MathF.Round ROUND
Math.Sign ou MathF.Sign SIGN
Math.Sin ou MathF.Sin SIN
Math.Sqrt ou MathF.Sqrt SQRT
Math.Tan ou MathF.Tan TAN
Math.Truncate ou MathF.Truncate TRUNC
DbFunctions.Random RAND

Essas conversões foram uma contribuição de @Marusyk. Muito obrigado!

Por exemplo:

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

Que se traduz em:

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

Conversões de DateTime

Método BCL Função integrada
DateTime.UtcNow GetCurrentDateTime

Essas conversões foram uma contribuição de @Marusyk. Muito obrigado!

Por exemplo:

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

Que se traduz em:

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

Consultas SQL brutas com FromSql

Problema do GitHub: nº 17311.

Às vezes, é necessário executar uma consulta SQL bruta em vez de usar o LINQ. Agora, há suporte a esse recurso no provedor do Azure Cosmos DB por meio do uso do método FromSql. Ele funciona da mesma forma de sempre nos provedores relacionais. Por exemplo:

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

Que é executado como:

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

Consultas distintas

Problema do GitHub: nº 16144.

As consultas simples que usam Distinct já são convertidas. Por exemplo:

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

Que se traduz em:

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

Diagnósticos

Problema do GitHub: nº 17298.

O provedor do Azure Cosmos DB agora registra mais informações de diagnóstico, incluindo eventos para inserir, consultar, atualizar e excluir dados do banco de dados. As RU (Unidades de Solicitação) são incluídas nesses eventos sempre que apropriado.

Observação

Os logs mostrados aqui usam EnableSensitiveDataLogging() para que os valores da ID sejam mostrados.

A inserção de um item no banco de dados do Azure Cosmos DB gera o evento CosmosEventId.ExecutedCreateItem. Por exemplo, este código:

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

Registra o seguinte evento de diagnóstico:

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'

A recuperação de itens do banco de dados do Azure Cosmos DB por meio de uma consulta gera o evento CosmosEventId.ExecutingSqlQuery e, em seguida, um ou mais eventos CosmosEventId.ExecutedReadNext para os itens lidos. Por exemplo, este código:

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

Registra os seguintes eventos de diagnóstico:

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

A recuperação de um só item do banco de dados do Azure Cosmos DB por meio de Find com uma chave de partição gera os eventos CosmosEventId.ExecutingReadItem e CosmosEventId.ExecutedReadItem. Por exemplo, este código:

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

Registra os seguintes eventos de diagnóstico:

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'

Se você salvar um item atualizado no banco de dados do Azure Cosmos DB, o evento CosmosEventId.ExecutedReplaceItem será gerado. Por exemplo, este código:

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

Registra o seguinte evento de diagnóstico:

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'

Se você excluir um item do banco de dados do Azure Cosmos DB, o evento CosmosEventId.ExecutedDeleteItem será gerado. Por exemplo, este código:

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

Registra o seguinte evento de diagnóstico:

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'

Configurar a taxa de transferência

Problema do GitHub: nº 17301.

O modelo do Azure Cosmos DB já pode ser configurado com a taxa de transferência manual ou de dimensionamento automático. Esses valores provisionam a taxa de transferência no banco de dados. Por exemplo:

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

Além disso, é possível configurar tipos de entidades individuais a fim de provisionar a taxa de transferência para o contêiner correspondente. Por exemplo:

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

Configurar vida útil

Problema do GitHub: nº 17307.

Os tipos de entidades no modelo do Azure Cosmos DB já podem ser configurados com a vida útil padrão e a vida útil para o repositório analítico. Por exemplo:

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

Resolver o alocador de clientes HTTP

Problema do GitHub: nº 21274. Esse recurso foi uma contribuição de @dnperfors. Muito obrigado!

O HttpClientFactory usado pelo provedor do Azure Cosmos DB já pode ser definido explicitamente. Isso pode ser especialmente útil durante os testes, por exemplo, para ignorar a validação de certificado ao usar o emulador do Azure Cosmos DB no Linux:

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

Observação

Confira Como usar o provedor do Azure Cosmos DB do EF Core em um test drive no blog do .NET para ver um exemplo detalhado de como aplicar os aprimoramentos do provedor do Azure Cosmos DB a um aplicativo existente.

Aprimoramentos no scaffolding de um banco de dados existente

O EF Core 6.0 contém vários aprimoramentos de engenharia reversa de um modelo do EF por meio de um banco de dados existente.

Scaffolding de relações muitos para muitos

Problema do GitHub: nº 22475.

O EF Core 6.0 detecta tabelas de junção simples e gera automaticamente um mapeamento muitos para muitos referente a elas. Por exemplo, considere as tabelas de Posts e Tags, além de uma tabela de junção PostTag conectando-as:

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

Essas tabelas podem ser geradas por scaffolding na linha de comando. Por exemplo:

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

Isso resulta em uma classe para 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; }
}

E uma classe para 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; }
}

Mas nenhuma classe para a tabela PostTag. Em vez disso, a configuração de uma relação muitos para muitos é gerada por scaffolding:

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

Gerar por scaffold tipos de referência anuláveis em C#

Problema do GitHub: nº 15520.

O EF Core 6.0 já gera por scaffold um modelo do EF e tipos de entidades que usam NRTs (tipos de referência anuláveis) em C#. O uso de NRT é gerado por scaffolding automaticamente quando o suporte do NRT é habilitado no projeto C# em que o código está sendo gerado por scaffolding.

Por exemplo, a seguinte tabela Tags contém colunas de cadeia de caracteres anuláveis e não anuláveis:

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

O resultado disso são propriedades de cadeia de caracteres anuláveis e não anuláveis correspondentes na classe gerada:

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

Da mesma forma, as seguintes tabelas Posts contêm uma relação obrigatória com a tabela 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]));

O resultado disso é o scaffolding da relação não anulável (obrigatória) entre os blogs:

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

E as postagens:

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

Por fim, as propriedades DbSet no DbContext gerado são criadas de maneira amigável ao NRT. Por exemplo:

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

Os comentários sobre o banco de dados são gerados por scaffolding nos comentários sobre o código

Problema do GitHub: nº 19113. Esse recurso foi uma contribuição de @ErikEJ. Muito obrigado!

Os comentários nas tabelas e colunas SQL já são gerados por scaffolding nos tipos de entidades criados durante a engenharia reversa de um modelo do EF Core por meio de um banco de dados existente do SQL Server.

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

Aprimoramentos nas consultas LINQ

O EF Core 6.0 contém vários aprimoramentos na conversão e na execução de consultas LINQ.

Suporte aprimorado de GroupBy

Problemas do GitHub: nº 12088, nº 13805 e nº 22609.

O EF Core 6.0 contém melhor suporte para consultas GroupBy. Especificamente, o EF Core agora:

  • Converte GroupBy seguido de FirstOrDefault (ou semelhante) em um grupo
  • Dá suporte à seleção dos principais N resultados de um grupo
  • Expande as navegações depois que o operador GroupBy é aplicado

Veja a seguir exemplos de consultas de relatórios de clientes e a conversão delas no SQL Server.

Exemplo 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]

Exemplo 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]

Exemplo 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)

Exemplo 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]

Exemplo 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))

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

Exemplo 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]

Exemplo 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]

Exemplo 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]

Exemplo 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]

Exemplo 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]

Exemplo 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]

Exemplo 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]

Modelo

Os tipos de entidades usados para esses exemplos são:

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

Converter String.Concat com vários argumentos

Problema do GitHub: nº 23859. Esse recurso foi uma contribuição de @wmeints. Muito obrigado!

A partir do EF Core 6.0, as chamadas a String.Concat com vários argumentos já são convertidas em SQL. Por exemplo, a seguinte consulta:

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

Será convertido no seguinte SQL ao usar o SQL Server:

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

Integração mais fácil com System.Linq.Async

Problema do GitHub: nº 24041.

O pacote System.Linq.Async adiciona o processamento LINQ assíncrono do lado do cliente. O uso desse pacote com versões anteriores do EF Core era complexo devido a um conflito de namespace nos métodos LINQ assíncronos. No EF Core 6.0, aproveitamos os padrões correspondentes C# para IAsyncEnumerable<T>, de modo que a DbSet<TEntity> exposta do EF Core não precise implementar a interface diretamente.

Observe que a maioria dos aplicativos não precisa usar System.Linq.Async, pois as consultas do EF Core costumam ser totalmente convertidas no servidor.

Problema do GitHub: nº 23921.

No EF Core 6.0, flexibilizamos os requisitos de parâmetro para FreeText(DbFunctions, String, String) e Contains. Com isso, é possível usar essas funções com colunas binárias ou com colunas mapeadas usando um conversor de valor. Por exemplo, considere um tipo de entidade com uma propriedade Name definida como um objeto de valor:

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

Isso é mapeado para o JSON no banco de dados:

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

Uma consulta já pode ser executada por meio de Contains ou FreeText, mesmo que o tipo da propriedade seja string, não Name. Por exemplo:

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

Isso gera o seguinte SQL ao usar o SQL Server:

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

Converter ToString no SQLite

Problema do GitHub: nº 17223. Esse recurso foi uma contribuição de @ralmsdeveloper. Muito obrigado!

As chamadas a ToString() já são convertidas em SQL ao usar o provedor de banco de dados SQLite. Isso pode ser útil para pesquisas de texto que envolvem colunas que não são de cadeia de caracteres. Por exemplo, considere um tipo de entidade User que armazena números de telefone como valores numéricos:

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

ToString pode ser usado para converter o número em uma cadeia de caracteres no banco de dados. Em seguida, podemos usar essa cadeia de caracteres com uma função como LIKE para localizar números que correspondam a um padrão. Por exemplo, para encontrar todos os números que contêm 555:

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

Isso resulta no seguinte SQL ao usar um banco de dados SQLite:

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

Observe que já há suporte à conversão de ToString() para o SQL Server no EF Core 5.0 e que a conversão poderá ter suporte de outros provedores de banco de dados.

EF.Functions.Random

Problema do GitHub: nº 16141. Esse recurso foi uma contribuição de @RaymondHuy. Muito obrigado!

EF.Functions.Random é mapeado para uma função de banco de dados retornando um número pseudoaleatório entre 0 e 1, exclusivo. As conversões foram implementadas no repositório do EF Core para o SQL Server, o SQLite e o Azure Cosmos DB. Por exemplo, considere um tipo de entidade User com uma propriedade Popularity:

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

Popularity pode ter valores de 1 a 5, inclusive. Usando EF.Functions.Random podemos escrever uma consulta para retornar todos os usuários com uma popularidade escolhida aleatoriamente:

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

Isso resulta no seguinte SQL ao usar um banco de dados do SQL Server:

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

Conversão aprimorada do SQL Server para IsNullOrWhitespace

Problema do GitHub: nº 22916. Esse recurso foi uma contribuição de @Marusyk. Muito obrigado!

Considere a consulta a seguir:

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

Antes do EF Core 6.0, isso era convertido no seguinte no 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''))

Essa conversão foi aprimorada no EF Core 6.0 para:

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''))

Como definir a consulta para o provedor em memória

Problema do GitHub: nº 24600.

Um novo método ToInMemoryQuery pode ser usado para gravar uma consulta de definição no banco de dados em memória para determinado tipo de entidade. Isso é mais útil para criar o equivalente de exibições no banco de dados em memória, especialmente quando essas exibições retornam tipos de entidades sem chave. Por exemplo, considere um banco de dados de clientes localizados no Reino Unido. Cada cliente tem um endereço:

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

Agora imagine que desejamos obter uma exibição desses dados que mostre quantos clientes existem em cada área de CEP. Podemos criar um tipo de entidade sem chave para representar isso:

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

Além disso, definir uma propriedade DbSet para ele no DbContext, acompanhado de conjuntos para outros tipos de entidades de nível superior:

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

Depois, em OnModelCreating, podemos escrever uma consulta LINQ que define os dados a serem retornados para 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()
                        }));
}

Em seguida, isso pode ser consultado da mesma forma que qualquer outra propriedade DbSet:

var results = context.CustomerDensities.ToList();

Converter Substring com um parâmetro individual

Problema do GitHub: nº 20173. Esse recurso foi uma contribuição de @stevendarby. Muito obrigado!

O EF Core 6.0 já converte os usos de string.Substring com um só argumento. Por exemplo:

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

Isso resulta no seguinte SQL ao usar o SQL Server:

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'

Consultas divididas para coleções que não são de navegação

Problema do GitHub: nº 21234.

O EF Core dá suporte à divisão de uma consulta LINQ individual em várias consultas SQL. No EF Core 6.0, esse suporte foi expandido para incluir os casos em que as coleções que não são de navegação estão contidas na projeção de consulta.

Veja a seguir exemplos de consultas que mostram a conversão no SQL Server em uma consulta individual ou várias consultas.

Exemplo 1:

Consulta LINQ:

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

Consulta SQL individual:

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]

Várias consultas 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]

Exemplo 2:

Consulta LINQ:

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

Consulta SQL individual:

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]

Várias consultas 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]

Exemplo 3:

Consulta LINQ:

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

Consulta SQL individual:

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]

Várias consultas 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]

Remover a última cláusula ORDER BY ao ingressar na coleção

Problema do GitHub: nº 19828.

Ao carregar entidades um-para-muitos relacionadas, o EF Core adiciona cláusulas ORDER BY para garantir que todas as entidades relacionadas de determinada entidade sejam agrupadas. No entanto, a última cláusula ORDER BY não é necessária para que o EF gere os agrupamentos necessários e pode ter um impacto no desempenho. Portanto, essa cláusula foi removida do EF Core 6.0.

Por exemplo, considere esta consulta:

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

Com o EF Core 5.0 no SQL Server, essa consulta é convertida em:

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]

Com o EF Core 6.0, ela é convertida em:

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]

Marcar consultas com o nome de arquivo e o número de linha

Problema do GitHub: nº 14176. Esse recurso foi uma contribuição de @michalczerwinski. Muito obrigado!

As marcas de consulta permitem adicionar uma marca textural a uma consulta LINQ de modo que ela seja incluída no SQL gerado. No EF Core 6.0, isso pode ser usado para marcar consultas com o nome de arquivo e o número de linha do código LINQ. Por exemplo:

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

Isso resulta no seguinte SQL ao usar o SQL Server:

-- 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%')

Alterações no tratamento de dependente opcional próprio

Problema do GitHub: nº 24558.

Fica complicado saber se uma entidade dependente opcional existe ou não quando ela compartilha uma tabela com a entidade principal. Isso ocorre porque há uma linha na tabela para o dependente, devido ao principal precisar dela, independentemente de o dependente existir ou não. A maneira de lidar com isso de maneira inequívoca é garantir que o dependente tenha, pelo menos, uma propriedade obrigatória. Como uma propriedade obrigatória não pode ser nula, isso significa que, se o valor da coluna dessa propriedade for nulo, a entidade dependente não existirá.

Por exemplo, considere uma classe Customer em que cada cliente tenha um Address próprio:

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

O endereço é opcional, o que significa que é válido salvar um cliente sem nenhum endereço:

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

No entanto, se um cliente tiver um endereço, esse endereço precisará ter, pelo menos, um CEP não nulo:

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

Isso é garantido marcando a propriedade Postcode como Required.

Agora, quando os clientes são consultados, se a coluna Postcode é nula, isso significa que o cliente não tem um endereço e a propriedade de navegação Customer.Address é deixada nula. Por exemplo, iterar pelos clientes e verificar se o Address é nulo:

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

Gera os seguintes resultados:

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

Considere, em vez disso, o caso em que nenhuma propriedade além do endereço é obrigatória:

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

Agora é possível salvar um cliente sem nenhum endereço e um cliente com um endereço em que todas as propriedades de endereço são nulas:

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

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

No entanto, no banco de dados, esses dois casos são indistinguíveis, como podemos ver consultando diretamente as colunas de banco de dados:

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

Por esse motivo, o EF Core 6.0 agora avisará quando você salvar um dependente opcional em que todas as propriedades são nulas. Por exemplo:

aviso: 27/9/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) A entidade do tipo 'Address' com os valores de chave primária {CustomerId: -2147482646} é um dependente opcional que usa o compartilhamento de tabela. A entidade não tem nenhuma propriedade com um valor não padrão para identificar se a entidade existe. Isso significa que, quando ela for consultada, nenhuma instância de objeto será criada em vez de uma instância com todas as propriedades definidas com os valores padrão. Todos os dependentes aninhados também serão perdidos. Não salve nenhuma instância com apenas os valores padrão ou marque a navegação recebida como obrigatória no modelo.

Isso fica ainda mais complicado quando o próprio dependente opcional funciona como um principal para outro dependente opcional, também mapeado para a mesma tabela. Em vez de apenas fornecer um aviso, o EF Core 6.0 não permite apenas casos de dependentes opcionais aninhados. Por exemplo, considere o seguinte modelo, no qual ContactInfo é propriedade de Customer e Address, por sua vez, é propriedade de 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; }
}

Agora, se ContactInfo.Phone for nulo, o EF Core não criará uma instância de Address se a relação for opcional, mesmo que o endereço em si possa conter dados. Para esse tipo de modelo, o EF Core 6.0 vai gerar a seguinte exceção:

System.InvalidOperationException: o tipo de entidade 'ContactInfo' é um dependente opcional que usa o compartilhamento de tabela e contém outros dependentes sem nenhuma propriedade não compartilhada obrigatória para identificar se a entidade existe. Se todas as propriedades anuláveis contiverem um valor nulo no banco de dados, uma instância de objeto não será criada na consulta, causando a perda dos valores dos dependentes aninhados. Adicione uma propriedade obrigatória para criar instâncias com valores nulos para outras propriedades ou marque a navegação recebida como obrigatória para sempre criar uma instância.

O fator crucial aqui é evitar o caso em que um dependente opcional pode conter todos os valores de propriedade anuláveis e compartilha uma tabela com o principal. Há três maneiras fáceis de evitar isso:

  1. Tornar o dependente obrigatório. Isso significa que a entidade dependente sempre terá um valor depois de consultada, mesmo que todas as respectivas propriedades sejam nulas.
  2. Verifique se o dependente contém, pelo menos, uma propriedade obrigatória, conforme descrito acima.
  3. Salve os dependentes opcionais em uma tabela própria, em vez de compartilhar uma tabela com o principal.

Um dependente pode ser exigido por meio do atributo Required em uma navegação própria:

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

Ou, então, especificando que ele é obrigatório em OnModelCreating:

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

Os dependentes podem ser salvos em uma tabela diferente pela especificação das tabelas a serem usadas em OnModelCreating:

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

Confira a OptionalDependentsSample no GitHub para ver mais exemplos de dependentes opcionais, incluindo casos com dependentes opcionais aninhados.

Novos atributos de mapeamento

O EF Core 6.0 contém vários novos atributos que podem ser aplicados ao código para alterar a maneira como ele é mapeado para o banco de dados.

UnicodeAttribute

Problema do GitHub: nº 19794. Esse recurso foi uma contribuição de @RaymondHuy. Muito obrigado!

A partir do EF Core 6.0, uma propriedade de cadeia de caracteres já pode ser mapeada para uma coluna não Unicode por meio de um atributo de mapeamento sem especificar o tipo de banco de dados diretamente. Por exemplo, considere um tipo de entidade Book com uma propriedade para o ISBN (International Standard Book Number) no formato "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; }
}

Como os ISBNs não podem conter caracteres não Unicode, o atributo Unicode fará com que um tipo de cadeia de caracteres não Unicode seja usado. Além disso, MaxLength é usado para limitar o tamanho da coluna de banco de dados. Por exemplo, ao usar o SQL Server, isso resultará na coluna de banco de dados varchar(22):

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

Observação

Por padrão, o EF Core mapeia as propriedades de cadeia de caracteres para colunas Unicode. UnicodeAttribute é ignorado quando o sistema de banco de dados dá suporte apenas a tipos Unicode.

PrecisionAttribute

Problema do GitHub: nº 17914. Esse recurso foi uma contribuição de @RaymondHuy. Muito obrigado!

A precisão e a escala de uma coluna de banco de dados já podem ser configuradas por meio de atributos de mapeamento sem especificar o tipo de banco de dados diretamente. Por exemplo, considere um tipo de entidade Product com uma propriedade decimal Price:

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

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

O EF Core mapeará essa propriedade para uma coluna de banco de dados com precisão 10 e escala 2. Por exemplo, no SQL Server:

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

EntityTypeConfigurationAttribute

Problema do GitHub: nº 23163. Esse recurso foi uma contribuição de @KaloyanIT. Muito obrigado!

As instâncias IEntityTypeConfiguration<TEntity> permitem que a configuração de ModelBuilder de cada tipo de entidade seja contida em uma classe de configuração própria. Por exemplo:

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

Normalmente, essa classe de configuração precisa ser instanciada e chamada em DbContext.OnModelCreating. Por exemplo:

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

A partir do EF Core 6.0, é possível colocar EntityTypeConfigurationAttribute em um tipo de entidade para que o EF Core possa encontrar e usar a configuração apropriada. Por exemplo:

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

Esse atributo significa que o EF Core usará a implementação IEntityTypeConfiguration especificada sempre que o tipo de entidade Book for incluído em um modelo. O tipo de entidade é incluído em um modelo por meio de um dos mecanismos habituais. Por exemplo, criando uma propriedade DbSet<TEntity> para o tipo de entidade:

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

    //...

Ou, então, registrando-a em OnModelCreating:

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

Observação

Os tipos EntityTypeConfigurationAttribute não serão descobertos automaticamente em um assembly. Os tipos de entidades precisam ser adicionados ao modelo antes que o atributo seja descoberto nesse tipo de entidade.

Aprimoramentos na criação de modelos

Além dos novos atributos de mapeamento, o EF Core 6.0 contém vários outros aprimoramentos no processo de criação de modelos.

Suporte a colunas esparsas do SQL Server

Problema do GitHub: nº 8023.

As colunas esparsas do SQL Server são colunas comuns otimizadas para armazenar valores nulos. Isso pode ser útil ao usar o mapeamento de herança TPH, em que as propriedades de um subtipo raramente usado resultarão em valores nulos de coluna na maioria das linhas da tabela. Por exemplo, considere uma classe ForumModerator que é estendida de ForumUser:

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

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

Talvez haja milhões de usuários, sendo apenas alguns deles moderadores. Isso significa que mapear o ForumName como esparso pode fazer sentido aqui. Agora isso pode ser configurado por meio de IsSparse em OnModelCreating. Por exemplo:

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

Em seguida, as migrações do EF Core marcarão a coluna como esparsa. Por exemplo:

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

Observação

As colunas esparsas têm limitações. Leia a documentação sobre colunas esparsas do SQL Server para verificar se as colunas esparsas são a escolha certa para seu cenário.

Aprimoramentos na API HasConversion

Problema do GitHub: nº 25468.

Antes do EF Core 6.0, as sobrecargas genéricas dos métodos HasConversion usavam o parâmetro genérico para especificar o tipo de destino da conversão. Por exemplo, considere uma enumeração Currency:

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

O EF Core pode ser configurado para salvar os valores dessa enumeração como as cadeias de caracteres "UsDollars", "PoundsStirling" e "Euros" por meio de HasConversion<string>. Por exemplo:

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

A partir do EF Core 6.0, o tipo genérico pode especificar um tipo de conversor de valor. Ele pode ser um dos conversores de valor internos. Por exemplo, para armazenar os valores de enumeração como números de 16 bits no banco de dados:

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

Ou, então, ele pode ser um tipo de conversor de valor personalizado. Por exemplo, considere um conversor que armazena os valores de enumeração como símbolos de moeda:

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

Agora isso pode ser configurado por meio do método genérico HasConversion:

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

Menos configuração nas relações muitos para muitos

Problema do GitHub: nº 21535.

Relações muitos para muitos inequívocas entre dois tipos de entidades são descobertas por convenção. Quando necessário ou se desejado, as navegações podem ser especificadas explicitamente. Por exemplo:

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

Nesses dois casos, o EF Core cria uma entidade compartilhada tipada com base em Dictionary<string, object> para funcionar como a entidade de junção entre os dois tipos. A partir do EF Core 6.0, UsingEntity pode ser adicionado à configuração para alterar somente esse tipo, sem a necessidade de configuração adicional. Por exemplo:

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

Além disso, o tipo de entidade de junção pode ser configurado adicionalmente sem a necessidade de especificar de maneira explícita as relações à esquerda e à direita. Por exemplo:

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

Por fim, a configuração completa pode ser fornecida. Por exemplo:

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

Permitir que conversores de valor convertam nulos

Problema do GitHub: nº 13850.

Importante

Devido aos problemas descritos abaixo, os construtores de ValueConverter que permitem a conversão de nulos foram marcados com [EntityFrameworkInternal] na versão 6.0 do EF Core. O uso desses construtores agora vai gerar um aviso de build.

Em geral, os conversores de valor não permitem a conversão de nulo em algum outro valor. Isso ocorre porque o mesmo conversor de valor pode ser usado para tipos anuláveis e não anuláveis, o que é muito útil para combinações de PK/FK em que a FK geralmente é anulável e a PK não.

A partir do EF Core 6.0, um conversor de valor que converte nulos pode ser criado. No entanto, a validação desse recurso revelou ser muito problemática na prática com muitas armadilhas. Por exemplo:

Esses não são problemas triviais e, para os problemas de consulta, não são fáceis de serem detectados. Portanto, marcamos esse recurso como interno no EF Core 6.0. Você ainda poderá usá-lo, mas receberá um aviso do compilador. O aviso pode ser desabilitado por meio de #pragma warning disable EF1001.

Um exemplo de um caso em que a conversão de nulos pode ser útil é quando o banco de dados contém nulos, mas o tipo de entidade deseja usar outro valor padrão para a propriedade. Por exemplo, considere uma enumeração em que o valor padrão seja "Desconhecida":

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

No entanto, o banco de dados pode ter valores nulos quando a raça é desconhecida. No EF Core 6.0, um conversor de valor pode ser usado para considerar isso:

    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
    }

Os gatos com a raça "Desconhecida" terão a coluna Breed definida como nula no banco de dados. Por exemplo:

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

O que gera as seguintes instruções INSERT no 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();

Aprimoramentos no alocador DbContext

O AddDbContextFactory também registra o DbContext diretamente

Problema do GitHub: nº 25164.

Às vezes, é útil ter um tipo DbContext e um alocador para contextos desse tipo registrados no contêiner de DI (injeção de dependência) de aplicativos. Isso permite, por exemplo, que uma instância com escopo do DbContext seja resolvida com base no escopo da solicitação, enquanto o alocador pode ser usado para criar várias instâncias independentes, quando necessário.

Para dar suporte a esse recurso, AddDbContextFactory agora também registra o tipo DbContext como um serviço com escopo. Por exemplo, considere este registro no contêiner de DI do aplicativo:

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

Com esse registro, o alocador pode ser resolvido com base no contêiner de DI raiz, assim como nas versões anteriores:

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

Observe que as instâncias de contexto criadas pelo alocador precisam ser explicitamente descartadas.

Além disso, uma instância de DbContext pode ser resolvida diretamente com base em um escopo de contêiner:

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

Nesse caso, a instância de contexto é descartada quando o escopo do contêiner é descartado. O contexto não deve ser descartado explicitamente.

Em um nível mais alto, isso significa que o DbContext do alocador pode ser injetado em outros tipos de DI. Por exemplo:

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

Ou:

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

O DbContextFactory ignora um construtor DbContext sem parâmetros

Problema do GitHub: nº 24124.

O EF Core 6.0 já permite o uso de um construtor DbContext sem parâmetros e um construtor que usa DbContextOptions no mesmo tipo de contexto quando o alocador é registrado por meio de AddDbContextFactory. Por exemplo, o contexto usado nos exemplos acima contém os dois construtores:

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

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

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

O pool de DbContext pode ser usado sem injeção de dependência

Problema do GitHub: nº 24137.

O tipo PooledDbContextFactory passou a ser público, de modo que ele possa ser usado como um pool autônomo para instâncias de DbContext, sem a necessidade do seu aplicativo ter um contêiner de injeção de dependência. O pool é criado com uma instância de DbContextOptions que será usada para criar instâncias de contexto:

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

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

Em seguida, o alocador pode ser usado para criar e agrupar instâncias. Por exemplo:

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

As instâncias são retornadas ao pool quando são descartadas.

Melhorias diversas

Por fim, o EF Core contém vários aprimoramentos em áreas não abordadas acima.

Usar [ColumnAttribute.Order] ao criar tabelas

Problema do GitHub: nº 10059.

A propriedade Order de ColumnAttribute já pode ser usada para ordenar colunas ao criar uma tabela com migrações. Por exemplo, considere o seguinte modelo:

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

Por padrão, o EF Core ordena as colunas de chave primária primeiro, seguidas das propriedades do tipo de entidade e dos tipos próprios e, por fim, das propriedades de tipos base. Por exemplo, a seguinte tabela é criada no 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]));

No EF Core 6.0, ColumnAttribute pode ser usado para especificar outra ordem de coluna. Por exemplo:

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

No SQL Server, a tabela gerada agora é:

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

Com isso, as colunas FistName e LastName são movidas para o início, mesmo que sejam definidas em um tipo base. Observe que os valores de ordem de coluna podem ter lacunas, permitindo que os intervalos sejam usados para sempre colocar as colunas no final, mesmo quando usados por vários tipos derivados.

Este exemplo também mostra como o mesmo ColumnAttribute pode ser usado para especificar o nome da coluna e a ordem.

A ordenação de colunas também pode ser configurada por meio da API ModelBuilder em OnModelCreating. Por exemplo:

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

A ordenação no construtor de modelos com HasColumnOrder tem precedência sobre qualquer ordem especificada com ColumnAttribute. Isso significa que HasColumnOrder pode ser usado para substituir a ordenação feita com atributos, incluindo a resolução de conflitos quando atributos em propriedades diferentes especificam o mesmo número de ordem.

Importante

Observe que, de modo geral, a maioria dos bancos de dados só dá suporte a colunas de ordenação quando a tabela é criada. Isso significa que o atributo de ordem de coluna não pode ser usado para ordenar novamente as colunas de uma tabela existente. Uma exceção notável disso é o SQLite, em que as migrações recompilarão toda a tabela com novas ordens de colunas.

API mínima do EF Core

Problema do GitHub: nº 25192.

O .NET Core 6.0 inclui modelos atualizados que apresentam "APIs mínimas" simplificadas que removem grande parte do código clichê tradicionalmente necessário nos aplicativos .NET.

O EF Core 6.0 contém um novo método de extensão que registra um tipo DbContext e fornece a configuração de um provedor de banco de dados em uma só linha. Por exemplo:

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==");

Eles são exatamente equivalentes a:

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=="));

Observação

As APIs mínimas do EF Core só dão suporte ao registro e à configuração muito básicos de um DbContext e um provedor. Use AddDbContext, AddDbContextPool, AddDbContextFactory etc. para acessar todos os tipos de registro e configuração disponíveis no EF Core.

Confira estes recursos para saber mais sobre as APIs mínimas:

Preservar o contexto de sincronização em SaveChangesAsync

Problema do GitHub: nº 23971.

Alteramos o código do EF Core na versão 5.0 para definir Task.ConfigureAwait como false em todos os lugares com uma ocorrência de código assíncrono await. Geralmente, essa é uma opção melhor para o uso do EF Core. No entanto, SaveChangesAsync é um caso especial porque o EF Core definirá valores gerados nas entidades rastreadas após a conclusão da operação assíncrona de banco de dados. Em seguida, essas alterações podem disparar notificações que, por exemplo, talvez precisem ser executadas no thread da interface do usuário. Portanto, revertemos essa alteração no EF Core 6.0 somente para o método SaveChangesAsync.

Banco de dados em memória: validar se as propriedades obrigatórias não são nulas

Problema do GitHub: nº 10613. Esse recurso foi uma contribuição de @fagnercarvalho. Muito obrigado!

Agora o banco de dados em memória do EF Core vai gerar uma exceção se for feita uma tentativa de salvar um valor nulo em uma propriedade marcada como obrigatória. Por exemplo, considere um tipo User com uma propriedade Username obrigatória:

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

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

A tentativa de salvar uma entidade com um Username nulo resultará na seguinte exceção:

Microsoft.EntityFrameworkCore.DbUpdateException: as propriedades obrigatórias '{'Username'}' estão ausentes na instância do tipo de entidade 'User' com o valor da chave '{Id: 1}'.

Essa validação pode ser desabilitada, se necessário. Por exemplo:

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

Informações de fonte do comando para diagnósticos e interceptores

Problema do GitHub: nº 23719. Esse recurso foi uma contribuição de @Giorgi. Muito obrigado!

O CommandEventData fornecido para fontes de diagnósticos e interceptores já contém um valor de enumeração que indica a parte do EF responsável por criar o comando. Ele pode ser usado como um filtro no diagnóstico ou no interceptor. Por exemplo, talvez desejemos obter um interceptor que só seja aplicado aos comandos provenientes de 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;
    }
}

Isso filtra o interceptor apenas para os eventos SaveChanges, quando usado em um aplicativo que também gera migrações e consultas. Por exemplo:

Saving changes for CustomersContext:

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

Melhor tratamento de valores temporários

Problema do GitHub: nº 24245.

O EF Core não expõe valores temporários em instâncias de tipo de entidade. Por exemplo, considere um tipo de entidade Blog com uma chave gerada pelo repositório:

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

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

A propriedade de chave Id obterá um valor temporário assim que um Blog for rastreado pelo contexto. Por exemplo, ao chamar DbContext.Add:

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

O valor temporário pode ser obtido do controlador de alterações de contexto, mas não é definido na instância da entidade. Por exemplo, este código:

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

Isso gera a saída a seguir:

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

Isso é bom, porque impede o vazamento do valor temporário para o código do aplicativo, no qual ele pode ser tratado acidentalmente como não temporário. No entanto, às vezes, é útil lidar diretamente com valores temporários. Por exemplo, um aplicativo pode desejar gerar valores temporários próprios para um grafo de entidades antes de elas serem rastreadas, de modo que sejam usadas para formar relações por meio de chaves estrangeiras. Isso pode ser feito pela marcação explícita dos valores como temporários. Por exemplo:

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

No EF Core 6.0, o valor permanecerá na instância da entidade, embora agora seja marcado como temporário. Por exemplo, o código acima gera a seguinte saída:

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

Da mesma forma, os valores temporários gerados pelo EF Core podem ser definidos explicitamente em instâncias de entidade e marcados como valores temporários. Isso pode ser usado para definir explicitamente as relações entre novas entidades usando os respectivos valores de chave temporários. Por exemplo:

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

Resultando em:

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

Anotação de tipos de referência anuláveis em C# no EF Core

Problema do GitHub: nº 19007.

Toda a base de código do EF Core já usa NRTs (tipos de referência anuláveis) em C#. Isso significa que você obterá as indicações corretas do compilador para o uso de nulos ao usar o EF Core 6.0 por meio do seu código.

Microsoft.Data.Sqlite 6.0

Dica

Você pode executar e depurar todas as amostras indicadas a seguir baixando o código de exemplo do GitHub.

Pool de conexões

Problema do GitHub: nº 13837.

É uma prática comum manter as conexões de banco de dados abertas pelo menor tempo possível. Isso ajuda a impedir a contenção no recurso de conexão. É por isso que bibliotecas como o EF Core abrem a conexão imediatamente antes de executar uma operação de banco de dados e a fecham imediatamente em seguida. Por exemplo, considere este código do 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.");
    }
}

A saída do código, com o registro em log para conexões ativado, é:

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.

Observe que a conexão é aberta e fechada rapidamente para cada operação.

Porém, na maioria dos sistemas de banco de dados, abrir uma conexão física com o banco de dados é uma operação cara. Portanto, a maior parte dos provedores ADO.NET cria um pool de conexões físicas e as aluga para as instâncias de DbConnection conforme necessário.

O SQLite é um pouco diferente, pois o acesso ao banco de dados costuma ser apenas de um arquivo. Isso significa que abrir uma conexão com um banco de dados SQLite geralmente é muito rápido. No entanto, esse nem sempre é o caso. Por exemplo, abrir uma conexão com um banco de dados criptografado pode ser muito lento. Portanto, as conexões do SQLite agora são agrupadas quando o Microsoft.Data.Sqlite 6.0 é usado.

Suporte a DateOnly e TimeOnly

Problema do GitHub: nº 24506.

O Microsoft.Data.Sqlite 6.0 dá suporte aos novos tipos DateOnly e TimeOnly do .NET 6. Eles também podem ser usados no EF Core 6.0 com o provedor do SQLite. Como sempre no SQLite, o sistema de tipos nativos significa que os valores desses tipos precisam ser armazenados como um dos quatro tipos com suporte. O Microsoft.Data.Sqlite os armazena como TEXT. Por exemplo, uma entidade que usa estes tipos:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    
    public DateOnly Birthday { get; set; }
    public TimeOnly TokensRenewed { get; set; }
}

É mapeada para a seguinte tabela no banco de dados 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);

Em seguida, os valores podem ser salvos, consultados e atualizados como de costume. Por exemplo, esta consulta LINQ do EF Core:

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

É convertida no seguinte no SQLite:

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

Além disso, só retorna os usos com aniversários antes de 1900 CE:

Found 'ajcvickers'
Found 'wendy'

API de Pontos de Salvamento

Problema do GitHub: nº 20228.

Temos padronizado uma API comum para pontos de salvamento em provedores ADO.NET. O Microsoft.Data.Sqlite já dá suporte a essa API, incluindo:

O uso de um ponto de salvamento permite que parte de uma transação seja revertida sem reverter toda a transação. Por exemplo, o código abaixo:

  • Cria uma transação
  • Envia uma atualização ao banco de dados
  • Cria um ponto de salvamento
  • Envia outra atualização ao banco de dados
  • Reverte a transação para o ponto de salvamento criado anteriormente
  • Confirma a transação
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();

O resultado disso será a confirmação da primeira atualização no banco de dados, enquanto a segunda atualização não é confirmada, pois o ponto de salvamento foi revertido antes da confirmação da transação.

Tempo limite do comando na cadeia de conexão

Problema do GitHub: nº 22505. Esse recurso foi uma contribuição de @nmichels. Muito obrigado!

Os provedores ADO.NET dão suporte a dois tempos limite distintos:

  • O tempo limite de conexão, que determina o tempo máximo de espera ao fazer uma conexão com o banco de dados.
  • O tempo limite de comando, que determina o tempo máximo de espera da conclusão da execução de um comando.

O tempo limite de comando pode ser definido no código por meio de DbCommand.CommandTimeout. Muitos provedores também já estão expondo esse tempo limite do comando na cadeia de conexão. O Microsoft.Data.Sqlite está seguindo essa tendência com a palavra-chave da cadeia de conexão Command Timeout. Por exemplo, "Command Timeout=60;DataSource=test.db" usará 60 segundos como o tempo limite padrão para os comandos criados pela conexão.

Dica

O Sqlite trata Default Timeout como sinônimo de Command Timeout e pode ser usado, se desejado.