Expresiones regulares en C#: Regex, grupos, Replace y GeneratedRegex

Validar un código postal, sacar la fecha de una línea de registro o cambiar el formato de todas las fechas de un texto: las expresiones regulares lo hacen en una línea. En C# están en la clase Regex, y desde .NET 7 el compilador puede convertir los patrones en código.

¿Encaja o no?

Todo está en System.Text.RegularExpressions, que viene con .NET. Para comprobar si un texto tiene una forma concreta, como un código postal de cinco cifras, basta Regex.IsMatch:

using System.Text.RegularExpressions;

string[] codigos = ["28013", "2801", "28O13", "08001"];
foreach (var c in codigos)
{
    Console.WriteLine($"{c,-6} {Regex.IsMatch(c, @"^\d{5}$")}");
}
salida28013  True
2801   False
28O13  False
08001  True

^ y $ anclan el patrón al principio y al final: sin ellos, \d{5} encontraría cinco cifras dentro de cualquier texto más largo. El tercer código lleva una letra O en vez de un cero, y por eso falla.

La arroba delante del patrón

Los patrones se escriben casi siempre como cadenas literales, con @ delante, porque las barras invertidas tienen significado en un texto normal de C#. Sin la arroba, \d no es una secuencia válida y el programa no compila:

using System.Text.RegularExpressions;

Console.WriteLine(Regex.IsMatch("28013", "^\d{5}$"));
dotnet buildescape.cs(3,44): error CS1009: Secuencia de escape no reconocida

Con @"^\d{5}$" la barra llega tal cual a la expresión regular. La alternativa es duplicarla, "^\\d{5}$", que es mucho más difícil de leer.

Grupos con nombre y varias coincidencias

using System.Text.RegularExpressions;

string linea = "2026-10-25 09:30:12 ERROR Disco lleno en /var/log";
var m = Regex.Match(linea, @"^(?<fecha>\S+) (?<hora>\S+) (?<nivel>[A-Z]+) (?<mensaje>.*)$");

if (m.Success)
{
    Console.WriteLine($"nivel:   {m.Groups["nivel"].Value}");
    Console.WriteLine($"cuándo:  {m.Groups["fecha"].Value} a las {m.Groups["hora"].Value}");
    Console.WriteLine($"mensaje: {m.Groups["mensaje"].Value}");
}

string texto = "Escribe a ana@ejemplo.com o a soporte@decodigo.com, no a nadie@";
foreach (Match correo in Regex.Matches(texto, @"[\w.+-]+@[\w-]+\.[\w.]+"))
{
    Console.WriteLine($"{correo.Value} (posición {correo.Index})");
}
salidanivel:   ERROR
cuándo:  2026-10-25 a las 09:30:12
mensaje: Disco lleno en /var/log
ana@ejemplo.com (posición 10)
soporte@decodigo.com (posición 30)

Un grupo (?<nombre>...) guarda lo que encajó en esa parte del patrón, y se lee con m.Groups["nombre"]. Comprueba siempre m.Success: si no hay coincidencia, los grupos están vacíos pero no fallan. Regex.Matches devuelve todas las coincidencias, cada una con su texto y su posición. El patrón de correo es deliberadamente sencillo: validar de verdad una dirección de correo con una expresión regular es casi imposible, y lo único fiable es enviar un mensaje.

Reemplazar y partir

using System.Text.RegularExpressions;

string texto = "Entregas: 25/10/2026, 3/11/2026 y 15/12/2026.";

// $3, $2 y $1 son los grupos, en el orden en que se abren los paréntesis
Console.WriteLine(Regex.Replace(texto, @"(\d{1,2})/(\d{1,2})/(\d{4})", "$3-$2-$1"));

// Con una función: cada coincidencia se cambia por lo que devuelva
string precios = "café 4.5, té 3.25, cacao 6";
Console.WriteLine(Regex.Replace(precios, @"\d+(\.\d+)?", m => $"${decimal.Parse(m.Value, System.Globalization.CultureInfo.InvariantCulture):F2} USD"));

Console.WriteLine(string.Join(" | ", Regex.Split("uno,  dos;tres ;cuatro", @"\s*[,;]\s*")));
salidaEntregas: 2026-10-25, 2026-11-3 y 2026-12-15.
café $4.50 USD, té $3.25 USD, cacao $6.00 USD
uno | dos | tres | cuatro

En el texto de reemplazo, $1, $2… son los grupos (el 3 de noviembre queda como 2026-11-3, sin cero: el reemplazo copia lo que encontró, tal cual). Si el cambio depende de lo encontrado, se pasa una función (aquí, una lambda) que recibe cada coincidencia. Regex.Split parte por un separador variable, algo que string.Split no sabe hacer.

Patrones generados al compilar

Desde .NET 7, el atributo [GeneratedRegex] hace que el compilador convierta el patrón en código C# normal. Es la forma recomendada para patrones fijos: arranca más rápido, el patrón se comprueba al compilar (uno mal escrito es un error de compilación, no una excepción al ejecutar) y funciona con AOT.

using System.Text.RegularExpressions;

string[] entradas = ["ABC-1234", "abc-1234", "AB-12345", "XYZ-0001"];
foreach (var e in entradas)
{
    Console.WriteLine($"{e,-9} {Patrones.Matricula().IsMatch(e)}");
}

static partial class Patrones
{
    [GeneratedRegex(@"^[A-Z]{3}-\d{4}$")]
    public static partial Regex Matricula();
}
salidaABC-1234  True
abc-1234  False
AB-12345  False
XYZ-0001  True

Patrones que no terminan nunca

using System.Text.RegularExpressions;

string entrada = new string('a', 30) + "!";

try
{
    var lenta = new Regex(@"^(a+)+$", RegexOptions.None, TimeSpan.FromMilliseconds(200));
    Console.WriteLine(lenta.IsMatch(entrada));
}
catch (RegexMatchTimeoutException)
{
    Console.WriteLine("El patrón tardó más de 200 ms y se cortó");
}

var segura = new Regex(@"^(a+)+$", RegexOptions.NonBacktracking);
Console.WriteLine($"Con NonBacktracking: {segura.IsMatch(entrada)}");
salidaEl patrón tardó más de 200 ms y se cortó
Con NonBacktracking: False
El retroceso catastrófico. Un patrón como ^(a+)+$, con un cuantificador dentro de otro, prueba todas las formas de repartir las letras entre los grupos antes de rendirse: con 30 letras son más de mil millones. Si el texto lo escribe un usuario, es una forma fácil de colgar un servidor (se llama ReDoS). Defensas: pon siempre un tiempo máximo cuando el texto venga de fuera, o usa RegexOptions.NonBacktracking, que garantiza un tiempo proporcional al texto a cambio de no admitir algunas funciones, como las referencias hacia atrás.

Resumen

ParaUsa
Comprobar una formaRegex.IsMatch(texto, @"^...$")
Sacar partesRegex.Match y m.Groups["nombre"]
Todas las coincidenciasRegex.Matches
CambiarRegex.Replace con $1 o una función
Un patrón fijo y rápido[GeneratedRegex] en un método partial
Texto que no controlasun TimeSpan máximo o NonBacktracking

Todo el código de esta página se compiló con el .NET SDK 10.0.403 (C# 14) y se ejecutó con .NET 10.0.14 en un contenedor limpio antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.