Entity Framework Core 3.0: ¿qué novedades nos trae?

La imagen muestra el logo de Entity Framework Core

Hace unas semanas desde que se hizo pública la versión 3.0 de .Net Core y nos ha traído muchas novedades: C# 8, soporte ampliado en Windows, mejoras de rendimiento, Worker Services… Y como no, también nos trae una nueva versión de Entity Framework Core.

Son muchas las novedades que nos trae esta nueva release, que nos aportan mejoras de rendimiento, pero también cambios en el diseño. En esta entrada vamos a ver algunas de las características más importantes de esta nueva versión, aunque te recomiendo que le eches un ojo a la lista completa de cambios que trae Entity Framework Core 3.0 (y también a la lista de breaking changes).

Instrucción SQL única por consulta LINQ

Se ha mejorado el motor de generación de sentencias SQL, anteriormente, consultas que hiciésemos con varias tablas, podían llevar a ejecutar varias consultas a la base de datos en vez de una sola (que es lo que esperamos al utilizarlo). Entity Framework Core 3.0 garantiza que solo se va a generar una única consulta que lanzar a la base de datos, y en caso de no poder traducirse a una única consulta, nos lanzara una excepción que nos lo va a indicar para que podamos refactorizar el código.

Restricción de la evaluación en cliente

En versiones anteriores, si una consulta no se podía traducir a SQL, se realizaba lo que se conoce como evaluación en cliente. Esto significa que se iba a lanzar a la base de datos una consulta con los máximos filtros posibles aplicados, y los que no se pueden traducir, se aplicaban en el propio cliente, a costa de traerse los datos en bruto y procesarlos en nuestra aplicación. Imagina este código (obtenido de la documentación oficial):

public static string StandardizeUrl(string url)
{
    url = url.ToLower();
    if (!url.StartsWith("http://"))
    {
        url = string.Concat("http://", url);
    }
    return url;
}

var blogs = context.Blogs
    .Where(blog => blog.Date > DateTime.Now.AddYears(-1) StandardizeUrl(blog.Url).Contains("dotnet"))
    .ToList();

Estamos utilizando una condición que la base de datos no puede ejecutar, por lo que el comportamiento va a ser:

  1. Hacemos una consulta que nos devuelve toda la tabla filtrada por fecha.
  2. Filtramos en memoria toda la tabla para aplicar nuestra condición personalizada.
  3. Generamos una lista con los resultados filtrados

Aunque esto a veces puede ser útil, si la tabla se hace grande va a ser una gran perdida de rendimiento. Por defecto, en versiones anteriores se permitía la consulta y se indicaba un warning. Este comportamiento por defecto a cambiado en Entity Framework Core 3.0, ahora va a lanzar una excepción que nos lo va a indicar. Si queremos hacer la evaluación en cliente, tenemos que hacerlo de forma explicita:

var blogs = context.Blogs
    .Where(blog => blog.Date > DateTime.Now.AddYears(-1))
    .AsEnumerable() //Cambiamos a Linq de objetos
    .Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"))
    .ToList();

Compatibilidad con C# 8

La nueva versión de C# nos trae muchas mejoras y nuevas APIs, y entre ellas IAsyncEnumerable, lo que junto a «await foreach», nos va a permitir mejorar la velocidad a la que se ejecutan las consultas, ya que vamos a ir procesando los datos a medida que están disponibles. Gracias a C# en Entity Framework Core 3.0 podemos hacer algo como esto:

var orders = 
  from o in context.Orders
  where o.Status == OrderStatus.Pending
  select o;

//Cada nuevo nuevo dato obtenido los vamos a procesar inmediatamente
await foreach(var o in orders.AsAsyncEnumerable())
{
    Process(o);
} 

El tooling de Entity Framework Core 3.0 ya no forma parte de .Net Core 3.0

Un cambio de diseño importante es que .Net Core ya no trae integrado el set de herramientas para trabajar con Entity Framework Core. Es decir, vamos a necesitar instalar a parte el set de herramientas si realmente nos interesa usarlo. Esto lo podemos hacer de manera local con el archivo de manifiesto de herramientas o de manera global con el comando:

dotnet tool install --global dotnet-ef

Esta desarrollado para .Net Standard 2.1

Esta nueva versión de Entity Framework Core, está desarrollado cumpliendo con .Net Standard 2.1, esto es lo que le permite tener grandes mejoras de rendimiento, pero limita las plataformas donde se puede utilizar, .Net Core 3.0 es compatible con .Net Standard 2.1, pero .Net Framework no es compatible, por lo qué si necesitas usarlo en .Net Framework, no vas a poder utilizar Entity Framework Core 3.0.

Y un largo etcétera…

La verdad es que no se limitan a esto los cambios que nos ofrece esta nueva versión. Yo he querido resaltar los que a mi parecer son los más importantes o necesarios tener en cuenta si vienes de trabajar con versiones anteriores, pero te recomiendo encarecidamente que le eches un ojo a las nuevas características y sobre todo, a la lista de cambios que no son retrocompatibles. Verás que esta es una nueva versión muy interesante y con muchas mejoras.

Worker Service: Cómo crear un servicio .Net Core 3 multiplataforma

La imagen muestra el logo de .Net Core 3.0

Hace unos meses, hablamos sobre como crear servicios multiplataforma con .Net Core 2, donde veíamos que había que hacer algunas «perrerías» para poder conseguirlo… Ha llegado .Net Core 3, y me ha parecido interesante hablar sobre la nueva manera que nos trae de conseguir esto.

En primer lugar, tenemos una nueva plantilla disponible para esto: Worker Service. Usando esta plantilla, vamos a poder tener un ejemplo para construir nuestro servicio. Aunque perfectamente podemos hacerlo desde una aplicación de consola añadiendo nosotros el código, vamos a «estrenar» la plantilla (si nos dan trabajo hecho, tampoco vamos a decir que no… xD )

Creando la solución

Lo primero por supuesto, es crear nuestra solución, y en ella, vamos a elegir la plantilla «Servicio de trabajo» (Worker Service en inglés):

La imagen muestra la plantilla seleccionada

Esto nos va a crear una solución con dos archivos, Program.cs y Worker.cs. Si abrimos Program.cs, vamos a poder comprobar que nuestro proyecto no es más que una aplicación de consola que crea un «Host» (al igual que hacíamos en .Net Core 2.X).

namespace PostWorker
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();
                });
    }
}

El segundo fichero que tenemos, Worker.cs, tiene una pinta parecida a esta:

namespace PostWorker
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

La principal novedad al crear nuestras tareas dentro del servicio, es que ahora heredamos de la clase abstracta «BackgroundService» (y no de IHostedService como hacíamos en .Net Core 2.X). Esto simplemente es para aportarnos una capa de abstracción sobre su funcionamiento, y realmente es esta clase la que implementa IHostedService y nos ofrece 3 métodos para trabajar con ella:

protected abstract Task ExecuteAsync(CancellationToken stoppingToken)
public virtual Task StartAsync(CancellationToken cancellationToken)
public virtual Task StopAsync(CancellationToken cancellationToken)        

Para crear nuestro servicio básico, tal como lo hace el ejemplo, basta con implementar el primero de ellos (que además al ser abstracto estamos obligados a implementar). Pero vamos a profundizar un poco más…

El primero de ellos, va a ser lo que queremos que se ejecute dentro de nuestro servicio, con la implementación por defecto, cuando arranque el servicio, se va a llamar directamente a ese método en una tarea. De igual manera, cuando el servicio se pare, se cancelará el token. Es importante tener en cuenta el CancellationToken como sistema de control del ciclo de vida, para evitar tener tareas bloqueadas al parar nuestro servicio.

Si necesitamos una funcionalidad un poco más avanzada, como ejecutar algo de código previo al arranque o en la parada, lo que vamos a tener que hacer es sobrescribir los métodos StartAsync y StopAsync según necesitemos, ya que no es obligatorio hacerlo con los dos. Un caso en el que nos podría hacer falta, es cuando necesitamos diferenciar las labores de arranque del servicio con las propias de ejecución. Por ejemplo, si quieres loguear cuando arranca o para tu servicio .Net Core 3:

namespace PostWorker
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        public override async Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Worker starts");
            await base.StartAsync(cancellationToken);
        }

        public override async Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Worker stops");
            await base.StopAsync(cancellationToken);
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

Es importante señalar, que podemos registrar tantos «Workers» como necesitemos, basta con que lo añadamos en el inyector de dependencias. Por ejemplo, si tuviésemos una clase Worker1 y otra Worker2, nuestro Program.cs se vería algo así:

namespace PostWorker
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker1>();
                    services.AddHostedService<Worker2>();                    
                });
    }
}

Con esto, ya tenemos un código perfectamente funcional como servicio, de hecho, si lo instalamos en Linux como servicio (daemon), podremos comprobar que funciona perfectamente. En cambio, si lo instalamos en Windows, para nuestra sorpresa (o no), simplemente esto no va a funcionar.

Instalando nuestro servicio .Net Core 3 en Windows

Si alguna vez has creado un servicio en Windows, sabes que tiene sus cositas… En .Net Core 2.X, teníamos que hacer ciertas perrerías para conseguirlo, pero por suerte, ahora tenemos un paquete NuGet que nos permite hacerlo sin dificultades (básicamente, hace las perrerías por nosotros). Este paquete es «Microsoft.Extensions.Hosting.WindowsServices«, basta con que lo añadamos a nuestro proyecto y que añadamos su uso en Program.cs con «UseWindowsService» sobre el HostBuilder:

namespace PostWorker
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .UseWindowsService() //Registramos el paquete
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();                  
                });
    }
}

Este paquete lo que va a hacer, es registrar los servicios necesarios en el inyector de dependencias si corremos sobre Windows, y no hacer nada en el resto de los casos.

Con esto, ya tenemos nuestro servicio en .Net Core 3 listo para instalarlo en cualquier máquina, independientemente del sistema operativo que utilicemos. Si quieres probarlo tú mismo, he dejado el código en GitHub para que puedas salsearlo sin problemas.

Como medir el rendimiento de nuestro código

      2 comentarios en Como medir el rendimiento de nuestro código
La imagen muestra un medidor de velocidad para el entrada "Como medir el rendimiento de nuestro código"

A raíz de las entradas sobre la serie de entradas sobre la reflexión que publicamos antes del parón de verano, he estado escribiéndome con un lector sobre algunas posibles optimizaciones en el código, todas ellas midiendo el rendimiento del código para ver cuál era mejor. Llevaba tiempo queriendo escribir sobre este tema, ¿y que mejor momento qué ahora? 🙂

¿Cómo se mide el rendimiento del código?

Para conseguir datos que nos sirvan para esto, se utilizan pruebas de rendimiento o «benchmarks», los cuales nos van a servir para obtener las métricas que vamos a comparar (tiempo, cpu consumida, ram utilizada…) para decidir si nuestro código tiene el rendimiento esperado. Normalmente, esto se consigue midiendo los datos a comprobar y ejecutando el código un número suficiente de veces para asegurar que los posibles «ruidos externos» no afectan a las métricas.

Imagina que simplemente medimos el tiempo de ejecución de método ejecutándolo solo una vez, cualquier cosa que el sistema operativo ejecute podría falsear los resultados…

En el ecosistema .Net, existe una librería que se distribuye a través de NuGet que nos facilita enormemente esta labor, y es de la que os voy a hablar ahora, esta librería es BenchmarkDotNet, Vamos a ponernos en faena:

Como usar BenchmarkDotNet

Vamos a imaginar que dentro de nuestro código tenemos algo como esto:

public class OperacionesMatematicas
{
    public double Suma(double a, double b)
    {
        return a + b;
    }

    public double Multiplicacion(double a, double b)
    {
        return a + b;
    }

    public double Potencia(double @base, double exponente)
    {
        return Math.Pow(@base, exponente);
    }

    public double Potencia2(double @base, double exponente)
    {
        if (exponente == 0)
            return 1;

        var resultado = @base;
        for (int i = 1; i < exponente; i++)
        {
            resultado = resultado * @base;
        }
        return resultado;
    }
}

Simplemente es una clase que va a hacer ciertas operaciones matemáticas, teniendo además dos maneras de calcular una potencia, utilizando Math.Pow y multiplicando la base por si misma tantas veces como indica el exponente (lo que es una potencia vamos…).

Lo primero que vamos a necesitar para medir el rendimiento de nuestro código, es crear un proyecto de consola que será el que tenga el código para las pruebas, en este caso, lo voy a llamar «BenchmarkRunnerProject». A este proyecto le vamos a añadir el paquete «BenchmarkDotNet«, y vamos a crear una clase donde vamos a añadir la lógica de la prueba:

public class OperacionesMatematicasBenchmark
{
    [Benchmark]
    public void Suma()
    {
        var operaciones = new OperacionesMatematicas();
        operaciones.Suma(10, 20);
    }

    [Benchmark]
    public void Multiplicacion()
    {
        var operaciones = new OperacionesMatematicas();
        operaciones.Multiplicacion(10, 20);
    }

    [Benchmark]
    public void Potencia()
    {
        var operaciones = new OperacionesMatematicas();
        operaciones.Potencia(2, 2);
    }

    [Benchmark]
    public void Potencia2()
    {
        var operaciones = new OperacionesMatematicas();
        operaciones.Potencia2(2, 2);
    }
}

Por último, solo nos queda añadir al método Main la ejecución de las pruebas:

class Program
{
    static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<OperacionesMatematicasBenchmark>();
        Console.Read();
    }
}

Una vez hecho esto, ya solo necesitamos ejecutar el proyecto para que nuestra prueba funcione. Pero ojo, tiene que estar en «Release» para poder medir el rendimiento del código con las optimizaciones del compilador. En caso contrario, nos mostrará un mensaje indicándonos que lo hagamos o desactivemos que tenga que ser Release:

La imagen muestra la salida de la consola cuando ejecutamos un benchamrk sin estar en release

Una vez que se ejecute, nos mostrará los resultados en la propia consola:

La imagen muestra un ejemplo de la salida básica

Con esto, ya tenemos la funcionalidad básica, donde solo medimos el tiempo de ejecución con los parámetros que hemos puesto «hardcoded». Pero tenemos la opción de indicar diferentes parámetros con el atributo «Params»:

public class OperacionesMatematicasBenchmark
{
    [Params(2, 3)]
    public int A { get; set; }

    [Params(2, 200)]
    public int B { get; set; }

    [Benchmark]
    public void Suma()
    {
        var operaciones = new OperacionesMatematicas();
        operaciones.Suma(A, B);
    }

    [Benchmark]
    public void Multiplicacion()
    {
        var operaciones = new OperacionesMatematicas();
        operaciones.Multiplicacion(A, B);
    }

    [Benchmark]
    public void Potencia()
    {
        var operaciones = new OperacionesMatematicas();
        operaciones.Potencia(A, B);
    }

    [Benchmark]
    public void Potencia2()
    {
        var operaciones = new OperacionesMatematicas();
        operaciones.Potencia2(A, B);
    }
}

Con este cambio, vamos a conseguir que se ejecute una prueba con cada una de las configuraciones:

La imagen muestra los resultados de la ejecución son parametros

E incluso ordenarlas si añadimos el atributo «RankColumn» a la clase:

La imagen muestra el resultado con el atributo rankcolum

Existen muchísimas opciones y parametrizaciones que pueden servirte para configurar cada uno de los aspectos del benchmark, e incluso comparando la ejecución en diferentes entornos (.Net Framework, .Net Core, Mono) y diferentes versiones de los entornos. Te recomiendo que le eches un ojo a la documentación para ver todas las opciones disponibles, ya que, es imposible hablar de todas ellas en una única entrada. (Por ejemplo, para medir la RAM con Diagnosers).

Como siempre, dejo el enlace al repositorio en GitHub con el código de la entrada.