Saltar al contenido

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

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

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

¡Microsoft MVP!
Mostrando entradas con la etiqueta trucos. Mostrar todas las entradas
Mostrando entradas con la etiqueta trucos. Mostrar todas las entradas
martes, 12 de marzo de 2024
Blazor

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.

martes, 5 de marzo de 2024
Blazor

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.

martes, 20 de febrero de 2024
.NET

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.

martes, 6 de febrero de 2024
C#

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.

martes, 23 de enero de 2024
Blazor

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; }
    }
}

martes, 12 de diciembre de 2023
.NET

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?

martes, 14 de noviembre de 2023
C#

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#.

martes, 7 de noviembre de 2023
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.

martes, 24 de octubre de 2023
ASP.NET Core

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.

martes, 4 de julio de 2023
.NET

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

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

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

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

    return users;
}

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

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

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

martes, 27 de junio de 2023
ASP.NET Core

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

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

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

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

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

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

martes, 20 de junio de 2023
C#

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

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

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

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

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

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

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

Time Elapsed 00:00:02.63

D:\Projects\ConsoleApp78\ConsoleApp78>_

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

Visual Studio mostrando el warning CS8618 sobre la propiedad

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

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

martes, 30 de mayo de 2023
.NET

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

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

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

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

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

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

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

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

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

martes, 23 de mayo de 2023
ASP.NET Core

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

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

martes, 25 de abril de 2023
ASP.NET Core

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

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

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

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

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

martes, 28 de marzo de 2023
C#

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

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

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

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

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

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

martes, 7 de febrero de 2023
JavaScript

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 ;)

martes, 31 de enero de 2023
.NET

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".

martes, 29 de noviembre de 2022
ASP.NET Core

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.

martes, 18 de octubre de 2022
ASP.NET Core

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.