0

Asp.Net y Entity Framework 7 Preview 5

Es el momento de ver las novedades de Asp.Net y Entity Framework 5 preview 7. Como toda preview, tenemos nuevas características y muchos arreglos, veamos.

Asp.Net 7 Preview 5

Novedades de esta versión preliminar:

  • Mejoras en la autenticación JWT y configuración de autenticación automática
  • Minimal API, Soporte de parámetros para la simplificación de la lista de argumentos

Mejoras en la autenticación JWT y configuración de autenticación automática

La configuración de la autenticación (AuthN) y la autorización (AuthZ) para una aplicación ASP.NET Core hoy requiere numerosos cambios, incluida la adición y configuración de servicios, y la adición de middleware en diferentes etapas del proceso de inicio de la aplicación. Se han recibido comentarios de que los usuarios consideran que configurar la autenticación y la autorización es una de las cosas más difíciles de crear API con ASP.NET Core. Dada la importancia crítica de configurar correctamente la autenticación y la autorización para proteger las aplicaciones web, realizamos algunas mejoras destinadas a simplificar los aspectos más comunes de esta área para ASP.NET Core, con un enfoque inicial en la autenticación del portador JWT, que se usa comúnmente. para proteger las API web.

Simplified authentication configuration

Las opciones de autenticación ahora se pueden configurar automáticamente directamente desde el sistema de configuración de la aplicación, gracias a la adición de una sección de configuración predeterminada al configurar la autenticación a través de la nueva propiedad Autenticación en WebApplicationBuilder de la siguiente manera:

var builder = WebApplication.CreateBuilder(args);

builder.Authentication.AddJwtBearer(); // New top-level property for setting up authentication

var app = builder.Build();

Esta nueva propiedad proporciona un lugar central en el código de la aplicación para configurar la autenticación, lo que proporciona un fácil acceso a la instancia de AuthenticationBuilder desde la cual se pueden agregar y configurar esquemas de autenticación. La configuración de la autenticación a través de esta nueva propiedad también se encargará de agregar automáticamente el middleware necesario al pipeline de solicitudes, de forma similar a como lo hace WebApplicationBuilder para el enrutamiento.

Veamos un ejemplo de una configuración de aplicación para usar la autenticación de portador JWT con dos endpoints, uno que requiere autorización y otro que no (requiere el paquete Microsoft.AspNetCore.Authentication.JwtBearer NuGet):

using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Authentication.AddJwtBearer();

var app = builder.Build();

app.MapGet("/", () => "Hello, World!");
app.MapGet("/secret", (ClaimsPrincipal user) => $"Hello {user.Identity?.Name}. This is a secret!")
    .RequireAuthorization();

app.Run();

Además, los esquemas de autenticación individuales pueden tener sus opciones configuradas automáticamente desde la configuración de la aplicación, lo que facilita su configuración entre diferentes entornos (por ejemplo, desarrollo local frente a producción). Para esta versión, solo se ha actualizado el esquema portador de JWT para admitir este mecanismo, pero se actualiza más esquemas de autenticación para admitirlo en el futuro.

Este es un ejemplo del archivo appsettings.Development.json de una aplicación, actualizado para configurar las opciones de autenticación a través de la nueva sección «Autenticación»:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Authentication": {
    "DefaultScheme" : "JwtBearer",
    "Schemes": {
      "JwtBearer": {
        "Audiences": [ "http://localhost:5000", "https://localhost:5001" ],
        "ClaimsIssuer": "dotnet-user-jwts"
      }
    }
  }
}

Endpoint-specific authorization policies

El ejemplo anterior incluía una definición de punto final que solo permitía el acceso a usuarios autenticados (RequireAuthorization()). Pero, ¿qué pasa si los requisitos de autorización del endpoint son un poco más complejos? ¿Permitir solo a los usuarios con un claim de «alcance» específico? Un conjunto de requisitos de autorización se define en una política» que normalmente se define globalmente como parte de la configuración de la autorización en los servicios de la aplicación y luego se hace referencia por su nombre al configurar el punto final. Esto es bueno para la reutilización, pero puede ser difícil de descubrir y demasiado complejo para algunos escenarios.

