O C# 12 introduziu os Interceptors — um mecanismo experimental (mas já amplamente usado pelo próprio ASP.NET Core internamente) que permite substituir chamadas de métodos específicos por implementações alternativas em tempo de compilação, sem reflection e sem proxies em runtime. É a base por trás das otimizações de startup do ASP.NET Core no modo Native AOT e da geração de código de RequestDelegate nas Minimal APIs.
Neste artigo veremos como eles funcionam, quando fazem sentido, e como construir um Source Generator simples que os utiliza.
O que são Interceptors
Um Interceptor é um método que intercepta — ou seja, substitui — a chamada a outro método em um local específico do código-fonte (arquivo + linha + coluna). A substituição acontece em tempo de compilação: o compilador redireciona a chamada original para o método interceptor antes mesmo de gerar IL.
A anotação que faz isso é [System.Runtime.CompilerServices.InterceptsLocation]:
// Método original que queremos interceptar
public static class Calculadora
{
public static int Somar(int a, int b) => a + b;
}
// Interceptor — gerado por um Source Generator ou escrito manualmente
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
sealed class InterceptsLocationAttribute(string filePath, int line, int character)
: Attribute { }
}
public static class CalculadoraInterceptors
{
// Intercepta a chamada em Program.cs, linha 12, coluna 14
[InterceptsLocation("Program.cs", line: 12, character: 14)]
public static int Somar_Interceptado(int a, int b)
{
Console.WriteLine($"Interceptado: Somar({a}, {b})");
return a + b;
}
}
Depois da compilação, a chamada em Program.cs:12 chama Somar_Interceptado — não Somar — sem nenhuma indireção em runtime.
Por que isso importa
Problemas que Interceptors resolvem melhor que alternativas tradicionais:
| Técnica | Overhead runtime | AOT-safe | Legibilidade do código original |
|---|---|---|---|
| Decorator Pattern | Chamada extra de interface | ✅ | ✅ (composição explícita) |
| DispatchProxy / Castle DynamicProxy | Alto (reflection + IL emit) | ❌ | ✅ |
| Fody / PostSharp (IL weaving) | Zero | ✅ | ✅ |
| Interceptors (C# 12) | Zero | ✅ | ✅ (não altera o original) |
O ASP.NET Core usa Interceptors exatamente por isso: ao rodar em modo Native AOT, ele precisa substituir o registro dinâmico de rotas por código gerado estaticamente. Os Interceptors permitem que a API de programação do usuário (app.MapGet(...)) permaneça idêntica, enquanto o Source Generator substitui a implementação runtime por uma versão AOT-safe gerada em compile time.
Habilitando no projeto
Interceptors ainda são experimentais no C# 12/13. Para ativar, adicione ao .csproj:
<PropertyGroup>
<LangVersion>preview</LangVersion>
<Features>InterceptorsPreviewNamespaces=System.Runtime.CompilerServices</Features>
</PropertyGroup>
E declare o atributo (se não estiver disponível no SDK alvo):
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
sealed class InterceptsLocationAttribute(string filePath, int line, int character)
: Attribute { }
}
Caso de uso 1: logging automático sem AOP
O cenário clássico: adicionar logging ao redor de chamadas de serviço sem modificar o código original nem usar proxies dinâmicos.
// Código do usuário — não muda
public class PedidoService
{
public async Task<Pedido> CriarAsync(CriarPedidoRequest request)
{
// lógica de negócio
return new Pedido(request.ClienteId);
}
}
// Em algum handler:
var pedido = await pedidoService.CriarAsync(request); // linha 42, col 26
O Source Generator detecta chamadas anotadas com [LogCall] (um atributo customizado) e gera o interceptor:
// Gerado pelo Source Generator em obj/GeneratedInterceptors.cs
public static class LogInterceptors
{
[InterceptsLocation("Handlers/PedidoHandler.cs", line: 42, character: 26)]
public static async Task<Pedido> CriarAsync_Logged(
this PedidoService service,
CriarPedidoRequest request)
{
var logger = LoggerProvider.GetLogger<PedidoService>();
logger.LogInformation("CriarAsync chamado com ClienteId={Id}", request.ClienteId);
var sw = Stopwatch.StartNew();
try
{
var result = await service.CriarAsync(request);
logger.LogInformation("CriarAsync concluído em {Ms}ms", sw.ElapsedMilliseconds);
return result;
}
catch (Exception ex)
{
logger.LogError(ex, "CriarAsync falhou após {Ms}ms", sw.ElapsedMilliseconds);
throw;
}
}
}
Construindo um Source Generator simples com Interceptors
Um Source Generator que intercepta chamadas a qualquer método anotado com [Timed] e adiciona medição de tempo:
// No projeto do Generator (separado)
[Generator]
public class TimedInterceptorGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. Encontra invocações de métodos anotados com [Timed]
var invocations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is InvocationExpressionSyntax,
transform: GetTimedInvocation)
.Where(x => x is not null);
// 2. Combina com informações de compilação
var combined = invocations.Combine(context.CompilationProvider);
// 3. Gera o arquivo de interceptors
context.RegisterSourceOutput(combined, GenerateInterceptor);
}
private static InvocationInfo? GetTimedInvocation(
GeneratorSyntaxContext ctx, CancellationToken ct)
{
var invocation = (InvocationExpressionSyntax)ctx.Node;
var symbol = ctx.SemanticModel.GetSymbolInfo(invocation, ct).Symbol
as IMethodSymbol;
if (symbol is null) return null;
// Verifica se o método tem [Timed]
var timedAttr = symbol.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.Name == "TimedAttribute");
if (timedAttr is null) return null;
var location = invocation.GetLocation().GetLineSpan();
return new InvocationInfo(
FilePath: location.Path,
Line: location.StartLinePosition.Line + 1,
Character: location.StartLinePosition.Character + 1,
MethodName: symbol.Name,
TypeName: symbol.ContainingType.ToDisplayString()
);
}
private static void GenerateInterceptor(
SourceProductionContext ctx,
(InvocationInfo? Inv, Compilation Comp) source)
{
var inv = source.Inv;
if (inv is null) return;
var code = $$"""
// <auto-generated/>
using System.Runtime.CompilerServices;
using System.Diagnostics;
public static class {{inv.MethodName}}_TimedInterceptor
{
[InterceptsLocation({{inv.FilePath.Replace("\\", "\\\\")}}, {{inv.Line}}, {{inv.Character}})]
public static void {{inv.MethodName}}_Timed(this {{inv.TypeName}} target)
{
var sw = Stopwatch.StartNew();
target.{{inv.MethodName}}();
Console.WriteLine($"{{inv.MethodName}} levou {sw.ElapsedMilliseconds}ms");
}
}
""";
ctx.AddSource($"{inv.MethodName}_interceptor.g.cs", code);
}
}
record InvocationInfo(string FilePath, int Line, int Character, string MethodName, string TypeName);
Caso de uso 2: cache de resultado sem proxy
// Atributo marcador
[AttributeUsage(AttributeTargets.Method)]
public sealed class CacheResultAttribute(int ttlSeconds = 60) : Attribute
{
public int TtlSeconds { get; } = ttlSeconds;
}
// Método original — o Source Generator detecta e gera o interceptor
public class ProdutoService
{
[CacheResult(ttlSeconds: 300)]
public async Task<List<Produto>> ListarAtivosAsync()
{
return await _db.Produtos.Where(p => p.Ativo).ToListAsync();
}
}
// Interceptor gerado (simplificado):
public static class ProdutoServiceInterceptors
{
private static readonly MemoryCache _cache = new(new MemoryCacheOptions());
[InterceptsLocation("Services/ProdutoService.cs", 12, 18)]
public static async Task<List<Produto>> ListarAtivosAsync_Cached(
this ProdutoService service)
{
const string key = "ProdutoService.ListarAtivosAsync";
if (_cache.TryGetValue(key, out List<Produto>? cached))
return cached!;
var result = await service.ListarAtivosAsync();
_cache.Set(key, result, TimeSpan.FromSeconds(300));
return result;
}
}
Comparação com Decorators
O padrão Decorator resolve o mesmo problema de forma mais explícita e compatível. Quando escolher cada um:
| Critério | Decorator | Interceptor |
|---|---|---|
| Legibilidade | Explícito, rastreável | Transparente ao leitor |
| Maturidade | Estável há décadas | Experimental (C# 12+) |
| Overhead | Uma indireção de interface | Zero |
| AOT | ✅ | ✅ |
| Aplicação em massa | Requer registro manual | Source Generator automatiza |
| Depuração | Simples — stack trace claro | Stack trace aponta para interceptor gerado |
Use Decorators para cross-cutting concerns em serviços registrados na DI — é a solução madura, bem conhecida pelo time e fácil de testar. Use Interceptors quando você está construindo um framework/biblioteca que precisa ser transparente para quem usa, ou quando está gerando código AOT-safe em larga escala (como faz o ASP.NET Core).
Limitações atuais
- Experimental: a API pode mudar em versões futuras. A Microsoft sinalizou intenção de estabilizar, mas sem data confirmada.
- Localização frágil: o interceptor aponta para linha e coluna exatas. Refatorações que movem código quebram o interceptor se o Source Generator não regenerar.
- Visibilidade limitada: desenvolvedores que não conhecem o mecanismo podem se confundir ao depurar — o código em tela não é o que executa.
- Não funciona com métodos virtuais via polimorfismo: o interceptor substitui a chamada no ponto de chamada estático, não na implementação do método em si.
- Source Generator obrigatório para uso em escala: escrever interceptors manualmente (arquivo + linha + coluna) é impraticável. O uso real exige sempre um gerador.
Como o ASP.NET Core usa internamente
No .NET 8+, quando você publica uma Minimal API com PublishAot=true, o Source Generator de roteamento analisa todos os app.MapGet(), app.MapPost() etc. e gera:
- Código de parsing de parâmetros sem reflection
- Código de serialização JSON via
JsonSerializerContext - Interceptors que substituem as chamadas
MapGet/MapPostpelas versões AOT-safe geradas
Você escreve app.MapGet("/produtos", GetProdutos) e a API gerada em produção não usa reflection nem DynamicMethod — tudo foi resolvido em compile time, inclusive o binding de parâmetros e a serialização.
Conclusão
Interceptors são uma ferramenta poderosa para autores de frameworks e bibliotecas que precisam de zero overhead runtime e compatibilidade AOT. Para aplicações de negócio do dia a dia, o padrão Decorator continua sendo mais legível, mais estável e igualmente eficiente. O valor real dos Interceptors emerge quando combinados com Source Generators — especialmente em contextos onde você precisa escalar cross-cutting concerns para centenas de pontos de chamada sem exigir mudanças no código do usuário.
Precisa resolver isso na prática?
Se você está construindo uma biblioteca ou framework .NET e precisa de geração de código AOT-safe, podemos ajudar a desenhar a arquitetura certa — Source Generators, Interceptors e tudo mais.
Falar com um especialista →