Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
lunes, 18 de septiembre de 2023
Enlaces interesantes

La verdad es que este año me ha dado algo de pereza volver a abrir el blog después de este periodo veraniego de descanso. Aunque disfrute escribiendo y compartiendo información, mantener esto medio vivo requiere esfuerzo y tiempo, que obviamente dejo de dedicar a otras cosas que también me gustan 🙂

Pero bueno, la cuestión es que por aquí andamos de nuevo, listos para inaugurar oficialmente la temporada 2023-2024. ¿Y qué mejor forma de hacerlo que con una nueva recopilación de enlaces interesantes? ¡Pues vamos allá!

Por si te lo perdiste...

.NET Core / .NET

miércoles, 12 de julio de 2023
Playa de Costa Ballena

Si sois habituales del blog, probablemente ya sabréis lo que os voy a decir ;)

Como todos los años por estas fechas, empieza el periodo vacacional y aprovecharé para bajar un poco el ritmo, descansar unos días y disfrutar de familia y amigos. 

Por tanto, dejaré el blog en modo de bajo consumo hasta bien entrado septiembre, cuando, ya con las pilas recargadas, volveré al ataque con nuevos artículos y enlaces interesantes. Eso sí, durante este periodo seguiré echando el ojo periódicamente a comentarios o mensajes que me dejéis, aunque no los responderé con la misma celeridad que en otras épocas del año.

Aprovecho para desearos a todos un feliz verano y que disfrutéis de las vacaciones. ¡Nos vemos a la vuelta!

Publicado en Variable not found.

lunes, 10 de julio de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

viernes, 7 de julio de 2023
MVP

Por decimotercer año consecutivo, me complace enormemente informaros de que Microsoft ha tenido a bien reconocerme de nuevo como MVP (Most Valuable Professional) en tecnologías de desarrollo. ¡13 años ya, uau! ¡Pero si el primero parece que fue ayer!

Muchas gracias al increíble equipo de Microsoft que hay detrás de este programa por haberme honrado con este reconocimiento, y especialmente a nuestra gran Cristina González por su dedicación, cercanía y ponérnoslo todo tan fácil cuando la necesitamos.

También agradeceros a vosotros, mis queridos amigos, que sois los que con vuestras visitas, lecturas, comentarios y apoyo incondicional hacéis que este sueño siga siendo posible año tras año.

¡Nos vemos por aquí!

Publicado en Variable not found.

martes, 4 de julio de 2023
.NET

Seguro que habéis visto más de una vez un código parecido al siguiente, en el que llamamos a una API  REST externa y su resultado es deserializado a un objeto .NET para introducirlo en el flujo de la aplicación:

async Task<User[]> GetUsersAsync()
{
    var httpClient = _httpClientFactory.CreateClient();
    
    // Hacemos la llamada
    var response = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/users");

    // Si la cosa no fue bien, retornamos
    if (!response.IsSuccessStatusCode)
        return Array.Empty<User>();

    // Descargamos la respuesta y la deserializamos
    var usersAsJson = await response.Content.ReadAsStringAsync();
    var users = JsonSerializer.Deserialize<User[]>(usersAsJson);

    return users;
}

Fijaos que el JSON de la respuesta de la API lo guardamos en una cadena de caracteres para, justo después, deserializarlo y convertirlo en un array de objetos User. Que levante la mano el que no lo haya hecho nunca 😉

¿Y veis dónde está el problema? A la salida de este método, tendremos en memoria dos copias de los datos de los usuarios, una en forma de string JSON y otra en el objeto que hemos deserializado.

Si estamos hablando de respuestas pequeñas o con poca concurrencia, probablemente el impacto es inapreciable. Pero si las estructuras retornadas por la API tuvieran un tamaño considerable o estamos en un escenario de múltiples llamadas simultáneas, esta duplicidad sería un auténtico derroche de recursos.

lunes, 3 de julio de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 27 de junio de 2023
ASP.NET Core

Cuando en ASP.NET Core MVC usamos rutado por convención, lo habitual es que accedamos a las acciones mediante rutas definidas en el patrón, como [controller]/[action]. Así, podemos encontrarnos con rutas como /PendingInvoices/ViewAll para acceder a la siguiente acción:

public class PendingInvoicesController : Controller
{
    public IActionResult ViewAll() => Content("Show all pending invoices");
}