Para los casos en los que no es necesario compartir una política de autorización entre puntos finales, ahora puede definir fácilmente una política de autorización directamente en un punto final a través de metadatos como este:

app.MapGet("/special-secret", () => "This is a special secret!")
    .RequireAuthorization(p => p.RequireClaim("scope", "myapi:secrets"));

Una vez protegidos los endpoints protegidos por la autenticación JWT, sería bueno verificar fácilmente que están configurados correctamente en un entorno de desarrollo local, sin la necesidad de un servicio completo de administración de identidades y usuarios. Para eso, necesitaremos algo para emitir JWT para usar con nuestra aplicación localmente, que es el trabajo de la nueva herramienta de línea de comando dotnet user-jwts.

Minimal API, Soporte de parámetros para la simplificación de la lista de argumentos

En esta versión para admitir la refactorización de una API mínima que toma un conjunto de parámetros en uno que toma un solo objeto con propiedades de nivel superior que representan lo que alguna vez fueron argumentos.

Por ejemplo, la siguiente API enumera todos los productos en una categoría determinada:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Requires the Microsoft.EntityFrameworkCore.InMemory package
builder.Services.AddDbContext<MyDb>(options => options.UseInMemoryDatabase("products"));
var app = builder.Build();

app.MapGet("/categories/{categoryId}/products", (int categoryId, int pageSize, int page, ILogger<Program> logger, MyDb db) =>
{
    logger.LogInformation("Getting products for page {Page}", page);
    return db.Products.Where(p => p.CategoryId == categoryId).Skip((page - 1) * pageSize).Take(pageSize);
});

app.Run();

record Product (int Id, string Name, int CategoryId);

class MyDb : DbContext
{
    public MyDb(DbContextOptions options) : base(options) { }
    public DbSet<Product> Products { get; set; }
}

Ahora podemos refactorizar los parámetros API a un tipo y agregar el nuevo atributo AsParameters a su parámetro:

app.MapGet("/categories/{categoryId}/products", ([AsParameters] ProductRequest req) =>
{
    req.Logger.LogInformation("Getting products for page {Page}", req.Page);
    return req.Db.Products.Where(p => p.CategoryId == req.CategoryId).Skip((req.Page - 1) * req.PageSize).Take(req.PageSize);
});

record struct ProductRequest(
    int CategoryId, 
    int PageSize, 
    int Page, 
    ILogger<ProductRequest> Logger, 
    MyDb Db);

Las reglas de vinculación de parámetros se aplicarán a las propiedades de nivel superior del nuevo tipo o a los parámetros de constructor parametrizados. Además, los mismos atributos de enlace (FromRoute, FromQuery, FromServices, etc.) son compatibles y se les pueden aplicar.


Actualicemos el ejemplo anterior para enlazar tanto Page como PageSize desde los encabezados de solicitud en lugar de la cadena de consulta:

record struct ProductRequest(
    int CategoryId,
    [FromHeader(Name = "PageSize")] int PageSize,
    [FromHeader(Name = "Page")] int Page,
    ILogger<ProductRequest> Logger, 
    MyDb Db);

Se admiten tanto clases como estructuras (se recomienda el uso de estructuras para evitar la asignación de memoria adicional). Sin embargo, los tipos abstractos y las interfaces no son compatibles. En nuestro ejemplo anterior, el mismo tipo (actualmente una estructura de registro) podría definirse como una clase:

class ProductRequest
{
    public int CategoryId { get; set; }
    [FromHeader(Name = "PageSize")]
    public int PageSize { get; set; }
    [FromHeader(Name = "Page")]
    public int Page { get; set; }
    public ILogger<ProductRequest> Logger { get; set; }
    public MyDb Db { get; set; }
}

Las siguientes reglas se aplican durante el enlace de parámetros:

