Como sabéis, con ASP.NET Core 8 se ha incluido la posibilidad de que una página Blazor sea capaz de procesar directamente una petición HTTP, renderizando sus componentes y retornando el resultado HTML completo al lado cliente de forma estática. Es ya lo vimos hace algún tiempo cuando hablamos de Server-Side Rendering (SSR), una de las piezas clave para conseguir que Blazor sea un framework fullstack.
En la práctica, esta opción posibilita la implementación de sitios estáticos completos, como los que construiríamos con ASP.NET Core MVC, Razor Pages o cualquier otro tipo de tecnología de servidor, pero con la gran ventaja de que en este caso estaremos beneficiándonos del fantástico modelo de componentes Blazor, que es una gozada en términos de productividad y facilidad de uso, y sin perder la posibilidad de activar la interactividad de los componentes (su capacidad para reaccionar ante eventos del usuario), una vez que el HTML ha sido descargado en el navegador.
En aquél momento vimos que SSR no requiere una programación específica: los mismos componentes interactivos que luego podemos ejecutar en el lado cliente o servidor pueden ser renderizados mediante SSR de forma estática en el backend. Sin embargo, en algunos momentos nos podría resultar interesante saber si un componente está funcionando en modo estático (SSR) o interactivo (los tradicionales Blazor WebAssembly o Blazor Server).
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
HttpContext
porque 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.
En Blazor es posible acceder a valores de parámetros de la query string exclusivamente desde componentes de tipo página, es decir, aquellos definidos con la directiva @page
.
Para ello, bastaba con declarar una propiedad pública y decorarla con los atributos [Parameter]
y [SupplyParameterFromQuery]
. Por ejemplo, si desde una página quisiésemos obtener el valor del parámetro term
de la query string, podríamos hacerlo de la siguiente forma:
@page "/search"
<p>Searching term: @Term</p>
@code {
[Parameter]
[SupplyParameterFromQuery]
public string Term { get; set; }
}
Sin embargo, como sabéis, esto no funcionaba si intentábamos acceder así a estos parámetros desde componentes que no fueran páginas, es decir, que no fueran instanciados por el sistema de routing.
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.
Hace unos días, veíamos por aquí los constructores primarios de C#, una característica recientemente introducida en el lenguaje que permite omitir el constructor si en éste lo único que hacemos es asignar los valores de sus parámetros a campos de la clase.
Veamos un ejemplo a modo de recordatorio rápido:
// Clase con constructor tradicional
public class Person
{
private readonly string _name;
public Person(string name)
{
_name = name;
}
public override string ToString() => _name;
}
// La misma clase usando el constructor primario:
public class Person(string name)
{
public override string ToString() => name;
}
Como comentamos en el post, los parámetros del constructor primarios eran internamente convertidos en campos privados de la clase, generados automáticamente e inaccesibles desde el código.
Como sabemos, gran parte de las validaciones en formularios Blazor las implementamos usando anotaciones de datos, o data annotations. Estas anotaciones nos permiten validar los datos de entrada antes de que ejecutemos la lógica de negocio, que normalmente se encontrará en el handler del evento OnValidSubmit
del formulario.
Vemos un ejemplo de formulario Blazor aquí, y el código C# justo debajo:
@page "/friend"
<h3>FriendEditor</h3>
<EditForm Model="Friend" OnValidSubmit="Save">
<DataAnnotationsValidator />
<div class="form-group mb-2">
<label for="name">Name:</label>
<ValidationMessage For="()=>Friend.Name" />
<InputText @bind-Value="Friend.Name" class="form-control" id="name" />
</div>
<div class="form-group mb-2">
<label for="score">Score:</label>
<ValidationMessage For="()=>Friend.Score" />
<InputNumber @bind-Value="Friend.Score" class="form-control" id="score" />
</div>
<div class="form-group mb-2">
<label for="birthDate">BirthDate:</label>
<ValidationMessage For="()=>Friend.BirthDate" />
<InputDate @bind-Value="Friend.BirthDate" class="form-control" id="birthDate" />
</div>
<div class="form-group mb-2">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</EditForm>
@code {
private FriendViewModel Friend = new();
private async Task Save()
{
await Task.Delay(1000);
Friend = new FriendViewModel();
}
public class FriendViewModel
{
[Required(ErrorMessage = "El nombre es obligatorio")]
public string Name { get; set; }
[Range(0, 10, ErrorMessage = "La puntuación debe estar entre {1} y {2}")]
public int Score { get; set; }
public DateTime BirthDate { get; set; }
}
}
Los records son una interesante fórmula para definir tipos en C# de forma rápida gracias a su concisa sintaxis, además de ofrecer otras ventajas, entre las que destacan la inmutabilidad o la implementación automática de métodos como Equals()
, GetHashCode()
o ToString()
.
Por si no tenéis esto muy fresco, aquí va un ejemplo de record y la clase tradicional equivalente en C#:
// Record:
public record Person(string FirstName, string LastName);
// Clase equivalente (generada automáticamente):
public class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public override bool Equals(object obj)
{
return obj is Person person &&
FirstName == person.FirstName &&
LastName == person.LastName;
}
public override int GetHashCode()
{
return HashCode.Combine(FirstName, LastName);
}
public Person With(string FirstName = null, string LastName = null)
{
return new Person(FirstName ?? this.FirstName, LastName ?? this.LastName);
}
public void Deconstruct(out string firstName, out string lastName)
{
firstName = this.FirstName;
lastName = this.LastName;
}
}
Como podéis comprobar, hay pocas características de C# que ofrezcan una relación código/funcionalidad tan bárbara como los records. Por ello, poco a poco van ganando popularidad y comenzamos a verlos ya de forma habitual en código propio y ajeno.
Sin embargo, su concisa sintaxis hacen que a veces no sea fácil intuir cómo resolver algunos escenarios que, usando las clases tradicionales, serían triviales.
Por ejemplo, hoy vamos a centrarnos en un escenario muy concreto pero frecuente, cuya solución seguro que veis que puede ser aplicada en otros casos: ya que en los records no definimos propiedades de forma explícita, ¿cómo podríamos aplicarles atributos?
Desde su llegada con la versión 7 del lenguaje C#, allá por 2017, nuestro lenguaje favorito dispone de soporte para tuplas. Sin embargo, no he visto muchos proyectos donde estén siendo utilizadas de forma habitual; quizás sea porque pueden hacer el código menos legible, o quizás por desconocimiento, o simplemente porque lo que aportan podemos conseguirlo normalmente de otras formas y preferimos hacerlo como siempre para no sorprender al que venga detrás a tocar nuestro código.
Pero bueno, en cualquier caso, es innegable que las tuplas han venido para quedarse, así que en este post vamos a ver algunos usos posibles, y a veces curiosos, de esta característica del lenguaje C#.
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.