Lo mismo ocurre con páginas Razor. Si usamos las rutas por defecto, al archivo /Pages/ShowAllPendingInvoices.cshtml podríamos acceder mediante la ruta /ShowAllPendingInvoices. No es que sean rutas terribles, pero tampoco podemos decir que sean lo mejor del mundo en términos de legibilidad y conveniencia.

El kebab-casing consiste en separar con un guion "-" las distintas palabras que componen los fragmentos de la ruta, por lo que en los casos anteriores tendríamos /pending-invoices/view-all y show-all-pending-invoices, algo bastante más legible, elegante, y apropiado desde el punto de vista del SEO.

El nombre kebab-casing viene de que visualmente el resultado es similar a un pincho atravesando trozos de comida. Imaginación que no falte 😉

En este post vamos a ver cómo aprovechar los puntos de extensibilidad del sistema de routing de ASP.NET Core para modificar la forma en que genera rutas y así adaptarlo a nuestras necesidades.

lunes, 26 de junio de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 20 de junio de 2023
C#

Desde la aparición de los nullable reference types, o tipos referencia anulables, en C# 8, nos encontramos frecuentemente con el warning de compilación CS8618, que nos recuerda que las propiedades de tipo referencia definidas como no anulables (es decir, que no pueden contener nulos) deben ser inicializadas obligatoriamente porque de lo contrario contendrán el valor null, algo que iría en contra de su propia definición.

Para verlo con un ejemplo, consideremos una aplicación de consola con el siguiente código:

var friend = new Friend() {Name = "John", Age = 32};
Console.WriteLine($"Hello, {friend.Name}");

public class Friend
{
    public string Name { get; set; }
    public int Age { get; set; }
}

La aplicación se ejecutará sin problema, aunque al compilarla obtendremos el warning CS8618:

D:\Projects\ConsoleApp78>dotnet build
MSBuild version 17.4.1+9a89d02ff for .NET
  Determining projects to restore...
  Restored D:\Projects\ConsoleApp78\ConsoleApp78.csproj (in 80 ms).
D:\Projects\ConsoleApp78\Program.cs(8,19): warning CS8618: Non-nullable property 'Name' 
 must contain a non-null value when exiting constructor. Consider declaring the 
 property as nullable.
[...]

[D:\Projects\ConsoleApp78\ConsoleApp78.csproj]
    1 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.63

D:\Projects\ConsoleApp78\ConsoleApp78>_

También en Visual Studio podremos ver el subrayado marcando la propiedad como incorrecta:

Visual Studio mostrando el warning CS8618 sobre la propiedad

Aunque muchas veces este warning viene bien porque nos ayudará a evitar errores, hay otras ocasiones en las que puede llegar a ser molesta tanta insistencia. Y en estos casos, ¿cómo podemos librarnos de este aviso?

Disclaimer: algunas de las soluciones mostradas no son especialmente recomendables, o incluso no tienen sentido en la práctica, pero seguro que son útiles para ver características de uso poco habitual en C#.

lunes, 19 de junio de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 13 de junio de 2023
C#

Ya he comentado alguna vez que el hecho de trabajar a diario con C# no implica que conozcamos todas sus funcionalidades, detalles, trampas y recovecos. Después de muchos años, yo sigo encontrándome sorpresas bastante a menudo.

Hace poco, andaba haciendo algunas pruebas y llegué a un código como el siguiente:

var a = 1;
var (b, c) = 4;
var (d, e, f) = 9;
Console.WriteLine(a + b + c + d + e + f ); // Muestra por consola "14"

Así al vistazo, diréis que el código no compila tal cual, pero la pregunta es: ¿sería posible que compilara y, además, mostrara la salida que pretendemos sin tocar una sola línea de las que vemos ahí?

Si lo pensáis un poco seguro que podéis responder a las preguntas. Y si no, pulsad aquí para ver la solución 👇👇

Pues en efecto, el código tal y como está no compila, así que debemos pasar a la siguiente parte de la pregunta: qué podemos hacer para que compile y, además, muestre por consola el valor que buscamos, sin tocar ni una coma de esas cuatro líneas de código propuesto como punto de partida.

Si nos fijamos bien, la primera línea es una asignación normal, pero en la segunda y tercera línea estamos asignando valores a variables usando sintaxis propia de tuplas. En el fondo, ambas líneas son iguales, y fallan en compilación porque no podemos asignar un entero a un tupla. ¿O quizás sí?

