Cuando estamos usando alguna clase de .NET en nuestro código, a veces tenemos interés o necesidad de ver qué hace por dentro, para lo que nos gustaría tener acceso rápido a su código fuente. Aunque los IDEs modernos disponen en muchos casos de herramientas o extensiones que lo permiten, está bien saber que hay otras fórmulas sencillas para conseguirlo muy rápida y cómodamente.
Hace poco me he topado con una de ellas, que me ha parecido muy útil y quería compartirla con vosotros, por si hay alguien más que aún no la conoce: la página https://source.dot.net.
Publicado por José M. Aguilar a las 8:05 a. m.
Etiquetas: .net, herramientas, trucos
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.
Hace unas semanas estuvimos echando un vistazo a HybridCache
, la alternativa de .NET 9 al sistema de caché distribuida que apareció con .NET Core años atrás. Como vimos entonces, HybridCache
aporta una serie de ventajas respecto a su predecesor, como la protección contra estampidas, la caché multinivel, invalidación por etiquetas, y otras mejoras.
Sin embargo, también introduce algunos inconvenientes que debemos tener en cuenta si queremos sacarle el máximo partido. En esta ocasión, nos centraremos en la penalización debido a excesiva deserialización de objetos de la caché local, un detalle que puede pasar desapercibido pero que puede tener un impacto significativo en el rendimiento de nuestra aplicación.
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.
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.
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
Cuando trabajamos con diccionarios en .NET, es muy común comprobar si existe un elemento con una clave determinada antes de obtenerlo para evitar una excepción KeyNotFoundException
:
var numbers = new Dictionary<int, string>()
{
[1] = "One",
[2] = "Two",
[3] = "Three"
};
// Aseguramos que el elemento existe antes de obtenerlo
if (numbers.ContainsKey(3))
{
Console.WriteLine(numbers[3]);
}
Sin embargo, esta comprobación tiene coste. Es decir, aunque el acceso a los diccionarios sea muy rápido -O(1)-, no quiere decir que no consuma tiempo, por lo que es interesante conocer alternativas más eficientes para estos escenarios.
Hace poco vimos cómo serializar y deserializar datos en JSON de forma personalizada usando custom converters e implementamos un ejemplo simple capaz de introducir en campos de tipo int
de .NET casi cualquier valor que pudiera venir en un JSON.
Pero como comentamos en su momento, la serialización y deserialización de objetos más complejos no es una tarea tan sencilla y requiere algo más de esfuerzo. En este post vamos a ver la solución para un escenario que creo que puede ser relativamente habitual: deserializar un objeto JSON a un diccionario Dictionary<string, object>
.
En otras palabras, queremos que funcione algo como el siguiente código:
using static System.Console;
...
var json = """
{
"name": "Juan",
"age": 30
}
""";
var dict = ... // Código de deserialización
WriteLine($"{dict["name"]} tiene {dict["age"]} años"); // --> Juan tiene 30 años
Hace poco, andaba enfrascado en el proceso de modernización de una aplicación antigua que, entre otras cosas, guardaba datos en formato JSON en un repositorio de archivos. Dado que se trataba de una aplicación creada con .NET "clásico", la serialización y deserialización de estos datos se realizaba utilizando la popular biblioteca Newtonsoft.Json.
Al pasar a versiones modernas de .NET, esta biblioteca ya no es la mejor opción, pues ya el propio framework nos facilita las herramientas necesarias para realizar estas tareas de forma más eficiente mediante los componentes del espacio de nombres System.Text.Json
. Y aquí es donde empiezan a explotar las cosas 😉.
Si habéis trabajado con este tipo de asuntos, probablemente habréis notado que, por defecto, los componentes de deserialización creados por James Newton-King son bastante permisivos y dejan pasar cosas que System.Text.Json
no permite. Por ejemplo, si tenemos una clase .NET con una propiedad de tipo string
y queremos deserializar un valor JSON numérico sobre ella, Newtonsoft.Json
lo hará sin problemas, pero System.Text.Json
nos lanzará una excepción. Esa laxitud de Newtonsoft.Json
es algo que en ocasiones nos puede venir bien, pero en otras puede puede hacer pasar por alto errores en nuestros datos que luego, al ser procesados por componentes de deserialización distintos, podrían ocasionar problemas.
Por ejemplo, observad el siguiente código:
var json = """
{
"Count": "1234"
}
""";
// Deserializamos usando Newtonsoft.Json:
var nsj = Newtonsoft.Json.JsonConvert.DeserializeObject<Data>(json);
Console.WriteLine("Newtonsoft: " + nsj.Count);
// Intentamos deserializar usando System.Text.Json
// y se lanzará una excepción:
var stj = System.Text.Json.JsonSerializer.Deserialize<Data>(json);
Console.WriteLine("System.Text.Json: " + stj.Count);
Console.Read();
// La clase de datos utilizada
record Data(int Count);
Para casos como este, nos vendrá bien conocer qué son los custom converters y cómo podemos utilizarlos.
Sabemos que está feo y no sea especialmente recomendable en muchos casos, pero hay veces en las que es totalmente necesario acceder a miembros privados de una clase. Por ejemplo, sería un requisito imprescindible si fuéramos a implementar un serializador o deserializador personalizado, o necesitásemos clonar objetos, o simplemente realizar pruebas unitarias a una clase que no ha sido diseñada para ello.
Para conseguirlo, siempre hemos utilizado los mecanismos de introspección de .NET, conocidos por todos como reflexión o por su término en inglés reflection. Por ejemplo, imaginemos una clase como la siguiente, virtualmente inexpugnable:
public class BlackBox
{
private string _message = "This is a private message";
private void ShowMessage(string msg) => Console.WriteLine(msg);
public override string ToString() => _message;
}
Aunque a priori no podemos hacer nada con ella desde fuera, usando reflexión podríamos acceder sin problema a sus miembros privados, ya sean propiedades, campos, métodos o cualquier tipo de elemento, como podemos ver en el siguiente ejemplo:
var instance = new BlackBox();
// Obtenemos el valor del campo privado _message:
var field = typeof(BlackBox)
.GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance);
var value = field!.GetValue(instance);
// Ahora llamamos al método privado ShowMessage():
var method = typeof(BlackBox)
.GetMethod("ShowMessage", BindingFlags.NonPublic | BindingFlags.Instance);
method.Invoke(instance, [value]);
// Al ejecutar, se nuestra en consola: "This is a private message"
Sin embargo, de todos es sabido que la reflexión es un mecanismo muy lento, a veces farragoso en su implementación y, sobre todo, incompatible con tree-shaking, que es como se suele llamar la técnica que usan los compiladores de eliminar del código final todas las clases, métodos y propiedades que no se utilizan en el código final. Esta técnica va tomando cada vez más relevancia conforme los compiladores son capaces de crear directamente ejecutables nativos puros, porque permiten reducir de forma considerable el peso de los artefactos creados.
Por esta razón, en .NET 8 se ha incluido una nueva fórmula para acceder a miembros privados, mucho más eficiente y amistosa con AOT, porque se lleva a tiempo de compilación algo que antes obligatoriamente se resolvía en tiempo de ejecución.
Seguro que más de una vez habéis tenido que construir una abstracción sobre DateTime
para poder controlar apropiadamente la obtención de la fecha/hora actual y otras operaciones relacionadas con el tiempo.
Suelen ser bastante útiles cuando creamos pruebas unitarias de métodos que dependan del momento actual. Por ejemplo, ¿cómo testearíamos de forma automática que las dos líneas de ejecución del siguiente método DoSomething()
funcionan correctamente? Sería imposible salvo que ejecutásemos las pruebas a una hora determinada, algo que se antoja complicado 😉
public class MyClass
{
public string DoSomething()
{
var now = DateTime.Now;
return now.Second == 0
? "A new minute is starting"
: "Current second " + now.Second;
}
}
Sin duda, una forma mejor y más test friendly sería contar con una abstracción sobre el proveedor de tiempos capaz de retornar la fecha y hora actual, por ejemplo:
public interface IDateTimeProvider
{
DateTime GetCurrentDateTime();
}
De esta forma, podríamos reescribir la clase MyClass
de forma que recibiera por inyección de dependencias nuestro proveedor IDateTimeProvider
. Así sería realmente sencillo crear un par de pruebas unitarias que, suministrando los valores correctos a través de esta dependencia, podrían recrear los escenarios a cubrir:
public class MyClass
{
private readonly IDateTimeServices _dateTimeServices;
public TimeHelpers(IDateTimeServices dateTimeServices)
{
_dateTimeServices = dateTimeServices;
}
public string DoSomething()
{
var now = _dateTimeServices.GetCurrentDateTime();
return now.Second == 0
? "A new minute is starting"
: "Current second " + now.Second;
}
}
Aunque hacerlo de esta manera en nuestras aplicaciones es lo ideal, hay partes que se quedarían fuera de esta posibilidad, como las bibliotecas de terceros que de alguna forma dependan de las funcionalidades proporcionadas por DateTime
.
Por esta razón, .NET 8 va a introducir una abstracción que nos permitirá gestionar estos escenarios de forma más homogénea y generalizada en aplicaciones y componentes .NET.
Os presento la clase abstracta TimeProvider
😁
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.
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.
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));
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.
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
.
Seguimos hablando de problemas que es habitual solucionarlos de una determinada manera, quizás por costumbre, quizás por pereza, o tal vez por desconocimiento de que haya otras formas de hacerlo. En este caso, hablaremos de una necesidad que probablemente habréis tenido alguna vez: transformar un GUID a una cadena de caracteres eliminando los habituales guiones.
Es decir, dado un GUID con el valor 1f5772a6-91ca-4035-8b6d-9676ec6d0eaa
, queremos obtener su representación como cadena de caracteres, pero eliminando los guiones, resultando "1f5772a691ca40358b6d9676ec6d0eaa"
.
En código que veo, incluso escrito por mí un tiempo atrás, es muy habitual encontrar comparaciones de cadenas de caracteres en las que, para asegurar que el casing no sea tenido en cuenta, se fuerza una conversión de alguno de los operandos, o incluso ambos, a mayúsculas o minúsculas.
En escenarios muy simples esto funcionará bien y no tendrá contraindicaciones especialmente graves para nuestro sistema. Veamos unos ejemplos:
// Ejemplo 1: conversión de un único operando
int Calculate(string op, int a, int b)
{
// Pasamos a minúsculas el operador para
// asegurar que encaja con la constante
if(op.ToLower()=="add")
{
return a+b;
}
else if(op.ToLower()=="sub")
{
return a-b;
}
...
}
// Ejemplo 2: conversión de ambos operandos
bool AreBrothers(User user1, User user2)
{
// Pasamos a mayúsculas ambos apellidos por
// si alguno se ha escrito usando otro casing
var areBrothers = user1.Surname.ToUpper() == user2.Surname.ToUpper();
return areBrothers;
}
Sin embargo, aunque pueda parecer despreciable, estas operaciones de transformación a mayúsculas o minúsculas tienen un coste importante, que se pone especialmente de manifiesto cuando estamos hablando de aplicaciones con mucha carga de usuarios, alojada en infraestructura muy ajustada o cuando se requiere un rendimiento extremo.
En un vídeo del canal de Nick Chapsas, al que por cierto os recomiendo suscribiros, he descubierto que .NET 7 introducirá un mecanismo para "decorar" parámetros, propiedades y miembros de tipo string
de forma que podamos aportar información sobre el tipo de contenido que esperan almacenar.
Para que lo entendáis mejor, observad el siguiente ejemplo, una función que recibe un mensaje y un formato de fecha, y que escribe por consola la fecha actual en el formato indicado seguido del mensaje.
void Log(string message, string dateFormat)
{
Console.WriteLine(DateTime.UtcNow.ToString(dateFormat) + " - " + message);
}
Log("Hello!", "dd/MM/yyyy hh:mm");
Desde el punto de vista del consumidor de la función Log()
, gracias a las ayudas del IDE podremos deducir que el segundo parámetro de tipo string
, llamado dateFormat
, debería ser un formato de fecha válido en .NET. Sin embargo, el entorno de desarrollo no podrá ofrecer ningún tipo de ayuda a la hora de codificar la llamada ni detectar si se producen errores, pues no dispone de información suficiente sobre el tipo de contenido esperado en la cadena de texto que se le suministra.