Todos hemos tenido que lidiar con situaciones en las que una función o método puede devolver diferentes tipos de datos dependiendo de ciertas condiciones. El ejemplo clásico es una función que puede devolver un resultado exitoso o un error, lo que a menudo se maneja con tipos como Result<T> o haciendo algunos malabarismos con composiciones de clases o herencia.
var result = GetUser(1);
if(result.Success)
{
var user = result.Value;
// Hacer algo con el usuario
}
else
{
var error = result.Error;
// Manejar el error
}
Son artificios que, aunque funcionan, dan trabajo, complican la legibilidad y ocultan la intención real del desarrollador a la hora de escribir código de procedimientos que manejen múltiples tipos de retorno.
Los tipos unión son una de las características más solicitadas en C# durante años, y finalmente los tendremos disponibles en la versión 15 del lenguaje, que vendrá de la mano de NET 11 en noviembre de 2026.
Con esta nueva capacidad, podremos crear tipos que pueden contener valores de varios tipos diferentes, con controles en tiempo de compilación y sin apenas complejidad adicional.
Lo vemos en detalle a continuación.
Al desarrollar aplicaciones ASP.NET Core (APIs, MVC, Razor Pages o Blazor) y ejecutarlas en local, ya sabes que todas ellas comparten el mismo nombre de host: localhost, y lo único que las diferencia es el puerto en el que se encuentran a la escucha.
Esto puede dar lugar a ciertos problemas. Aparte de no ser sencillo identificarlas a partir de la URL mostrada en el navegador, hay ciertos recursos web que pueden mezclarse entre proyectos (por ejemplo, las cookies) y pueden dar lugar a comportamientos inesperados.
En ASP.NET Core 10 se ha puesto solución a esto añadiendo el soporte para el TLD (Top Level Domain) .localhost, lo que permite que cada aplicación pueda tener su propio nombre de host único.
Lo vemos a continuación.
Soporte para TLD .localhost en ASP.NET Core 10
El nombre .localhost es un dominio de nivel superior (TLD, Top Level Domain) reservado por las RFC 2606 y 6761 para ser utilizado en entornos de desarrollo y pruebas, y siempre está asociado a la dirección de loopback, es decir, a la dirección local del propio equipo.
Por tanto, cualquier nombre de host que termine en .localhost (por ejemplo, miapp.localhost, api.localhost, MyApp.dev.localhost, etc.) debería resolverse siempre a la dirección IP 127.0.0.1 o ::1, dependiendo de si se utiliza IPv4 o IPv6.
A partir de ASP.NET Core 10, Kestrel entenderá que todos los nombres de host que terminen en .localhost son alias válidos para localhost, por lo que podemos utilizarlos para configurar el servidor, definir las URLs de nuestras aplicaciones (tanto en launchSettings.json como en variables de entorno), etc. Esto nos permitirá usar distintos nombres de host para cada aplicación, evitando los problemas mencionados anteriormente.
Otro tema que han actualizado es el certificado HTTPS utilizado en desarrollo. Dado que el antiguo certificado solo era válido para localhost, al utilizar otros nombres de host el navegador mostraría advertencias de seguridad. Por esta razón, el certificado que se registra al instalar .NET 10 es un wildcard del dominio *.dev.localhost.
Observad que no han podido hacerlo directamente para soportar
*.localhost. Han tenido que introducir el subdominio.devpor delante porque no es posible crear wildcards sobre top level domains comolocalhost.
En la práctica, esto implica que podremos asignar a nuestras aplicaciones ASP.NET Core nombres de host como miapp.dev.localhost, api.dev.localhost, etc., aunque no es algo que esté habilitado por defecto.
Para activar esta característica, debemos seleccionar la opción "Use the .dev.localhost TLD in the application URL" al crear una nueva aplicación, como se muestra en la siguiente imagen:
Al hacerlo, se configurará el launchsettings.json para que utilice el nombre de proyecto como subdominio. Por ejemplo, a continuación se muestra la configuración generada para el proyecto MyWebApplication, donde podemos ver el uso del host mywebapplication.dev.localhost:
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://mywebapplication.dev.localhost:5244",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://mywebapplication.dev.localhost:7279;
http://mywebapplication.dev.localhost:5244",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
También podemos conseguir lo mismo si utilizamos la línea de comandos de .NET para crear los proyectos. Por ejemplo, el siguiente comando crea una aplicación MVC que utiliza el TLD .dev.localhost:
C:\MyProjects\MyWebApp>dotnet new web --localhost-tld
The template "ASP.NET Core Empty" was created successfully.
Processing post-creation actions...
Restoring C:\MyProjects\MyWebApp\MyWebApp.csproj:
Restore succeeded.
C:\MyProjects\MyWebApp>type .\Properties\launchSettings.json
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://mywebapp.dev.localhost:5024",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://mywebapp.dev.localhost:7125;
http://mywebapp.dev.localhost:5024",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
C:\MyProjects\MyWebApp>_
Con estas configuraciones, al lanzar el proyecto se utilizarán estos nombres de host exclusivos para el proyecto, evitando los problemas de compartir el mismo localhost entre varias aplicaciones.
C:\MyProjects\MyWebApp> dotnet run
Using launch settings from C:\MyProjects\MyWebApp\Properties\launchSettings.json...
Building...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://mywebapp.dev.localhost:5024
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5024
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\MyProjects\MyWebApp
_
Un último detalle importante: podremos utilizar los nombres de host personalizados cuando accedamos a las aplicaciones desde el navegador web, pero probablemente no desde otras herramientas como curl, Postman, etc., o al menos de directamente.
Esto se debe a que, aunque el TLD .localhost está reservado para este propósito, no todas las aplicaciones cliente o los sistemas operativos resuelven automáticamente estos nombres a la dirección de loopback. Por esta razón, en estos casos tendríamos que añadir entradas manuales en el archivo hosts de nuestro sistema operativo para que los nombres personalizados funcionen correctamente.
Afortunadamente, los navegadores web modernos implementan esta resolución de forma automática, por lo que funcionará directamente 🙂
Publicado en Variable not found.
Hace unos días se lanzó .NET 10 y, con él, C# 14, una nueva versión del lenguaje que viene con varias novedades interesantes que mejoran la productividad y la experiencia de desarrollo.
Las más destacables son:
- Miembros extensores
- Asignaciones condicionales
- Propiedades semi-automáticas
- Simplificación del uso de
nameofcon tipos genéricos - Eventos y constructores parciales
- Conversiones implícitas para
Span<T>yReadOnlySpan<T> - Simplificación de parámetros de lambdas
Les damos un vistazo rápido a continuación.
Si, por curiosidad o necesidad, alguna vez has abierto un archivo de solución de Visual Studio (.sln) con un editor cualquiera, habrás comprobado que se trata de un archivo de texto plano.
Su estructura, aunque no es excesivamente compleja y está bien documentada, es propietaria de Visual Studio y no es evidente a simple vista, resulta demasiado verbosa, tiene mucha información irrelevante duplicada y, en general, no es nada amigable para los desarrolladores que se ven obligados a modificarla, sobre todo cuando se trata de resolver conflictos en sistemas de control de versiones. Y, por supuesto, es algo que se agrava conforme crece el número de proyectos en la solución.
Esto llevó a Microsoft a presentar hace unos meses el nuevo formato de archivo de solución .slnx, una alternativa basada en XML mucho más ligera, concisa y fácil de entender, que será el utilizado por defecto a partir de la llegada de .NET 10.
Lo vemos en profundidad a continuación.
Los métodos extensores aparecieron con C# 3.0, hace casi veinte años. Desde entonces, se han convertido en una herramienta muy útil para los desarrolladores de C#, permitiéndonos "añadir" métodos a tipos existentes sin modificar su código fuente ni crear un nuevo tipo derivado. Sin embargo, desde aquella lejana versión, este mecanismo no había sufrido cambios significativos.
Con la llegada de C# 14 y .NET 10, el concepto de extensión se ha hecho mucho más ambicioso. Como veremos a continuación, no solo es posible definir métodos extensores, sino también propiedades y otros tipos de miembros, tanto de instancia como estáticos. Además, la sintaxis se ha mejorado para hacerla más clara y fomentar la cohesión entre los distintos miembros de extensión que implementemos para un mismo tipo de datos.
Lo vemos a continuación.
Desde hace ya bastante tiempo, el equipo de .NET está introduciendo mejoras en el SDK para simplificar algunos escenarios y facilitar el acceso a la tecnología de desarrolladores que, o bien están empezando, o bien proceden de otras plataformas.
Una de estas mejoras fueron los top level statements, que permiten escribir los entry points de nuestras aplicaciones C# sin necesidad de definir una clase o un método Main. También las directivas using globales e implícitas ayudaban a reducir el boilerplate necesario para escribir una aplicación C#.
Pero esta vez han ido más lejos 🙂
Con .NET 10, se está trabajando en eliminar toda la ceremonia necesaria para crear y ejecutar una aplicación .NET simple. De la misma forma que se puede hacer en otras plataformas y lenguajes como Node.js o Python, ahora bastará con crear un archivo con extensión .cs y ejecutar el comando dotnet run para compilar y ejecutar el archivo sobre la marcha. Es decir, pasamos de aplicaciones basadas en proyecto a aplicaciones basadas en archivos.
Pero, además, esto abre interesantes posibilidades para la creación de prototipos, pruebas rápidas o incluso para la creación de scripts que aprovechen el poder de .NET y C# sin necesidad de crear un proyecto completo.
Lo vemos en profundidad a continuación.
El operador "?." (también conocido como safe navigation operator, null-conditional operator, optional chaining operator o incluso Elvis operator) se introdujo en C# 6.0, en el año 2015. Este conocido operador simplifica el acceso a miembros de un objeto que ocasionalmente podría ser nulo, evitando la necesidad de realizar comprobaciones explícitas de nulidad.
var friend = GetFriendOrNull(1);
var name = friend?.Name; // Si friend es nulo, name será nulo, pero no explotará
Sin embargo, si queremos acceder a una propiedad para asignarle un valor, el operador "?." no es suficiente. Por ejemplo, si queremos asignar un valor a la propiedad Age de un objeto friend sospechoso de ser nulo, debemos comprobarlo manualmente antes de realizar la asignación:
var friend = GetFriendOrNull(1);
if (friend != null)
{
friend.Age = 23;
}
Esto cambiará con C# 14, cuyo lanzamiento está previsto para noviembre de 2025, acompañando a .NET 10, que extenderá el uso de este operador también a la asignación de valores a miembros, completando así su funcionalidad.
Se trata de una característica muy esperada por la comunidad desde hace años, ya que simplificará y hará el código más legible. En este artículo, exploraremos cómo funcionará esta nueva característica y qué implicaciones tendrá en nuestro código.
Cuando hablamos de comparar números enteros, está claro que 1 es menor que 2, y éste menor que 10:
1 < 2 < 10
Sin embargo, si estos valores están en una cadena de texto, la comparación se hace carácter a carácter, y en ese caso "10" es menor que "2":
"1" < "10" < "2"
Este criterio de ordenación puede ser molesto en muchos escenarios, sobre todo cuando queremos ordenar o comparar valores numéricos se encuentran en el interior de cadenas de texto donde hay otros contenidos. En todos estos casos, la comparación lexicográfica tradicional no nos dará los resultados esperados:
- Una lista de nombres de archivo como "file1.txt", "file2.txt"... así hasta "file10.txt". Al ordenarlos, "file10.txt" aparecerá antes que "file2.txt".
- Versiones de un software, como "1.2" y "1.10". Si las comparamos, la versión "1.10" será anterior a la "1.2", cuando en realidad es al revés.
- De la misma forma, una lista de direcciones IP, como "10.10.2.10", "10.10.10.10", si queremos mostrarlas de forma ordenada, obtendremos "10.10.10.10" y "10.10.2.10", que no es su orden numérico real.
- O simplemente, si comparamos las horas "01:10" con "1:10", la primera será anterior a la segunda, cuando en realidad se trata de la misma hora.
Hasta la versión 10 de .NET no existía una forma sencilla de resolver estos casos, por lo que teníamos que implementar soluciones propias, bastante farragosas en algunos casos, o bien utilizar bibliotecas de terceros. Sin embargo, en .NET 10 se ha añadido una forma nativa para conseguirlo, que vemos a continuación.
A principios de siglo, con un C# aún bastante primitivo, las propiedades que almacenaban datos en una clase teníamos que definirlas siempre usando un backing field, es decir, un campo privado que almacenaba el valor, y accediendo a él mediante métodos getter y setter. Es decir, algo así:
public class Customer
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
...
}
Esto cambió en 2007 con la llegada de las propiedades automáticas, una maravilla para nuestros cansados dedos, que desde entonces usamos en la mayoría de ocasiones. Gracias a ellas, podemos definir una propiedad sin tener que implementar sus métodos accesores ni su campo privado correspondiente, porque el compilador se encargaba de crearlos por nosotros. De esta forma, código anterior quedaba así:
public class Customer
{
public string Name { get; set; }
...
}
En realidad, se trataba sólo de un azucarillo sintáctico. Por detrás, el compilador está generando el campo privado y transformando nuestro código para utilizarlo; de hecho, el código generado de la clase anterior es algo así (algo simplificado para que se pueda leer mejor):
public class Customer
{
private string <Name>k__BackingField;
public string Name
{
get { return <Name>k__BackingField; }
set { <Name>k__BackingField = value; }
}
}
Aunque obviamente usar propiedades automáticas es mucho más cómodo que la opción anterior, tiene un inconveniente: no tenemos forma de acceder al campo generado por el compilador, algo que puede resultar interesante si nos interesa realizar algún tipo de validación o lógica adicional en el getter o setter más allá de simplemente devolver o asignar el valor.
Por ejemplo, imaginemos que queremos que el nombre de nuestro cliente siempre esté en mayúsculas. Con una propiedad automática, simplemente no podemos hacerlo, y tendremos que recurrir a las verbosas propiedades tradicionales con backing fields.
Y aquí es donde entran en escena las propiedades semiautomáticas...
Me encanta LINQ. Bueno, ciertamente la sintaxis de consultas integrada en el lenguaje no la uso desde hace muchísimos años, pero los métodos de extensión que proporciona son una maravilla para recorrer y operar sobre colecciones de datos en memoria o almacenados en sistemas externos.
LINQ ha sido uno de los objetivos de .NET 9. Además de mejorarlo internamente y optimizar su rendimiento (en algunos casos, considerablemente), se han incluido tres nuevos métodos, CountBy(), AggregateBy() e Index(), que amplían las posibilidades que teníamos hasta el momento, simplificando la escritura de código y aumentando su legibilidad en algunos escenarios.
Vamos a verlos en detalle.
La clase Task de .NET dispone de algunos métodos estáticos para trabajar con colecciones de tareas de forma sencilla. Uno de ellos es Task.WhenAll(), que nos permite esperar a que todas las tareas que le pasemos como argumento se completen, o Task.WhenAny() para esperar a que finalice sólo alguna de ellas.
Sin embargo, hasta ahora no existía una forma sencilla y directa de procesar las tareas conforme se fueran completando. O bien esperábamos a que todas finalizaran con Task.WhenAll(), o bien teníamos que crear un bucle que fuera esperando la finalización de la siguiente tarea con Task.WhenAny(), saliendo cuando todas hubieran acabado.
Afortunadamente, .NET 9 ha añadido el nuevo método llamado Task.WhenEach() que lo pone más fácil, permitiéndonos detectar sobre la marcha la finalización de las tareas conforme va ocurriendo. Esto nos facilita el proceso de sus resultados secuencialmente, en el orden en el que se completan y sin esperar a que acaben todas.
Vamos a verlo, pero para entenderlo mejor, primero vamos a recordar las opciones que teníamos antes de la llegada de este método.
Seguimos analizando las novedades de .NET 9, y en esta ocasión le toca el turno a la actualización en miembros de clases parciales de C# 13.
Las clases o tipos parciales de C# están con nosotros casi desde los inicios de .NET, desde los tiempos de C# 2.0. Como sabemos, éstas permiten dividir la implementación de un tipo en varios archivos. Su existencia se debía a varios motivos:
- Permitir a los desarrolladores trabajar en clases de forma concurrente sin bloquearse (recordemos que por aquellos tiempos aún pululaban por ahí infames sistemas de control de código fuente como SourceSafe).
- Trocear clases extensas para facilitar su comprensión y mantenimiento.
- Y, el que es más importante, posibilitar el uso sencillo de herramientas de generación de código que, de alguna forma, tuvieran que completar la implementación de código del usuario.
Acompañando a las clases parciales, se introdujo también el soporte para métodos parciales. Aunque su uso era limitado y podía depararnos algunas sorpresas, eran útiles para comunicar de forma segura distintos fragmentos de la clase. Básicamente, consistía en que una porción de una clase podía definir un método e invocarlo desde su código, delegando a otras porciones su implementación. Si no se implementaba en ninguna otra parte, simplemente no se generaba el código de la invocación.
Veamos un ejemplo del uso de estas características:
// Archivo: Ejemplo.Main.cs
public partial class Ejemplo
{
// El método parcial se declara sin implementación...
partial void Log(string msg);
public void RealizarAlgo()
{
hacerAlgoComplejo();
Log("¡Ojo!"); // Usamos el método
// parcial declarado antes
}
}
// Archivo: Ejemplo.Log.cs
public partial class Ejemplo
{
partial void Log(string msg)
{
Console.WriteLine(msg);
}
}
Pues bien, casi dos décadas más tarde, los tipos parciales van a recibir algunas actualizaciones interesantes en C# 13, posibilitando que, además de métodos, puedan definirse propiedades parciales.
Una de las novedades más destacables de .NET 9 es, sin duda, el nuevo sistema de caché híbrida (Hybrid cache), una puesta al día del sistema de caché distribuida que nos acompaña desde las primeras versiones de .NET Core, y al que iban haciendo falta ya algunas mejoras.
Este nuevo sistema está construido encima de la infraestructura de caching existente (Microsoft.Extensions.Caching), añadiendo mejoras que hacen que su uso sea más sencillo y contemplando de serie funcionalidades que antes nos veíamos obligados a implementar manualmente.
Vamos a echarle un vistazo a sus principales características :)
Disclaimer: lo que vamos a ver a continuación está basado en .NET 9 RC2, por lo que todavía es posible que haya cambios en la versión final.
Cada vez que llega una nueva versión del framework, todos estamos ansiosos por conocer las novedades, especialmente las más espectaculares. Sin embargo, a veces hay pequeñas mejoras que, sin ser revolucionarias, nos pueden resultar útiles en algunos escenarios.
El modificador params permite especificar que un método o función acepta un número variable de argumentos, como en el siguiente ejemplo, donde vemos una función Sum() que puede ser invocada con cualquier número de valores:
// Declaración de la función:
int Sum(params int[] values)
{
return values.Sum();
}
// Ejemplos de uso:
Console.WriteLine(Sum(1)); // Enviamos un único valor -> "1"
Console.WriteLine(Sum(1, 2, 3, 4, 5)); // Enviamos cinco valores -> "15"
Console.WriteLine(Sum()); // No enviamos elementos, se recibirá un array vacío -> "0"
Console.WriteLine(Sum([1, 2, 3])); // Aquí pasamos un array directamente -> "6"
Hasta ahora, el parámetro param sólo podía ser un array, lo que limitaba un poco su uso. Por ejemplo, no podríamos llamar a la función Sum() pasándole directamente un List<int> o un IEnumerable<int> sin convertirlos antes a array porque fallarían en tiempo de compilación, al no tratarse de un int[], que es el tipo del parámetro esperado.
¿Qué ha cambiado?
Probablemente, todos hemos usado en algún momento el método Guid.NewGuid() de .NET para generar identificadores únicos globales. Y muchos hemos sufrido con el problema que supone su (pseudo) aleatoriedad, principalmente cuando necesitamos ordenarlos... sobre todo si su destino final es acabar siendo el campo clave de una base de datos relacional.
Pero pocos éramos conscientes de que el método Guid.NewGuid() de .NET retorna un GUID versión 4, y que existen otras versiones de UUID (¡hasta 8!) que pueden resultar más beneficiosas en determinados escenarios.
Como sabemos, una forma habitual de registrar servicios en el contenedor de dependencias de .NET consiste en indicar al framework la implementación de la interfaz o clase abstracta que debe ser utilizada cuando se solicite una instancia de la misma. Por ejemplo, en el siguiente código, que podría pertenecer a la inicialización de una aplicación ASP.NET Core, se registra la implementación FriendDbRepository para la interfaz IFriendRepository con un ámbito scoped o por petición:
builder.Services.AddScoped<IFriendRepository, FriendDbRepository>();
Hecho esto, podríamos solicitar una instancia de IFriendRepository en cualquier parte de nuestra aplicación que admita inyección de dependencias, como los controladores, manejadores Minimal API, otras dependencias, etc. El inyector encargará de proporcionarnos una instancia de FriendDbRepository allá donde la necesitemos:
public class FriendsController: Controller
{
public FriendsController(IFriendRepository friendRepository)
{
// ... Aquí la tenemos!
}
}
O bien, podríamos obtenerla directamente desde la colección de servicios:
public void DoSomething(IServiceProvider services)
{
var repo = services.GetRequiredService<IFriendRepository>();
...
}
Como vemos en ambos casos, la única forma disponible para identificar el servicio que deseamos obtener es utilizar su tipo, o muy habitualmente, la abstracción (interfaz o clase abstracta) a la que está asociado.
Pero eso cambió con la llegada de .NET 8...
Publicado por José M. Aguilar a las 8:05 a. m.
Etiquetas: .net, .net8, aspnetcore, novedades, trucos
Acercándose el lanzamiento de .NET 9 el próximo mes de noviembre, podemos ver ya algunas de las novedades que traerá la nueva versión, con las que podemos jugar desde hace algunos meses instalando las previews que van apareciendo cada mes.
En esta ocasión vamos a ver algunas novedades más interesantes que se han añadido a la biblioteca de serialización JSON oficial, conocida por todos como System.Text.Json:
- Opciones de personalización de la indentación
- Singleton de opciones de serialización y deserialización para la web
- Soporte de anotaciones de anulabilidad
- Comprobación de parámetros obligatorios del constructor
- Descripción de tipos .NET usando JSON Schema
¡Vamos a ello!
Nota: Este artículo está basado en .NET 9 RC1, por lo que algunas de las características descritas podrían cambiar en la versión final.
1. Opciones de personalización de la indentación
Hasta .NET 8, las opciones de personalización relativas al indentado de JSON generado era bastante limitada. De hecho, sólo podíamos decidir si lo queríamos indentar o no usando la propiedad WriteIndented de JsonOptions:
var obj = new
{
Name = "John",
Age = 30,
Address = new { Street = "123 Main St", City = "New York" }
};
var jsonOptions = new JsonSerializerOptions()
{
WriteIndented = true, // Queremos indentar el JSON
};
Console.WriteLine(JsonSerializer.Serialize(obj, jsonOptions));
El resultado será siempre un JSON indentado a dos espacios como el siguiente:
{
"Name": "John",
"Age": 30,
"Address": {
"Street": "123 Main St",
"City": "New York"
}
}
En .NET 9 disponemos de dos nuevas propiedades para personalizar la indentación del JSON generado. La propiedad IndentCharacter permite especificar el carácter a utilizar (espacio o tabulador, exclusivamente), y mediante IndentSize podemos indicar el número de caracteres a utilizar en cada nivel de indentación.
var jsonOptions = new JsonSerializerOptions()
{
WriteIndented = true,
IndentCharacter = '\t',
IndentSize = 2
};
Console.WriteLine(JsonSerializer.Serialize(obj, jsonOptions));
En este caso, el resultado será así (2 tabuladores por nivel de indentación):
{
"Name": "John",
"Age": 30,
"Address": {
"Street": "123 Main St",
"City": "New York"
}
}
2. Singleton de opciones de serialización y deserialización para la web
ASP.NET Core utiliza una configuración por defecto para serializar y deserializar objetos JSON, que ahora está disponible de forma pública en la propiedad JsonSerializerOptions.Web.
El objeto retornado es un singleton JsonSerializerOptions de sólo lectura, que estará disponible en cualquier punto de la aplicación.
Por defecto, este objeto está configurado de la siguiente manera:
PropertyNameCaseInsensitiveestá establecido atrue, por lo que la deserialización de propiedades es insensible a mayúsculas y minúsculas.JsonPropertyNamingPolicytiene el valorJsonNamingPolicy.CamelCase, así que las propiedades se serializan en formato camelCase, el habitual en JavaScript y JSON.NumberHandlingesJsonNumberHandling.AllowReadingFromString, así que será posible deserializar números que vengan especificados como texto.
Un ejemplo de utilización de este objeto sería el siguiente:
var obj = new
{
Name = "John",
Age = 30,
Address = new { Street = "123 Main St", City = "New York" }
};
Console.WriteLine(JsonSerializer.Serialize(obj, JsonSerializerOptions.Web));
// Resultado:
// {"name":"John","age":30,"address":{"street":"123 Main St","city":"New York"}}
Dado que el objeto es de sólo lectura, no podemos modificar sus propiedades. Si necesitásemos algún tipo de cambio en la configuración, podríamos crear una configuración nueva partiendo de él, e introducir las modificaciones deseadas, por ejemplo:
var jsonOptions = new JsonSerializerOptions(JsonSerializerOptions.Web)
{
WriteIndented = true,
IndentCharacter = '\t',
IndentSize = 2
};
3. Soporte de anotaciones de anulabilidad
En .NET 8 y versiones anteriores, System.Text.Json no respetaba las anotaciones de anulabilidad de C# en las propiedades de los tipos de referencia. Por ejemplo, si tenemos una propiedad de tipo referencia que no puede ser nula como en el siguiente caso, podemos ver que el siguiente código deserializará sobre ella un valor nulo sin problema:
var myFriend = JsonSerializer.Deserialize<Friend>("""{ "Name": null }""");
Console.WriteLine(myFriend.Name ?? "Unknown"); // Muestra "Unknown"
// Friend tiene un campo "Name" que no puede ser nulo
public record Friend (string Name);
A partir de .NET 9, podemos indicar opcionalmente a System.Text.Json que respete la anulabilidad de propiedades, es decir, si una propiedad no es anulable, al deserializarla no podremos establecerla al valor nulo.
Esto podemos conseguirlo simplemente estableciendo a true la propiedad RespectNullableAnnotations de las opciones de deserialización:
var opt = new JsonSerializerOptions() { RespectNullableAnnotations = true };
var myFriend = JsonSerializer.Deserialize<Friend>("""{ "Name": null }""", opt);
Console.WriteLine(myFriend.Name ?? "Unknown"); // Lanza una excepción
Según la documentación, otra posibilidad es hacerlo de forma global para todo el proyecto, añadiendo la siguiente configuración en el archivo .csproj:
<ItemGroup>
<RuntimeHostConfigurationOption
Include="System.Text.Json.JsonSerializerOptions.RespectNullableAnnotations"
Value="true" />
</ItemGroup>
Supongo que, debido a que aún estamos jugando con versiones preview del compilador, esta última opción no conseguí que me funcionara.
Otra cosa que he visto curiosa es que el siguiente código, a mi entender, debería lanzar una excepción, pero no lo hace:
var opt = new JsonSerializerOptions() { RespectNullableAnnotations = true };
var myFriend = JsonSerializer.Deserialize<Friend>("{ }", opt);
Console.WriteLine(myFriend.Name ?? "Unknown"); // Muestra "Unknown"
En principio, si la propiedad Name hemos dicho que no era anulable, ¿no debería lanzar una excepción al deserializar un objeto que no especifica su valor? Pues no, y parece ser que se trata de un comportamiento esperado; hay un interesante hilo al respecto en GitHub discutiendo el por qué de esta decisión, aunque en el siguiente apartado veremos que existe en algunos casos existe una forma alternativa de evitar esta situación.
Por último, la propiedad RespectNullableAnnotations también afecta a la serialización, de forma que si una propiedad no anulable tiene un valor nulo al ser serializada, se lanzará una excepción:
var opt = new JsonSerializerOptions() { RespectNullableAnnotations = true };
var myFriend = new Friend(null);
Console.WriteLine(JsonSerializer.Serialize(myFriend, opt)); // Lanza una excepción
4. Comprobación de parámetros obligatorios del constructor
En .NET 8 y anteriores, los parámetros del constructor del tipo a deserializar eran tratados como siempre como opcionales. Por esta razón, el código siguiente no provoca errores:
var myFriend = JsonSerializer.Deserialize<Friend>("""{ }""");
Console.WriteLine(myFriend.Name ?? "Unknown"); // Muestra "Unknown"
public record Friend(string Name);
En .NET 9, es posible indicar al deserializador que queremos respetar los parámetros requeridos del constructor, de forma que si no pueden ser satisfechos se lanzará una excepción. Esto se consigue con el nuevo flag RespectRequiredConstructorParameters en las opciones de deserialización:
var opt = new JsonSerializerOptions() { RespectRequiredConstructorParameters = true };
var myFriend = JsonSerializer.Deserialize<Friend>("""{ }""", opt);
Console.WriteLine(myFriend.Name ?? "Unknown"); // Lanza una excepción
La excepción, de tipo JsonException es bastante clara en su texto de descripción: 'JSON deserialization for type 'Friend' was missing required properties including: 'Name'.'.
Como en el caso anterior, este comportamiento puede ser también configurado de forma global para todo el proyecto, añadiendo la siguiente configuración en el archivo .csproj:
<ItemGroup>
<RuntimeHostConfigurationOption
Include="System.Text.Json.JsonSerializerOptions.RespectRequiredConstructorParameters"
Value="true" />
</ItemGroup>
Y como en el caso anterior, tampoco he conseguido que me funcione de esta forma 😆 Esperemos que las siguientes preview o la versión final lo solucionen.
5. Descripción de tipos .NET usando JSON Schema
La nueva clase estática JsonSchemaExporter expone el método GetJsonSchemaAsNode(), que permite obtener el esquema JSON de un tipo .NET en forma de objeto JsonNode.
Su uso es muy sencillo, simplemente pasamos las opciones a utilizar para serializar el esquema, y el tipo del que queremos obtenerlo. Observad el siguiente ejemplo:
var schema = JsonSchemaExporter.GetJsonSchemaAsNode(
JsonSerializerOptions.Default,
typeof(Friend)
);
Console.WriteLine(schema);
public record Friend(string Name, string? nickName, int Age = 20);
El resultado que obtendremos es una descripción del tipo, siguiendo el estándar JSON Schema, que en este caso sería algo así:
{
"type": [
"object",
"null"
],
"properties": {
"Name": {
"type": "string"
},
"nickName": {
"type": [
"string",
"null"
]
},
"Age": {
"type": "integer",
"default": 20
}
},
"required": [
"Name",
"nickName"
]
}
Conclusión
En este artículo hemos visto algunas de las novedades que traerá la nueva versión de .NET 9 en la biblioteca de serialización JSON oficial, System.Text.Json. Aunque no son cambios revolucionarios, sí que son mejoras que facilitarán la vida a los desarrolladores que trabajamos con JSON en nuestras aplicaciones.
¡Espero que os haya resultado interesante!
Me da la impresión de que el operador with de C# no es demasiado conocido, y creo que vale la pena echarle un vistazo porque puede resultarnos útil en nuestro día a día.
Introducido con C# 9, allá por 2020, las expresiones with proporcionan una forma sencilla de crear nuevas instancias de objetos "copiando" las propiedades de otro objeto, pero con la posibilidad de modificar algunas de ellas sobre la marcha. Esto, aunque puede usarse con varios tipos de objeto, es especialmente interesante cuando estamos trabajando con componentes inmutables, como son los records, porque la única posibilidad que tenemos de alterarlos es creando copias con las modificaciones que necesitemos.
Hoy va un post rápido sobre una característica de C# 12 que, aunque pequeña, nos puede resultar interesante en determinadas situaciones y puede pasar fácilmente desapercibida entre todas las demás novedades de esta versión del lenguaje.
Hasta C# 11 (.NET 7), no teníamos forma de definir una expresión lambda con parámetros opcionales y valores por defecto, como podemos hacer en los métodos o funciones normales, lo que complicaba la implementación de algunos escenarios.
Los que llevamos muchos años programando con ASP.NET/ASP.NET Core y todos los frameworks que han ido surgiendo en su ecosistema, estamos familiarizados con el concepto de "contexto HTTP".
Materializado en forma de objeto de tipo HttpContext, el contexto HTTP es una de las piezas fundamentales de la infraestructura de ASP.NET Core, y nos permite acceder a información sobre la petición HTTP que se está procesando, como los encabezados, el cuerpo de la petición, las cookies, etc., así como base para la generación de la propia respuesta a través de su propiedad Response. En muchos escenarios, se trata de un recurso imprescindible para procesar la petición de la forma adecuada, por lo que estamos acostumbrados a usarlo cuando es conveniente.
Sin embargo, cuando saltamos a Blazor, pronto nos llama la atención que HttpContext no está disponible. Y si lo pensamos, esto tiene bastante sentido en los dos modos de renderización clásicos:
-
En Blazor WebAssembly, dado que el código .NET se ejecuta directamente en el cliente, no existen peticiones que procesar y, por tanto, no existe
HttpContext. Se trata de una abstracción que sólo existe en el backend. -
En Blazor Server, aunque el código se está ejecutando en el servidor, tampoco tenemos disponible
HttpContextporque realmente no existen peticiones: el lado cliente y servidor se comunican mediante un canal websockets implementado con SignalR, que es por donde viajan ascendentemente las acciones realizadas por el usuario y descendentemente las actualizaciones del DOM de la página.
Con la llegada de Blazor 8, ha tomado relevancia el nuevo modo de renderizado, llamado Server-Side Rendering (SSR) o renderización en el lado servidor, que ya vimos por aquí hace algún tiempo.
Como ya sabemos, el funcionamiento de Blazor SSR es similar al de otros frameworks de backend puros, como MVC o Razor Pages: el servidor recibe la petición, la procesa y genera una respuesta HTML que se envía al cliente. En este escenario, durante el proceso del componente Blazor sí existe un contexto HTTP.

