Interceptors¶
DecoWeaver uses C# 11's experimental interceptors feature to rewrite dependency injection registration calls at compile time. This page explains how interceptors work and how DecoWeaver uses them.
What are Interceptors?¶
Interceptors are a C# 11 feature that allows you to redirect method calls to different implementations at compile time. They work by annotating a method with [InterceptsLocation] attributes that specify which call sites to intercept.
Basic Example¶
// Original code
Console.WriteLine("Hello");
// Interceptor
file static class Interceptors
{
[InterceptsLocation(version: 1, data: "base64-encoded-hash")]
public static void WriteLine(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
// At compile time, the original call is redirected to the interceptor
When compiled, the original Console.WriteLine("Hello") call is redirected to the interceptor method, which adds a [LOG] prefix.
How DecoWeaver Uses Interceptors¶
DecoWeaver intercepts DI registration calls to apply decorators automatically.
Without DecoWeaver¶
Traditional manual decoration:
services.AddScoped<IUserRepository>(sp =>
{
var impl = new UserRepository(sp.GetRequiredService<IDbContext>());
var cached = new CachingRepository(impl, sp.GetRequiredService<IMemoryCache>());
var logged = new LoggingRepository(cached, sp.GetRequiredService<ILogger>());
return logged;
});
With DecoWeaver¶
Automatic decoration at compile time:
[DecoratedBy<CachingRepository>]
[DecoratedBy<LoggingRepository>]
public class UserRepository : IUserRepository { }
// Your code
services.AddScoped<IUserRepository, UserRepository>();
// DecoWeaver generates an interceptor that rewrites this to:
services.AddScoped<IUserRepository>(/* factory with decorators */);
Generated Interceptor Code¶
When you apply [DecoratedBy] attributes, DecoWeaver generates interceptor code like this:
// Your code
services.AddScoped<IUserRepository, UserRepository>();
// Generated interceptor
file static class DecoWeaverInterceptors
{
[InterceptsLocation(version: 1, data: "base64-encoded-hash")]
public static IServiceCollection AddScoped_IUserRepository_UserRepository(
this IServiceCollection services)
{
// Register undecorated implementation with keyed service
services.AddKeyedScoped<IUserRepository, UserRepository>(
"IUserRepository|UserRepository");
// Register factory that applies decorators
services.AddScoped<IUserRepository>(sp =>
{
// Get undecorated implementation
var inner = sp.GetRequiredKeyedService<IUserRepository>(
"IUserRepository|UserRepository");
// Apply decorators in order
inner = new CachingRepository(
inner,
sp.GetRequiredService<IMemoryCache>());
inner = new LoggingRepository(
inner,
sp.GetRequiredService<ILogger<LoggingRepository>>());
return inner;
});
return services;
}
}
Key Components¶
[InterceptsLocation]: Tells the compiler which call site to intercept- Keyed Service: Registers the undecorated implementation
- Factory Registration: Wraps the implementation with decorators
- File-Scoped Class: Uses
filekeyword to prevent naming collisions
InterceptsLocation Attribute¶
The [InterceptsLocation] attribute identifies which call sites to intercept:
Parameters: - version: Always 1 (format version) - data: A base64-encoded hash that uniquely identifies the location of the code to intercept
The data parameter contains an encoded hash value that points to the specific call site in your source code. This hash is calculated by the source generator based on the file path and position of the method call.
Multiple Locations¶
A single interceptor method can intercept multiple call sites:
[InterceptsLocation(version: 1, data: "hash1")]
[InterceptsLocation(version: 1, data: "hash2")]
[InterceptsLocation(version: 1, data: "hash3")]
public static IServiceCollection AddScoped_IUserRepository_UserRepository(
this IServiceCollection services)
{
// Implementation
}
This allows DecoWeaver to intercept all registrations of UserRepository across your codebase.
File-Scoped Types¶
DecoWeaver uses file-scoped types (file keyword) to prevent naming collisions:
This prevents conflicts when generating interceptors in multiple files.
Viewing Generated Code¶
Visual Studio¶
- Build your project
- Solution Explorer → Show All Files
- Navigate to
obj/Debug/net8.0/generated/DecoWeaver.Generators/ - Open
DecoWeaver.Interceptors.g.cs
Rider¶
- Build your project
- Solution Explorer → Show Generated Files
- Expand the DecoWeaver.Generators node
- Open
DecoWeaver.Interceptors.g.cs
Visual Studio Code¶
- Build your project
- Navigate to
obj/Debug/net8.0/generated/DecoWeaver.Generators/ - Open
DecoWeaver.Interceptors.g.cs
Interceptor Requirements¶
For interceptors to work correctly:
C# Language Version¶
Interceptors require C# 11 or later:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<LangVersion>11</LangVersion> <!-- or "latest" -->
</PropertyGroup>
</Project>
The DecoWeaver package automatically enables the interceptors feature, so you don't need to manually configure any experimental feature flags.
Method Signature Match¶
Interceptor methods must exactly match the signature of the method they intercept:
// Original method
public static IServiceCollection AddScoped<TService, TImplementation>(
this IServiceCollection services)
where TService : class
where TImplementation : class, TService
// Interceptor must match exactly
[InterceptsLocation(...)]
public static IServiceCollection AddScoped<TService, TImplementation>(
this IServiceCollection services)
where TService : class
where TImplementation : class, TService
{
// Implementation
}
Generic Type Decoration Interceptors¶
For closed generic registrations with open generic decorators, DecoWeaver generates interceptors that close decorator types at runtime:
Registration Requirement
DecoWeaver requires closed generic registrations using the AddScoped<TService, TImplementation>() syntax. Open generic registrations using AddScoped(typeof(...), typeof(...)) are NOT intercepted.
// Your code - closed generic registration with open generic decorator
[DecoratedBy<CachingRepository<>>] // Open generic decorator
public class Repository<T> : IRepository<T> where T : class { }
services.AddScoped<IRepository<User>, Repository<User>>(); // Closed registration
// Generated interceptor for this specific closed registration
[InterceptsLocation(version: 1, data: "base64-encoded-hash")]
public static IServiceCollection AddScoped_IRepository_User_Repository_User(
this IServiceCollection services)
{
// Register undecorated implementation with keyed service
services.AddKeyedScoped<IRepository<User>, Repository<User>>(
"IRepository<User>|Repository<User>");
// Register factory that closes the open generic decorator
services.AddScoped<IRepository<User>>(sp =>
{
// Get undecorated implementation
var inner = sp.GetRequiredKeyedService<IRepository<User>>(
"IRepository<User>|Repository<User>");
// Close open generic decorator at runtime: CachingRepository<> → CachingRepository<User>
var decoratorType = typeof(CachingRepository<>).MakeGenericType(typeof(User));
// Resolve dependencies and construct
var cache = sp.GetRequiredService<IMemoryCache>();
return (IRepository<User>)Activator.CreateInstance(decoratorType, inner, cache);
});
return services;
}
Each closed generic registration gets its own interceptor with the decorator closed to the appropriate type.
Debugging Interceptors¶
Compile-Time Errors¶
If an interceptor fails to apply, you'll see compile errors:
Solution: Enable C# 11 and interceptors in your project file.
Solution: Ensure you're using Microsoft.Extensions.DependencyInjection 8.0+.
Runtime Errors¶
If decorators aren't being applied:
- Check that generated files exist in
obj/Debug/generated/ - Verify all decorator dependencies are registered
- Ensure keyed services are supported (.NET 8+)
- Rebuild the solution to regenerate interceptors
Viewing Interceptor Locations¶
Add logging to see which interceptors are generated:
Performance¶
Interceptors have zero runtime overhead:
- No reflection at runtime
- No dynamic code generation
- No assembly scanning
- Direct method calls in generated code
The only cost is during compilation when interceptors are generated.
Limitations¶
Cannot Intercept Non-Public Methods¶
Interceptors only work with public methods:
// ✅ Can intercept
public static IServiceCollection AddScoped<T, TImpl>(this IServiceCollection services)
// ❌ Cannot intercept
internal static IServiceCollection AddScoped<T, TImpl>(this IServiceCollection services)
Cannot Intercept Virtual Methods¶
Virtual methods cannot be intercepted:
// ✅ Can intercept
public static IServiceCollection AddScoped(...)
// ❌ Cannot intercept
public virtual IServiceCollection AddScoped(...)
Cannot Intercept Generic Methods with Constraints¶
Some complex generic constraints may not work:
// ✅ Works
public static void Method<T>() where T : class
// ⚠️ May not work
public static void Method<T>() where T : class, IDisposable, new()
Experimental Feature Status¶
Interceptors are currently experimental in C# 11:
- API may change in future versions
- Some IDEs may show warnings
- Feature may be refined or stabilized in C# 12+
DecoWeaver uses interceptors safely within their documented capabilities. The feature is stable enough for production use with .NET 8+.
Future of Interceptors¶
Microsoft is actively working on interceptors:
- Potential stabilization in future C# versions
- Improved IDE support
- Better debugging experience
- Additional use cases beyond DI
DecoWeaver will continue to support and leverage interceptors as the feature evolves.
Next Steps¶
- Learn about Source Generators
- Understand How It Works in detail
- See Testing Strategies for decorated services