Classes

  • Se usará un constructor público sin parámetros si está presente
  • Se usará un constructor parametrizado público si hay un solo constructor presente y todos los argumentos tienen una propiedad pública coincidente (que no distingue entre mayúsculas y minúsculas).
    • Si un parámetro de constructor no coincide con una propiedad, se lanzará InvalidOperationException si se intenta vincular.
  • Lance InvalidOperationException cuando se declare más de un parámetro y el constructor sin parámetros no esté presente.
  • Lance InvalidOperationException si no se encuentra un constructor adecuado.

Structs

  • Siempre se usará un constructor sin parámetros público declarado si está presente
  • Se utilizará un constructor parametrizado público si hay un solo constructor presente y todos los argumentos tienen una propiedad pública coincidente (que no distingue entre mayúsculas y minúsculas).
    • Si un parámetro de constructor no coincide con una propiedad, se lanzará InvalidOperationException si se intenta vincular.
  • Dado que struct siempre tiene un constructor predeterminado, se usará el constructor predeterminado si es el único presente o si hay más de un constructor parametrizado.

Entity Framework 7 Preview 5

Se ha agregado soporte para el mapeo de tipo Table-per-Concrete (TPC) y otras mejoras como:

  • Compatibilidad con AT TIME ZONE en SQL Server
  • Actualizaciones a la interceptación de comandos y conexiones 
  • Adición del atributo de comportamiento de eliminación

Table-per-concrete-type (TPC) mapping

De forma predeterminada, EF Core asigna una jerarquía de herencia de tipos .NET a una única tabla de base de datos. Esto se conoce como la estrategia de mapeo de tabla por jerarquía (TPH). EF Core 5.0 tenía la estrategia de tabla por tipo (TPT), que admitía la asignación de cada tipo de .NET a una tabla de base de datos diferente. En la versión preliminar 5.0 de EF Core 7.0,  ahora tenemos table-per-concrete-type (TPC) strategy.(TPC). TPC también asigna tipos de .NET a diferentes tablas, pero de una manera que soluciona algunos problemas de rendimiento comunes con la estrategia TPT.

Mapping inheritance hierarchies

Veamos los siguientes modelos de ejemplo:

public abstract class Animal
{
    public int Id { get; set; }
    public string Species { get; set; }
}

public class FarmAnimal : Animal
{
    public decimal Value { get; set; }
}

public class Pet : Animal
{
    public string Name { get; set; }
}

public class Cat : Pet
{
    public string EducationLevel { get; set; }
}

public class Dog : Pet
{
    public string FavoriteToy { get; set; }
}

Si vamos a recuperar algún objeto Animal de la base de datos, entonces debemos saber qué tipo de animal es. No queremos guardar a un gato y luego leerlo como un perro, o viceversa.  Así que esto significa el tipo de animal, esa es la clase real que se usa cuando el El animal se creó en C#; debe guardarse en la base de datos de alguna forma.

Además, se asocia diferente información con cada objeto Animal dependiendo de su tipo. Por ejemplo, en nuestro modelo, un animal de granja tiene algún valor monetario pero no tiene nombre, mientras que las mascotas no tienen precio y tienen nombre..

Las estrategias de asignación de herencia (TPH, TPT o TPC) definen cómo esta información de tipo orientada a objetos y la información específica de tipo se guardan en una base de datos relacional, donde la herencia no es un concepto natural.

Solo veremos la estrategia TPC, pero puedes consultar la documentación de las estrategias anteriores.

TPC mapping

La estrategia TPC es similar a la estrategia TPT excepto que se crea una tabla diferente para cada tipo concreto en la jerarquía, pero no se crean tablas para tipos abstractos, de ahí el nombre «tabla por tipo concreto». Al igual que con TPT, la propia tabla indica el tipo de objeto guardado. Sin embargo, a diferencia del mapeo TPT, cada tabla contiene columnas para cada propiedad en el tipo concreto y sus tipos base. Por ejemplo: 

   [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),
    [Species] nvarchar(max) NOT NULL,
    [Value] decimal(18,2) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id])
);

CREATE TABLE [Pets] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),
    [Species] nvarchar(max) NOT NULL,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Pets] PRIMARY KEY ([Id])
);

CREATE TABLE [Cats] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),
    [Species] nvarchar(max) NOT NULL,
    [Name] nvarchar(max) NOT NULL,
    [EducationLevel] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id])
);

CREATE TABLE [Dogs] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),
    [Species] nvarchar(max) NOT NULL,
    [Name] nvarchar(max) NOT NULL,
    [FavoriteToy] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id])
);

Nota:

  • No hay tabla para Animal, ya que es un tipo abstracto en el modelo de objetos. Recuerde que C# no permite instancias de tipos abstractos y, por lo tanto, no hay ninguna situación en la que se guarde uno en la base de datos.
  • El mapeo de propiedades en tipos base se repite para cada tipo concreto; por ejemplo, cada tabla tiene una columna Especie y tanto Gatos como Perros tienen una columna Nombre.

Guardar los mismos datos en esta base de datos da como resultado lo siguiente:

TPC: Pros y contras de las estrategias de mapeo

La estrategia TPC es una mejora con respecto a TPT porque garantiza que la información de una instancia de entidad dada siempre se almacene en una sola tabla. Esto significa que la estrategia TPC puede ser útil cuando la jerarquía asignada es grande y tiene muchos tipos concretos (generalmente hoja), cada uno con una gran cantidad de propiedades, y donde solo se usa un pequeño subconjunto de tipos en la mayoría de las consultas.

Usando las mismas consultas LINQ nuevamente, el SQL necesario cuando se consultan entidades de todos los tipos es mejor que para TPT, ya que requiere una tabla menos en la consulta. Esto se debe a que no existe una tabla para el tipo base abstracto. Además, se usa UNION ALL en lugar de LEFT JOIN necesario para TPT. UNION ALL no necesita realizar ninguna coincidencia entre filas o desduplicar filas, lo que lo hace más eficiente que las uniones utilizadas en las consultas TPT.


Dicho todo esto, en comparación con el SQL para TPH, el SQL para TPC en este caso todavía no es excelente:

SELECT [t].[Id], [t].[Species], [t].[Value], [t].[Name], [t].[EducationLevel], [t].[FavoriteToy], [t].[Discriminator]
FROM (
    SELECT [f].[Id], [f].[Species], [f].[Value], NULL AS [Name], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator]
    FROM [FarmAnimals] AS [f]
    UNION ALL
    SELECT [p].[Id], [p].[Species], NULL AS [Value], [p].[Name], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Pet' AS [Discriminator]
    FROM [Pets] AS [p]
    UNION ALL
    SELECT [c].[Id], [c].[Species], NULL AS [Value], [c].[Name], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[Species], NULL AS [Value], [d].[Name], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
) AS [t]
WHERE [t].[Species] LIKE N'F%'

Este es nuevamente el caso cuando se consultan entidades de un subconjunto de tipos:

SELECT [t].[Id], [t].[Species], [t].[Name], [t].[EducationLevel], [t].[FavoriteToy], [t].[Discriminator]
FROM (
    SELECT [p].[Id], [p].[Species], [p].[Name], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Pet' AS [Discriminator]
    FROM [Pets] AS [p]
    UNION ALL
    SELECT [c].[Id], [c].[Species], [c].[Name], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[Species], [d].[Name], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
) AS [t]
WHERE [t].[Species] IS NOT NULL AND ([t].[Species] LIKE N'F%')

Pero TPC es mucho mejor que TPT cuando se consultan entidades de un solo tipo de hoja, ya que toda la información de esas entidades proviene de una sola tabla:

SELECT [c].[Id], [c].[Species], [c].[Name], [c].[EducationLevel]
FROM [Cats] AS [c]
WHERE [c].[Species] LIKE N'F%'

Conclusiones

Muchas novedades interesantes. Los invito a investigar las estrategias de TPT y TPH que son muy interesantes en el manejo de nuestro ORM. En futuros post seguiremos viendo las novedades de Asp.Net y Entity Framework 7,

Fernando Sonego

Deja una respuesta

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