0

Entity Framework Release Candidate 1

En post anteriores vimos .Net 5 Release Candidate y Asp.Net 5 Release candidate. Ahora es el turno de Entity Framework Core 5.0 Release Candidates. Veremos todas las novedades que tenemos en esta versión.

Many-to-many

Ahora  se admite relaciones de varios a varios sin asignar explícitamente la tabla de combinación.

public class Post
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }
    public ICollection<Post> Posts { get; set; }
}

EF Core 5.0 reconoce esto como una relación de varios a varios por convención. Esto significa que no se requiere código en OnModelCreating:

public class BlogContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<Tag> Tags { get; set; }
}

Cuando se utiliza Migration para crear la base de datos, EF Core creará automáticamente la tabla de combinación. Por ejemplo, en SQL Server para este modelo, EF Core genera:

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id])
);

CREATE TABLE [Tag] (
    [Id] int NOT NULL IDENTITY,
    [Text] nvarchar(max) NULL,
    CONSTRAINT [PK_Tag] 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_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tag_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tag] ([Id]) ON DELETE CASCADE
);

CREATE INDEX [IX_PostTag_TagsId] ON [PostTag] ([TagsId]);

La creación y asociación de entidades Blog y Publicación da como resultado que las actualizaciones de la tabla de unión se realicen automáticamente. Por ejemplo:

var beginnerTag = new Tag {Text = "Beginner"};
var advancedTag = new Tag {Text = "Advanced"};
var efCoreTag = new Tag {Text = "EF Core"};

context.AddRange(
    new Post {Name = "EF Core 101", Tags = new List<Tag> {beginnerTag, efCoreTag}},
    new Post {Name = "Writing an EF database provider", Tags = new List<Tag> {advancedTag, efCoreTag}},
    new Post {Name = "Savepoints in EF Core", Tags = new List<Tag> {beginnerTag, efCoreTag}});

context.SaveChanges();

Después de insertar las publicaciones y las etiquetas, EF creará automáticamente filas en la tabla de combinación. Por ejemplo, en SQL Server:

SET NOCOUNT ON;
INSERT INTO [PostTag] ([PostsId], [TagsId])
VALUES (@p6, @p7),
(@p8, @p9),
(@p10, @p11),
(@p12, @p13),
(@p14, @p15),
(@p16, @p17);

Para consultas, Incluir y otras operaciones de consulta funcionan igual que para cualquier otra relación. Por ejemplo:

foreach (var post in context.Posts.Include(e => e.Tags))
{
    Console.Write($"Post \"{post.Name}\" has tags");

    foreach (var tag in post.Tags)
    {
        Console.Write($" '{tag.Text}'");
    }
}

El SQL generado utiliza la tabla de combinación automáticamente para recuperar todas las etiquetas relacionadas:

SELECT [p].[Id], [p].[Name], [t0].[PostsId], [t0].[TagsId], [t0].[Id], [t0].[Text]
FROM [Posts] AS [p]
LEFT JOIN (
    SELECT [p0].[PostsId], [p0].[TagsId], [t].[Id], [t].[Text]
    FROM [PostTag] AS [p0]
    INNER JOIN [Tag] AS [t] ON [p0].[TagsId] = [t].[Id]
) AS [t0] ON [p].[Id] = [t0].[PostsId]
ORDER BY [p].[Id], [t0].[PostsId], [t0].[TagsId], [t0].[Id]

A diferencia de EF6, EF Core permite la personalización completa de la tabla de combinación. Por ejemplo, el código siguiente configura una relación de varios a varios que también tiene navegaciones a la entidad de unión y en la que la entidad de unión contiene una propiedad de carga útil:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Community>()
        .HasMany(e => e.Members)
        .WithMany(e => e.Memberships)
        .UsingEntity<PersonCommunity>(
            b => b.HasOne(e => e.Member).WithMany().HasForeignKey(e => e.MembersId),
            b => b.HasOne(e => e.Membership).WithMany().HasForeignKey(e => e.MembershipsId))
        .Property(e => e.MemberSince).HasDefaultValueSql("CURRENT_TIMESTAMP");
}

Map entity types to queries

