La relación de muchos desarrolladores con las expresiones regulares es de amor-odio. Aunque indudablemente son una herramienta muy potente, su uso puede ser complicado y a menudo se convierten en un dolor de cabeza.
Pero hoy no vamos a hablar de su (oscura) sintaxis, ni de lo difícil que es depurarlas, ni de cómo utilizarlas en .NET, sino de distintas técnicas que pueden ayudarnos a disparar su velocidad de proceso, algo bastante importante si las utilizamos en los procesos críticos o hot paths de nuestra aplicación.
En este artículo vamos comparar el rendimiento de distintos escenarios de uso de expresiones regulares, y cómo podemos optimizar su uso en .NET.
Chequear direcciones de email usando expresiones regulares
Como punto de partida, echemos un vistazo al siguiente código, un ejemplo donde definimos la clase estática EmailValidator, con un método IsValid() que utiliza la clase RegEx para validar el email que recibe como parámetro:
Console.WriteLine(EmailValidator.IsValid("john@server.com")); // true
Console.WriteLine(EmailValidator.IsValid("john@smith@server.com")); // false
public static class EmailValidator
{
public static bool IsValid(string email)
{
string emailPattern = @"^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|""(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*"")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$";
var regex = new Regex(emailPattern, RegexOptions.IgnoreCase);
return regex.IsMatch(email);
}
}
No vamos a entrar en el debate de si la expresión regular que hemos utilizado es la mejor para validar un email. Simplemente es la recomendación de un LLM asegurando que cumple la RFC 5322, y, para la prueba que queremos hacer es totalmente válida porque tiene una cierta complejidad.
Si ejecutamos el código anterior, veremos que la expresión regular funciona correctamente y el método IsMatch() nos devuelve true o false dependiendo de si el email es válido o no. Y además, aparentemente la ejecución es bastante rápida, suficiente si no es algo que se vaya a ejecutar con mucha frecuencia.
Sin embargo, internamente, cada vez que llamamos a ese método estático IsValid(), estamos instanciando la clase Regex suministrándole el patrón de la expresión regular, que es parseado, verificado, optimizado, compilado y posteriormente ejecutado por un intérprete para realizar la validación que le estamos solicitando. Todo este proceso puede ser costoso en términos de rendimiento, sobre todo si esa parte del código se ejecuta con mucha frecuencia.
Seguro que podemos mejorar esto...
Primera mejora: reutilización de Regex
La primera optimización que podemos aplicar en este punto es reutilizar la instancia de Regex. De esta forma, evitaremos la sobrecarga de crear una nueva instancia cada vez que llamamos al método IsValid() y evitaremos el proceso de verificación y compilación de la expresión regular.
Esto podríamos conseguirlo fácilmente insertando en la clase anterior el siguiente código:
public static class EmailValidator
{
private const string EmailPattern = @"^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|""(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*"")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$";
private static Regex SharedInstance = new Regex(EmailPattern, RegexOptions.IgnoreCase);
public static bool IsValid_Shared(string email)
{
return SharedInstance.IsMatch(email);
}
}
Si ejecutamos de nuevo la aplicación, veremos que el funcionamiento es exactamente el mismo, y que aparentemente sigue siendo igual de rápido. Pero si usamos BenchmarkDotNet para medir el rendimiento de las dos implementaciones, nos llevaremos una sorpresa:
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|-----------------|-----------:|----------:|----------:|--------:|----------:|
| IsValid | 206.716 us | 1.5089 us | 1.2600 us | 27.3438 | 233969 B |
| IsValid_Shared | 2.742 us | 0.0312 us | 0.0276 us | - | - |
Esta segunda implementación se ejecuta casi 80 veces más rápido que la primera, sin consumo de memoria adicional. Impresionante, ¿verdad? Realmente se trata de una mejora brutal a cambio de muy poco esfuerzo de implementación.
Hay que tener en cuenta que las cifras no son siempre tan espectaculares, y que el rendimiento de la primera implementación puede variar dependiendo de la complejidad del patrón de la expresión regular. En expresiones más simples, la diferencia de rendimiento puede ser mucho menor, pero en cualquier caso habrá mejoras.
Pero... ¿aún podemos hacerlo mejor?
Segunda mejora: compilación de la expresión regular
Por defecto, las expresiones regulares se compilan a una serie de instrucciones de alto nivel que indican las operaciones que deben realizarse para comprobar si la cadena de texto suministrada coincide con el patrón de la expresión regular. Luego, en cada llamada a IsMatch() o métodos similares, un intérprete ejecuta esas instrucciones para realizar la validación.
Sin embargo, la clase Regex también permite compilar la expresión regular a código IL, por lo que el runtime de .NET puede ejecutarlo directamente e incluso, gracias al JIT, generar y ejecutar el código máquina nativo para la plataforma donde corre la aplicación, a cambio, eso sí, de consumir un poco más de memoria y tiempo durante su inicialización.
Esto lo conseguimos de nuevo con muy poco esfuerzo, simplemente añadiendo el RegexOptions.Compiled a la llamada al constructor de la clase Regex:
private static Regex SharedCompiledInstance
= new Regex(EmailPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
Si volvemos a llevarnos las tres opciones a BenchmarkDotNet, y medimos su rendimiento, veremos que en este último caso hemos mejorado algo más el rendimiento:
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|------------------ |-----------:|----------:|----------:|--------:|----------:|
| IsValid | 202.007 us | 2.4068 us | 2.2513 us | 27.3438 | 233969 B |
| IsValid_Shared | 2.606 us | 0.0276 us | 0.0258 us | - | - |
| IsValid_Compiled | 2.570 us | 0.0141 us | 0.0132 us | - | - |
En este caso la diferencia es mínima, pero es algo que también depende de la complejidad de las operaciones que hay que realizar para validar los valores contra la expresión regular. Por ejemplo, si en lugar de usar la expresión regular que hemos visto anteriormente para detectar emails, ejecutamos el mismo benchmark para un patrón aparentemente simple como "(\d+)*\1" y hacemos que se compruebe un string numérico muy largo (unos 100.000 dígitos), la diferencia de rendimiento es mucho más notable:
| Method | Mean | Error | StdDev | Allocated |
|----------------- |---------:|---------:|---------:|----------:|
| IsValid | 65.70 ms | 1.107 ms | 1.088 ms | 5401 B |
| IsValid_Shared | 63.74 ms | 0.925 ms | 0.772 ms | 57 B |
| IsValid_compiled | 19.52 ms | 0.147 ms | 0.130 ms | 12 B |
La expresión regular
"(\d+)*\1"permite buscar cadenas que contengan un número seguido de un número repetido, como por ejemplo123123,456456,789789, etc. Esta expresión regular es ejemplo conocido por dar lugar al llamado catastrophic backtracking, un problema que puede dar lugar a un rendimiento muy bajo en ciertas expresiones regulares, que incluso puede ser explotado en ataques de denegación de servicio (DoS) en aplicaciones web.
Estos resultados son fácilmente explicables: la diferencia de rendimiento entre la primera y segunda opción es pequeña, porque la expresión regular es muy simple y, por tanto, el coste de su compilación es bajo. Pero el rendimiento se multiplica por tres en la tercera opción porque la ejecución de la expresión regular se beneficia de la compilación a código IL.
Hasta aquí, hemos comprobado cómo realizando un par de modificaciones simples en el código podemos lograr mejorar considerablemente el rendimiento de las expresiones regulares en .NET. Pero aún hay más...
Tercera mejora: compilar la expresión regular en tiempo de diseño
La compilación de la expresión regular a código IL es una mejora muy interesante, pero tiene un coste adicional el términos de memoria y proceso, que se produce en el momento de la inicialización de la expresión regular, es decir, en tiempo de ejecución.
De hecho, también podemos realizar un benchmark del tiempo de creación de la instancia de Regex con y sin compilación, y veremos que la diferencia es prácticamente del triple, tanto en tiempo de proceso como en consumo de memoria:
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
|-------------------- |---------:|---------:|---------:|--------:|-------:|----------:|
| CreateRegex | 28.64 us | 0.204 us | 0.170 us | 3.4180 | - | 29.27 KB |
| CreateRegexCompiled | 99.51 us | 0.973 us | 0.863 us | 10.7422 | 1.4648 | 90 KB |
Si queremos evitar este sobrecoste, a partir de .NET 7 podemos compilar la expresión regular en tiempo de diseño usando source generators. De esta forma, el compilador de C# generará el código C# necesario para ejecutar la expresión regular, y lo incluirá en el ensamblado de la aplicación, por lo que no pagaremos ningún coste adicional en tiempo de ejecución. Pero además, como veremos algo más adelante, el código generado será mucho más eficiente que la versión compilada en tiempo de ejecución 🙂
Para conseguirlo, en una clase cualquiera debemos un método parcial de tipo Regex y asignarle el atributo [GeneratedRegex] especificando el patrón de la expresión regular y las opciones que queramos utilizar. Por ejemplo, en el siguiente código podemos ver el método al que hemos llamado GeneratedEmailRegex() sobre la clase estática parcial EmailRegex (ambos nombres son arbitrarios):
public static partial class EmailRegex
{
[GeneratedRegex(@"^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|""(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*"")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$", RegexOptions.IgnoreCase)]
public static partial Regex GeneratedEmailRegex();
}
Podéis ver fácilmente el código generado ejecutando la aplicación y, en el explorador de soluciones de Visual Studio, desplegando la carpeta "External Sources", el ensamblado de la aplicación, y abriendo el archivo
RegexGenerator.g.cs, o bien, siguiendo estos pasos.
Una vez tenemos este método disponible, para utilizar la expresión regular simplemente debemos usar la instancia de Regex retornada por el mismo, por ejemplo así:
public static class EmailValidator
{
public static bool IsValid(string email)
{
return EmailRegex.GeneratedEmailRegex().IsMatch(email);
}
}
Y si de nuevo nos llevamos estos cambios a BenchmarkDotNet, y medimos el rendimiento de las distintas implementaciones, de nuevo nos llevaremos una alegría:
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|-------------------------- |-------------:|------------:|------------:|--------:|----------:|
| IsValid_Initial | 213,564.0 ns | 1,532.91 ns | 1,280.05 ns | 27.3438 | 233969 B |
| IsValid_Shared_Instance | 2,667.7 ns | 39.86 ns | 35.33 ns | - | - |
| IsValid_Compiled_Instance | 2,745.5 ns | 37.81 ns | 35.37 ns | - | - |
| IsValid_UsingGenerators | 788.3 ns | 7.91 ns | 7.40 ns | - | - |
¡Uau! De nuevo hemos conseguido dividir por tres el tiempo de ejecución de la expresión regular respecto a la versión compilada en tiempo de ejecución. Y bueno, si lo comparamos con la versión inicial, la que implementamos sin pensar en ninguna optimización, es cerca de 300 veces más eficiente.
Conclusiones
A veces, el código que escribimos puede no ser el más óptimo: a veces por costumbre, a veces por comodidad, u otras simplemente porque no conocemos fórmulas mejores. En algunos casos no importará demasiado porque quizás nuestros requisitos de rendimiento no son excesivamente exigentes, pero en otros muchos escenarios sí debemos prestar atención a este tipo de detalles.
Lo que hemos visto en este post es un claro ejemplo de cómo las mejoras que se van introduciendo en el framework y el SDK de .NET pueden ayudarnos a mejorar el rendimiento de nuestras aplicaciones con muy poco de esfuerzo.
Publicado en: www.variablenotfound.com.Publicado por José M. Aguilar a las 8:05 a. m.
Etiquetas: .net, rendimiento, trucos
La semana pasada estuve unos días fuera y no publiqué la habitual recopilación de los lunes. Pero ya me he puesto al día, y, como se me acumuló el trabajo, en esta entrega tenemos más de ¡100! enlaces a contenidos a los que vale la pena echar un vistazo.
Por destacar algunos, en primer lugar el profundo análisis que está llevando a cabo Martin Stühmer sobre soluciones de scheduling en .NET, muy interesantes para estar al tanto de las opciones disponibles.
También Gerson Azabache ha publicado varios artículos interesantes sobre ASP.NET Core, como la comparativa entre Minimal APIs y controladores, resultados tipados y algunas buenas prácticas en el desarrollo de APIs empresariales.
Braulio Díez comparte sus reflexiones y experiencias sobre el impacto de la IA en la programación y el futuro de los desarrolladores de software, que igual no es tan malo como algunos pintan.
Y en la misma línea, José Manuel Alarcón habla sobre cómo los juniors deben enfocar su proceso de aprendizaje, integrando la IA como una aliada imprescindible.
Muchos más enlaces, a continuación.
Por si te lo perdiste...
- Mi controlador tiene muchos parámetros en el constructor, ¿estoy haciendo algo mal?
José M. Aguilar - HybridCache, la nueva caché híbrida de .NET 9
José M. Aguilar
.NET
- C# 14 New Feature: Field-Backed Properties
Ian Griffiths - Recent updates to NetEscapades.EnumGenerators: [EnumMember] support, analyzers, and bug fixes
Andrew Lock - “Classic” .NET Domain Events with Wolverine and EF Core
Jeremy D. Miller - .NET Job Scheduling: Quartz.NET for Enterprise Scale & Coravel and Fluent Simplicity & NCronJob and Native Minimalism & TickerQ and Modern Architecture
Martin Stühmer - How to Build a Production-Ready Invoice Builder in .NET Using IronPDF
Anton Martyniuk - How can my process read its own standard output?
Raymond Chen - Mime Type Helper in .NET 11
Steven Giesel - Single File Test Suites in Dotnet Csharp
Steve Smith - Create Types on Demand and Cecilifier
Roman Stoffel - Creating a custom MSBuild SDK to reduce boilerplate in .NET projects
Gérald Barré - Debug Dumps in Visual Studio
Stephen Cleary - Building Modular .NET Applications with CShells
Sipke Schoorstra - .NET 10 Networking Improvements
Marie Píchová - What the heck is a
\\.\nulpath and why is it breaking my Directory Files Lookup?
Rick Strahl - .NET 10 Validation
Ricardo Peres - .NET Performance: Efficient Async Code
Nick Kovalenko - .NET 10 and Memory: Less Heap, Smarter GC, Faster Apps
Hazem Ali - Immutable Collection Add() Trap: Don’t Get Burned — Use a Builder Instead
David McCarter - How .NET 10.0 boosted AIS.NET performance by 7%
Ian Griffiths - Resolving Overload Ambiguity with Collection Expressions
Gérald Barré - Typemock Architecture: Inside the .NET Isolator Engine (Part 2)
Eli Lopian - Cleaner Code: C# 14 Null-Conditional Assignment Operator
Dave Brock
<!--more-->
ASP.NET Core / ASP.NET / Blazor
- Minimal APIs vs Controllers en .NET 10: la guía corta y honesta & Por qué tus APIs en .NET deberían usar Result Contracts (Typed Results) & Las 5 mejores prácticas modernas para APIs empresariales en .NET (2026)
Gerson Azabache Martínez - Enterprise Patterns for ASP.NET Core Minimal API: Transaction Script Pattern – The Shortcut That Quietly Reshapes Your System
Chris Woodruff - How to upload files in an ASP.NET Core Web API
David Grace - Stop Letting Your Controllers Talk to SQL: Layered Architecture in ASP.NET Core
Chris Woodruff - Best Practices For Building REST APIs
Anton Martyniuk - How to Choose the Best Blazor Dropdown Component for Your Web App
Prince Oliver - mostlylucid.MinimalBlog - How Simple Can an ASP.NET Blog Really Be? (English)
Scott Galloway - Enterprise Patterns for ASP.NET Core Minimal API: Domain Model Pattern – When Your Core Rules Deserve Their Own Gravity
Chris Woodruff - Load Testing ASP.NET Core Applications with k6: Introduction & Practical Implementation
Scott Galloway - How Aspire composes itself: an overview of Aspire's Docker Compose integration
Safia Abdalla - Migrating from Bootstrap Blazor or MudBlazor to Blazorise
Mladen Macanović - Using Strategy Pattern with Dependency Injection in ASP.NET Core
Ali Hamza Ansari - Getting Started with the Aspire CLI - A Complete Guide
Chris Ayers
Azure / Cloud
- You Can't Use Azure Migrate to Move Between Tenants (Even if you Try to Run it in Azure)
Shannon B. Kuehn - How to Deploy a Spreadsheet Server on Azure App Service Using Visual Studio and Docker
Parthasarathy Ranjan
Conceptos / Patrones / Buenas prácticas
- Domain-Driven Design Misconceptions
Derek Comartin - Treat test code like production code
Mark Seemann - Aggregates in DDD: Model Rules, Not Relationships
Derek Comartin - 10 Habits That Make You a Great Programmer
Shalitha Suranga - 12 Essential Distributed System Design Patterns Every Architect Should Know
Anton Martyniuk - Power of Ten Rules: More Relevant Than Ever for .NET
Martin Stühmer - Stop Naming Your Variables "Flag": The Art of Boolean Prefixes
Christopher Johnson
Data
- Data Access in .NET: Comparing ORMs and Mapping Strategies (Part 1 & * Comparing ORMs and Mapping Strategies (Part 2
Scott Galloway - Debugging Entity Framework Core: 8 Real-World Query Anti‑Patterns (and How to Fix Them)
Chris Woodruff - Named global query filters in Entity Framework Core 10
Tim Deschryver - DbContext is Not Thread-Safe: Parallelizing EF Core Queries the Right Way
Milan Jovanović - Why your EF Core queries are slow and how to fix them
David Grace
Machine learning / IA
- Meet EuroLLM: Large language model made in Europe built to support all official 24 EU languages
EuroLLM - Introducing: Devstral 2 and Mistral Vibe CLI
Mistral - Introducing GPT-5.2
OpenAI - Introducing the Microsoft Agent Framework – A Dev-Friendly Recap
Bruno Capuano - Introducing Data Ingestion Building Blocks (Preview)
Luis Quintanilla - Anomaly Detection Using K-Means Clustering with JavaScript
James McCaffrey
Web / HTML / CSS / Javascript
- Cómo probar tu aplicación con Vue en GitHub Pages
Gisela Torres - Prevent a page from scrolling while a dialog is open
Geoff Graham - Getting Creative With “The Measure”
Andy Clarke - Scrollytelling on Steroids With Scroll-State Queries
Lee Meyer - Masonry: Things You Won’t Need A Library For Anymore
Patrick Brosset - Frederik Braun: Why the Sanitizer API is just
setHTML()
Frederik Braun - Creating Scroll-Based Animations in Full view()
Preethi - CSS Wrapped 2025
Chrome DevRel Team - Fit width text in 1 line of CSS
Geoff Graham - That Time I Tried Explaining HTML and CSS to My 5-Year Old Niece
Kevine Nzapdi - ::target-text: An easy way to style text fragments
Saron Yitbarek - Stop using JavaScript to solve CSS problems
Chizaram Ken - Simulating the Enigma Machine
Andrew S. Erwin - Tailwind CSS: Targeting Child Elements (when you have to)
Christian Ekrem - How to Prevent XSS Attacks in React Rich Text Editor
Thangavel E. - What's wrong with this HTML, and is it valid?
Patrick Brosset - How to create Liquid Glass effects with CSS and SVG
Rahul Chhodde - What Else Could Container Queries... Query?
Daniel Schwarz - Denial of Service and Source Code Exposure in React Server Components – React
The React Team
Visual Studio / Complementos / Herramientas
- Why changing keyboard shortcuts in Visual Studio isn’t as simple as it seems
Steven Miller - Unlock GitHub Copilot’s Full Potential: Why Every Repo Needs an AGENTS.md File
Chris Pietschmann - Progress on TypeScript 7 - December 2025
Daniel Rosenwasser - Introducing Stack Overflow AI Assist—a tool for the modern developer
Stack Overflow Team - Microsoft Learn MCP Server Elevates Development
Wendy Breiding - Trying out the Zed editor on Windows for .NET and Markdown
Andrew Lock - Previewing the JavaScript/TypeScript Modernizer for VS Code Insiders
Sayed Ibrahim Hashimi - DarkGPT: Malicious Visual Studio Code Extension Targeting Developers — Real-time Open Source Software Supply Chain Security
SafeDep Team - Streamlining your Git workflow with Visual Studio 2026
Mads Kristensen - Making Windows Terminal awesome with GitHub Copilot CLI
Linda Berns
.NET MAUI / Cross-Platform
- Cross-Platform Age Verification in .NET MAUI Applications
Gerald Versluis - Securing Sensitive Mobile Operations with Device-Bound Request Signing
Fernando de Oliveira - Mastering Popups in .NET MAUI: Alerts, Action Sheets, Overlays & More
Jayaleshwari N. - Implementing Cross-Platform In-App Billing in .NET MAUI Applications
Gerald Versluis - How to Add and Remove Digital Signatures in PDF Using .NET MAUI
Rangarajan Ashokan - How to Integrate Google Maps in .NET MAUI: A Cross-Platform Guide Using the Google Maps Tile API
Jeyasri Murugan - Avalonia MAUI Progress Update
Tim Miller - .NET 10: Quick UI Changes Worth Noticing in .NET MAUI
Leomaris Reyes
Otros
- El picar se va acabar
Braulio Díez - Aprende a programar en 2026: guía práctica para juniors (que tienen miedo a la IA, o no)
José Manuel Alarcón
Publicado en Variable not found.
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.
Esta semana me gustaría destacar en primer lugar el detallado análisis de Andrew Lock sobre el proceso de arranque de una aplicación .NET, muy interesante para saber cómo funcionan las cosas por dentro.
Bipin Joshi da un gran repaso a Kestrel, el motor de todas las aplicaciones ASP.NET Core: cómo configurarlo, tunearlo, ejecutarlo y buenas prácticas de uso.
Milan Jovanović nos recuerda que el happy path no es el único camino en nuestras aplicaciones, y nos muestra técnicas para desacoplar servicios para hacerlos más robustos.
Por último, Ricardo Peres nos habla de la relación entre tipos anulables y miembros requeridos en C#, algo que, cuando empezamos a trabajar con ellos, puede resultar algo confuso.
El resto de enlaces, a continuación.
Por si te lo perdiste...
- Soporte para colecciones en parámetros
paramde .NET 9
José M. Aguilar - Establecer textos por defecto y localizados en validaciones de ASP.NET Core MVC
José M. Aguilar
.NET
- TUnit — Why I Spent 2 Years Building a New .NET Testing Framework
Tom Longhurst - C# 14: User-Defined Compound Assignment Operators
Anthony Giretti - Under Pressure: How Queueing Systems Handle Backpressure with Examples in C#
Scott Galloway - Precision Matters in C#: Correctly Handling Money, Time Zones, and Date Ranges
Sudhir Mangla - Exploring the .NET boot process via host tracing
Andrew Lock - .NET Job Scheduling — The Landscape
Martin Stühmer - Why Do You Need To Write Architecture Tests in .NET
Anton Martyniuk - Fetching GitHub content from C#
Thomas Ardal - Using CSX Scripts for Quick C# Testing (English)
Scott Galloway - The Worst Security Vulnerability in Akka.NET
Aaron Stannard - IDistributedCache (Redis)
Josef Ottosson - Adding Intelligence to Blazor with Telerik Smart Components
Héctor Pérez - How to ensure your expert C# knowledge doesn't make you a TypeScript noob
Lewis Cianci - Ix.NET v7.0: .NET 10 and LINQ for IAsyncEnumerable<T>
Ian Griffiths - Real-Time Recommendation Engines in .NET: Hybrid Retrieval, Deep Learning, and Vector Search
Sudhir Mangla - One shot tool execution in .NET 10
Bart Wullems - .NET Job Scheduling — Hangfire and Persistent Reliability
Martin Stühmer - Nullable and Required Types
Ricardo Peres - Unit Testing HttpClient WITHOUT Mocks
Scott Galloway
Años atrás, en los inicios de la informática, se programaba de forma bastante diferente: conectando cables y usando paneles de interruptores. Por eso me ha parecido muy curioso el simulador del Minivac 601, un ordenador de los años 60, donde podemos probar de primera mano cómo era su experiencia de uso.
Y continuando con temas vintage, Microsoft ha anunciado la liberación del código fuente de los juegos Zork I, II y III, la mítica saga de aventuras conversacionales de los años 80. Pura historia del software.
También esta semana encontramos una interesante lectura de Sudhir Mangla donde explica cómo usar patrones modernos y características recientes de C# para construir modelos más expresivos, seguros y mantenibles que los que ofrece la aplicación estricta de SOLID.
El resto de enlaces interesantes recopilados esta semana, entre los que podéis encontrar información sobre .NET 10, ASP.NET Core, IA, desarrollo web y mucho más, los tenéis a continuación.
Por si te lo perdiste...
- Creando GUIDs con orden natural en .NET 9
José M. Aguilar - Localizar errores de validación del binding en ASP.NET Core MVC
José M. Aguilar
.NET
- Introducing C# 14
Uwe Keim - Introducing F# 10
Adam Boniecki - Exploring C# File-based Apps in .NET 10
Milan Jovanović - How to Upgrade to .NET 10 LTS - Complete Guide for .NET Global Tools with Multi-Targeting
John Smith - Post-Quantum Cryptography in .NET
Jeremy Barton - Reinventing how .NET Builds and Ships (Again)
Gábor Szabó - Companies complaining .NET moves too fast should just pay for post-EOL support
Andrew Lock - .NET 10 Testing: Microsoft Finally Fixed the Test Runner (Mostly)
Martin Stühmer - Optimize GUID creation performance in .NET applications
Gérald Barré - How to Update .NET on Wsl or Ubuntu
Steve Smith - No more public partial class Program in .NET 10
Steven Giesel - Streamlining Remote C# Scripts with .NET 10 runfile
Daniel Cazzulino - A Practical Approach to DI Variation in .NET: Introducing Child Service Providers
Fernando Escolar - State of Native AOT in .NET 10
Peter Ritchie
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.

