El otro día me descubrí escribiendo un código parecido al siguiente:
return $"Order: {Items.Length} items, {Total.ToString("#,##0.#0")}";
Mal, lo que se dice mal, no estaba; funcionaba perfectamente y cumplía los requisitos, pero me di cuenta de que no estaba aprovechando todo el potencial de las cadenas interpoladas de C# que ya habíamos comentado por aquí mucho tiempo atrás.
Y como siempre que tengo algún despiste de este tipo, pienso que quizás pueda haber alguien más al que le ocurra o no esté al tanto de esta posibilidad el lenguaje, así que vamos a ver cómo podíamos haberlo implementado de forma algo más simple.
Pues hoy vamos con un truquillo rápido ;)
Como sabemos, cuando usamos Razor Pages para construir aplicaciones sobre ASP.NET Core, la convención por defecto obliga a que nuestras páginas se encuentren en la carpeta /Pages del proyecto.
¿Pero qué ocurre si somos algo tiquismiquis y no nos gusta esa ubicación o no podemos usarla por cualquier motivo? En este post vamos a ver cómo cambiar esta convención para que nuestras páginas Razor se encuentren en otra carpeta.
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.
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.
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:
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#.
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));
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.
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
});
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.
Me gusta estar atento a las novedades que van apareciendo el lenguajes y frameworks que nos ayudan a mejorar la forma de hacer las cosas. Lamentablemente, en este mundo tan cambiante no es fácil estar al día en todo, y hay muchas veces que sigo haciendo cosas como siempre aunque existan fórmulas más modernas y mejores para conseguirlo.
Un ejemplo lo he encontrado hace poco, cuando, trabajando con JavaScript, una vez más he tenido necesidad de parsear la URL de la página actual en busca de los valores de los parámetros suministrados en la query string al cargar una página. Lo habitual en estos casos era tirar de Google o StackOverflow y acabar implementado una función parecida a la siguiente:
function getParameterValue(name, url = window.location.href) {
name = name.replace(/[\[\]]/g, '\\$&');
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
Casualmente, en este caso recordé haber leído de pasada sobre la existencia de un "nuevo" objeto de JavaScript que actualmente nos permite hacerlo de forma más sencilla, así que aproveché para ponerme un poco al día al respecto ;)
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"
.
Hace algunas semanas vimos cómo crear inline route constraints, o restricciones de ruta en línea en ASP.NET Core, y creamos un ejemplo simple que permitía al sistema de routing detectar si el valor suministrado a través de un parámetro de ruta era una palabra palíndroma.
Para ello, creamos la restricción "palindrome" que, implementada en la clase PalindromeConstraint
podíamos usar de la siguiente forma:
// Uso en minimal API:
app.MapGet("/test/{str:palindrome}", (string str) => $"{str} is palindrome");
// Uso en MVC:
public class TestController : Controller
{
[HttpGet("/test/{str}")]
public string Text(string str) => $"{str} is palindrome";
}
Sin embargo, si atendemos a la lista de restricciones disponibles de serie en ASP.NET Core, vemos que hay algunas de ellas que son parametrizadas, como maxlength
o range
:
Plantilla de ruta | Significado |
---|---|
/order/{orderId:minlength(5) |
orderId debe tener como mínimo 5 caracteres |
/setAge/{age:int:range(0,120) |
age debe ser un entero entre 0 y 120 |
En este post vamos a ver precisamente eso, cómo crear una restricción personalizada con parámetros.
Normalmente, nuestras aplicaciones web ASP.NET Core son hosteadas por aplicaciones de consola, que son las encargadas de crearlas, configurarlas y lanzarlas. Esto suele hacerse mediante una relación de uno a uno: una única aplicación de consola se encarga de gestionar todo el ciclo de vida de una única aplicación web.
Pero, ¿es así necesariamente? En este post veremos que no.
Las inline route constraints, o restricciones de ruta en línea son un interesante mecanismo de ASP.NET Core para especificar condiciones sobre los parámetros definidos en el interior de los patrones de ruta.
Por ejemplo, una acción MVC o un endpoint mapeado usando el patrón /product/{id}
, será ejecutado cuando entren peticiones hacia las rutas /product/1
y product/xps-15
.
Sin embargo, si en el momento del mapeo utilizamos el patrón /product/{id:int}
estaremos indicando que el manejador sólo debe ser ejecutado cuando el valor del parámetro id
sea un entero.
Esto podemos verlo mejor en un ejemplo. Observad la definición de la siguiente acción MVC, que será ejecutada sólo si el valor para el parámetro id
es un entero, es decir, responderá a peticiones como /product/1
o /product/234
, pero será ignorada si la petición entrante se dirige a la ruta /product/xps-15
:
public class ProductController : Controller
{
...
[HttpGet("product/{id:int}")]
public async Task<ActionResult<Product>> ShowDetails(int id)
{
var product = ... // Obtener producto
return product != null ? product: NotFound();
}
}
O lo que sería su equivalente usando las minimal APIs introducidas en .NET 6:
app.MapGet("/product/{id:int}", async (int id) =>
{
var product = await new ProductCatalog().GetByIdAsync(1); // Obtener producto
return product != null ? Results.Ok(product) : Results.NotFound();
});
Como ya habréis adivinado, en
{id:int}
es donde estamos especificando que el parámetro de rutaid
debe ser entero.
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.
En algunas ocasiones me he topado con escenarios en los que necesitaba contar, o incluso enumerar, las claves de los elementos presentes en una caché en memoria, inyectada en mis servicios en forma de objeto IMemoryCache
.
Aunque a priori pueda parecer sencillo, esta interfaz no proporciona métodos o propiedades que permitan acceder a la colección que actúa como almacén de los mismos, por lo que nos veremos obligados a usar una estructura de datos adicional (normalmente algún tipo de diccionario o hashset paralelo) para almacenar estos elementos.
¿O quizás tenemos otras fórmulas?
Como sabemos, la respuesta a todas las peticiones HTTP comienzan por un código de estado que indica el resultado de la operación. Ahí encontramos desde los códigos más célebres, como HTTP 200 (Ok) o HTTP 404 (Not found) hasta otras joyas menos conocidas como HTTP 429 (Too many requests) o HTTP 418 (I'm a teapot).
Sin embargo, pocas veces nos fijamos en el texto que acompaña al código de respuesta, denominado reason phrase (en los ejemplos anteriores va entre paréntesis, como "Ok" o "Not found"). Según se define en la RFC 7230 sección 3.1.2, la reason phrase...
"... existe con el único propósito de proporcionar una descripción textual asociada con el código de estado numérico, principalmente como una deferencia a los protocolos iniciales de Internet, que eran utilizados frecuentemente por clientes de texto interactivos. Un cliente DEBERÍA ignorar su contenido"
Por tanto, dado que se trata de un texto arbitrario y puramente informativo, deberíamos poder modificarlo a nuestro antojo, más allá de los textos estándar proporcionados por el framework.
Una de las (muchas) cosas buenas que trajo ASP.NET Core (y .NET Core en general) sin duda ha sido la popularización de la inyección de dependencias y la filosofía de implementación de componentes desacoplados que expone en múltiples puntos.
Esto ha provocado que en nuestras aplicaciones sea ya habitual encontrar secciones de código dedicadas al registro de decenas o centenares de servicios usando los distintos ámbitos disponibles (scoped, singleton o transient). El problema es que esta abundancia de servicios y la asiduidad con la que registramos nuevos componentes o modificamos sus dependencias hace que se nos puedan pasar por alto detalles que pueden hacer que nuestra aplicación falle. Por ejemplo, es fácil que olvidemos registrar algún servicio, o que, por un despiste, inyectemos servicios en componentes registrados con ámbitos incompatibles.
Como sabemos, ASP.NET Core viene configurado "de serie" para que el middleware que sirve los archivos estáticos (StaticFilesMiddleware
) los obtenga desde la carpeta wwwroot
. Y ya vimos hace bastante tiempo que si preferíamos utilizar otro nombre para guardar estos archivos, podíamos hacerlo con cierta facilidad.
Pero como a partir de ASP.NET Core 6 el nuevo modelo de configuración cambió varias piezas de sitio, es bueno volver a echar un vistazo y ver cómo podríamos hacerlo en las últimas versiones del framework.
De casualidad me he topado con un interesante cambio que .NET 5 introdujo en los componentes de serialización y deserialización System.Text.Json
y que, al menos para mí, pasó totalmente desapercibido en su momento. Por eso, he pensado que quizás sea buena idea dejarlo por aquí, por si hay algún despistado más al que pueda resultar útil.
Como seguro sabéis, al usar los componentes de System.Text.Json
para serializar o deserializar una clase, utilizamos el atributo [JsonIgnore]
para marcar las propiedades que queremos que sean ignoradas al convertir desde y hacia JSON.
Por ejemplo, dada la siguiente clase:
class User
{
public int Id { get; set; }
public string Email { get; set; }
[JsonIgnore]
public string Token { get; set; }
}
En ella estamos indicando expresamente que la propiedad Token
debe ser ignorada, por lo que ésta no aparecerá si serializamos un objeto a JSON:
var user = new User { Id = 42, Email = "john@server.com", Token = "ABCDEF"};
Console.WriteLine(JsonSerializer.Serialize(user));
// Result:
{"Id":42,"Email":"john@server.com"}
Y lo mismo ocurre en sentido contrario:
var jsonStr = "{ \"Id\": 42, \"Email\": \"john@server.com\", \"Token\": \"ABCDEF\"}";
var user = JsonSerializer.Deserialize<User>(jsonStr);
// ¡user.Token es nulo aquí!