Entity types  los asignamos comúnmente a tablas o vistas, de modo que EF Core extrae el contenido de la tabla o vista al consultar ese tipo. EF Core 5.0 permite que un tipo de entidad se asigne a una «defining query». (Es parcialmente compatible en versiones anteriores, pero ha mejorado mucho y tiene una sintaxis diferente en EF Core 5.0).

Por ejemplo, consideremos dos tablas; una con Posts; el otro con publicaciones heredadas. La tabla de Posts  tiene algunas columnas adicionales, pero para el propósito de nuestra aplicación, queremos que las publicaciones modernas y heredadas se combinen y mapeen a un tipo de entidad con todas las propiedades necesarias:

public class Post
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Category { get; set; }
    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

Ahora, ToSqlQuery podemos usarlo para asignar este tipo de entidad a una consulta que extrae y combina filas de ambas tablas:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>().ToSqlQuery(
        @"SELECT Id, Name, Category, BlogId FROM posts
          UNION ALL
          SELECT Id, Name, ""Legacy"", BlogId from legacy_posts");
}

Tengamos en cuenta que la tabla legacy_posts no tiene una columna de categoría, por lo que sintetizamos un valor predeterminado para todas las publicaciones heredadas.

Este tipo de entidad se puede utilizar de la forma habitual para consultas LINQ. Por ejemplo. la consulta LINQ:

var posts = context.Posts.Where(e => e.Blog.Name.Contains("Unicorn")).ToList();

La salida:

SELECT "p"."Id", "p"."BlogId", "p"."Category", "p"."Name"
FROM (
    SELECT Id, Name, Category, BlogId FROM posts
    UNION ALL
    SELECT Id, Name, "Legacy", BlogId from legacy_posts
) AS "p"
INNER JOIN "Blogs" AS "b" ON "p"."BlogId" = "b"."Id"
WHERE ('Unicorn' = '') OR (instr("b"."Name", 'Unicorn') > 0)

Event counters

La manera más eficiente de exponer las métricas de rendimiento de nuestra aplicación es por medio del contador de eventos de .Net. EF Core 5.0 incluye contadores de eventos en la categoría Microsoft.EntityFrameworkCore:

dotnet counters monitor Microsoft.EntityFrameworkCore -p 49496

Estos son los resultados:

Property bags

EF Core 5.0 nos permite que el mismo tipo de CLR se asigna a varios tipos de entidades diferentes. Estos tipos se conocen como tipos de entidad de tipo compartido. Esta característica combinada con las propiedades del indexador (incluidas en la vista previa 1) permite que las bolsas de propiedades se utilicen como tipo de entidad.

Por ejemplo, el DbContext a continuación configura el tipo BCL Dictionary <string, object> como un tipo de entidad de tipo compartido para productos y categorías.

public class ProductsContext : DbContext
{
    public DbSet<Dictionary<string, object>> Products => Set<Dictionary<string, object>>("Product");
    public DbSet<Dictionary<string, object>> Categories => Set<Dictionary<string, object>>("Category");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.SharedTypeEntity<Dictionary<string, object>>("Category", b =>
        {
            b.IndexerProperty<string>("Description");
            b.IndexerProperty<int>("Id");
            b.IndexerProperty<string>("Name").IsRequired();
        });

        modelBuilder.SharedTypeEntity<Dictionary<string, object>>("Product", b =>
        {
            b.IndexerProperty<int>("Id");
            b.IndexerProperty<string>("Name").IsRequired();
            b.IndexerProperty<string>("Description");
            b.IndexerProperty<decimal>("Price");
            b.IndexerProperty<int?>("CategoryId");

            b.HasOne("Category", null).WithMany();
        });
    }
}

Dictionary objects («property bags») ahora se pueden agregar al contexto como instancias de entidad y guardar. Por ejemplo:

var beverages = new Dictionary<string, object>
{
    ["Name"] = "Beverages",
    ["Description"] = "Stuff to sip on"
};

context.Categories.Add(beverages);

context.SaveChanges();

Estas entidades se pueden consultar y actualizar de la forma habitual:

var foods = context.Categories.Single(e => e["Name"] == "Foods");
var marmite = context.Products.Single(e => e["Name"] == "Marmite");

