Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
EF Core 6.0 ahora está disponible en NuGet. Esta página contiene información general sobre los cambios interesantes introducidos en esta versión.
Sugerencia
Puede ejecutar y depurar en los ejemplos que se muestran a continuación descargando el código de ejemplo de GitHub.
Tablas temporales de SQL Server
Problema de GitHub: n.º 4693.
Las tablas temporales de SQL Server realizan un seguimiento automático de todos los datos almacenados en una tabla, incluso después de actualizar o eliminar los datos. Esto se logra mediante la creación de una "tabla de historial" paralela en la que se almacenan los datos históricos con marcas de tiempo cada vez que se realiza un cambio en la tabla principal. Esto permite consultar datos históricos, como para auditorías, o restaurarlos, como para la recuperación después de una mutación o eliminación accidental.
EF Core ahora admite:
- 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 Employee
tipo de entidad:
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á este 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 ocultas datetime2
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 las propiedades de sombra en el modelo de EF Core, lo que les permite usarlas en consultas, como se muestra más adelante.
Importante
Las horas de estas columnas siempre son la hora UTC generada por SQL Server. Las horas UTC se usan para todas las operaciones que implican 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 configuración adicional al 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 controla 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 normal:
context.AddRange(
new Employee
{
Name = "Pinky Pie",
Address = "Sugarcube Corner, Ponyville, Equestria",
Department = "DevDiv",
Position = "Party Organizer",
AnnualSalary = 100.0m
},
new Employee
{
Name = "Rainbow Dash",
Address = "Cloudominium, Ponyville, Equestria",
Department = "DevDiv",
Position = "Ponyville weather patrol",
AnnualSalary = 900.0m
},
new Employee
{
Name = "Fluttershy",
Address = "Everfree Forest, Equestria",
Department = "DevDiv",
Position = "Animal caretaker",
AnnualSalary = 30.0m
});
await context.SaveChangesAsync();
Estos datos se pueden consultar, actualizar y eliminar de la manera normal. Por ejemplo:
var employee = await context.Employees.SingleAsync(e => e.Name == "Rainbow Dash");
context.Remove(employee);
await context.SaveChangesAsync();
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 con seguimiento. Por ejemplo:
var employees = await context.Employees.ToListAsync();
foreach (var employee in employees)
{
var employeeEntry = context.Entry(employee);
var validFrom = employeeEntry.Property<DateTime>("ValidFrom").CurrentValue;
var validTo = employeeEntry.Property<DateTime>("ValidTo").CurrentValue;
Console.WriteLine($" Employee {employee.Name} valid from {validFrom} to {validTo}");
}
Esto imprime:
Starting data:
Employee Pinky Pie valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
Employee Rainbow Dash valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
Employee Fluttershy valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
Observe que la ValidTo
columna (denominada PeriodEnd
de forma predeterminada ) contiene el datetime2
valor máximo. Esto siempre es el caso de las filas actuales de la tabla. Las ValidFrom
columnas (de forma predeterminada denominadas PeriodStart
) contienen la hora UTC a la que se insertó la fila.
Consulta de datos históricos
EF Core admite consultas que incluyen datos históricos a través de varios operadores de consulta nuevos:
-
TemporalAsOf
: devuelve filas que estaban activas (actuales) a la hora UTC especificada. Se trata de una sola fila de la tabla o tabla de historial actual para una clave principal determinada. -
TemporalAll
: devuelve todas las filas de los datos históricos. Suele ser muchas filas de la tabla de historial o de la tabla actual para una clave principal determinada. -
TemporalFromTo
: devuelve todas las filas que estaban activas entre dos horas UTC dadas. Puede ser muchas filas de la tabla de historial o de la tabla actual para una clave principal determinada. -
TemporalBetween
: igual queTemporalFromTo
, excepto que se incluyen las filas que se activaron en el límite superior. -
TemporalContainedIn
: devuelve todas las filas que comenzaron a estar activas y finalizaron siendo activas entre dos horas UTC dadas. Puede ser muchas filas de la tabla de historial o de la tabla actual para una clave principal determinada.
Nota:
Consulte la documentación de tablas temporales de SQL Server para obtener más información sobre qué filas se incluyen exactamente para cada uno de estos operadores.
Por ejemplo, después de realizar algunas actualizaciones y eliminaciones en nuestros datos, podemos ejecutar una consulta mediante TemporalAll
para ver los datos históricos:
var history = await context
.Employees
.TemporalAll()
.Where(e => e.Name == "Rainbow Dash")
.OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
.Select(
e => new
{
Employee = e,
ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
ValidTo = EF.Property<DateTime>(e, "ValidTo")
})
.ToListAsync();
foreach (var pointInTime in history)
{
Console.WriteLine(
$" Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Position}' from {pointInTime.ValidFrom} to {pointInTime.ValidTo}");
}
Observe cómo el método EF.Property se puede usar para tener acceso a los valores de las columnas de período. Esto se usa en la OrderBy
cláusula para ordenar los datos y, a continuación, en una proyección para incluir estos valores en los datos devueltos.
Esta consulta devuelve los siguientes datos:
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 dejó de estar activa a las 8/26/2021 4:44:59 p. m. Esto se debe a que la fila de Rainbow Dash se eliminó de la tabla principal en ese momento. Veremos más adelante cómo se pueden restaurar estos datos.
Las consultas similares se pueden escribir mediante TemporalFromTo
, TemporalBetween
o TemporalContainedIn
. Por ejemplo:
var history = await context
.Employees
.TemporalBetween(timeStamp2, timeStamp3)
.Where(e => e.Name == "Rainbow Dash")
.OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
.Select(
e => new
{
Employee = e,
ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
ValidTo = EF.Property<DateTime>(e, "ValidTo")
})
.ToListAsync();
Esta consulta devuelve las siguientes filas:
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 mencionó anteriormente, Rainbow Dash se eliminó de la Employees
tabla. Esto fue claramente un error, así que volvamos a un momento dado y restauremos la fila que falta desde ese momento.
var employee = await context
.Employees
.TemporalAsOf(timeStamp2)
.SingleAsync(e => e.Name == "Rainbow Dash");
context.Add(employee);
await context.SaveChangesAsync();
Esta consulta devuelve una sola fila para Rainbow Dash tal 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 un 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 la marcamos como Added
y, a continuación, llamamos a SaveChanges
.
Después de volver a insertar la fila Rainbow Dash, consultar los datos históricos muestra que la fila se restauró 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
Paquetes de migración
Problema de GitHub: #19693.
Las migraciones de EF Core se usan para generar actualizaciones de esquema de base de datos en función de los 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/implementación continua (C.I./C.D.).
EF Core ahora incluye una nueva manera de aplicar estas actualizaciones de esquema: agrupaciones de migración. Un paquete de migración es un archivo ejecutable pequeño que contiene migraciones y el código necesario para aplicar estas migraciones a la base de datos.
Nota:
Consulte Introducción a paquetes de migración de EF Core compatibles con DevOps en el blog de .NET para obtener una discusión más detallada sobre las migraciones, paquetes e implementación.
Los conjuntos de migración se crean mediante la dotnet ef
herramienta de línea de comandos. Asegúrese de que ha instalado la versión más reciente de la herramienta antes de continuar.
Un paquete necesita migraciones que incluir. Estos se crean mediante dotnet ef migrations add
, como se describe en la documentación de migraciones. Una vez que tenga migraciones listas para implementarse, cree un paquete 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 mi caso, esto es Windows x64, así que obtengo un efbundle.exe
colocado en mi carpeta local. Al ejecutar este archivo ejecutable se aplican las migraciones contenidas en él:
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 aún no se han aplicado. Por ejemplo, la ejecución del mismo paquete de nuevo no hace nada, ya que no hay nuevas migraciones por 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>
Sin embargo, si se realizan cambios en el modelo y se generan más migraciones con dotnet ef migrations add
, estos se pueden agrupar en un nuevo ejecutable listo para aplicarse. 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 --force
opción se puede usar para sobrescribir la agrupación existente con una nueva.
Al ejecutar esta nueva agrupación se aplican 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, el paquete usa la cadena de conexión de la base de datos de la configuración de su aplicación. Sin embargo, se puede migrar una base de datos diferente pasando 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 aplicaron las tres migraciones, ya que aún no se había aplicado ninguna a la base de datos de producción.
Otras opciones se pueden pasar a la línea de comandos. Algunas opciones comunes son:
-
--output
para especificar la ruta de acceso del archivo ejecutable que se va a crear. -
--context
para especificar el tipo DbContext que se va a usar cuando el proyecto contiene 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 compila antes de ejecutar el comando. Esto solo debe usarse si se sabe que el proyecto es up-to-date. -
--verbose
para ver información detallada sobre lo que hace el comando. Use esta opción al incluir información en 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. Consulte el 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.
Las versiones anteriores de EF Core requieren que la asignación de 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 cadenas y precisión decimal, así como la conversión de valores para el tipo de propiedad.
Esto requiere:
- Configuración del generador de modelos para cada propiedad
- Atributo de mapeo en cada propiedad
- Iteración explícita en 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 hacer de forma sólida porque es posible que la lista de tipos de entidad y propiedades asignadas no sea final en el momento en que se produce esta iteración.
EF Core 6.0 permite especificar esta configuración de asignación una vez para un tipo determinado. A continuación, se aplicará a todas las propiedades de ese tipo en el modelo. Esto se denomina "configuración del modelo previo a la convención", ya que configura aspectos del modelo que, a continuación, usan las convenciones de creación de modelos. Esta configuración se aplica invalidando ConfigureConventions
en DbContext
:
public class SomeDbContext : DbContext
{
protected override void ConfigureConventions(
ModelConfigurationBuilder configurationBuilder)
{
// Pre-convention model configuration goes here
}
}
Por ejemplo, considere los siguientes tipos de entidad:
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 DateTimes a longs:
configurationBuilder
.Properties<DateTime>()
.HaveConversion<long>();
Todas las propiedades bool se pueden convertir en los enteros 0
o 1
usar uno de los convertidores de valores integrados:
configurationBuilder
.Properties<bool>()
.HaveConversion<BoolToZeroOneConverter<int>>();
Suponiendo Session
que es una propiedad transitoria de la entidad y no debe conservarse, se puede omitir en todas partes del modelo:
configurationBuilder
.IgnoreAny<Session>();
La configuración del modelo anterior a la convención es muy útil al trabajar con objetos de valor. Por ejemplo, el tipo Money
del modelo anterior se representa mediante la 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
}
A continuación, se serializa a 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 está limitada 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 determinado. 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 cualquier propiedad asignada del modelo.
Nota:
Consulte 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: #1906.
Los modelos compilados pueden mejorar el tiempo de inicio de EF Core para las aplicaciones con modelos grandes. Un modelo grande normalmente significa de 100 a 1000 de tipos de entidad y relaciones.
El tiempo de inicio significa la hora de realizar la primera operación en un DbContext cuando se usa ese tipo DbContext por primera vez en la aplicación. Tenga en cuenta que simplemente crear 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 incluya llamar a DbContext.Add
o ejecutar la primera consulta.
Los modelos compilados se crean mediante la herramienta de dotnet ef
línea de comandos. Asegúrese de que ha instalado la versión más reciente de la herramienta antes de continuar.
Se usa un nuevo dbcontext optimize
comando para generar el modelo compilado. Por ejemplo:
dotnet ef dbcontext optimize
Las --output-dir
opciones 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 ejecutar 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");
Inicialización del modelo compilado
Normalmente no es necesario examinar el código de arranque generado. Sin embargo, a veces puede ser útil personalizar el modelo o su carga. El código de arranque tiene un aspecto similar al 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 modelos diferentes en función de alguna configuración en tiempo de ejecución. Deben colocarse en diferentes carpetas y espacios de nombres, como se muestra anteriormente. La información en tiempo de ejecución, como la cadena de conexión, se puede examinar y, si es necesario, devolver el modelo correcto. 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:
- No se admiten filtros de consulta globales.
- No se admiten la carga diferida y los servidores proxy de seguimiento de cambios.
- No se admiten implementaciones personalizadas de IModelCacheKeyFactory. Sin embargo, puede compilar varios modelos y cargar el adecuado según sea necesario.
- El modelo debe sincronizarse manualmente regenerando el modelo en cualquier momento en que cambie la definición o configuración del modelo.
Debido a estas limitaciones, solo debe usar modelos compilados si el tiempo de inicio de EF Core es demasiado lento. La compilación de modelos pequeños normalmente no merece la pena.
Si el soporte de cualquiera de estas características es fundamental para su éxito, vote por los asuntos adecuados mencionados anteriormente.
Pruebas comparativas
Sugerencia
Puede intentar compilar un modelo grande y ejecutar una prueba comparativa en él descargando el código de ejemplo de GitHub.
El modelo del repositorio de GitHub al que se hace referencia contiene 449 tipos de entidad, 6390 propiedades y 720 relaciones. Se trata de un modelo de tamaño moderado. Con BenchmarkDotNet para medir, el tiempo promedio de la primera consulta es de 1,02 segundos en un portátil razonablemente eficaz. El uso de modelos compilados reduce esto a 117 milisegundos en el mismo hardware. Una mejora de 8 a 10x como esta permanece relativamente constante a medida que aumenta el tamaño del modelo.
Nota:
Consulte Anuncio de Entity Framework Core 6.0 Preview 5: Modelos compilados en el blog de .NET para obtener una explicación más detallada sobre el 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 para EF Core 6.0. Concretamente:
- El rendimiento de EF Core 6.0 ahora es 70% más rápido en el banco de pruebas TechEmpower Fortunes estándar del sector, en comparación con 5.0.
- Esta es la mejora completa del rendimiento, incluidas las mejoras en el código de evaluación comparativa, en el runtime de .NET, etc.
- EF Core 6.0 es 31% más rápido ejecutar consultas sin seguimiento.
- Las asignaciones de montón se han reducido en 43% al ejecutar consultas.
Después de estas mejoras, la brecha entre el popular "micro-ORM" Dapper y EF Core en el banco de pruebas TechEmpower Fortunes se redujo de 55% a alrededor de un poco menos de 5%.
Nota:
Consulte Anuncio de Entity Framework Core 6.0 Preview 4: Performance Edition en el blog de .NET para obtener información detallada sobre 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 bases de datos de Azure Cosmos DB.
Sugerencia
Puede probar y depurar todos los ejemplos específicos de Cosmos descargando el código de ejemplo desde GitHub.
Configurar como predeterminado 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. Esto elimina la necesidad de muchas de las llamadas OwnsMany
y OwnsOne
en el modelo de Azure Cosmos DB. Esto facilita la inserción de tipos hijo en el documento para el tipo padre, 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 siguiente configuración:
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 resultantes de Azure Cosmos DB tienen los padres, los niños, las mascotas y la dirección de la familia insertados en el documento de la familia. 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 la configuración OwnsOne
/OwnsMany
debe usarse si necesita configurar más detalladamente estos tipos propiedad.
Colecciones de tipos primitivos
Problema de GitHub: n.º 14762.
EF Core 6.0 asigna nativamente colecciones de tipos primitivos al usar el proveedor de bases 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);
await context.SaveChangesAsync();
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
}
A continuación, estas colecciones se pueden actualizar de nuevo de la manera normal:
book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";
await context.SaveChangesAsync();
Limitaciones:
- Solo se admiten diccionarios con claves de cadena
- Actualmente no se admite la consulta en el contenido de colecciones primitivas. Vote por #16926, #25700 y #25701 si estas características son importantes para usted.
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 |
STRINGEQUALS |
Solo llamadas que no distinguen mayúsculas de minúsculas |
Las traducciones de LOWER
, LTRIM
, RTRIM
, TRIM
, UPPER
y SUBSTRING
fueron aportadas por @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 fueron aportadas por @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 fueron aportadas por @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 procesar en lugar de usar LINQ. Ahora se admite con el proveedor de Azure Cosmos DB mediante el uso del FromSql
método . Esto funciona de la misma manera que siempre ha hecho con 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();
El cual se ejecuta como:
SELECT c
FROM (
SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c
Consultas distintas
Problema de GitHub: n.º 16144.
Las consultas sencillas que usan Distinct
ahora se traducen. 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ósticos
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 corresponda.
Nota:
Los registros aquí usan EnableSensitiveDataLogging()
para que se muestren los valores de identificador.
La inserción de un elemento en la base de datos de Azure Cosmos DB genera el CosmosEventId.ExecutedCreateItem
evento . Por ejemplo, este código:
var triangle = new Triangle
{
Name = "Impossible",
PartitionKey = "TrianglesPartition",
Angle1 = 90,
Angle2 = 90,
InsertedOn = DateTime.UtcNow
};
context.Add(triangle);
await context.SaveChangesAsync();
Registra 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 CosmosEventId.ExecutingSqlQuery
evento y, a continuación, uno o varios CosmosEventId.ExecutedReadNext
eventos para los elementos leídos. Por ejemplo, este código:
var equilateral = await context.Triangles.SingleAsync(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 = await context.Triangles.FindAsync("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'
Guardar un elemento actualizado en la base de datos de Azure Cosmos DB genera el CosmosEventId.ExecutedReplaceItem
evento. Por ejemplo, este código:
triangle.Angle2 = 89;
await context.SaveChangesAsync();
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'
La eliminación de un elemento de la base de datos de Azure Cosmos DB genera el CosmosEventId.ExecutedDeleteItem
evento . Por ejemplo, este código:
context.Remove(triangle);
await context.SaveChangesAsync();
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: #17301.
El modelo de Azure Cosmos DB ahora se puede configurar para tener un rendimiento manual o uno de escalado automático. 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 establecer 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 de la factoría de cliente HTTP
Problema de GitHub: n.º 21274. Esta característica ha sido aportada por @dnperfors. Muchas gracias.
El HttpClientFactory
que usa el proveedor de Azure Cosmos DB ahora se puede establecer explícitamente. 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:
Consulte Uso del proveedor de Azure Cosmos DB de EF Core para obtener una versión de prueba 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 la ingeniería inversa de un modelo de EF a partir de una base de datos existente.
Implementar andamiaje en relaciones de muchos a muchos
Problema de GitHub: n.º 22475.
EF Core 6.0 detecta tablas de combinación sencillas 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 PostTag
de combinación que las conecte:
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);
Estas tablas se pueden generar desde la línea de comandos. Por ejemplo:
dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=BloggingWithNRTs" Microsoft.EntityFrameworkCore.SqlServer
El resultado es una clase denominada 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 no hay ninguna clase para la PostTag
tabla. En su lugar, la configuración de una relación de muchos a muchos se automatiza:
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");
});
Generar scaffolding para tipos de referencia anulables de C#
Problema de GitHub: #15520.
EF Core 6.0 ahora genera un modelo de EF y tipos de entidad que usan tipos de referencia anulables (NRTs) de C#. El uso de NRT se aplica scaffolding automáticamente cuando la compatibilidad con NRT está habilitada en el proyecto de C# en el que se aplica scaffolding al código.
Por ejemplo, la tabla siguiente Tags
contiene columnas de cadena que aceptan valores nulos y columnas de cadena que no los aceptan.
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 propiedades de cadena anulables y no anulables correspondientes 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; }
}
Del mismo modo, las tablas siguientes Posts
contienen una relación necesaria con la Blogs
tabla:
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 andamiaje de una relación no anulable (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 forma compatible con 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!;
Los comentarios de la base de datos se estructuran como comentarios de código.
Problema de GitHub: #19113. Esta característica ha sido aportada por @ErikEJ. Muchas gracias.
Los comentarios en las tablas y columnas de SQL ahora se generan automáticamente en los tipos de entidad creados al realizar ingeniería inversa de un modelo de EF Core desde una base de datos existente de SQL Server.
/// <summary>
/// The Blog table.
/// </summary>
public partial class Blog
{
/// <summary>
/// The primary key.
/// </summary>
[Key]
public int Id { get; set; }
}
Mejoras de consulta 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: #12088, #13805 y #22609.
EF Core 6.0 tiene mejor compatibilidad con GroupBy
consultas. En concreto, EF Core ahora:
- Traducir GroupBy seguido de
FirstOrDefault
(o similar) en un grupo - Permite seleccionar los N resultados principales de un grupo
- Expande las navegaciones después de aplicar el
GroupBy
operador
A continuación se muestran consultas de ejemplo de informes de clientes y su traducción en SQL Server.
Ejemplo 1:
var people = await context.People
.Include(e => e.Shoes)
.GroupBy(e => e.FirstName)
.Select(
g => g.OrderBy(e => e.FirstName)
.ThenBy(e => e.LastName)
.FirstOrDefault())
.ToListAsync();
SELECT [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t].[FirstName], [s].[Id], [s].[Age], [s].[PersonId], [s].[Style]
FROM (
SELECT [p].[FirstName]
FROM [People] AS [p]
GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
FROM (
SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName], [p0].[LastName]) AS [row]
FROM [People] AS [p0]
) AS [t1]
WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
LEFT JOIN [Shoes] AS [s] ON [t0].[Id] = [s].[PersonId]
ORDER BY [t].[FirstName], [t0].[FirstName]
Ejemplo 2:
var group = await context.People
.Select(
p => new
{
p.FirstName,
FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
})
.GroupBy(p => p.FirstName)
.Select(g => g.First())
.FirstAsync();
SELECT [t0].[FirstName], [t0].[FullName], [t0].[c]
FROM (
SELECT TOP(1) [p].[FirstName]
FROM [People] AS [p]
GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
SELECT [t1].[FirstName], [t1].[FullName], [t1].[c]
FROM (
SELECT [p0].[FirstName], (((COALESCE([p0].[FirstName], N'') + N' ') + COALESCE([p0].[MiddleInitial], N'')) + N' ') + COALESCE([p0].[LastName], N'') AS [FullName], 1 AS [c], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName]) AS [row]
FROM [People] AS [p0]
) AS [t1]
WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
Ejemplo 3:
var people = await context.People
.Where(e => e.MiddleInitial == "Q" && e.Age == 20)
.GroupBy(e => e.LastName)
.Select(g => g.First().LastName)
.OrderBy(e => e.Length)
.ToListAsync();
SELECT (
SELECT TOP(1) [p1].[LastName]
FROM [People] AS [p1]
WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))
FROM [People] AS [p]
WHERE ([p].[MiddleInitial] = N'Q') AND ([p].[Age] = 20)
GROUP BY [p].[LastName]
ORDER BY CAST(LEN((
SELECT TOP(1) [p1].[LastName]
FROM [People] AS [p1]
WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))) AS int)
Ejemplo 4:
var results = await (from person in context.People
join shoes in context.Shoes on person.Age equals shoes.Age
group shoes by shoes.Style
into people
select new
{
people.Key,
Style = people.Select(p => p.Style).FirstOrDefault(),
Count = people.Count()
})
.ToListAsync();
SELECT [s].[Style] AS [Key], (
SELECT TOP(1) [s0].[Style]
FROM [People] AS [p0]
INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
WHERE ([s].[Style] = [s0].[Style]) OR ([s].[Style] IS NULL AND [s0].[Style] IS NULL)) AS [Style], COUNT(*) AS [Count]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [s].[Style]
Ejemplo 5:
var results = await context.People
.GroupBy(e => e.FirstName)
.Select(g => g.First().LastName)
.OrderBy(e => e)
.ToListAsync();
SELECT (
SELECT TOP(1) [p1].[LastName]
FROM [People] AS [p1]
WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
FROM [People] AS [p]
GROUP BY [p].[FirstName]
ORDER BY (
SELECT TOP(1) [p1].[LastName]
FROM [People] AS [p1]
WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
Ejemplo 6:
var results = await context.People
.Where(e => e.Age == 20)
.GroupBy(e => e.Id)
.Select(g => g.First().MiddleInitial)
.OrderBy(e => e)
.ToListAsync();
SELECT (
SELECT TOP(1) [p1].[MiddleInitial]
FROM [People] AS [p1]
WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
FROM [People] AS [p]
WHERE [p].[Age] = 20
GROUP BY [p].[Id]
ORDER BY (
SELECT TOP(1) [p1].[MiddleInitial]
FROM [People] AS [p1]
WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
Ejemplo 7:
var size = 11;
var results
= await context.People
.Where(
p => p.Feet.Size == size
&& p.MiddleInitial != null
&& p.Feet.Id != 1)
.GroupBy(
p => new
{
p.Feet.Size,
p.Feet.Person.LastName
})
.Select(
g => new
{
g.Key.LastName,
g.Key.Size,
Min = g.Min(p => p.Feet.Size),
})
.ToListAsync();
Executed DbCommand (12ms) [Parameters=[@__size_0='11'], CommandType='Text', CommandTimeout='30']
SELECT [p0].[LastName], [f].[Size], MIN([f0].[Size]) AS [Min]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
LEFT JOIN [People] AS [p0] ON [f].[Id] = [p0].[Id]
LEFT JOIN [Feet] AS [f0] ON [p].[Id] = [f0].[Id]
WHERE (([f].[Size] = @__size_0) AND [p].[MiddleInitial] IS NOT NULL) AND (([f].[Id] <> 1) OR [f].[Id] IS NULL)
GROUP BY [f].[Size], [p0].[LastName]
Ejemplo 8:
var result = await context.People
.Include(x => x.Shoes)
.Include(x => x.Feet)
.GroupBy(
x => new
{
x.Feet.Id,
x.Feet.Size
})
.Select(
x => new
{
Key = x.Key.Id + x.Key.Size,
Count = x.Count(),
Sum = x.Sum(el => el.Id),
SumOver60 = x.Sum(el => el.Id) / (decimal)60,
TotalCallOutCharges = x.Sum(el => el.Feet.Size == 11 ? 1 : 0)
})
.CountAsync();
SELECT COUNT(*)
FROM (
SELECT [f].[Id], [f].[Size]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [f].[Id], [f].[Size]
) AS [t]
Ejemplo 9:
var results = await context.People
.GroupBy(n => n.FirstName)
.Select(g => new
{
Feet = g.Key,
Total = g.Sum(n => n.Feet.Size)
})
.ToListAsync();
SELECT [p].[FirstName] AS [Feet], COALESCE(SUM([f].[Size]), 0) AS [Total]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [p].[FirstName]
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 = await context.People
.GroupBy(i => i.LastName)
.Select(g => new { LastName = g.Key, Count = g.Count() , First = g.FirstOrDefault(), Take = g.Take(2)})
.OrderByDescending(e => e.LastName)
.ToListAsync();
SELECT [t].[LastName], [t].[c], [t0].[Id], [t2].[Id], [t2].[Age], [t2].[FirstName], [t2].[LastName], [t2].[MiddleInitial], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial]
FROM (
SELECT [p].[LastName], COUNT(*) AS [c]
FROM [People] AS [p]
GROUP BY [p].[LastName]
) AS [t]
LEFT JOIN (
SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
FROM (
SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[LastName] ORDER BY [p0].[Id]) AS [row]
FROM [People] AS [p0]
) AS [t1]
WHERE [t1].[row] <= 1
) AS [t0] ON [t].[LastName] = [t0].[LastName]
LEFT JOIN (
SELECT [t3].[Id], [t3].[Age], [t3].[FirstName], [t3].[LastName], [t3].[MiddleInitial]
FROM (
SELECT [p1].[Id], [p1].[Age], [p1].[FirstName], [p1].[LastName], [p1].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p1].[LastName] ORDER BY [p1].[Id]) AS [row]
FROM [People] AS [p1]
) AS [t3]
WHERE [t3].[row] <= 2
) AS [t2] ON [t].[LastName] = [t2].[LastName]
ORDER BY [t].[LastName] DESC, [t0].[Id], [t2].[LastName], [t2].[Id]
Ejemplo 12:
var grouping = await context.People
.Include(e => e.Shoes)
.OrderBy(e => e.FirstName)
.ThenBy(e => e.LastName)
.GroupBy(e => e.FirstName)
.Select(g => new { Name = g.Key, People = g.ToList()})
.ToListAsync();
SELECT [t].[FirstName], [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t0].[Id0], [t0].[Age0], [t0].[PersonId], [t0].[Style]
FROM (
SELECT [p].[FirstName]
FROM [People] AS [p]
GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], [s].[Id] AS [Id0], [s].[Age] AS [Age0], [s].[PersonId], [s].[Style]
FROM [People] AS [p0]
LEFT JOIN [Shoes] AS [s] ON [p0].[Id] = [s].[PersonId]
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
ORDER BY [t].[FirstName], [t0].[Id]
Ejemplo 13:
var grouping = await context.People
.GroupBy(m => new {m.FirstName, m.MiddleInitial })
.Select(am => new
{
Key = am.Key,
Items = am.ToList()
})
.ToListAsync();
SELECT [t].[FirstName], [t].[MiddleInitial], [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial]
FROM (
SELECT [p].[FirstName], [p].[MiddleInitial]
FROM [People] AS [p]
GROUP BY [p].[FirstName], [p].[MiddleInitial]
) AS [t]
LEFT JOIN [People] AS [p0] ON (([t].[FirstName] = [p0].[FirstName]) OR ([t].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AND (([t].[MiddleInitial] = [p0].[MiddleInitial]) OR ([t].[MiddleInitial] IS NULL AND [p0].[MiddleInitial] IS NULL))
ORDER BY [t].[FirstName], [t].[MiddleInitial]
Modelo
Los tipos de entidad usados para estos ejemplos son:
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.º 23859. Esta característica ha sido aportada por @wmeints. Muchas gracias.
A partir de EF Core 6.0, las llamadas a String.Concat con varios argumentos ahora se traducen a SQL. Por ejemplo, la consulta siguiente:
var shards = await context.Shards
.Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToListAsync();
Se traducirá al siguiente 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.º 24041.
El paquete System.Linq.Async agrega procesamiento LINQ asincrónico del lado cliente. El uso de este paquete con versiones anteriores de EF Core era complicado debido a un conflicto de espacio de nombres para los métodos LINQ asincrónicos. En EF Core 6.0 hemos aprovechado el emparejamiento de patrones de C# para IAsyncEnumerable<T> de modo que el EF Core DbSet<TEntity> expuesto no necesite implementar la interfaz directamente.
Tenga en cuenta que la mayoría de las aplicaciones no necesitan usar System.Linq.Async, ya que las consultas de EF Core suelen traducirse completamente en el servidor.
Búsqueda de texto libre de SQL Server más flexible
Problema de GitHub: n.º 23921.
En EF Core 6.0, hemos relajado los requisitos de parámetro para FreeText(DbFunctions, String, String) y Contains. Esto permite usar estas funciones con columnas binarias o con columnas asignadas mediante un convertidor de valores. Por ejemplo, considere un tipo de entidad con una Name
propiedad definida como un 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; }
}
Esto se vincula con 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));
Se puede ejecutar una consulta mediante Contains
o FreeText
aunque el tipo de la propiedad es Name
y no string
. Por ejemplo:
var result = await context.Customers.Where(e => EF.Functions.Contains(e.Name, "Martin")).ToListAsync();
Esto genera el siguiente código SQL cuando se usa SQL Server:
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.º 17223. Esta característica ha sido aportada por @ralmsdeveloper. Muchas gracias.
Las llamadas a ToString() ahora se traducen a SQL cuando se usa el proveedor de base de datos SQLite. Esto puede ser útil para las búsquedas de texto que implican columnas que no son de cadena. Por ejemplo, considere un User
tipo de entidad que almacena 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; }
}
ToString
se puede usar para convertir el número en una cadena de la base de datos. A continuación, podemos 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 = await context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToListAsync();
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: #16141. Esta característica ha sido aportada por @RaymondHuy. Muchas gracias.
EF.Functions.Random
se asigna a una función de base de datos que devuelve un número pseudoaleatorio entre 0 y 1 exclusivo. Las traducciones se han implementado en el repositorio de EF Core para SQL Server, SQLite y Azure Cosmos DB. Por ejemplo, considere un User
tipo de entidad con una Popularity
propiedad :
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public int Popularity { get; set; }
}
Popularity
puede tener valores comprendidos entre 1 y 5. Con EF.Functions.Random
podemos escribir una consulta para devolver todos los usuarios con una popularidad elegida aleatoriamente:
var users = await context.Users.Where(u => u.Popularity == (int)(EF.Functions.Random() * 4.0) + 1).ToListAsync();
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)
Traducción mejorada de SQL Server para IsNullOrWhitespace
Problema de GitHub: n.º 22916. Esta característica ha sido aportada por @Marusyk. Muchas gracias.
Considere la consulta siguiente:
var users = await context.Users.Where(
e => string.IsNullOrWhiteSpace(e.FirstName)
|| string.IsNullOrWhiteSpace(e.LastName)).ToListAsync();
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 la 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 resulta más útil para crear el equivalente de vistas en la base de datos en memoria, especialmente cuando esas vistas devuelven tipos de entidad sin clave. Por ejemplo, considere una base de datos de clientes para los clientes basados en el 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 muestra cuántos clientes hay en cada área de código postal. Podemos crear un tipo de entidad sin clave para representar esto:
public class CustomerDensity
{
public string Postcode { get; set; }
public int CustomerCount { get; set; }
}
Y defina 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; }
A continuación, en OnModelCreating
, podemos escribir una consulta LINQ que defina los datos que se van a devolver 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 de DbSet:
var results = await context.CustomerDensities.ToListAsync();
Traducción de subcadena con un único parámetro
Problema de GitHub: #20173. Esta característica ha sido aportada por @stevendarby. Muchas gracias.
EF Core 6.0 ahora traduce los usos de string.Substring
con un único argumento. Por ejemplo:
var result = await context.Customers
.Select(a => new { Name = a.Name.Substring(3) })
.FirstOrDefaultAsync(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'
Consultas divididas para colecciones que no son de navegación
Problema de GitHub: n.º 21234.
EF Core admite la división de una sola consulta LINQ en varias consultas SQL. En EF Core 6.0, esta compatibilidad se ha ampliado para incluir casos en los que las colecciones que no son de navegación se encuentran en la proyección de consultas.
A continuación se muestran consultas de ejemplo que muestran la traducción en SQL Server en una sola consulta o en varias consultas.
Ejemplo 1:
Consulta LINQ:
await context.Customers
.Select(
c => new
{
c,
Orders = c.Orders
.Where(o => o.Id > 1)
})
.ToListAsync();
Consulta SQL única:
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]
Varias 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:
await context.Customers
.Select(
c => new
{
c,
OrderDates = c.Orders
.Where(o => o.Id > 1)
.Select(o => o.OrderDate)
})
.ToListAsync();
Consulta SQL única:
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]
Varias 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:
await context.Customers
.Select(
c => new
{
c,
OrderDates = c.Orders
.Where(o => o.Id > 1)
.Select(o => o.OrderDate)
.Distinct()
})
.ToListAsync();
Consulta SQL única:
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]
Varias 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]
Quitar la última cláusula ORDER BY al realizar una unión para la colección
Problema de GitHub: #19828.
Al cargar entidades relacionadas de uno a varios, EF Core agrega cláusulas ORDER BY para asegurarse de que todas las entidades relacionadas de una entidad específica se agrupen juntas. Sin embargo, la última cláusula ORDER BY no es necesaria para que EF genere las agrupaciones necesarias y puede tener un impacto en el rendimiento. Por lo tanto, en EF Core 6.0 se elimina esta cláusula.
Por ejemplo, considere esta consulta:
await context.Customers
.Select(
e => new
{
e.Id,
FirstOrder = e.Orders.Where(i => i.Id == 1).ToList()
})
.ToListAsync();
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]
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]
Etiquetar las consultas con el nombre de archivo y el número de línea.
Problema de GitHub: n.º 14176. Esta característica ha sido aportada por @michalczerwinski. Muchas gracias.
Las etiquetas de consulta permiten agregar una etiqueta textural a una consulta LINQ de modo que se incluya en el CÓDIGO SQL generado. En EF Core 6.0, se puede usar para etiquetar consultas con el nombre de archivo y el número de línea del código LINQ. Por ejemplo:
var results1 = await context
.Customers
.TagWithCallSite()
.Where(c => c.Name.StartsWith("A"))
.ToListAsync();
Esto da como resultado el siguiente código SQL generado al usar SQL Server:
-- file: C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\TagWithFileAndLineSample.cs:21
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N'A%')
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 el dependiente porque el principal la necesita, independientemente de si el dependiente existe o no. La manera de controlar esta inequívocamente es asegurarse de que el dependiente tiene al menos una propiedad necesaria. Puesto que una propiedad requerida no puede ser null, significa si el valor de la columna para esa propiedad es NULL, la entidad dependiente no existe.
Por ejemplo, considere una Customer
clase en la que cada cliente tiene una Address
propia.
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álida para guardar un cliente sin dirección:
context.Customers1.Add(
new()
{
Name = "Foul Ole Ron"
});
Sin embargo, si un cliente tiene una dirección, esa dirección 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 Postcode
propiedad como Required
.
Ahora, cuando se consultan los clientes, si la columna Código Postal es nula, esto significa que el cliente no tiene una dirección y la propiedad de navegación Customer.Address
se deja nula. Por ejemplo, iterando a través de los clientes y comprobando si la dirección es null:
await foreach (var customer in context.Customers1.AsAsyncEnumerable())
{
Console.Write(customer.Name);
if (customer.Address == null)
{
Console.WriteLine(" has no address.");
}
else
{
Console.WriteLine($" has postcode {customer.Address.Postcode}.");
}
}
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 fuera 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 un cliente sin dirección y un cliente con una dirección donde todas las propiedades de dirección sean nulas:
context.Customers2.Add(
new()
{
Name = "Foul Ole Ron"
});
context.Customers2.Add(
new()
{
Name = "Havelock Vetinari",
Address = new()
});
Sin embargo, en la base de datos, estos dos casos son indistinguibles, 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 advertirá ahora al guardar una dependencia opcional en la que todas sus propiedades sean null. Por ejemplo:
warn: 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 dependiente opcional mediante compartición 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 valores predeterminados o marque la navegación entrante según sea necesario en el modelo.
Esto es aún más complicado cuando el dependiente opcional actúa como un principal para otro dependiente opcional, también asignado a la misma tabla. En lugar de limitarse a advertir, EF Core 6.0 prohíbe los casos de dependientes opcionales que están anidados. Por ejemplo, considere el siguiente modelo, donde ContactInfo
es propiedad de Customer
y Address
es 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 la propia dirección pueda tener 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 mediante 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 hará que se pierdan los valores dependientes anidados. Agregue una propiedad necesaria para crear instancias con valores NULL para otras propiedades o marque la navegación entrante según sea necesario para crear siempre una instancia.
Lo fundamental aquí es evitar el caso en el que un objeto dependiente opcional puede contener todos los valores de propiedad que aceptan valores nulos y comparte una tabla con su entidad principal. Hay tres maneras fáciles de evitar esto:
- Haga que el dependiente sea obligatorio. Esto significa que la entidad dependiente siempre tendrá un valor después de consultarlo, aunque todas sus propiedades sean null.
- Asegúrese de que el dependiente contiene al menos una propiedad necesaria, como se ha descrito anteriormente.
- Guarde los dependientes opcionales en su propia tabla, en lugar de compartir una tabla con el principal.
Se puede hacer obligatoria una dependencia utilizando 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 especificando que es necesario en OnModelCreating
:
modelBuilder.Entity<WithRequiredNavigation.Customer>(
b =>
{
b.OwnsOne(e => e.Address);
b.Navigation(e => e.Address).IsRequired();
});
Los elementos dependientes se pueden guardar en una tabla diferente al especificar las tablas que se utilizará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 obtener más ejemplos de dependientes opcionales, incluidos los casos con dependientes opcionales anidados.
Nuevos atributos de mapeo
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.º 19794. Esta característica ha sido aportada por @RaymondHuy. Muchas gracias.
A partir de EF Core 6.0, ahora se puede asignar una propiedad de cadena a una columna que no es Unicode mediante un atributo de asignación sin especificar directamente el tipo de base de datos. Por ejemplo, considere un Book
tipo de entidad con una propiedad para el número de libro estándar internacional (ISBN) 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 Unicode
atributo hará que se use un tipo de cadena que no sea Unicode. Además, MaxLength
se usa 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 de 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:
EF Core asigna propiedades de cadena a columnas Unicode de forma predeterminada.
UnicodeAttribute
se omite cuando el sistema de base de datos solo admite tipos Unicode.
PrecisionAttribute
Problema de GitHub: n.º 17914. Esta característica ha sido aportada por @RaymondHuy. Muchas gracias.
La precisión y la escala de una columna de base de datos ahora se pueden configurar mediante atributos de asignación sin especificar directamente el tipo de base de datos. Por ejemplo, considere un Product
tipo de entidad con una propiedad decimal Price
:
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 precisión 10 y escala 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]));
Atributo de Configuración de Tipo de Entidad
Problema de GitHub: n.º 23163. Esta característica ha sido aportada por @KaloyanIT. Muchas gracias.
IEntityTypeConfiguration<TEntity> Las instancias permiten que ModelBuilder la configuración de cada tipo de entidad se contenga 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, se debe crear una instancia de esta clase de configuración e invocarla desde DbContext.OnModelCreating. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
new BookConfiguration().Configure(modelBuilder.Entity<Book>());
}
A partir de EF Core 6.0, un EntityTypeConfigurationAttribute
puede colocarse en el tipo de entidad de manera 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 especificada IEntityTypeConfiguration
siempre que el Book
tipo de entidad 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 DbSet<TEntity> propiedad para el tipo de entidad:
public class BooksContext : DbContext
{
public DbSet<Book> Books { get; set; }
//...
O registrándolo en OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>();
}
Nota:
EntityTypeConfigurationAttribute
Los tipos no se detectarán automáticamente en un ensamblado. Los tipos de entidad deben agregarse 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 mapeo, EF Core 6.0 incluye varias mejoras adicionales en el proceso de construcción de modelos.
Compatibilidad con columnas dispersas de SQL Server
Problema de GitHub: #8023.
Las columnas dispersas de SQL Server son columnas normales que están optimizadas para almacenar valores NULL. Esto puede ser útil cuando se utiliza la asignación de herencia de TPH, donde las propiedades de un subtipo raramente usado llevarán a valores nulos de columna para la mayoría de las filas de la tabla. Por ejemplo, considere una ForumModerator
clase 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, con solo unos pocos moderadores. Esto significa que mapear ForumName
como disperso podría tener sentido aquí. Ahora se puede configurar mediante IsSparse
en OnModelCreating. Por ejemplo:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<ForumModerator>()
.Property(e => e.ForumName)
.IsSparse();
}
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. Asegúrese de leer la documentación de columnas dispersas de SQL Server para asegurarse de que las columnas dispersas son la opción adecuada para su escenario.
Mejoras en la API HasConversion
Problema de GitHub: n.º 25468.
Antes de EF Core 6.0, las sobrecargas genéricas de los métodos HasConversion
usaron el parámetro genérico para especificar el tipo al que se va a convertir. Por ejemplo, considere una Currency
enumeración:
public enum Currency
{
UsDollars,
PoundsSterling,
Euros
}
EF Core se puede configurar para guardar los valores de esta enumeración como las cadenas "DólaresEEUU", "LibrasEsterlinas" y "Euros" mediante HasConversion<string>
. Por ejemplo:
modelBuilder.Entity<TestEntity1>()
.Property(e => e.Currency)
.HasConversion<string>();
A partir de EF Core 6.0, el tipo genérico puede especificar 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)
{
}
}
Ahora se puede configurar mediante el método genérico HasConversion
:
modelBuilder.Entity<TestEntity3>()
.Property(e => e.Currency)
.HasConversion<CurrencyToSymbolConverter>();
Menos configuración para las relaciones de muchos a muchos
Problema de GitHub: n.º 21535.
Las relaciones de muchos a muchos no ambiguas entre dos tipos de entidad se descubren 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 basada en Dictionary<string, object>
para actuar como entidad de combinación entre los dos tipos. A partir de EF Core 6.0, UsingEntity
se puede agregar 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 se puede configurar de forma adicional 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 }));
Y, 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: #13850.
Importante
Debido a los problemas descritos a continuación, los constructores para ValueConverter
que permiten la conversión de valores NULL se han marcado con [EntityFrameworkInternal]
para la versión de 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 el mismo convertidor de valores se puede usar tanto para tipos que aceptan valores NULL como para tipos que no aceptan valores NULL, lo que resulta muy útil para las combinaciones PK/FK en las que el FK suele ser nullable y el PK no lo es.
A partir de EF Core 6.0, se puede crear un convertidor de valores que convierta valores NULL. Sin embargo, la validación de esta característica ha demostrado ser muy problemática en la práctica con muchos problemas. Por ejemplo:
- La conversión de valores a NULL en el almacén genera consultas incorrectas
- La conversión de valores de null en el almacenamiento genera consultas incorrectas
- Los convertidores de valores no controlan los casos en los que la columna de base de datos tiene varios valores diferentes que se convierten en el mismo valor.
- Permitir que los convertidores de valores cambien la nulabilidad de las columnas
Estos no son problemas triviales y para los problemas de consulta no son fáciles de detectar. Por lo tanto, hemos marcado esta característica como interna para EF Core 6.0. Todavía puede usarlo, pero recibirá una advertencia del compilador. La advertencia se puede deshabilitar mediante #pragma warning disable EF1001
.
Un ejemplo de dónde convertir valores NULL puede ser ú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 donde su valor predeterminado es "Unknown":
public enum Breed
{
Unknown,
Burmese,
Tonkinese
}
Sin embargo, 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 tener en cuenta lo siguiente:
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 con una raza de "Desconocido" tendrán su Breed
columna 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 });
await context.SaveChangesAsync();
Esto 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 la factoría 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 de aplicaciones (D.I.). Esto permite, por ejemplo, resolver una instancia de DbContext con ámbito limitado en el ámbito de la solicitud, mientras que la factoría puede usarse para crear varias instancias independientes según se necesite.
Para apoyar esto, AddDbContextFactory
ahora también registra el tipo DbContext como un servicio de alcance específico. Por ejemplo, considere este registro en el contenedor D.I. de la aplicación:
var container = services
.AddDbContextFactory<SomeDbContext>(
builder => builder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample;ConnectRetryCount=0"))
.BuildServiceProvider();
Con este registro, la fábrica se puede resolver desde el contenedor raíz de D.I., al igual que en 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 dbContext de la fábrica se puede insertar en otros tipos D.I. Por ejemplo:
private class MyController2
{
private readonly IDbContextFactory<SomeDbContext> _contextFactory;
public MyController2(IDbContextFactory<SomeDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task DoSomething()
{
using var context1 = _contextFactory.CreateDbContext();
using var context2 = _contextFactory.CreateDbContext();
var results1 = await context1.Blogs.ToListAsync();
var results2 = await context2.Blogs.ToListAsync();
// Contexts obtained from the factory must be explicitly disposed
}
}
O:
private class MyController1
{
private readonly SomeDbContext _context;
public MyController1(SomeDbContext context)
{
_context = context;
}
public async Task DoSomething()
{
var results = await _context.Blogs.ToListAsync();
// Injected context is disposed when the request scope is disposed
}
}
DbContextFactory 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 toma DbContextOptions
como argumento usarse en el mismo tipo de contexto cuando la factoría se registra correctamente a través de 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 PooledDbContextFactory
tipo se ha hecho público para que se pueda usar como un grupo independiente para las 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;ConnectRetryCount=0")
.Options;
var factory = new PooledDbContextFactory<SomeDbContext>(options);
A continuación, se puede usar la factoría 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 pool cuando se eliminan.
Otras mejoras
Por último, EF Core contiene varias mejoras en las áreas que no se han tratado anteriormente.
Utilice [ColumnAttribute.Order] al crear tablas
Problema de GitHub: #10059.
La Order
propiedad de ColumnAttribute
ahora se puede usar para ordenar columnas al crear una tabla con migraciones. Por ejemplo, considere 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 ordena primero las columnas de clave principal, siguiendo las propiedades del tipo de entidad y los tipos de propiedad, y finalmente las propiedades de los tipos base. Por ejemplo, la tabla siguiente se crea 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 columna 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 hace que las columnas FistName
y LastName
se muevan hacia la parte superior, aunque estén definidas en un tipo base. Tenga en cuenta que los valores del orden de las columnas pueden tener huecos, permitiendo el uso de rangos para colocar columnas siempre al final, incluso cuando los utilizan varios tipos derivados.
En este ejemplo también se muestra cómo se puede usar el mismo ColumnAttribute
para especificar tanto el nombre de columna como el orden.
La ordenación de columnas también se puede configurar mediante la ModelBuilder
API en 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);
});
Ordenar en el generador de modelos con HasColumnOrder
tiene prioridad sobre cualquier orden especificado con ColumnAttribute
. Esto significa que HasColumnOrder
se puede usar para anular la ordenación realizada con atributos, incluyendo la resolución de cualquier conflicto cuando los atributos de diferentes propiedades 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 columnas de ordenación cuando se crea la tabla. Esto significa que el atributo de orden de columna no se puede usar para volver a ordenar columnas en una tabla existente. Una excepción notable a esto es SQLite, donde las migraciones volverán a generar toda la tabla con nuevos pedidos 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 una gran cantidad del código reutilizable que tradicionalmente se necesita en las aplicaciones de .NET.
EF Core 6.0 contiene un nuevo método de extensión que registra un tipo DbContext y proporciona la configuración de 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;ConnectRetryCount=0"));
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 un registro y una configuración muy básicos 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:
- La presentación de las API mínimas en .NET 6 de Maria Naggaga
- Ejemplo de tareas pendientes de la API mínima de .NET 6 en el blog de Scott Hanselman
- El resumen de las API mínimas de un vistazo por David Fowler
- Un área de juegos de API mínima de Damian Edwards en GitHub
Conservar el contexto de sincronización en SaveChangesAsync
Problema de GitHub: n.º 23971.
Hemos cambiado el código de EF Core en la versión 5.0 para configurar Task.ConfigureAwaitfalse
en todos los lugares donde usamos código asincrónico. Por lo general, es una mejor opción para el uso de EF Core. Sin embargo, SaveChangesAsync es un caso especial porque EF Core establecerá valores generados en entidades con seguimiento una vez completada la operación de base de datos asincrónica. Estos cambios pueden desencadenar notificaciones que, por ejemplo, pueden tener que ejecutarse en el hilo de la interfaz de usuario. Por lo tanto, estamos revirtiendo este cambio en EF Core 6.0 solo para el método SaveChangesAsync.
Base de datos en memoria: valide que las propiedades necesarias no sean NULL.
Problema de GitHub: #10613. Esta característica ha sido aportada por @fagnercarvalho. Muchas gracias.
La base de datos en memoria de EF Core iniciará ahora una excepción si se intenta guardar un valor NULL para una propiedad marcada como necesaria. Por ejemplo, considere un User
tipo con una propiedad requerida Username
:
public class User
{
public int Id { get; set; }
[Required]
public string Username { get; set; }
}
Si intenta guardar una entidad con un valor NULL Username
, se producirá la siguiente excepción:
Microsoft.EntityFrameworkCore.DbUpdateException: faltan las propiedades necesarias '{'Username'}' para la instancia del 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));
}
Información de origen del comando para diagnósticos e interceptores
Problema de GitHub: n.º 23719. Esta característica ha sido aportada por @Giorgi. Muchas gracias.
El CommandEventData
proporcionado para los orígenes de diagnóstico y los interceptores ahora contiene un valor de enumeración que indica qué parte de EF era responsable de crear el comando. Esto se puede usar como filtro en el diagnóstico o interceptor. Por ejemplo, es posible que deseemos 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 específicamente a eventos SaveChanges
al usar 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();
Mejor control de valores temporales
Problema de GitHub: n.º 24245.
EF Core no expone valores temporales en instancias de tipo de entidad. Por ejemplo, considere un Blog
tipo de entidad 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
obtendrá un valor temporal tan pronto como el contexto Blog
realice un seguimiento. Por ejemplo, al llamar a DbContext.Add
:
var blog = new Blog();
context.Add(blog);
El valor temporal se puede obtener del rastreador 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 bueno porque impide que el valor temporal se filtre en el código de la aplicación, donde se puede tratar accidentalmente como si no fuera temporal. Sin embargo, a veces resulta útil tratar directamente los valores temporales. Por ejemplo, una aplicación puede querer generar sus propios valores temporales para un gráfico de entidades antes de realizar 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 se pueden establecer explícitamente en instancias de entidad y marcados como valores temporales. Esto se puede usar para establecer explícitamente relaciones entre nuevas entidades mediante sus valores de clave temporal. 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 con anotaciones para tipos de referencia anulables en C#
Problema de GitHub: #19007.
El código base de EF Core ahora usa tipos de referencia anulable (NRT) de C# en todo el código. Esto significa que obtendrá las indicaciones del compilador correctas para el uso nulo al usar EF Core 6.0 desde su propio código.
Microsoft.Data.Sqlite 6.0
Sugerencia
Puede ejecutar y depurar en todos los ejemplos que se muestran a continuación descargando 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. Este es el motivo por el que las bibliotecas como EF Core abren la conexión inmediatamente antes de realizar una operación de base de datos y vuelven a cerrarla inmediatamente después. Por ejemplo, considere este código de EF Core:
Console.WriteLine("Starting query...");
Console.WriteLine();
var users = await context.Users.ToListAsync();
Console.WriteLine();
Console.WriteLine("Query finished.");
Console.WriteLine();
foreach (var user in users)
{
if (user.Username.Contains("microsoft"))
{
user.Username = "msft:" + user.Username;
Console.WriteLine("Starting SaveChanges...");
Console.WriteLine();
await context.SaveChangesAsync();
Console.WriteLine();
Console.WriteLine("SaveChanges finished.");
}
}
La salida de este código, con el registro de conexiones activadas, es:
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.
Sin embargo, para la mayoría de los sistemas de base de datos, abrir una conexión física a la base de datos es una operación costosa. Por lo tanto, la mayoría de los proveedores de ADO.NET crean un grupo de conexiones físicas y los alquilan a DbConnection
instancias según sea necesario.
SQLite es un poco diferente, ya que el acceso a la base de datos normalmente es simplemente acceder a un archivo. Esto significa que abrir una conexión a una base de datos SQLite suele ser muy rápida. Sin embargo, esto no siempre es el caso. Por ejemplo, abrir una conexión a una base de datos cifrada puede ser muy lenta. Por lo tanto, las conexiones de SQLite ahora se agrupan al usar Microsoft.Data.Sqlite 6.0.
Compatibilidad con DateOnly y TimeOnly
Problema de GitHub: n.º 24506.
Microsoft.Data.Sqlite 6.0 admite los nuevos DateOnly
tipos y TimeOnly
de .NET 6. También se pueden usar en EF Core 6.0 con el proveedor SQLite. Como siempre con SQLite, su sistema de tipos nativo 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; }
}
Corresponde a la siguiente tabla en la base de datos 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);
A continuación, los valores se pueden guardar, consultar y actualizar de la manera normal. Por ejemplo, esta consulta LINQ de EF Core:
var users = await context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToListAsync();
Se traduce en 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 usos con cumpleaños antes del año 1900 d.C.
Found 'ajcvickers'
Found 'wendy'
API de puntos de guardado
Problema de GitHub: #20228.
Hemos estado estandarizando una API común para puntos de control en proveedores de ADO.NET. Microsoft.Data.Sqlite ahora admite esta API, entre las que se incluyen:
- Save(String) para crear un punto de guardado en la transacción
- Rollback(String) para revertir a un punto de guardado anterior
- Release(String) para liberar un punto de guardado
El uso de un punto de retorno permite revertir parte de una transacción sin revertir toda la transacción. Por ejemplo, el código siguiente:
- Crea una transacción
- Envía una actualización a la base de datos
- Crea un punto de guardado
- Envía otra actualización a la base de datos
- Revierte al punto de guardado creado previamente.
- Confirma la transacción
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
await connection.OpenAsync();
await using var transaction = await connection.BeginTransactionAsync();
using (var command = connection.CreateCommand())
{
command.CommandText = @"UPDATE Users SET Username = 'ajcvickers' WHERE Id = 1";
await command.ExecuteNonQueryAsync();
}
await transaction.SaveAsync("MySavepoint");
using (var command = connection.CreateCommand())
{
command.CommandText = @"UPDATE Users SET Username = 'wfvickers' WHERE Id = 2";
await command.ExecuteNonQueryAsync();
}
await transaction.RollbackAsync("MySavepoint");
await transaction.CommitAsync();
Esto hará que la primera actualización se confirme en la base de datos, mientras que la segunda actualización no se confirma, ya que el punto de guardado se revierte antes de confirmar la transacción.
Tiempo de espera del comando en la cadena de conexión
Problema de GitHub: n.º 22505. Esta característica ha sido aportada por @nmichels. Muchas gracias.
los proveedores de ADO.NET admiten dos tiempos de espera distintos:
- Tiempo de espera de conexión, que determina el tiempo máximo de espera al realizar una conexión a la base de datos.
- Tiempo de espera del comando, que determina el tiempo máximo para esperar a 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 este tiempo de espera de comando en la cadena de conexión. Microsoft.Data.Sqlite sigue esta tendencia con la palabra clave de la cadena de conexión Command Timeout
. Por ejemplo, "Command Timeout=60;DataSource=test.db"
usará 60 segundos como tiempo de espera predeterminado para los comandos creados por la conexión.
Sugerencia
Sqlite trata Default Timeout
como sinónimo de Command Timeout
y, por tanto, se puede usar en su lugar si se prefiere.