Si recordáis, hace mucho tiempo hablamos por aquí de la deconstrucción de clases, un interesante mecanismo que, al más puro estilo de cheff sofisticado, permitía deconstruir o descomponer objetos en tuplas, simplemente implementando el método Deconstruct().

Pues bien, resulta que este método Deconstruct() puede implementarse de forma externa al tipo que va a ser deconstruido mediante extension methods. Seguro que ya empezáis a ver por dónde van los tiros... 😉 En efecto, podríamos implementar el método extensor Deconstruct() sobre el tipo int e introducir en él la lógica que nos interese.

Una posible implementación sería la siguiente, en la que tenemos sobrecargas de Deconstruct() para dos y tres parámetros de salida, entre los que repartimos equitativamente el valor del entero a deconstruir:

public static class IntegerExtensions
{
    public static void Deconstruct(this int i, out int i1, out int i2)
    {
        (i1, i2) = (i / 2, i / 2);
    }
    public static void Deconstruct(this int i, out int i1, out int i2, out int i3)
    {
        (i1, i2, i3) = (i / 3, i / 3, i / 3);
    }
}

Si ejecutamos ahora mentalmente el código propuesto, veremos que se cumplen los requisitos iniciales:

var a = 1;         // a=1
var (b, c) = 4;    // "4" se deconstruye en (2,2). Por tanto: b=c=2
var (d, e, f) = 9; // "9" se deconstruye en (3,3,3). Por tanto, d=e=f=3
Console.WriteLine(a + b + c + d + e + f ); // 1+2+2+3+3+3 -> Muestra "14": 

Bonito y maquiavélico uso de tuplas y deconstrucción, ¿verdad? 😉


Publicado en Variable not found.
lunes, 12 de junio de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 6 de junio de 2023
.NET

Hace unos días hablábamos de la serialización polimórfica en .NET 6, y vimos qué posibilidades teníamos para conseguirlo sin tener que escribir un custom converter o conversor personalizado. Y aunque realmente .NET 6 permite hacerlo, no es lo más elegante del mundo porque teníamos que operar sobre tipos object.

Pero por suerte, en .NET 7 la cosa ha mejorado y ya tenemos opciones razonables para conseguirlo basadas en los dos nuevos atributos [JsonDerivedType] y [JsonPolymorphic]. Veamos cómo utilizarlos.

lunes, 5 de junio de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 30 de mayo de 2023
.NET

Imaginad una clase como la siguiente, que representa las características básicas de los archivos almacenados en una aplicación:

public class File
{
    public string FileName { get; set; }
    public ulong SizeBytes { get; set; }
}

Y ahora, imaginemos también una clase que hereda de la anterior para modelar específicamente, aunque también de forma resumida, los archivos de vídeo:

public class VideoFile: File
{
    public string Codec { get; set; }
    public TimeSpan Duration { get; set; }
}

Y puestos a imaginar, acabemos con el siguiente método, que retorna la representación JSON del objeto File que recibe como parámetro:

string SerializeFile(File file) => JsonSerializer.Serialize(file);

Gracias al polimorfismo, ese pilar imprescindible de la Programación Orientada a Objetos, podríamos invocar este método con objetos de tipo File, VideoFile o cualquier descendiente de alguno de ambos, puesto que en todos los casos se trata de objetos de tipo File:

var file = new File 
{ 
    FileName = "file.txt", SizeBytes = 1024 
};
Console.WriteLine(SerializeFile(file));

var videoFile = new VideoFile 
{ 
    FileName = "video.mp4", 
    SizeBytes = 1024 * 1024, 
    Codec = "H264", 
    Duration = TimeSpan.FromMinutes(3)
};
Console.WriteLine(SerializeFile(videoFile));

lunes, 29 de mayo de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 23 de mayo de 2023
ASP.NET Core

Hace pocos meses hablábamos de la vuelta del clásico [OutputCache] en ASP.NET Core 7 y veíamos cómo podía simplificarnos la vida a la hora de cachear en el servidor respuestas de peticiones.

Haciendo un rápido recordatorio, la novedad era la posibilidad de introducir en el pipeline el middleware OutputCacheMiddleware, que se encargaría de almacenar las respuestas de endpoints y reutilizarlas en posteriores peticiones que cumplieran los requisitos apropiados.

