콘텐츠로 이동

C# 12 Interceptors — 컴파일 타임 메서드 교체

Interceptors는 C# 12에서 도입된 실험적 기능으로, 특정 호출 사이트(call site)의 메서드 호출을 컴파일 타임에 다른 메서드로 교체합니다. Source Generator와 함께 사용해 AOT 최적화, 성능 개선, 코드 분석 도구 등에 활용됩니다.

주의: .NET 8 기준 실험적 기능([Experimental])입니다. 향후 API가 변경될 수 있습니다.


.csproj
<PropertyGroup>
<InterceptorsPreviewNamespaces>MyApp.Generated</InterceptorsPreviewNamespaces>
</PropertyGroup>

// 원본 코드
namespace MyApp;
public class Greeter
{
public string Hello(string name) => $"Hello, {name}!";
}
// 호출 사이트 (Program.cs, 5번째 줄 14번째 열)
var g = new Greeter();
var msg = g.Hello("World"); // ← 이 호출을 교체
// Generated/Interceptors.cs (Source Generator 출력)
using System.Runtime.CompilerServices;
namespace MyApp.Generated;
file static class GreeterInterceptors
{
[InterceptsLocation("Program.cs", line: 5, character: 14)]
public static string Hello_Intercepted(this Greeter g, string name)
=> $"안녕하세요, {name}!"; // 교체된 구현
}

[Generator]
public class InterceptorGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var calls = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => node is InvocationExpressionSyntax,
transform: (ctx, _) => GetCallInfo(ctx))
.Where(info => info is not null);
context.RegisterSourceOutput(calls, (ctx, info) =>
{
var source = GenerateInterceptor(info!);
ctx.AddSource($"Interceptor_{info!.Location}.g.cs", source);
});
}
}

ASP.NET Core의 Minimal API는 Interceptors를 이용해 JsonSerializer.Deserialize<T>() 호출을 AOT 친화적인 Source Generated 버전으로 자동 교체합니다.

// 원본 호출
app.MapPost("/user", (User u) => Results.Ok(u));
// 컴파일 후 — Interceptor가 AOT 직렬화 코드로 교체
// JsonSerializer.Deserialize<User>(...)
// → UserJsonContext.Default.User 사용
// 원본
logger.LogInformation("User {Id} logged in", userId);
// Interceptor → 컴파일 타임 LoggerMessage 생성으로 교체
// → 런타임 문자열 보간 제거

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class InterceptsLocationAttribute : Attribute
{
public InterceptsLocationAttribute(string filePath, int line, int character) { }
}
  • filePath: 소스 파일 경로 (프로젝트 기준 상대 경로)
  • line: 호출 사이트의 줄 번호 (1 기반)
  • character: 호출 사이트의 열 번호 (1 기반)

항목내용
상태실험적 기능 (Experimental)
적용 범위인스턴스 메서드, 정적 메서드, 확장 메서드
불가 대상생성자, 연산자, 속성 접근자
도구Source Generator 없이 수동 작성도 가능 (비권장)

Terminal window
# 생성된 코드 확인 (obj 폴더)
# obj/Debug/net8.0/generated/MyGenerator/MyGenerator.InterceptorGenerator/
# 빌드 시 진단 활성화 (.csproj)
# <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
# <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
// 생성된 파일 경로를 Visual Studio에서 직접 열기 가능
// 솔루션 탐색기 → 의존성 → 분석기 → MyGenerator

EF Core DbCommandInterceptor는 다른 컨셉이지만 유사한 목적으로 쿼리를 가로챕니다.

// EF Core: SaveChanges 로깅 인터셉터
public class AuditInterceptor : SaveChangesInterceptor
{
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
var context = eventData.Context!;
var entries = context.ChangeTracker.Entries<IAuditable>();
foreach (var entry in entries)
{
if (entry.State == EntityState.Modified)
entry.Entity.UpdatedAt = DateTime.UtcNow;
}
return await base.SavingChangesAsync(eventData, result, ct);
}
}
builder.Services.AddDbContext<AppDbContext>(options =>
options.AddInterceptors(new AuditInterceptor()));

// Interceptors 사용 시 실험적 기능 경고 억제
#pragma warning disable CS9113 // Experimental feature
var msg = g.Hello("World");
#pragma warning restore CS9113

Interceptors는 직접 사용보다는 프레임워크와 라이브러리가 내부적으로 활용하는 메커니즘입니다. ASP.NET Core, EF Core 등이 AOT 최적화를 위해 적극 채택 중이며, 동작 원리를 이해하면 생성된 코드를 읽고 디버깅하는 데 큰 도움이 됩니다. 생성된 파일은 obj/generated/ 폴더에서 직접 확인하고, EF Core의 SaveChangesInterceptor는 별도 개념이지만 함께 이해하면 C# 인터셉터 생태계를 넓게 파악할 수 있습니다.