.NET C# 12 Source Generators Performance

Interceptors no C# 12: alterando comportamento de métodos em tempo de compilação

Aprenda a usar Interceptors do C# 12 para substituir chamadas de método em tempo de compilação sem reflection.

N
Neryx Digital Architects
2 de dezembro de 2025
13 min de leitura
260 profissionais leram
Categoria: .NET Público: Times de engenharia e produto Etapa: Aprendizado

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/MapPost pelas 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 →

Quer transformar esse aprendizado em plano de ação?

Se o tema deste artigo se parece com o momento do seu time, podemos ajudar a decidir o próximo passo com clareza.

Falar com um especialista

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.