lunes, 22 de mayo de 2023
Enlaces interesantes

Tras una semana de parón por una semanilla de relax en Roma (ciudad que, por cierto, os recomiendo que visitéis si no lo habéis hecho ya), ahí van los enlaces recopilados durante los últimos días. Como de costumbre, espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 9 de mayo de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

lunes, 8 de mayo de 2023
¡17 años de Variable not found! (Imagen generada con DALL-E 2)

El 8 de mayo de 2006 decidí subir a un tren, teniendo más o menos clara la dirección, pero sin saber en qué parada me bajaría.

Cuando en aquél post inicial decía literalmente "espero que el viaje sea largo", probablemente no me refería a tan largo. Aún con la tenacidad de la que hacía gala, era imposible imaginar que diecisiete años después seguiría por aquí, contemplando el paisaje y escribiendo sobre lo que veo, intentando contribuir a una comunidad a la que tanto debo.

Más de 1.430 posts, más de 3.000 comentarios de lectores, más de cinco millones de consultas... pero sobre todo, muchísimo aprendido por el camino y, con suerte, espero que no solo por mi parte 😉

martes, 2 de mayo de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 25 de abril de 2023
ASP.NET Core

Atributos como [FromRoute], [FromForm], [FromQuery] o [FromBody], entre otros, permiten ser muy precisos a la hora de indicar al framework cómo poblar los parámetros de los handlers de nuestros endpoints contruídos con Minimal APIs.

Por ejemplo, en la siguiente API sencilla se puede intuir que el parámetro id del manejador será obtenido de la ruta, mientras que number se obtendrá desde la query string:

app.MapPost("/friends/{id}/phones", ([FromRoute] int id, [FromQuery] string number) =>
{
    // Añadir un número de teléfono al amigo
});

Y otro ejemplo, en el que usamos  [FromBody] para especificar que el parámetro de tipo Friend queremos obtenerlo desde el cuerpo de la petición:

app.MapPut("/friends/{id}", ([FromRoute] int id, [FromBody] Friend friend) =>
{
    // Actualizar amigo
});

lunes, 24 de abril de 2023
martes, 18 de abril de 2023
Entity Framework

Versión tras versión, Entity Framework Core sigue introduciendo novedades que lo hacen cada vez más potente y atractivo para los desarrolladores. Sin ir más lejos, EF7, la última entrega disponible, incorporó bastantes mejoras en términos de rendimiento, soporte para columnas JSON, mapeo TPC, mapeo a procedimientos almacenados y muchas otras.

Pero una de las novedades que me resultó más interesante, quizás por ser muy esperada, fue la posibilidad de realizar borrados y actualizaciones de forma masiva sin tener que recurrir a lanzar directamente comandos SQL. O dicho de otra forma, con EF7 podremos ejecutar sentencias DELETE y UPDATE sobre la base de datos sin abandonar LINQ :)

lunes, 17 de abril de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 11 de abril de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

lunes, 3 de abril de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 28 de marzo de 2023
C#

A raíz del artículo publicado hace algunas semanas sobre las ventajas de usar diccionarios en lugar de listas, me llegaba vía comentarios un escenario en el que se utilizaba una clase List<T> para almacenar objetos a los que luego se accedía mediante clave. Lo diferencial del caso es que dichos objetos tenían varias claves únicas a través de las cuales podían ser localizados.

Por verlo por un ejemplo, el caso era más o menos como el que sigue:

public class FriendsCollection
{
    private List<Friend> _friends = new();
    ...
    public void Add(Friend friend)
    {
        _friends.Add(friend);
    }

    public Friend? GetById(int id) 
        => _friends.FirstOrDefault(f => f.Id == id);

    public Friend? GetByToken(string token) 
        => _friends.FirstOrDefault(f => f.Token == token);
}

Obviamente en este escenario no podemos sustituir alegremente la lista por un diccionario, porque necesitamos acceder a los elementos usando dos claves distintas. Pero, por supuesto, podemos conseguir también la ansiada búsqueda O(1) si le echamos muy poquito más de tiempo.

lunes, 27 de marzo de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 21 de marzo de 2023
C#

El pattern matching de C# proporciona la capacidad de analizar expresiones para ver si cumplen determinados "patrones" o presentan características determinadas. Podéis ver algunos ejemplos interesantes en el post Un vistazo a los patrones relacionales y combinadores.

Aunque ya los tengo bastante interiorizados y hago uso de ellos cuando toca, todavía no se me ha dado el caso de necesitar los patrones de listas, introducidos hace unos meses en C# 11. Así que no está de más echarles un vistazo para cuando se dé la ocasión 😉

lunes, 20 de marzo de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 14 de marzo de 2023
C#

A veces, los problemas de rendimiento de las aplicaciones, o determinadas funcionalidades de ellas, vienen derivados del uso de estructuras incorrectas para almacenar los datos, ya sea en memoria, base de datos o en cualquier tipo de almacén.

En este post vamos a centrarnos en un caso específico que me he encontrado demasiadas veces en código real: el uso indebido del tipo List<T> cuando sólo nos interesa buscar en esta colección por una propiedad que actúa como identificador único del objeto T.

lunes, 13 de marzo de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 7 de marzo de 2023
Visual Studio

Si desarrollas APIs, probablemente utilizarás Postman, Fiddler o herramientas similares para probarlas. Cualquier utilidad capaz de lanzar peticiones y analizar la respuesta, incluso basadas en línea de comandos como el desconocido Dotnet HTTP REPL, viene de perlas para ponernos en el lugar del cliente y comprobar cómo responde nuestra API ante determinados parámetros de entrada, o qué se siente al consumirla.

Tiempo atrás, el descubrimiento de la extensión REST client para Visual Studio Code supuso una bocanada de aire fresco en la forma de lanzar peticiones y examinar respuestas, para mi gusto mucho más cómoda que las que estaba acostumbrado a utilizar. Esta extensión permite definir peticiones HTTP en archivos de texto con extensión .http o .rest y ejecutarlas de forma realmente sencilla, sin abandonar el IDE.

Pues bien, la última revisión de Visual Studio 2022 (17.5) ha incluido el soporte nativo para este tipo de archivos, así que cada vez lo tenemos más sencillo. Y como es posible que alguno de vosotros aún no lo conozca, vamos a echarle un vistazo ;)

lunes, 6 de marzo de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

lunes, 27 de febrero de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

martes, 21 de febrero de 2023
C#

Aunque muchos de nosotros trabajamos a diario con C#, siempre hay algo nuevo por aprender o formas de utilizar algunas características que nunca se nos habían ocurrido. Siempre.

En un nuevo capítulo de la serie de C# bizarro, hoy os planteo un reto sobre este código:

var sum = (int a, int b) => a + b;
var sub = (int a, int b) => a - b;
var mul = (int a, int b) => a * b;
var result = sum - sub + mul;

Console.WriteLine("Resultado: " + result(3, 2));
¿Compila? Y si es así, ¿qué aparece por consola? ¡No sigáis leyendo! Echad un vistazo al código e intentad averiguarlo antes de ver la solución pulsando aquí :)
Pues sí, este código es totalmente válido y compilará sin problema. Y al ejecutarlo, por consola veremos lo siguiente.
Resultado: 6

En primer lugar el código compila correctamente porque las variables sum, sub y mult, que hemos definido usando expresiones lambda de tipo Func<int, int, int>, a la postre son simplemente delegados.

Además, los tres delegados tienen la firma idéntica (reciben dos valores int y retornan un int), podemos utilizar los operadores de combinación suma "+" y resta "-", lo que da lugar a un delegado de multidifusión (multicast delegate).

En nuestro código, creamos el nuevo delegado de multidifusión result combinando sum y mult, que son los dos delegados que se suman. Por otra parte, la resta de sub es simplemente una maniobra de distracción, pues se intentará eliminar de la combinación un delegado que no existía previamente, por lo que la operación será ignorada.

var result = sum - sub + mul;

Tras ejecutar esta línea, result será un Func<int, int, int> cuya invocación provocará que se ejecuten secuencialmente, y por orden de llegada, los delegados que han sido combinados.

Por tanto, cuando se evalúa la expresión result(3, 2), se ejecutará primero la función sum(3, 2) y luego mul(3, 2), y será el resultado de esta última la que se retorne finalmente. De ahí obtenemos el 6 que va a la consola.

¿Qué, habías acertado?


Publicado en Variable not found.
lunes, 20 de febrero de 2023
Enlaces interesantes

Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET