Regex é onipresente em aplicações .NET — validação de e-mail, parsing de logs, extração de dados, sanitização de entrada. O problema: a API clássica do System.Text.RegularExpressions.Regex tem três modos de operação com trade-offs radicalmente diferentes, e a maioria dos projetos usa o pior deles por padrão.
O .NET 7 introduziu o atributo [GeneratedRegex], que usa Source Generators para mover toda a compilação da regex do runtime para o build time. O resultado: performance equivalente ao modo RegexOptions.Compiled sem o custo de JIT compilation em runtime, sem alocação para construção do automato, e com suporte total a AOT.
Os três modos de Regex no .NET
Antes de entender [GeneratedRegex], é fundamental entender o que existe antes dele:
1. Interpretado (padrão)
// Cria e interpreta o automato a cada chamada
var match = Regex.Match(input, @"\d{4}-\d{2}-\d{2}");
// Ou cached implicitamente (mas ainda interpretado)
var regex = new Regex(@"\d{4}-\d{2}-\d{2}");
O modo interpretado percorre o padrão caractere por caractere em tempo de execução usando um autômato de estados interpretado. É o mais lento para padrões chamados frequentemente, mas tem startup instantâneo e não aloca estruturas pesadas.
2. Compilado (RegexOptions.Compiled)
private static readonly Regex _regex =
new Regex(@"\d{4}-\d{2}-\d{2}", RegexOptions.Compiled);
O modo compilado usa Reflection.Emit para gerar IL assembly em runtime — essencialmente, faz JIT de um autômato personalizado na primeira vez que o Regex é instanciado. É muito mais rápido nas chamadas subsequentes, mas tem custo de startup alto (dezenas de ms por regex) e não funciona com Native AOT.
3. Source-Generated ([GeneratedRegex])
public partial class ValidadorService
{
[GeneratedRegex(@"\d{4}-\d{2}-\d{2}")]
private static partial Regex DataRegex();
}
O Source Generator analisa o padrão em tempo de compilação e gera um método C# que implementa o automato diretamente — sem Reflection.Emit, sem JIT em runtime, sem startup cost. Funciona perfeitamente com Native AOT e tem o mesmo throughput que o modo compilado (ou melhor, porque o JIT do .NET pode otimizar código C# gerado mais eficientemente que o IL emitido via Reflection.Emit).
Sintaxe completa
O método deve ser static partial e retornar Regex. A classe que o contém deve ser partial:
using System.Text.RegularExpressions;
public partial class Validacoes
{
// Básico
[GeneratedRegex(@"^\d{4}-\d{2}-\d{2}$")]
public static partial Regex Data();
// Com opções
[GeneratedRegex(@"^[\w.-]+@[\w.-]+\.[a-z]{2,}$",
RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture)]
public static partial Regex Email();
// Com timeout (recomendado para inputs externos)
[GeneratedRegex(@"<[^>]+>", RegexOptions.None, matchTimeoutMilliseconds: 1000)]
public static partial Regex TagHtml();
// Com grupo nomeado
[GeneratedRegex(@"(?<ano>\d{4})-(?<mes>\d{2})-(?<dia>\d{2})")]
public static partial Regex DataComGrupos();
}
O compilador chama o Source Generator que gera, por exemplo, um método DataImpl() com o código do automato. Você nunca vê esse código a não ser que abra o arquivo gerado em obj/.
Uso prático
// Validação simples
bool dataValida = Validacoes.Data().IsMatch("2026-07-18"); // true
// Grupos nomeados
var m = Validacoes.DataComGrupos().Match("2026-07-18");
if (m.Success)
{
var ano = m.Groups["ano"].Value; // "2026"
var mes = m.Groups["mes"].Value; // "07"
var dia = m.Groups["dia"].Value; // "18"
}
// Enumerar todos os matches sem alocação de lista
foreach (var match in Validacoes.TagHtml().EnumerateMatches("<b>texto</b>"))
{
// match é um ValueMatch (struct) — zero alocação
Console.WriteLine(match.Index);
}
// Replace
string limpo = Validacoes.TagHtml().Replace(html, string.Empty);
Suporte a Span<char>
A partir do .NET 7, o modo source-generated suporta ReadOnlySpan<char> em alguns métodos — o que é impossível com o Regex compilado via Reflection.Emit, pois string e Span têm APIs distintas:
public partial class Parser
{
[GeneratedRegex(@"\d+")]
private static partial Regex Numeros();
public static bool TemNumero(ReadOnlySpan<char> input)
=> Numeros().IsMatch(input); // Span — zero cópia, zero alocação
}
// Uso com slice de string sem alocar substring
var linha = "Pedido #12345: aprovado";
bool temId = Parser.TemNumero(linha.AsSpan(8, 5)); // "12345"
Isso é especialmente útil em parsing de arquivos grandes, onde você quer trabalhar diretamente com spans de um buffer em vez de criar substrings.
Benchmark: os números reais
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
public class RegexBenchmark
{
private const string Input = "Data de validade: 2026-07-18. Renovar antes de 2027-01-01.";
// Modo interpretado — instância reutilizada
private static readonly Regex _interpretado = new(@"\d{4}-\d{2}-\d{2}");
// Modo compilado
private static readonly Regex _compilado =
new(@"\d{4}-\d{2}-\d{2}", RegexOptions.Compiled);
// Source-generated
[GeneratedRegex(@"\d{4}-\d{2}-\d{2}")]
private static partial Regex GeradoImpl();
[Benchmark(Baseline = true)]
public int Interpretado() =>
_interpretado.Matches(Input).Count;
[Benchmark]
public int Compilado() =>
_compilado.Matches(Input).Count;
[Benchmark]
public int Gerado() =>
GeradoImpl().Matches(Input).Count;
[Benchmark]
public int Gerado_EnumerateMatches()
{
int count = 0;
foreach (var _ in GeradoImpl().EnumerateMatches(Input)) count++;
return count;
}
}
Resultados representativos (.NET 8, x64):
| Método | Média | Ratio | Alocações |
|---|---|---|---|
| Interpretado | 620 ns | 1.00× | 320 B |
| Compilado | 180 ns | 0.29× | 264 B |
| Gerado | 165 ns | 0.27× | 264 B |
| Gerado + EnumerateMatches | 130 ns | 0.21× | 0 B |
O [GeneratedRegex] é ~4% mais rápido que o Compiled no throughput, mas a vantagem real é outra: ele não tem o custo de startup (construção do automato no primeiro acesso) e funciona com AOT. Combinado com EnumerateMatches, zeramos as alocações — relevante em caminhos de alta frequência.
Regras e grupos nomeados em cenários reais
Parsing de log estruturado
public partial class LogParser
{
// Formato: [2026-07-18 14:32:01] ERROR RequestId=abc123 Message=Timeout
[GeneratedRegex(
@"\[(?<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (?<level>\w+) RequestId=(?<rid>\S+) Message=(?<msg>.+)",
RegexOptions.ExplicitCapture)]
private static partial Regex LinhaLog();
public static LogEntry? Parse(string linha)
{
var m = LinhaLog().Match(linha);
if (!m.Success) return null;
return new LogEntry(
Timestamp: DateTime.Parse(m.Groups["ts"].Value),
Level: m.Groups["level"].Value,
RequestId: m.Groups["rid"].Value,
Message: m.Groups["msg"].Value
);
}
}
Validação de CPF/CNPJ (formato, não dígito verificador)
public static partial class DocumentoRegex
{
[GeneratedRegex(@"^\d{3}\.\d{3}\.\d{3}-\d{2}$")]
public static partial Regex Cpf();
[GeneratedRegex(@"^\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}$")]
public static partial Regex Cnpj();
public static bool ValidarFormato(string doc)
=> doc.Length == 14 ? Cpf().IsMatch(doc) : Cnpj().IsMatch(doc);
}
Extração de URLs de texto
public partial class UrlExtractor
{
[GeneratedRegex(
@"https?://[\w\-.]+(?:/[\w\-./?%&=]*)?",
RegexOptions.IgnoreCase,
matchTimeoutMilliseconds: 500)]
private static partial Regex Url();
public static IEnumerable<string> Extrair(string texto)
{
foreach (var m in Url().EnumerateMatches(texto))
yield return texto.Substring(m.Index, m.Length);
}
}
Migrando código existente
A migração de static readonly Regex para [GeneratedRegex] é quase mecânica:
// Antes
public class Servico
{
private static readonly Regex _emailRegex =
new Regex(@"^[\w.-]+@[\w.-]+\.[a-z]{2,}$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public bool ValidarEmail(string email) => _emailRegex.IsMatch(email);
}
// Depois
public partial class Servico // <-- partial obrigatório
{
[GeneratedRegex(@"^[\w.-]+@[\w.-]+\.[a-z]{2,}$",
RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture)]
private static partial Regex EmailRegex();
public bool ValidarEmail(string email) => EmailRegex().IsMatch(email);
}
O analisador Roslyn SYSLIB1045 detecta automaticamente instâncias de Regex elegíveis para migração e oferece um code fix automático no Visual Studio / Rider. Você pode ativar o warning em todo o projeto via .editorconfig:
[*.cs]
dotnet_diagnostic.SYSLIB1045.severity = warning
Limitações
- Padrão deve ser constante em compile time: não funciona com padrões dinâmicos (strings de banco de dados, configurações). Para esses casos, use
Regex.IsMatch(input, pattern, options, timeout)com timeout explícito. - Classe deve ser partial: em alguns projetos legados, isso requer refatoração adicional.
- Não suporta todos os recursos de Regex: alguns lookbehind variáveis e backtracking complexo podem não ser suportados pelo Source Generator — o compilador avisa quando isso acontece.
- Código gerado aumenta tamanho do assembly: cada
[GeneratedRegex]gera uma classe de estado. Para dezenas de expressões simples, o impacto no binário pode ser perceptível em ambientes AOT com restrição de tamanho.
Conclusão
[GeneratedRegex] é a forma correta de usar Regex em qualquer código .NET novo ou migrado. O caso de uso é claro: padrões conhecidos em compile time usados em caminhos de alta frequência. O ganho sobre o modo interpretado é ~3–4×, o startup cost é zero (ao contrário do compilado), e a compatibilidade com Native AOT abre a porta para deployments em containers minimalistas.
A migração é simples — adicione partial na classe, troque o campo estático pelo método com atributo, e o Source Generator faz o resto. Ative o warning SYSLIB1045 e deixe o analisador guiar a migração no código existente.
Precisa resolver isso na prática?
Se você tem APIs com regex em caminhos quentes ou está migrando para Native AOT e precisa revisar o código para compatibilidade, podemos ajudar com uma revisão técnica focada.
Falar com um especialista →