marmite["CategoryId"] = foods["Id"];
marmite["Description"] = "Yummy when spread _thinly_ on buttered Toast!";

context.SaveChanges();

SaveChanges interception and events

EF Core 5.0 tenemos eventos .NET y un interceptor de EF Core que se activa cuando se llama SaveChanges.

context.SavingChanges += (sender, args) =>
{
    Console.WriteLine($"Saving changes for {((DbContext)sender).Database.GetConnectionString()}");
};

context.SavedChanges += (sender, args) =>
{
    Console.WriteLine($"Saved {args.EntitiesSavedCount} changes for {((DbContext)sender).Database.GetConnectionString()}");
};

Tener en cuenta:

  • El remitente del evento es la instancia de DbContext
  • Los argumentos del evento SavedChanges contienen el número de entidades guardadas en la base de datos

El interceptor está definido por ISaveChangesInterceptor, pero a menudo es conveniente heredar de SaveChangesInterceptor para evitar implementar todos los métodos. Por ejemplo:

public class MySaveChangesInterceptor : SaveChangesInterceptor
{
    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData,
        InterceptionResult<int> result)
    {
        Console.WriteLine($"Saving changes for {eventData.Context.Database.GetConnectionString()}");

        return result;
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = new CancellationToken())
    {
        Console.WriteLine($"Saving changes asynchronously for {eventData.Context.Database.GetConnectionString()}");

        return new ValueTask<InterceptionResult<int>>(result);
    }
}

Más para tener en cuenta:

  • El interceptor tiene métodos de sincronización y asíncronos. Esto puede resultar útil si necesita realizar E/S asíncronas, como escribir en un servidor de auditoría.
  • El interceptor permite omitir SaveChanges utilizando el mecanismo InterceptionResult común a todos los interceptores.

Una de las desventajas de los interceptores es que deben registrarse en DbContext cuando se está construyendo. Por ejemplo:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .AddInterceptors(new MySaveChangesInterceptor())
            .UseSqlite("Data Source = test.db");

Exclude tables from migrations

Muchas veces es útil tener un solo tipo de entidad mapeado en varios DbContexts. Esto sucede cuando se utilizan contextos delimitados, para los cuales es común tener un tipo DbContext diferente para cada contexto delimitado.

Por ejemplo, un tipo de usuario puede ser necesario tanto para un contexto de autorización como para un contexto de informes. Si se realiza un cambio en el tipo de usuario, las migraciones para ambos DbContexts intentarán actualizar la base de datos. Para evitar esto, el modelo para uno de los contextos se puede configurar para excluir la tabla de sus migraciones.

En el siguiente ejemplo, AuthorizationContext generará migración para cambios en la tabla de Usuarios, pero ReportingContext no lo hará, evitando que las migraciones entren en conflicto.

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

    public Address HomeAddress { get; set; }
    public Address WorkAddress { get; set; }
}

public class Address
{
    public string Line1 { get; set; }
    public string Line2 { get; set; }
    public string City { get; set; }
    public string Region { get; set; }
    public string Country { get; set; }
    public string Postcode { get; set; }
}

Required 1:1 dependents

En EF Core 3.1, una relación uno a uno siempre se consideró opcional. Esto fue más evidente cuando se utilizaron entidades propias. Por ejemplo, considere el siguiente modelo y configuración:

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

    public Address HomeAddress { get; set; }
    public Address WorkAddress { get; set; }
}

public class Address
{
    public string Line1 { get; set; }
    public string Line2 { get; set; }
    public string City { get; set; }
    public string Region { get; set; }
    public string Country { get; set; }
    public string Postcode { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>(b =>
    {
        b.OwnsOne(e => e.HomeAddress,
            b =>
            {
                b.Property(e => e.Line1).IsRequired();
                b.Property(e => e.City).IsRequired();
                b.Property(e => e.Region).IsRequired();
                b.Property(e => e.Postcode).IsRequired();
            });

        b.OwnsOne(e => e.WorkAddress);
    });
}

Esto da como resultado en el migration:

CREATE TABLE "People" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_People" PRIMARY KEY AUTOINCREMENT,
    "Name" TEXT NULL,
    "HomeAddress_Line1" TEXT NULL,
    "HomeAddress_Line2" TEXT NULL,
    "HomeAddress_City" TEXT NULL,
    "HomeAddress_Region" TEXT NULL,
    "HomeAddress_Country" TEXT NULL,
    "HomeAddress_Postcode" TEXT NULL,
    "WorkAddress_Line1" TEXT NULL,
    "WorkAddress_Line2" TEXT NULL,
    "WorkAddress_City" TEXT NULL,
    "WorkAddress_Region" TEXT NULL,
    "WorkAddress_Country" TEXT NULL,
    "WorkAddress_Postcode" TEXT NULL
);

Tengamos en cuenta que todas las columnas son anulables, aunque algunas de las propiedades HomeAddress se han configurado según sea necesario. Además, al consultar por una persona, si todas las columnas para la dirección de casa o del trabajo son nulas, EF Core dejará las propiedades HomeAddress y / o WorkAddress como nulas, en lugar de establecer una instancia vacía de Address.

En EF Core 5.0, la navegación HomeAddress ahora se puede configurar como un dependiente requerido. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>(b =>
    {
        b.OwnsOne(e => e.HomeAddress,
            b =>
            {
                b.Property(e => e.Line1).IsRequired();
                b.Property(e => e.City).IsRequired();
                b.Property(e => e.Region).IsRequired();
                b.Property(e => e.Postcode).IsRequired();
            });
        b.Navigation(e => e.HomeAddress).IsRequired();

        b.OwnsOne(e => e.WorkAddress);
    });
}

La tabla creada por Migraciones ahora incluye columnas que no aceptan valores NULL para las propiedades requeridas del dependiente requerido:

CREATE TABLE "People" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_People" PRIMARY KEY AUTOINCREMENT,
    "Name" TEXT NULL,
    "HomeAddress_Line1" TEXT NOT NULL,
    "HomeAddress_Line2" TEXT NULL,
    "HomeAddress_City" TEXT NOT NULL,
    "HomeAddress_Region" TEXT NOT NULL,
    "HomeAddress_Country" TEXT NULL,
    "HomeAddress_Postcode" TEXT NOT NULL,
    "WorkAddress_Line1" TEXT NULL,
    "WorkAddress_Line2" TEXT NULL,
    "WorkAddress_City" TEXT NULL,
    "WorkAddress_Region" TEXT NULL,
    "WorkAddress_Country" TEXT NULL,
    "WorkAddress_Postcode" TEXT NULL
);

Además, EF Core lanzará una excepción si se intenta guardar un propietario que tiene un dependiente requerido nulo. En este ejemplo, EF Core se lanzará al intentar salvar a una persona con una HomeAddress nula.

Finalmente, EF Core seguirá creando una instancia de un dependiente requerido incluso cuando todas las columnas del dependiente requerido tengan valores nulos.

Options for migration generation

EF Core 5.0 tiene un mayor control sobre la generación de migrations para diferentes propósitos. Esto incluye la capacidad de:

  • Saber si la migración se genera para un script o para ejecución inmediata
  • Saber si se está generando un script idempotente
  • Saber si el script debe excluir las declaraciones de transacciones (consulte Scripts de migraciones con transacciones a continuación).

Este comportamiento se especifica mediante una enumeración MigrationsSqlGenerationOptions, que ahora se puede pasar a IMigrator.GenerateScript.

También se incluye una mejor generación de scripts idempotentes con llamadas a EXEC en SQL Server cuando sea necesario. Esto permite mejoras similares a los scripts generados por otros proveedores de bases de datos, incluido PostgreSQL.

Migrations scripts with transactions

Los scripts SQL generados a partir migrations ahora contienen declaraciones para iniciar y confirmar transacciones según corresponda el migration. Por ejemplo, el siguiente script de migración se generó a partir de dos migraciones. Observe que ahora cada migración se aplica dentro de una transacción, es posible desactivarlo.

BEGIN TRANSACTION;
GO

CREATE TABLE [Groups] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NULL,
    CONSTRAINT [PK_Groups] PRIMARY KEY ([Id])
);
GO

CREATE TABLE [Members] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NULL,
    [GroupId] int NULL,
    CONSTRAINT [PK_Members] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Members_Groups_GroupId] FOREIGN KEY ([GroupId]) REFERENCES [Groups] ([Id]) ON DELETE NO ACTION
);
GO

CREATE INDEX [IX_Members_GroupId] ON [Members] ([GroupId]);
GO

INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20200910194835_One', N'6.0.0-alpha.1.20460.2');
GO

COMMIT;
GO

BEGIN TRANSACTION;
GO

EXEC sp_rename N'[Groups].[Name]', N'GroupName', N'COLUMN';
GO

INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20200910195234_Two', N'6.0.0-alpha.1.20460.2');
GO

COMMIT;

See pending migrations

El comando dotnet ef migrations list ahora muestra qué migraciones aún no se han aplicado a la base de datos. Por ejemplo:

ajcvickers@avickers420u:~/AllTogetherNow/Daily$ dotnet ef migrations list
Build started...
Build succeeded.
20200910201647_One
20200910201708_Two
20200910202050_Three (Pending)
ajcvickers@avickers420u:~/AllTogetherNow/Daily$

ModelBuilder API for value comparers

Las propiedades de EF Core para tipos mutables personalizados requieren un comparador de valores para que los cambios de propiedad se detecten correctamente. Ahora se puede especificar como parte de la configuración de la conversión de valor para el tipo. Por ejemplo:

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyProperty)
    .HasConversion(
        v => JsonSerializer.Serialize(v, null),
        v => JsonSerializer.Deserialize<List<int>>(v, null),
        new ValueComparer<List<int>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToList()));

EntityEntry TryGetValue methods

Tenemos un método TryGetValue a EntityEntry.CurrentValues y EntityEntry.OriginalValues. Esto permite solicitar el valor de una propiedad sin verificar primero si la propiedad está asignada en el modelo EF. Por ejemplo:

if (entry.CurrentValues.TryGetValue(propertyName, out var value))
{
    Console.WriteLine(value);
}

Default max batch size for SQL Server

A partir de EF Core 5.0, el tamaño de lote máximo predeterminado para SaveChanges en SQL Server ahora es 42. 

Default environment to Development

Las herramientas de línea de comandos de EF Core ahora configuran automáticamente las variables de entorno ASPNETCORE_ENVIRONMENT y DOTNET_ENVIRONMENT en «Desarrollo». Esto hace que la experiencia al usar el host genérico coincida con la experiencia de ASP.NET Core durante el desarrollo. 

Better migrations column ordering

Las columnas para las clases base no asignadas ahora se ordenan después de otras columnas para los tipos de entidades asignadas. Tengamos en cuenta que esto solo afecta a las tablas recién creadas. El orden de las columnas de las tablas existentes permanece sin cambios.

Query improvements

Algunas mejoras de traducción de consultas adicionales:

  • La traducción está en Cosmos.
  • Las funciones asignadas por el usuario ahora se pueden anotar para controlar la propagación nula.
  • Soporte para la traducción de GroupBy con agregados condicionales.
  • Traducción del operador Distinct sobre el elemento de grupo antes de la agregación.

Model building for fields

Finalmente, para RC1, EF Core ahora permite el uso de los métodos lambda en ModelBuilder para campos y propiedades. Por ejemplo, si es reacio a las propiedades por alguna razón y decide usar campos públicos, estos campos ahora se pueden asignar usando los constructores lambda:

public class Post
{
    public int Id;
    public string Name;
    public string Category;
    public int BlogId;
    public Blog Blog;
}

public class Blog
{
    public int Id;
    public string Name;
    public ICollection<Post> Posts;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>(b =>
    {
        b.Property(e => e.Id);
        b.Property(e => e.Name);
    });

    modelBuilder.Entity<Post>(b =>
    {
        b.Property(e => e.Id);
        b.Property(e => e.Name);
        b.Property(e => e.Category);
        b.Property(e => e.BlogId);
        b.HasOne(e => e.Blog).WithMany(e => e.Posts);
    });
}

Conclusiones

El equipo sigue realizando Builds diarios para el seguimiento de la comunidad. Tampoco debemos olvidar que podemos dar feedback en cualquier momento a todo el equipo de .Net 5. Por el momento, tenemos mucha infor para probar, no se priven de hacerlo ya que en noviembre tenemos la versión final.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *