Novedades en EF Core 6.0

EF Core 6.0 se ha enviado a NuGet. Esta página contiene información general sobre los cambios interesantes introducidos en esta versión.

Sugerencia

Puede ejecutar y depurar los ejemplos que se muestran a continuación si descarga el código de ejemplo de GitHub.

Tablas temporales de SQL Server

Problema de GitHub nº. 4693.

Las tablas temporales de SQL Server realizarán automáticamente el seguimiento de todos los datos almacenados en una tabla, incluso después de que se hayan actualizado o eliminado. Esto se logra mediante la creación de una "tabla de historial" paralela en la que se almacenan los datos históricos con marca de tiempo cada vez que se realiza un cambio en la tabla principal. Esto permite consultar datos históricos, como en auditorías, o restaurarlos, como para la recuperación después de una mutación o eliminación accidentales.

Ahora en EF Core se admite lo siguiente:

  • Creación de tablas temporales mediante migraciones
  • Transformación de tablas existentes en tablas temporales, de nuevo mediante migraciones
  • Consulta de datos históricos
  • Restauración de datos desde algún punto del pasado

Configuración de una tabla temporal

El generador de modelos se puede usar para configurar una tabla como temporal. Por ejemplo:

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

Al usar EF Core para crear la base de datos, la nueva tabla se configurará como una tabla temporal con los valores predeterminados de SQL Server para las marcas de tiempo y la tabla de historial. Por ejemplo, considere un tipo de entidad 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; }
}

La tabla temporal creada tendrá el siguiente aspecto:

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 SQL Server crea dos columnas datetime2 ocultas denominadas PeriodEnd y PeriodStart. Estas "columnas de período" representan el intervalo de tiempo durante el que existían los datos de la fila. Estas columnas se asignan a propiedades reemplazadas en el modelo de EF Core, lo que permite usarlas en consultas como se muestra más adelante.

Importante

Las horas de estas columnas siempre son horas UTC generadas por SQL Server. Las horas UTC se usan para todas las operaciones con tablas temporales, como en las consultas que se muestran a continuación.

Observe también que se crea automáticamente una tabla de historial asociada denominada EmployeeHistory. Los nombres de las columnas de período y la tabla de historial se pueden cambiar con una configuración adicional para el generador de modelos. Por ejemplo:

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

Esto se refleja en la tabla creada por 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]))');

Uso de tablas temporales

La mayoría de las veces, las tablas temporales se usan igual que cualquier otra tabla. Es decir, SQL Server administra de forma transparente las columnas de período y los datos históricos para que la aplicación pueda omitirlos. Por ejemplo, las nuevas entidades se pueden guardar en la base de datos de la manera 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();

Después, estos datos se pueden consultar, actualizar y eliminar de la manera habitual. Por ejemplo:

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

Además, después de una consulta de seguimiento normal, se puede acceder a los valores de las columnas de período de los datos actuales desde las entidades de las que se realiza el seguimiento. Por ejemplo:

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

Esto muestra:

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 la columna ValidTo (denominada PeriodEnd de forma predeterminada) contiene el valor datetime2 máximo. Esto es lo que siempre sucede con las filas actuales de la tabla. Las columnas ValidFrom (denominadas PeriodStart de manera predeterminada) contienen la hora UTC a la que se ha insertado la fila.

Consulta de datos históricos

EF Core admite consultas que incluyen datos del historial a través de varios operadores de consulta nuevos:

  • TemporalAsOf: devuelve las filas que estaban activas (actuales) a la hora UTC dada. Se trata de una sola fila de la tabla actual o la tabla del historial para una clave principal concreta.
  • TemporalAll: devuelve todas las filas de los datos históricos. Normalmente, son muchas filas de la tabla de historial o de la tabla actual para una clave principal concreta.
  • TemporalFromTo: devuelve todas las filas que estaban activas entre dos horas UTC determinadas. Pueden ser muchas filas de la tabla de historial o de la tabla actual para una clave principal concreta.
  • TemporalBetween: igual que TemporalFromTo, salvo que se incluyen filas que se han activado en el límite superior.
  • TemporalContainedIn: devuelve todas las filas que empezaron a estar activas y terminaron activas entre dos horas UTC determinadas. Pueden ser muchas filas de la tabla de historial o de la tabla actual para una clave principal concreta.

Nota

Consulte la documentación sobre tablas temporales de SQL Server para obtener más información sobre qué filas exactas se incluyen para cada uno de estos operadores.

Por ejemplo, después de realizar algunas actualizaciones y eliminaciones en los datos, se puede ejecutar una consulta con TemporalAll para ver los datos 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 el método EF.Property se puede usar para acceder a los valores de las columnas de periodo. Esto se usa en la cláusula OrderBy para ordenar los datos y, después, en una proyección para incluir estos valores en los datos devueltos.

Esta consulta devuelve los datos siguientes:

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 la última fila devuelta ha dejado de estar activa el 26/8/2021 a las 16:44:59. Esto se debe a que en ese momento se eliminó la fila de Rainbow Dash de la tabla principal. Más adelante se verá cómo se pueden restaurar estos datos.

Se pueden escribir consultas similares mediante TemporalFromTo, TemporalBetween o TemporalContainedIn. Por ejemplo:

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

Esta consulta devuelve las filas siguientes:

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

Restauración de datos históricos

Como se ha mencionado antes, se ha eliminado Rainbow Dash de la tabla Employees. Claramente se trata de un error, por lo que volverá a un momento dado y restaurará la fila que falta desde ese momento.

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

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

Esta consulta devuelve una sola fila para Rainbow Dash como estaba a la hora UTC dada. Todas las consultas que usan operadores temporales no son de seguimiento de forma predeterminada, por lo que no se realiza el seguimiento de la entidad devuelta aquí. Esto tiene sentido, ya que actualmente no existe en la tabla principal. Para volver a insertar la entidad en la tabla principal, simplemente se marca como Added y, después, se llama a SaveChanges.

Después de volver a insertar la fila Rainbow Dash, al consultar los datos históricos se muestra que la fila ha se restaurado tal como estaba a la hora UTC dada:

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

Agrupaciones de migración

Problema de GitHub nº. 19693.

Las migraciones de EF Core se usan para generar actualizaciones de esquema de base de datos basadas en cambios en el modelo de EF. Estas actualizaciones de esquema se deben aplicar en el momento de la implementación de la aplicación, a menudo como parte de un sistema de integración continua o implementación continua (C.I. o C.D.).

Ahora en EF Core se incluye una nueva manera de aplicar estas actualizaciones de esquema: agrupaciones de migración. Una agrupación de migración es un pequeño ejecutable que contiene migraciones y el código necesario para aplicarlas a la base de datos.

Nota:

Vea Introducing DevOps-friendly EF Core Migration Bundles (Introducción a las agrupaciones de migración de EF Core compatibles con DevOps) en el blog de .NET para obtener una explicación más detallada sobre las migraciones, las agrupaciones y la implementación.

Las agrupaciones de migración se crean con la herramienta de línea de comandos dotnet ef. Asegúrese de que ha instalado la versión más reciente de la herramienta antes de continuar.

Una agrupación necesita que se incluyan migraciones. Se crean mediante dotnet ef migrations add como se describe en la documentación sobre migraciones. Una vez que tenga las migraciones listas para implementarse, cree una agrupación mediante dotnet ef migrations bundle. Por ejemplo:

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>

La salida es un ejecutable adecuado para el sistema operativo de destino. En este caso, es Windows x64, por lo que aparece una instancia de efbundle.exe en la carpeta local. Al ejecutar este ejecutable se aplican las migraciones que contiene:

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

Las migraciones se aplican a la base de datos solo si todavía no se han aplicado. Por ejemplo, la ejecución de la misma agrupación de nuevo no hace nada, ya que no hay migraciones nuevas que aplicar:

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

Pero si se realizan cambios en el modelo y se generan más migraciones con dotnet ef migrations add, se pueden agrupar en un nuevo ejecutable listo para su aplicación. Por ejemplo:

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>

Tenga en cuenta que la opción --force se puede usar para sobrescribir la agrupación existente con una nueva.

La ejecución de esta nueva agrupación aplica estas dos migraciones nuevas a la base de datos:

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

De forma predeterminada, la agrupación usa la cadena de conexión de base de datos de la configuración de la aplicación. Pero se puede realizar la migración de otra base de datos si se pasa la cadena de conexión en la línea de comandos. Por ejemplo:

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>

Tenga en cuenta que esta vez se han aplicado las tres migraciones, ya que ninguna de ellas se había aplicado todavía a la base de datos de producción.

Se pueden pasar otras opciones a la línea de comandos. Algunas opciones comunes son:

  • --output para especificar la ruta del archivo ejecutable que se va a crear.
  • --context para especificar el tipo DbContext que se va a usar cuando el proyecto contenga varios tipos de contexto.
  • --project para especificar el proyecto que se va a usar. El valor predeterminado es el directorio de trabajo actual.
  • --startup-project para especificar el proyecto de inicio que se va a usar. El valor predeterminado es el directorio de trabajo actual.
  • --no-build para evitar que el proyecto se compile antes de ejecutar el comando. Solo se debe usar si se sabe que el proyecto está actualizado.
  • --verbose para ver información detallada sobre lo que hace el comando. Use esta opción al incluir información en los informes de errores.

Use dotnet ef migrations bundle --help para ver todas las opciones disponibles.

Tenga en cuenta que, de forma predeterminada, cada migración se aplica en su propia transacción. Vea Problema de GitHub nº 22616 para obtener una explicación de las posibles mejoras futuras en esta área.

Configuración del modelo anterior a la convención

Problema de GitHub nº 12229.

En versiones anteriores de EF Core es necesario que la asignación para cada propiedad de un tipo determinado se configure explícitamente cuando esa asignación difiere del valor predeterminado. Esto incluye "facetas", como la longitud máxima de las cadenas y la precisión decimal, así como la conversión de valores para el tipo de propiedad.

Esto era necesario para lo siguiente:

  • Configuración del generador de modelos para cada propiedad
  • Un atributo de asignación en cada propiedad
  • Iteración explícita por todas las propiedades de todos los tipos de entidad y uso de las API de metadatos de bajo nivel al compilar el modelo.

Tenga en cuenta que la iteración explícita es propensa a errores y es difícil de realizar de forma sólida, ya que es posible que la lista de tipos de entidad y las propiedades asignadas no sean definitivos en el momento en que se produce esta iteración.

En EF Core 6.0 se permite especificar esta configuración de asignación una vez para un tipo determinado. Después, se aplicará a todas las propiedades de ese tipo en el modelo. Esto se denomina "configuración del modelo anterior a la convención", ya que configura aspectos del modelo que luego usan las convenciones de creación de modelos. Esta configuración se aplica al invalidar ConfigureConventions en DbContext:

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

Por ejemplo, considere los tipos de entidad siguientes:

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 las propiedades de cadena se pueden configurar para que sean ANSI (en lugar de Unicode) y tengan una longitud máxima de 1024:

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

Todas las propiedades DateTime se pueden convertir en enteros de 64 bits en la base de datos, mediante la conversión predeterminada de valores DateTime en valores long:

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

Todas las propiedades bool se pueden convertir en enteros 0 o 1 mediante uno de los convertidores de valores integrados:

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

Suponiendo que Session es una propiedad transitoria de la entidad y que no se debe conservar, se puede omitir en la totalidad del modelo:

configurationBuilder
    .IgnoreAny<Session>();

La configuración del modelo anterior a la convención es muy útil cuando se trabaja con objetos de valor. Por ejemplo, el tipo Money del modelo anterior se representa mediante una estructura de solo lectura:

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
}

Después, se serializa hacia y desde JSON mediante un convertidor de valores personalizado:

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

Este convertidor de valores se puede configurar una vez para todos los usos de Money:

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

Observe también que se pueden especificar facetas adicionales para la columna de cadena en la que se almacena el JSON serializado. En este caso, la columna se limita a una longitud máxima de 64.

Las tablas creadas para SQL Server mediante migraciones muestran cómo se ha aplicado la configuración a todas las columnas asignadas:

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

También es posible especificar una asignación de tipos predeterminada para un tipo concreto. Por ejemplo:

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

Esto rara vez es necesario, pero puede ser útil si se usa un tipo en la consulta de una manera que no está relacionada con ninguna propiedad asignada del modelo.

Nota:

Vea Announcing Entity Framework Core 6.0 Preview 6: Configure Conventions (Anuncio de Entity Framework Core 6.0 Preview 6: configuración de convenciones) en el blog de .NET para obtener más información y ejemplos de configuración del modelo anterior a la convención.

Modelos compilados

Problema de GitHub nº 1906.

Los modelos compilados pueden mejorar el tiempo de inicio de EF Core para aplicaciones con modelos grandes. Un modelo grande suele contener de cientos a miles de tipos de entidad y relaciones.

El tiempo de inicio es la hora para realizar la primera operación en un objeto DbContext cuando ese tipo de DbContext se usa por primera vez en la aplicación. Tenga en cuenta que la creación de una instancia de DbContext no hace que se inicialice el modelo de EF. En su lugar, las primeras operaciones típicas que hacen que el modelo se inicialice incluyen llamar a DbContext.Add o ejecutar la primera consulta.

Los modelos compilados se crean con la herramienta de línea de comandos dotnet ef. Asegúrese de que ha instalado la versión más reciente de la herramienta antes de continuar.

Se usa un nuevo comando dbcontext optimize para generar el modelo compilado. Por ejemplo:

dotnet ef dbcontext optimize

Las opciones --output-dir y --namespace se pueden usar para especificar el directorio y el espacio de nombres en el que se generará el modelo compilado. Por ejemplo:

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>

La salida de la ejecución de este comando incluye un fragmento de código para copiar y pegar en la configuración de DbContext para hacer que EF Core use el modelo compilado. Por ejemplo:

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

Arranque de modelos compilados

Normalmente no es necesario mirar el código de arranque generado, pero a veces puede ser útil personalizar el modelo o su carga. El código de arranque tiene una apariencia similar a la siguiente:

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

Se trata de una clase parcial con métodos parciales que se pueden implementar para personalizar el modelo según sea necesario.

Además, se pueden generar varios modelos compilados para tipos DbContext que pueden usar diferentes modelos en función de alguna configuración del entorno de ejecución. Estos deben colocarse en diferentes carpetas y espacios de nombres, como se muestra anteriormente; a continuación se puede examinar la información del entorno de ejecución, como la cadena de conexión, y se devuelve el modelo correcto según sea necesario. Por ejemplo:

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

Limitaciones

Los modelos compilados tienen algunas limitaciones:

Debido a estas limitaciones, solo debe usar modelos compilados si el tiempo inicio de EF Core es demasiado lento. La compilación de modelos pequeños normalmente no merece la pena.

Si la compatibilidad con cualquiera de estas características es fundamental para el éxito, vote por los problemas adecuados vinculados anteriormente.

Pruebas comparativas

Sugerencia

Puede intentar compilar un modelo grande y ejecutar un punto de referencia en él si descarga el código de ejemplo de GitHub.

El modelo del repositorio de GitHub al que se hace referencia anteriormente contiene 449 tipos de entidad, 6390 propiedades y 720 relaciones. Se trata de un modelo moderadamente grande. Si se usa BenchmarkDotNet para realizar la medición, el tiempo medio de la primera consulta es de 1,02 segundos en un portátil relativamente potente. El uso de modelos compilados hace que esto se rebaje a 117 milisegundos en el mismo hardware. Una mejora de 8 a 10 veces superior como esta permanece relativamente constante a medida que aumenta el tamaño del modelo.

Compiled model performance improvement

Nota:

Vea Announcing Entity Framework Core 6.0 Preview 5: Compiled Models (Anuncio de Entity Framework Core 6.0 Preview 5: modelos compilados) en el blog de .NET para obtener una explicación más detallada del rendimiento de inicio de EF Core y los modelos compilados.

Rendimiento mejorado en TechEmpower Fortunes

Problema de GitHub nº 23611.

Hemos realizado mejoras significativas en el rendimiento de las consultas en EF Core 6.0. Concretamente:

  • Ahora el rendimiento de EF Core 6.0 es un 70 % más rápido en el punto de referencia TechEmpower Fortunes estándar del sector, en comparación con la versión 5.0.
    • Esta es la mejora de rendimiento de pila completa, incluidas las mejoras en el código de punto de referencia, el entorno de ejecución de .NET, etc.
  • El propio EF Core 6.0 es un 31 % más rápido al ejecutar consultas sin seguimiento.
  • Las asignaciones de montón se han reducido un 43 % al ejecutar consultas.

Después de estas mejoras, la brecha entre el popular Dapper "micro-ORM" y EF Core en el punto de referencia de TechEmpower Fortunes se ha recortado de un 55 % a aproximadamente algo menos del 5 %.

Nota:

Vea Announcing Entity Framework Core 6.0 Preview 4: Performance Edition (Anuncio de Entity Framework Core 6.0 Preview 4: edición del rendimiento) en el blog de .NET para obtener una explicación detallada de las mejoras de rendimiento de las consultas en EF Core 6.0.

Mejoras del proveedor de Azure Cosmos DB

EF Core 6.0 contiene muchas mejoras en el proveedor de base de datos Azure Cosmos DB.

Sugerencia

Puede ejecutar y compilar en todos los ejemplos específicos de Cosmos si descarga el código de ejemplo de GitHub.

El valor predeterminado es la propiedad implícita

Problema de GitHub nº 24803.

Al compilar un modelo para el proveedor de Azure Cosmos DB, EF Core 6.0 marcará los tipos de entidad secundarios como propiedad de su entidad primaria de forma predeterminada. De esta forma se elimina la necesidad de la mayoría de las llamadas a OwnsMany y OwnsOne en el modelo de Azure Cosmos DB. Esto facilita la inserción de tipos secundarios en el documento para el tipo primario, que suele ser la manera adecuada de modelar elementos primarios y secundarios en una base de datos de documentos.

Por ejemplo, considere estos tipos de entidad:

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

En EF Core 5.0, estos tipos se habrían modelado para Azure Cosmos DB con la configuración siguiente:

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

En EF Core 6.0, la propiedad es implícita, lo que reduce la configuración del modelo a:

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

Los documentos de Azure Cosmos DB resultantes tienen los padres, los hijos, las mascotas y la dirección de la familia insertados en el documento familiar. Por ejemplo:

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

Nota:

Es importante recordar que se debe usar la configuración OwnsOne/OwnsMany si necesita configurar aún más estos tipos de propiedad.

Colecciones de tipos primitivos

Problema de GitHub nº 14762.

EF Core 6.0 asigna de forma nativa colecciones de tipos primitivos cuando se usa el proveedor de base de datos de Azure Cosmos DB. Por ejemplo, considere este tipo de entidad:

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

Tanto la lista como el diccionario se pueden rellenar e insertar en la base de datos de la manera 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);
context.SaveChanges();

Esto da como resultado el siguiente 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
}

Después, estas colecciones se pueden actualizar, también de la manera habitual:

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

context.SaveChanges();

Limitaciones:

  • Solo se admiten diccionarios con claves de cadena
  • Actualmente no se admite la consulta en el contenido de colecciones primitivas. Vote por nº 16926, nº 25700 y nº 25701 si considera importantes estas características.

Traducciones a funciones integradas

Problema de GitHub nº 16143.

El proveedor de Azure Cosmos DB ahora traduce más métodos de biblioteca de clases base (BCL) a funciones integradas de Azure Cosmos DB. En las tablas siguientes se muestran las traducciones que son nuevas en EF Core 6.0.

Traducciones de cadenas

Método BCL Función integrada Notas
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 STRINGEQUAL Solo llamadas sin distinción de mayúsculas y minúsculas

Las traducciones para LOWER, LTRIM, RTRIM, TRIM, UPPER y SUBSTRING las ha aportado @Marusyk. Muchas gracias.

Por ejemplo:

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

Lo que se traduce en:

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

Traducciones matemáticas

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

Estas traducciones las ha aportado @Marusyk. Muchas gracias.

Por ejemplo:

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

Lo que se traduce en:

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

Traducciones de DateTime

Método BCL Función integrada
DateTime.UtcNow GetCurrentDateTime

Estas traducciones las ha aportado @Marusyk. Muchas gracias.

Por ejemplo:

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

Lo que se traduce en:

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

Consultas SQL sin procesar con FromSql

Problema de GitHub nº 17311.

A veces es necesario ejecutar una consulta SQL sin formato en lugar de usar LINQ. Ahora esto se admite con el proveedor de Azure Cosmos DB mediante el uso del método FromSql. Funciona de la misma manera habitual de los proveedores relacionales. Por ejemplo:

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

Que se ejecuta como:

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

Consultas distintivas

Problema de GitHub nº 16144.

Ahora se traducen las consultas simples que usan Distinct. Por ejemplo:

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

Lo que se traduce en:

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

Diagnóstico

Problema de GitHub nº 17298.

El proveedor de Azure Cosmos DB ahora registra más información de diagnóstico, incluidos los eventos para insertar, consultar, actualizar y eliminar datos de la base de datos. Las unidades de solicitud (RU) se incluyen en estos eventos siempre que sea necesario.

Nota:

Los registros muestran dónde usar EnableSensitiveDataLogging() para que se muestren esos valores de id.

La inserción de un elemento en la base de datos de Azure Cosmos DB genera el evento CosmosEventId.ExecutedCreateItem. Por ejemplo, este código:

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

Registra el siguiente 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'

La recuperación de elementos de la base de datos de Azure Cosmos DB mediante una consulta genera el evento CosmosEventId.ExecutingSqlQuery y, después, uno o varios eventos CosmosEventId.ExecutedReadNext para los elementos leídos. Por ejemplo, este código:

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

Registra los siguientes 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

La recuperación de un único elemento de la base de datos de Azure Cosmos DB mediante Find con una clave de partición genera los eventos CosmosEventId.ExecutingReadItem y CosmosEventId.ExecutedReadItem. Por ejemplo, este código:

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

Registra los siguientes 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'

Al guardar un elemento actualizado en la base de datos de Azure Cosmos DB se genera el evento CosmosEventId.ExecutedReplaceItem. Por ejemplo, este código:

triangle.Angle2 = 89;
context.SaveChanges();

Registra el siguiente 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'

Al eliminar un elemento de la base de datos de Azure Cosmos DB se genera el evento CosmosEventId.ExecutedDeleteItem. Por ejemplo, este código:

context.Remove(triangle);
context.SaveChanges();

Registra el siguiente 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'

Configuración del rendimiento

Problema de GitHub nº 17301.

El modelo de Azure Cosmos DB se puede configurar con rendimiento manual o de escalabilidad automática. Estos valores aprovisionan el rendimiento en la base de datos. Por ejemplo:

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

Además, se pueden configurar tipos de entidad individuales para aprovisionar el rendimiento del contenedor correspondiente. Por ejemplo:

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

Configuración del período de vida

Problema de GitHub nº 17307.

Los tipos de entidad del modelo de Azure Cosmos DB ahora se pueden configurar con el período de vida predeterminado y el período de vida para el almacén analítico. Por ejemplo:

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

Resolución del generador de cliente HTTP

Problema de GitHub nº 21274. Esta característica la ha aportado @dnperfors. Muchas gracias.

Ahora se puede establecer explícitamente el valor HttpClientFactory del proveedor de Azure Cosmos DB. Esto puede ser especialmente útil durante las pruebas, por ejemplo, para omitir la validación de certificados al usar el emulador de Azure Cosmos DB en Linux:

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

Nota:

Vea Taking the EF Core Azure Cosmos DB Provider for a Test Drive (Prueba del proveedor de Azure Cosmos DB de EF Core) en el blog de .NET para obtener un ejemplo detallado de cómo aplicar las mejoras del proveedor de Azure Cosmos DB a una aplicación existente.

Mejoras en el scaffolding desde una base de datos existente

EF Core 6.0 contiene varias mejoras al realizar ingeniería inversa en un modelo de EF a partir de una base de datos existente.

Scaffolding de relaciones de varios a varios

Problema de GitHub nº 22475.

EF Core 6.0 detecta tablas de combinación simples y genera automáticamente una asignación de varios a varios para ellas. Por ejemplo, considere las tablas para Posts y Tags, y una tabla de combinación PostTag que las conecta:

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

Se puede aplicar scaffolding a estas tablas desde la línea de comandos. Por ejemplo:

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

Esto da como resultado una clase 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; }
}

Y una clase 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; }
}

Pero ninguna clase para la tabla PostTag. En su lugar, se aplica scaffolding a la configuración de una relación de varios a varios:

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

Scaffold de tipos de referencias que aceptan valores NULL de C#

Problema de GitHub nº 15520.

EF Core 6.0 ahora aplica scaffolding de un modelo de EF y tipos de entidad que usan tipos de referencia que aceptan valores NULL (NRT) de C#. Se aplica scaffolding automáticamente al uso de NRT cuando se habilita la compatibilidad con NRT en el proyecto de C# en el que se aplica scaffolding al código.

Por ejemplo, la tabla Tags siguiente contiene tanto columnas de cadena que aceptan valores NULL como que no:

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

Esto da como resultado las correspondientes propiedades de cadena que aceptan y que no aceptan valores NULL en la clase generada:

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

De forma similar, la tabla Posts siguiente contiene una relación necesaria con la tabla 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]));

Esto da como resultado el scaffolding de la relación que no acepta valores NULL (obligatorio) entre 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; }
}

Y publicaciones:

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 último, las propiedades DbSet del DbContext generado se crean de una manera que admite NRT. Por ejemplo:

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

Cambio de los comentarios de las bases de datos a comentarios de código con scaffolding

Problema de GitHub número 19113. Esta característica la ha aportado @ErikEJ. Muchas gracias.

Ahora se aplica scaffolding a los comentarios en tablas y columnas de SQL para convertirlos en los tipos de entidad que se crean al aplicar ingeniería inversa a un modelo de EF Core de una base de datos de SQL Server existente.

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

Mejoras de consultas LINQ

EF Core 6.0 contiene varias mejoras en la traducción y ejecución de consultas LINQ.

Compatibilidad mejorada con GroupBy

Problemas de GitHub: n.º 12088, n.º 13805 y n.º 22609.

EF Core 6.0 contiene una mejor compatibilidad con las consultas GroupBy. Específicamente, ahora EF Core:

  • Traduce GroupBy seguido de FirstOrDefault (o similar) en un grupo.
  • Admite la selección de los principales N resultados de un grupo.
  • Expande las navegaciones después de que se haya aplicado el operador GroupBy.

A continuación se muestran consultas de ejemplo de informes de clientes y su traducción en SQL Server.

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

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

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

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

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

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

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

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

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

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

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

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

Ejemplo 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

Los tipos de entidad utilizados para estos ejemplos son los siguientes:

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

Traducción de String.Concat con varios argumentos

Problema de GitHub número 23859. Esta característica la ha aportado @wmeints. Muchas gracias.

A partir de EF Core 6.0, las llamadas a String.Concat con varios argumentos se traducen a SQL. Por ejemplo, la siguiente consulta:

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

Se traducirá al siguiente código SQL al usar 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

Integración más fluida con System.Linq.Async

Problema de GitHub número 24041.

El paquete System.Linq.Async agrega procesamiento LINQ asincrónico del lado cliente. En versiones anteriores de EF Core, usar este paquete era algo engorroso, ya que había un conflicto relativo al espacio de nombres para los métodos LINQ asincrónicos. En EF Core 6.0, hemos aprovechado la coincidencia de patrones de C# para IAsyncEnumerable<T>, de tal modo que el elemento DbSet<TEntity> expuesto de EF Core no necesite implementar la interfaz directamente.

Tenga en cuenta que la mayoría de aplicaciones no necesitan usar System.Linq.Async, ya que las consultas de EF Core normalmente se traducen por completo en el servidor.

Problema de GitHub número 23921.

En EF Core 6.0, hemos relajado los requisitos de los parámetros para FreeText(DbFunctions, String, String) y Contains. Esto permite usar dichas funciones con columnas binarias o con columnas asignadas mediante un convertidor de valores. Por ejemplo, pongamos por caso un tipo de entidad con una propiedad Name definida como 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; }
}

Se asigna a un elemento JSON en la base de datos:

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

Ahora se puede ejecutar una consulta con Contains o FreeText, aunque el tipo de la propiedad sea Name, no string. Por ejemplo:

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

Al usar SQL Server, esto genera el código SQL siguiente:

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

Traducción de ToString en SQLite

Problema de GitHub número 17223. Esta característica la ha aportado @ralmsdeveloper. Muchas gracias.

Las llamadas a ToString() ahora se traducen a SQL cuando se usa el proveedor de base de datos de SQLite. Esto puede ser útil para las búsquedas de texto que implican columnas que no son de cadena. Por ejemplo, considere un tipo de entidad User que almacena los números de teléfono como valores numéricos:

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

Se puede usar ToString para convertir el número en una cadena de la base de datos. Después, se puede usar esta cadena con una función como LIKE para buscar números que coincidan con un patrón. Por ejemplo, para buscar todos los números que contienen 555:

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

Esto se traduce en el siguiente código SQL cuando se usa una base de datos de SQLite:

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

Tenga en cuenta que la traducción de ToString() para SQL Server ya se admite en EF Core 5.0 y también puede ser compatible con otros proveedores de bases de datos.

EF.Functions.Random

Problema de GitHub número 16141. Esta característica la ha aportado @RaymondHuy. Muchas gracias.

EF.Functions.Random se asigna a una función de base de datos que devuelve un número seudoaleatorio entre 0 y 1, ambos no incluidos. Las traducciones se han implementado en el repositorio de EF Core para SQL Server, SQLite y Azure Cosmos DB. Por ejemplo, considere un tipo de entidad User con una propiedad Popularity:

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

Popularity puede tener valores entre 1 y 5, ambos incluidos. Con EF.Functions.Random, podemos escribir una consulta para devolver todos los usuarios con una popularidad elegida aleatoriamente:

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

Esto se traduce en el siguiente código SQL cuando se usa una base de datos de SQL Server:

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

Mejora de la traducción de SQL Server para IsNullOrWhitespace

Problema de GitHub número 22916. Esta característica la ha aportado @Marusyk. Muchas gracias.

Considere la siguiente consulta:

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

Antes de EF Core 6.0, esto se traducía a lo siguiente en 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''))

Esta traducción se ha mejorado para EF Core 6.0 a:

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

Definición de una consulta para el proveedor en memoria

Problema de GitHub nº 24600.

Se puede usar un nuevo método ToInMemoryQuery para escribir una consulta de definición en la base de datos en memoria para un tipo de entidad determinado. Esto es muy útil para crear el equivalente de las vistas en la base de datos en memoria, especialmente cuando dichas vistas devuelven tipos de entidad sin clave. Por ejemplo, considere una base de datos de clientes para clientes del Reino Unido. Cada cliente tiene una dirección:

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

Ahora, imagine que queremos una vista de estos datos que muestre cuántos clientes hay en cada área de código postal. Se puede crear un tipo de entidad sin clave para representarlo:

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

Y definir una propiedad DbSet para ella en DbContext, junto con conjuntos para otros tipos de entidad de nivel superior:

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

Después, en OnModelCreating, se puede escribir una consulta LINQ que defina los datos que se devolverán 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()
                        }));
}

A continuación, se puede consultar como cualquier otra propiedad DbSet:

var results = context.CustomerDensities.ToList();

Traducción de subcadena con un solo parámetro

Problema de GitHub nº 20173. Esta característica la ha aportado @stevendarby. Muchas gracias.

EF Core 6.0 ahora traduce los usos de string.Substring con un solo argumento. Por ejemplo:

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

Esto se traduce en el siguiente código SQL cuando se usa 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'

División de consultas para colecciones que no son de navegación

Problema de GitHub nº 21234.

EF Core permite dividir una sola consulta LINQ en varias consultas SQL. En EF Core 6.0, se ha ampliado esta compatibilidad para incluir casos en los que las colecciones que no son de navegación estén contenidas en la proyección de consulta.

A continuación se muestran consultas de ejemplo que muestran la traducción de SQL Server en una sola consulta o en varias consultas.

Ejemplo 1:

Consulta LINQ:

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

Una sola consulta SQL:

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

Múltiples 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]

Ejemplo 2:

Consulta LINQ:

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

Una sola consulta SQL:

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

Múltiples 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]

Ejemplo 3:

Consulta LINQ:

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

Una sola consulta SQL:

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

Múltiples 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]

Se ha quitado la última cláusula ORDER BY al realizar la unión en colecciones.

Problema de GitHub nº 19828.

Al cargar entidades uno a varios relacionadas, EF Core agrega cláusulas ORDER BY para asegurarse de que todas las entidades relacionadas de una entidad determinada se agrupan juntas. Pero la última cláusula ORDER BY no es necesaria para que EF genere las agrupaciones necesarias y puede afectar al rendimiento. Por lo tanto, EF Core 6.0 se quita esta cláusula.

Por ejemplo, considere esta consulta:

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

Con EF Core 5.0 en SQL Server, esta consulta se traduce a:

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]

En su lugar, con EF Core 6.0, se traduce a:

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]

Etiquetado de consultas con el nombre de archivo y el número de línea

Problema de GitHub nº 14176. Esta característica la ha aportado @michalczerwinski. Muchas gracias.

Las etiquetas de consulta permiten agregar una etiqueta textural a una consulta LINQ de modo que se incluya en el archivo SQL generado. En EF Core 6.0, se puede usar para etiquetar las consultas con el nombre de archivo y el número de línea del código LINQ. Por ejemplo:

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

Al usar SQL Server, esto tiene como resultado el siguiente código SQL generado:

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

Cambios en el control dependiente opcional de propiedad

Problema de GitHub nº 24558.

Resulta complicado saber si existe o no una entidad dependiente opcional cuando comparte una tabla con su entidad principal. Esto se debe a que hay una fila en la tabla para la entidad dependiente porque la entidad de seguridad la necesita, independientemente de si la dependiente existe o no. La manera de controlar esto de forma inequívoca es asegurarse de que la entidad dependiente tiene al menos una propiedad necesaria. Puesto que una propiedad obligatoria no puede ser NULL, significa que si el valor de la columna de esa propiedad es NULL, la entidad dependiente no existe.

Por ejemplo, considere una clase Customer en la que cada cliente tiene una Address en propiedad:

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

La dirección es opcional, lo que significa que es válido guardar un cliente sin dirección:

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

Pero si un cliente tiene una dirección, esta debe tener al menos un código postal que no sea NULL:

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

Esto se garantiza marcando la propiedad Postcode como Required.

Ahora, cuando se consulta a los clientes, si la columna Postcode es NULL, significa que el cliente no tiene una dirección y la propiedad de navegación Customer.Address se deja NULL. Por ejemplo, iterando a través de los clientes y comprobando si la dirección es NULL:

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

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

Genera los siguientes resultados:

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

Considere en su lugar el caso en el que no se requiere ninguna propiedad además de la dirección:

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

Ahora es posible guardar tanto un cliente sin dirección como un cliente con una dirección en la que todas las propiedades de dirección sean NULL:

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

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

Pero, en la base de datos, estos dos casos no se pueden distinguir, como podemos ver consultando directamente las columnas de la base de datos:

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

Por este motivo, EF Core 6.0 le avisará al guardar un elemento dependiente opcional donde todas sus propiedades sean NULL. Por ejemplo:

advertencia: 27/9/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) La entidad de tipo "Address" con valores de clave principal {CustomerId: -2147482646} es un elemento dependiente opcional que usa el uso compartido de tablas. La entidad no tiene ninguna propiedad con un valor no predeterminado para identificar si la entidad existe. Esto significa que cuando se consulta no se creará ninguna instancia de objeto en lugar de una instancia con todas las propiedades establecidas en valores predeterminados. También se perderán los dependientes anidados. No guarde ninguna instancia con solo los valores predeterminados o marque la navegación entrante según sea necesario en el modelo.

Esto se vuelve aún más complicado cuando el propio dependiente opcional actúa como una entidad de seguridad para un dependiente opcional adicional, también asignado a la misma tabla. En lugar de simplemente advertir, EF Core 6.0 no permite solo los casos de dependientes opcionales anidados. Por ejemplo, observe el siguiente modelo, donde ContactInfo es propiedad de Customer y Address está en propiedad 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; }
}

Ahora, si ContactInfo.Phone es NULL, EF Core no creará una instancia de Address si la relación es opcional, aunque pueda que la propia dirección tenga datos. Para este tipo de modelo, EF Core 6.0 producirá la siguiente excepción:

System.InvalidOperationException: el tipo de entidad "ContactInfo" es un dependiente opcional que utiliza el uso compartido de tablas y que contiene otros dependientes sin ninguna propiedad no compartida necesaria para identificar si la entidad existe. Si todas las propiedades que aceptan valores NULL contienen un valor NULL en la base de datos, no se creará una instancia de objeto en la consulta, lo que provocará la pérdida de los valores del dependiente anidado. Agregue una propiedad necesaria para crear instancias con valores NULL para otras propiedades o marque la navegación entrante como necesaria para que siempre se cree una instancia.

Lo importante es evitar el caso en el que un dependiente opcional pueda contener todos los valores de propiedad que aceptan valores NULL y comparte una tabla con su entidad de seguridad. Hay tres formas sencillas de evitarlo:

  1. Hacer que el dependiente sea necesario. Esto significa que la entidad dependiente siempre tendrá un valor después de consultarla, incluso si todas sus propiedades son NULL.
  2. Asegúrese de que el dependiente contiene al menos una propiedad necesaria, como se describió anteriormente.
  3. Guarde los dependientes opcionales en su propia tabla, en lugar de compartir una tabla con la entidad de seguridad.

Se puede hacer necesario un elemento dependiente mediante el atributo Required en su navegación:

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

O bien, es necesario especificarlo en OnModelCreating:

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

Los dependientes se pueden guardar en otra tabla especificando las tablas que se usarán en OnModelCreating:

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

Consulte OptionalDependentsSample en GitHub para ver más ejemplos de dependientes opcionales, incluidos los casos con dependientes opcionales anidados.

Nuevos atributos de asignación

EF Core 6.0 contiene varios atributos nuevos que se pueden aplicar al código para cambiar la forma en que se asigna a la base de datos.

UnicodeAttribute

Problema de GitHub número 19794. Esta característica la ha aportado @RaymondHuy. Muchas gracias.

A partir de EF Core 6.0, una propiedad de cadena ahora puede asignarse a una columna no Unicode mediante un atributo de asignación sin especificar el tipo de base de datos directamente. Por ejemplo, considere un tipo de entidad Book con una propiedad para el ISBN (Número Internacional Normalizado del Libro) con el 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; }
}

Dado que los ISBN no pueden contener caracteres no Unicode, el atributo Unicode hará que se use un tipo de cadena no Unicode. Además, se usa MaxLength para limitar el tamaño de la columna de base de datos. Por ejemplo, al usar SQL Server, esto da como resultado una columna de base de datos varchar(22):

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

Nota:

De forma predeterminada, EF Core asigna propiedades de cadena a las columnas Unicode. UnicodeAttribute se omite cuando el sistema de base de datos solo admite tipos Unicode.

PrecisionAttribute

Problema de GitHub número 17914. Esta característica la ha aportado @RaymondHuy. Muchas gracias.

Ahora se pueden configurar la precisión y la escala de una columna de base de datos mediante atributos de asignación sin especificar el tipo de base de datos directamente. Por ejemplo, considere un tipo de entidad Product con una propiedad Price decimal:

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

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

EF Core asignará esta propiedad a una columna de base de datos con una precisión de 10 y una escala de 2. Por ejemplo, en SQL Server:

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

EntityTypeConfigurationAttribute

Problema de GitHub número 23163. Esta característica la ha aportado @KaloyanIT. Muchas gracias.

Las instancias de IEntityTypeConfiguration<TEntity> permiten la configuración de ModelBuilder para cada tipo de entidad que se incluya en su propia clase de configuración. Por ejemplo:

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

Normalmente, es necesario crear una instancia de esta clase de configuración y llamarla desde DbContext.OnModelCreating. Por ejemplo:

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

A partir de EF Core 6.0, se puede colocar un elemento EntityTypeConfigurationAttribute en el tipo de entidad para que EF Core pueda encontrar y usar la configuración adecuada. Por ejemplo:

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

Este atributo significa que EF Core usará la implementación de IEntityTypeConfiguration especificada siempre que el tipo de entidad Book se incluya en un modelo. El tipo de entidad se incluye en un modelo mediante uno de los mecanismos normales. Por ejemplo, mediante la creación de una propiedad DbSet<TEntity> para el tipo de entidad:

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

    //...

O bien, mediante su registro en OnModelCreating:

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

Nota

Los tipos EntityTypeConfigurationAttribute no se detectarán automáticamente en un ensamblado. Los tipos de entidad se deben agregar al modelo antes de que se detecte el atributo en ese tipo de entidad.

Mejoras en la creación de modelos

Además de los nuevos atributos de asignación, EF Core 6.0 contiene otras mejoras en el proceso de creación del modelo.

Compatibilidad con columnas dispersas de SQL Server

Problema de GitHub número 8023.

Las columnas dispersas de SQL Server son columnas ordinarias que se optimizan para almacenar valores NULL. Esto puede ser útil cuando se usa la asignación de herencia de tabla por jerarquía, donde las propiedades de un subtipo poco usado darán como resultado valores de columna NULL para la mayoría de las filas de la tabla. Por ejemplo, considere una clase ForumModerator que se extiende desde ForumUser:

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

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

Puede haber millones de usuarios, pero solo unos pocos de ellos son moderadores. Esto significa que, en este caso, puede tener sentido la asignación de ForumName como disperso. Esto ahora puede configurarse mediante IsSparse en OnModelCreating. Por ejemplo:

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

Después, las migraciones de EF Core marcarán la columna como dispersa. Por ejemplo:

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

Nota:

Las columnas dispersas tienen limitaciones. Le recomendamos que lea la documentación sobre columnas dispersas de SQL Server para asegurarse de que las columnas dispersas son la opción correcta para su escenario.

Mejoras en HasConversion API

Problema de GitHub nº 25468.

Antes de EF Core 6.0, las sobrecargas genéricas de los métodos HasConversion usaban el parámetro genérico para especificar el tipo al que se iba a convertir. Por ejemplo, considere una enumeración Currency:

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

EF Core se puede configurar para guardar los valores de esta enumeración como las cadenas "UsDollars", "PoundsStirling" y "Euros" mediante HasConversion<string>. Por ejemplo:

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

A partir EF Core 6.0, el tipo genérico puede especificar en su lugar un tipo de convertidor de valores. Puede ser uno de los convertidores de valores integrados. Por ejemplo, para almacenar los valores de enumeración como números de 16 bits en la base de datos:

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

O bien, puede ser un tipo de convertidor de valores personalizado. Por ejemplo, considere un convertidor que almacena los valores de enumeración como símbolos de moneda:

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

Esto ahora se puede configurar mediante el método genérico HasConversion:

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

Menos configuración para relaciones de varios a varios

Problema de GitHub nº 21535.

Las relaciones inequívocas de varios a varios entre dos tipos de entidad se detectan por convención. Cuando sea necesario o si lo desea, las navegaciones se pueden especificar explícitamente. Por ejemplo:

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

En ambos casos, EF Core crea una entidad compartida con tipos basada en Dictionary<string, object> para actuar como la entidad de combinación entre los dos tipos. A partir EF Core 6.0, se puede agregar UsingEntity a la configuración para cambiar solo este tipo, sin necesidad de configuración adicional. Por ejemplo:

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

Además, el tipo de entidad de combinación puede ser configurada adicionalmente sin necesidad de especificar explícitamente las relaciones izquierda y derecha. Por ejemplo:

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

Por último, se puede proporcionar la configuración completa. Por ejemplo:

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 los convertidores de valores conviertan valores NULL

Problema de GitHub nº 13850.

Importante

Debido a los problemas descritos a continuación, los constructores de ValueConverter que permiten la conversión de valores NULL se han marcado con [EntityFrameworkInternal] para la versión EF Core 6.0. El uso de estos constructores ahora generará una advertencia de compilación.

Por lo general, los convertidores de valores no permiten la conversión de NULL a otro valor. Esto se debe a que se puede usar el mismo convertidor de valores tanto para tipos que aceptan valores NULL como para tipos que no los aceptan, lo que resulta muy útil para las combinaciones PK/FK en las que la clave suele aceptan valores NULL y la PK no.

A partir de EF Core 6.0, se puede crear un convertidor de valores que convierte valores NULL. Sin embargo, la validación de esta característica ha demostrado ser muy problemática en la práctica. Por ejemplo:

Estos no son problemas triviales y no son fáciles de detectar en los problemas de consulta. Por lo tanto, hemos marcado esta característica como interna para EF Core 6.0. Sigue pudiéndola usar, pero recibirá una advertencia del compilador. La advertencia se puede deshabilitar mediante #pragma warning disable EF1001.

Un ejemplo de dónde la conversión de valores NULL puede resultar útil es cuando la base de datos contiene valores NULL, pero el tipo de entidad quiere usar algún otro valor predeterminado para la propiedad. Por ejemplo, considere una enumeración en la que su valor predeterminado es "Unknown":

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

Pero la base de datos puede tener valores NULL cuando se desconoce la raza. En EF Core 6.0, se puede usar un convertidor de valores para que lo tenga en cuenta:

    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
    }

Los gatos para los que el valor de raza sea "Unknown" tendrán su columna Breed establecida en NULL en la base de datos. Por ejemplo:

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

Lo cual genera las siguientes instrucciones INSERT en 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();

Mejoras en el generador de DbContext

AddDbContextFactory también registra DbContext directamente

Problema de GitHub nº 25164.

A veces resulta útil tener un tipo DbContext y un generador para contextos de ese tipo registrados en el contenedor de inserción de dependencias (DI) de aplicaciones. Esto permite, por ejemplo, que una instancia con ámbito de DbContext se resuelva desde el ámbito de la solicitud, mientras que el generador se puede usar para crear varias instancias independientes cuando sea necesario.

Para admitir esto, AddDbContextFactory ahora también registra el tipo DbContext como un servicio con ámbito. Por ejemplo, observe este registro en el contenedor de DI de la aplicación:

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

Con este registro, el generador se puede resolver desde el contenedor de DI raíz, igual que en las versiones anteriores:

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

Tenga en cuenta que las instancias de contexto creadas por el generador deben eliminarse explícitamente.

Además, una instancia de DbContext se puede resolver directamente desde un ámbito de contenedor:

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

En este caso, la instancia de contexto se elimina cuando se elimina el ámbito del contenedor; el contexto no debe eliminarse explícitamente.

En un nivel superior, esto significa que el DbContext del generador se puede insertar en otros tipos de DI. Por ejemplo:

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

O:

private class MyController1
{
    private readonly SomeDbContext _context;

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

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

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

DbContextFactory omite el constructor sin parámetros DbContext.

Problema de GitHub nº 24124.

EF Core 6.0 ahora permite un constructor DbContext sin parámetros y un constructor que usa DbContextOptions en el mismo tipo de contexto cuando el generador se registra mediante AddDbContextFactory. Por ejemplo, el contexto usado en los ejemplos anteriores contiene ambos constructores:

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

    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }
    
    public DbSet<Blog> Blogs { get; set; }
}

La agrupación de DbContext se puede usar sin inserción de dependencias.

Problema de GitHub nº 24137.

El tipo PooledDbContextFactory se ha hecho público para que se pueda usar como grupo independiente para instancias de DbContext, sin necesidad de que la aplicación tenga un contenedor de inserción de dependencias. El grupo se crea con una instancia de DbContextOptions que se usará para crear instancias de contexto:

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

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

Después, el generador se puede usar para crear y agrupar instancias. Por ejemplo:

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

Las instancias se devuelven al grupo cuando se eliminan.

Otras mejoras

Por último, EF Core contiene varias mejoras en las áreas no cubiertas anteriormente.

Usar [ColumnAttribute.Order] al crear tablas

Problema de GitHub nº 10059.

La propiedad Order de ColumnAttribute ahora se puede usar para ordenar columnas al crear una tabla con migraciones. Por ejemplo, tomemos el siguiente 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; }
}

De forma predeterminada, EF Core primero ordena las columnas de clave principal, luego las propiedades del tipo de entidad y los tipos de propiedad y, por último, las propiedades de los tipos base. Por ejemplo, se ha creado la tabla siguiente en 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]));

En EF Core 6.0, ColumnAttribute se puede usar para especificar un orden de columnas diferente. Por ejemplo:

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

En SQL Server, la tabla generada ahora es:

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

Esto mueve las columnas FistName y LastName a la parte superior, aunque estén definidas en un tipo base. Tenga en cuenta que los valores de orden de columna pueden tener espacios, lo que permite usar intervalos para colocar siempre columnas al final, incluso cuando los usan varios tipos derivados.

En este ejemplo también se muestra cómo el mismo ColumnAttribute se puede usar para especificar tanto el nombre como el orden de la columna.

El orden de las columnas también se puede configurar mediante la API ModelBuilder de OnModelCreating. Por ejemplo:

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

La ordenación en el generador de modelos con HasColumnOrder tiene prioridad sobre cualquier orden especificado con ColumnAttribute. Esto significa que HasColumnOrder se puede usar para invalidar la ordenación realizada con atributos, incluida la resolución de conflictos cuando los atributos de propiedades diferentes especifican el mismo número de orden.

Importante

Tenga en cuenta que, en el caso general, la mayoría de las bases de datos solo admiten la ordenación de columnas cuando se crea la tabla. Esto significa que el atributo de orden de columna no se puede usar para volver a ordenar las columnas de una tabla existente. Una excepción importante de ello es SQLite, donde las migraciones recompilarán toda la tabla con nuevos órdenes de columna.

API mínima de EF Core

Problema de GitHub nº 25192.

.NET Core 6.0 incluye plantillas actualizadas que incluyen "API mínimas" simplificadas que quitan gran parte del código reutilizable que tradicionalmente se necesitaba en las aplicaciones .NET.

EF Core 6.0 contiene un nuevo método de extensión que registra un tipo DbContext y proporciona la configuración para un proveedor de base de datos en una sola línea. Por ejemplo:

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

Son exactamente 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"));
var builder = WebApplication.CreateBuilder(args);

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

Nota:

Las API mínimas de EF Core solo admiten el registro y la configuración muy básica de un DbContext y un proveedor. Use AddDbContext, AddDbContextPool, AddDbContextFactory, etc. para acceder a todos los tipos de registro y configuración disponibles en EF Core.

Consulte estos recursos para obtener más información sobre las API mínimas:

Conservación del contexto de sincronización en SaveChangesAsync

Problema de GitHub número 23971.

En la versión 5.0, hemos modificado el código de EF Core para establecer Task.ConfigureAwait en false en todas las ubicaciones donde se use el operador await para el código asincrónico. En general, esta suele ser una mejor opción al usar EF Core. De todas formas, SaveChangesAsync es un caso especial, porque EF Core establece los valores generados en entidades de las que se hace un seguimiento tras completar la operación de una base de datos asincrónica. Es posible que estos cambios desencadenen notificaciones que, por ejemplo, deban ejecutarse en el subproceso de interfaz de usuario. Por lo tanto, en EF Core 6.0, vamos a revertir este cambio solo para el método SaveChangesAsync.

Base de datos en memoria: valide que las propiedades obligatorias no son NULL

Problema de GitHub número 10613. Esta característica la ha aportado @fagnercarvalho. Muchas gracias.

La base de datos en memoria de EF Core ahora producirá una excepción si se intenta guardar un valor NULL para una propiedad marcada como obligatoria. Por ejemplo, considere un tipo User con una propiedad Username obligatoria:

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

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

Si intenta guardar una entidad con un valor Username NULL, se producirá la siguiente excepción:

Microsoft.EntityFrameworkCore.DbUpdateException: Required properties '{'Username'}' are missing for the instance of entity type 'User' with the key value '{Id: 1}' (Microsoft.EntityFrameworkCore.DbUpdateException: faltan las propiedades "{'Username'}" obligatorias para la instancia de tipo de entidad "User" con el valor de clave "{Id: 1}").

Esta validación se puede deshabilitar si es necesario. Por ejemplo:

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

Ordenar información de origen para diagnósticos e interceptores

Problema de GitHub nº 23719. Esta característica la ha aportado @Giorgi. Muchas gracias.

El elemento CommandEventData proporcionado a los orígenes de diagnóstico y los interceptores ahora contiene un valor de enumeración que indica qué parte de EF fue responsable de crear el comando. Se puede usar como filtro en el interceptor o diagnóstico. Por ejemplo, es posible que se quiera un interceptor que solo se aplique a los comandos que proceden 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;
    }
}

Esto filtra el interceptor solo a eventos SaveChanges cuando se usa en una aplicación que también genera migraciones y consultas. Por ejemplo:

Saving changes for CustomersContext:

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

Control mejorado de valores temporales

Problema de GitHub nº 24245.

EF Core no expone valores temporales en instancias de tipo de entidad. Por ejemplo, considere un tipo de entidad Blog con una clave generada por el almacén:

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

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

La propiedad de clave Id recibirá un valor temporal en cuanto el contexto realice el seguimiento de Blog. Por ejemplo, al llamar a DbContext.Add:

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

El valor temporal se puede obtener del control de cambios de contexto, pero no se establece en la instancia de entidad. Por ejemplo, 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}");

Se genera el siguiente código resultado:

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

Esto es correcto porque impide que el valor temporal se filtre en el código de la aplicación, donde se puede tratar accidentalmente como no temporal. Pero a veces resulta útil tratar directamente con valores temporales. Por ejemplo, es posible que una aplicación quiera generar sus propios valores temporales para un gráfico de entidades antes de que se realice el seguimiento para que se puedan usar para formar relaciones mediante claves externas. Esto se puede hacer marcando explícitamente los valores como temporales. Por ejemplo:

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

En EF Core 6.0, el valor permanecerá en la instancia de entidad aunque ahora esté marcado como temporal. Por ejemplo, el código anterior genera la siguiente salida:

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

Del mismo modo, los valores temporales generados por EF Core pueden establecerse explícitamente en instancias de entidad y marcarse como valores temporales. Esto se puede usar para establecer explícitamente relaciones entre nuevas entidades mediante sus valores de clave temporales. Por ejemplo:

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

Elemento resultante:

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

EF Core anotado para tipos de referencia que aceptan valores NULL de C#

Problema de GitHub nº 19007.

El código base de EF Core ahora usa tipos de referencia que aceptan valores NULL (NRT) de C#. Esto significa que se obtienen las indicaciones correctas del compilador para el uso de NULL al usar EF Core 6.0 a partir de su propio código.

Microsoft.Data.Sqlite 6.0

Sugerencia

Puede ejecutar y depurar todos los ejemplos que se muestran a continuación si descarga el código de ejemplo de GitHub.

Agrupar conexiones

Problema de GitHub nº 13837.

Es habitual mantener abiertas las conexiones de base de datos durante el menor tiempo posible. Esto ayuda a evitar la contención en el recurso de conexión. Por este motivo, en bibliotecas como EF Core se abre la conexión inmediatamente antes de realizar una operación de base de datos y se cierra inmediatamente después. Por ejemplo, considere este código de 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.");
    }
}

La salida de este código, con el registro para las conexiones activadas, es la siguiente:

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 la conexión se abre y cierra rápidamente para cada operación.

Pero, para la mayoría de los sistemas de base de datos, la apertura de una conexión física a la base de datos es una operación costosa. Por tanto, los proveedores de ADO.NET crean un grupo de conexiones físicas y las alquilan a instancias de DbConnection según sea necesario.

SQLite es un poco diferente, ya que el acceso a la base de datos normalmente solo suele ser el acceso a un archivo. Esto significa que la apertura de una conexión a una base de datos de SQLite suele ser muy rápida. Sin embargo, este no siempre es el caso. Por ejemplo, la apertura de una conexión a una base de datos cifrada puede ser muy lenta. Por tanto, las conexiones de SQLite ahora se agrupan cuando se usa Microsoft.Data.Sqlite 6.0.

Compatibilidad con DateOnly y TimeOnly

Problema de GitHub nº 24506.

Microsoft.Data.Sqlite 6.0 admite los nuevos tipos DateOnly y TimeOnly de .NET 6. También se pueden usar en EF Core 6.0 con el proveedor de SQLite. Como siempre con SQLite, su sistema de tipos nativos significa que los valores de estos tipos deben almacenarse como uno de los cuatro tipos admitidos. Microsoft.Data.Sqlite los almacena como TEXT. Por ejemplo, una entidad que usa estos tipos:

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

Asigna a la tabla siguiente en la base de datos de 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);

Los valores se pueden guardar, consultar y actualizar de la manera normal. Por ejemplo, esta consulta LINQ de EF Core:

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

Se traduce a lo siguiente en SQLite:

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

Y devuelve solo los usos con fechas de nacimiento anteriores a 1900 CE:

Found 'ajcvickers'
Found 'wendy'

API de puntos de retorno

Problema de GitHub número 20228.

Hemos estandarizado una API común para puntos de retorno en proveedores de ADO.NET. Microsoft.Data.Sqlite ahora admite esta API, incluido lo siguiente:

El uso de un punto de retorno permite que parte de una transacción se revierta sin necesidad de revertirla entera. Por ejemplo, el código siguiente:

  • Crea una transacción.
  • Envía una actualización a la base de datos.
  • Crea un punto de retorno.
  • Envía otra actualización a la base de datos.
  • Revierte al punto de retorno creado previamente.
  • Confirma la transacción.
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();

Esto hará que la primera actualización se confirme en la base de datos, pero no la segunda porque el punto de retorno se revirtió antes de confirmar la transacción.

Tiempo de espera del comando en la cadena de conexión

Problema de GitHub número 22505. Esta característica la ha aportado @nmichels. Muchas gracias.

Los proveedores de ADO.NET admiten dos tiempos de espera distintos:

  • El tiempo de espera de conexión, que determina el tiempo máximo de espera al establecer una conexión con la base de datos.
  • El tiempo de espera del comando, que determina el tiempo máximo que hay que esperar hasta que se complete la ejecución de un comando.

El tiempo de espera del comando se puede establecer desde el código mediante DbCommand.CommandTimeout. Muchos proveedores ahora también exponen el tiempo de espera del comando en la cadena de conexión. Microsoft.Data.Sqlite sigue esta tendencia con la palabra clave Command Timeout de la cadena de conexión. Por ejemplo, "Command Timeout=60;DataSource=test.db" usará 60 segundos como tiempo de espera predeterminado para los comandos que cree la conexión.

Sugerencia

SQLite trata Default Timeout como sinónimo de Command Timeout, por lo que se puede usar en su lugar si se prefiere.