Compile-time dependency injection auto-registration for .NET — add [Scoped], [Singleton], or [Transient] to your services and AutoWire generates the IServiceCollection registration code at build time.
Zero runtime overhead. No reflection. No startup cost.
| Method | Mean | vs Manual | Allocated |
|---|---|---|---|
| Manual (baseline) | 3.01 µs | 1.0× | 11.24 KB |
| AutoWire | 3.68 µs | 1.2× | 11.24 KB |
| Scrutor | 57.43 µs | 19.1× | 32.42 KB |
AutoWire is ~19× faster than Scrutor at registration time and allocates 65% less memory — because all work happens at compile time with zero reflection.
AutoWire is fully compatible with .NET Native AOT.
Why? AutoWire does all registration work at compile time. It generates normal IServiceCollection calls into your build output, so there is no reflection, no runtime assembly scanning, and no dynamic code generation at startup.
dotnet publish -r linux-x64 -p:PublishAot=trueThis is a major differentiator versus Scrutor and similar reflection-based registration libraries, which rely on runtime scanning and are therefore a poor fit for Native AOT. If you need compile-time DI registration that stays AOT-safe, AutoWire is designed for exactly that.
Every .NET project accumulates this:
// Program.cs — grows forever, breaks when you forget to update itbuilder.Services.AddScoped<IOrderService,OrderService>();builder.Services.AddScoped<IProductService,ProductService>();builder.Services.AddScoped<IInventoryService,InventoryService>();builder.Services.AddSingleton<IEmailSender,SmtpEmailSender>();builder.Services.AddSingleton<ICacheService,RedisCacheService>();builder.Services.AddTransient<IReportGenerator,PdfReportGenerator>();// ... 40 more lines// Put the registration intent next to the class — where it belongs.[Scoped]publicclassOrderService:IOrderService{ ...}[Scoped]publicclassProductService:IProductService{ ...}[Singleton]publicclassRedisCacheService:ICacheService{ ...}[Transient]publicclassPdfReportGenerator:IReportGenerator{ ...}// Program.cs — one line, forever.builder.Services.AddAutoWireServices();AutoWire generates the full registration code at compile time — the code in obj/ is exactly what you'd have written by hand.
dotnet add package AutoWire
That's it. No other packages required.
usingAutoWire;// Register against all implemented non-system interfaces (auto-discovery)[Scoped]publicclassOrderService:IOrderService,IAuditableService{}// → generates: services.AddScoped<IOrderService, OrderService>();// services.AddScoped<IAuditableService, OrderService>();// Register against a specific interface only[Singleton(typeof(ICache))]publicclassRedisCache:ICache,IDisposable{}// → generates: services.AddSingleton<ICache, RedisCache>();// Register as concrete type (no interface)[Transient]publicclassPdfExporter{}// → generates: services.AddTransient<PdfExporter>();// Keyed service (.NET 8+)[Scoped(Key="primary")]publicclassSqlOrderRepository:IOrderRepository{}// → generates: services.AddKeyedScoped<IOrderRepository, SqlOrderRepository>("primary");// ASP.NET Corebuilder.Services.AddAutoWireServices();// Generic Host / Worker Serviceservices.AddAutoWireServices();publicclassCheckoutController(IOrderServiceorders,ICachecache){// resolved from AutoWire-generated registrations}| Attribute | DI lifetime | Generated call |
|---|---|---|
[Scoped] | Scoped | services.AddScoped<TService, TImpl>() |
[Singleton] | Singleton | services.AddSingleton<TService, TImpl>() |
[Transient] | Transient | services.AddTransient<TService, TImpl>() |
[TryScoped] | Scoped | services.TryAddScoped<TService, TImpl>() |
[TrySingleton] | Singleton | services.TryAddSingleton<TService, TImpl>() |
[TryTransient] | Transient | services.TryAddTransient<TService, TImpl>() |
[HostedService] | Singleton | services.AddHostedService<T>() |
[Factory(typeof(IFoo))] | Singleton (factory) + Scoped (product) | services.AddSingleton<FooFactory>() + services.AddScoped<IFoo>(sp => ...) |
[Options("Section")] | — | services.AddOptions<T>().BindConfiguration("Section").ValidateDataAnnotations().ValidateOnStart() |
[HttpClient] | — | services.AddHttpClient<T>() |
[Validate] | Scoped | services.AddScoped<IValidator<T>, ValidatorClass>() |
[Interceptor(typeof(IFoo))] | Scoped (proxy) | Generates a file sealed proxy class; registers it as IFoo implementation |
[Endpoint("GET", "/route")] | — | app.MapGet("/route", ClassName.Handle) via generated MapAutoWireEndpoints() |
All attributes (except [HostedService] and [Factory]) support these shared properties:
| Property | Type | Description |
|---|---|---|
ServiceType | Type? | Single explicit service type. Default: all non-system interfaces |
| (multi-type) | (Type, Type, params Type[]) | Two or more explicit service types in one attribute: [Scoped(typeof(IFoo), typeof(IBar))] |
Key | object? | Keyed service key — accepts a string or any enum value (.NET 8+) |
Duplicate | DuplicateStrategy | Add / Skip / Replace |
IncludeSelf | bool | Also register as concrete type |
Profile | string? | Only register when profile matches |
Condition | string? | Wrap in #if SYMBOL ... #endif at compile time |
IncludeLazy | bool | Also register Lazy<T> via AddTransient |
Module | string? | Place service in a named module — excluded from AddAutoWireServices(), gets its own Add{Module}Module() method |
ConfigKey | string? | Resolve the lifetime at runtime from configuration["AutoWire:Lifetime:" + ConfigKey] ("Scoped"/"Singleton"/"Transient", case-insensitive), falling back to the attribute's declared lifetime when absent/unrecognized. Only applies to the six registration attributes (Scoped/Singleton/Transient/Try*). Requires an IConfiguration? configuration = null parameter on the generated AddAutoWireServices() (added automatically when your project references Microsoft.Extensions.Configuration.Abstractions) |
// Auto-discover all non-system interfaces[Scoped]// Register against one specific interface[Scoped(typeof(IMyService))]// Register against multiple interfaces in one attribute (v1.18.0+)[Scoped(typeof(IOrderReader),typeof(IOrderWriter))]// Keyed service (.NET 8+)[Scoped(Key="keyName")]// Keyed + explicit interface (.NET 8+)[Scoped(typeof(IMyService),Key="keyName")]// Also register the concrete type alongside the interface(s)[Scoped(IncludeSelf=true)]When you want a class available both via its interface and directly by its concrete type, use IncludeSelf = true:
[Scoped(IncludeSelf=true)]publicclassAnalyticsService:IAnalyticsService{}// → services.AddScoped<IAnalyticsService, AnalyticsService>();// → services.AddScoped<AnalyticsService>(); ← additional- Decorator pattern — the inner concrete type needs to be injectable separately
- Integration tests — resolve the real implementation by type while still respecting interface registrations
- Mixed consumers — one consumer needs
IAnalyticsService; another needsAnalyticsServicedirectly
IncludeSelf = true always adds the concrete type, regardless of how many interfaces were registered:
[Scoped(typeof(INotificationService),IncludeSelf=true)]publicclassNotificationService:INotificationService,IEmailSender{}// → services.AddScoped<INotificationService, NotificationService>();// → services.AddScoped<NotificationService>();// IEmailSender is NOT registered — explicit ServiceType controls which interfaces win.All attributes support AllowMultiple = true. You can register a class against several explicitly-specified interfaces using either one attribute with multiple types, or stacked attributes:
// ✅ Preferred (v1.18.0+) — one attribute, multiple types[Scoped(typeof(IOrderReader),typeof(IOrderWriter))]publicclassOrderService:IOrderReader,IOrderWriter,IDisposable{}// → services.AddScoped<IOrderReader, OrderService>();// → services.AddScoped<IOrderWriter, OrderService>();// IDisposable is excluded.// Also works — stacked attributes (equivalent, any version)[Scoped(typeof(IOrderReader))][Scoped(typeof(IOrderWriter))]publicclassOrderService:IOrderReader,IOrderWriter,IDisposable{}AW003 validates all types in the multi-type constructor — any unimplemented interface is a build error.
Control what happens when the same service type is registered more than once using the Duplicate property:
publicenumDuplicateStrategy{Add,// (default) AddScoped — last registration winsSkip,// TryAddScoped — skipped if service type already registeredReplace// RemoveAll + AddScoped — removes all prior registrations, then adds}[Scoped]publicclassDefaultOrderService:IOrderService{}// registered first[Scoped(Duplicate=DuplicateStrategy.Replace)]publicclassPremiumOrderService:IOrderService{}// removes prior, registers itself// → services.AddScoped<IOrderService, DefaultOrderService>(); // then...// → services.RemoveAll<IOrderService>();// → services.AddScoped<IOrderService, PremiumOrderService>();// Result: only PremiumOrderService is registered for IOrderService.[Scoped]publicclassProductionMessageBus:IMessageBus{}// always registered[Scoped(Duplicate=DuplicateStrategy.Skip)]publicclassFallbackMessageBus:IMessageBus{}// skipped — IMessageBus already takenConvenience attributes that always use TryAdd semantics — ideal for NuGet library authors who want to provide sensible defaults without overriding the consumer's registrations:
// In your library:[TryScoped]publicclassDefaultRetryPolicy:IRetryPolicy{}// → services.TryAddScoped<IRetryPolicy, DefaultRetryPolicy>();// Consumer's host:services.AddAutoWireServices();// DefaultRetryPolicy registeredservices.AddScoped<IRetryPolicy,AggressiveRetryPolicy>();// consumer overrides — fine// Or consumer pre-registers before your library:services.AddScoped<IRetryPolicy,AggressiveRetryPolicy>();// registered firstservices.AddAutoWireServices();// TryAdd is skipped — no override[TryScoped] / [TrySingleton] / [TryTransient] are equivalent to [Scoped(Duplicate = DuplicateStrategy.Skip)].
[HostedService] registers a background service with a single attribute — no manual AddHostedService<T>() required.
usingAutoWire;usingMicrosoft.Extensions.Hosting;[HostedService]publicclassDataSyncWorker:BackgroundService{protectedoverrideasyncTaskExecuteAsync(CancellationTokenstoppingToken){while(!stoppingToken.IsCancellationRequested){awaitSyncDataAsync();awaitTask.Delay(TimeSpan.FromMinutes(5),stoppingToken);}}}// Program.cs — one call registers everything, including hosted servicesbuilder.Services.AddAutoWireServices();AutoWire generates:
services.AddHostedService<global::DataSyncWorker>();AddHostedService uses TryAddEnumerable internally — calling AddAutoWireServices() multiple times is safe and idempotent.
AutoWire ships seventeen built-in diagnostics that surface problems as squiggles in the IDE — no runtime surprises.
| ID | Severity | Condition |
|---|---|---|
| AW001 | ⚠ Warning | [Scoped] / [HostedService] etc. applied to an abstract class — will never be registered |
| AW002 | ℹ Info | Multiple non-keyedAdd-strategy registrations for the same service type |
| AW003 | ❌ Error | Explicit ServiceType in [Scoped(typeof(IFoo))] is not implemented by the decorated class |
| AW004 | ⚠ Warning | [Singleton] depends on a [Scoped] service — captive dependency that bypasses scope disposal |
| AW005 | ⚠ Warning | A type matches more than one[AutoWireScan] configuration — first match wins |
| AW006 | ⚠ Warning | [Transient] service implements IDisposable / IAsyncDisposable — container won't track disposal |
| AW007 | ℹ Info | Registered class has no non-system interfaces — consider extracting an abstraction |
| AW008 | ⚠ Warning | [Singleton] injects IHttpContextAccessor or DbContext — both are request/scope-bound |
| AW009 | ⚠ Warning | [HostedService] injects a [Scoped] service — captive dependency in a long-lived background worker |
| AW010 | ⚠ Warning | Duplicate [Interceptor] targets — two attributes on the same class target the same interface |
| AW011 | ⚠ Warning | [Interceptor] target interface has no interceptable methods — proxy would be empty |
| AW012 | ❌ Error | [DecorateScoped] / [DecorateSingleton] / [DecorateTransient] targets a service the decorator does not implement |
| AW013 | ⚠ Warning | A registered service constructor depends on a type that is not registered in the AutoWire graph |
| AW014 | ❌ Error | [ScanAssembly] marker type does not come from a referenced assembly |
| AW015 | ⚠ Warning | [ScanAssembly] resolved the assembly, but found no AutoWire-attributed public services in it |
| AW016 | ❌ Error | Circular dependency detected between AutoWire-registered services' constructors |
| AW017 | ℹ Info | An AutoWire-registered service appears unused anywhere in the compilation |
// ⚠ AW001: Abstract class 'BaseHandler' decorated with [Scoped] will not be registered.[Scoped]publicabstractclassBaseHandler:IHandler{}// ❌ AW003 Error: 'ReportService' does not implement 'IOrderService'.[Scoped(typeof(IOrderService))]publicclassReportService:IReportService{}// wrong type!// ✅ Fix:[Scoped(typeof(IReportService))]publicclassReportService:IReportService{}// ⚠ AW004: Singleton 'ReportingService' depends on Scoped service 'IOrderService'.// The scoped service will be captured for the singleton's lifetime, bypassing scope disposal.[Singleton]publicclassReportingService{publicReportingService(IOrderServiceorders){_orders=orders;}// ← captive!}// ✅ Fix — inject IServiceScopeFactory and create a scope explicitly:[Singleton]publicclassReportingService{privatereadonlyIServiceScopeFactory_scopeFactory;publicReportingService(IServiceScopeFactoryscopeFactory){_scopeFactory=scopeFactory;}publicvoidGenerateReport(){usingvarscope=_scopeFactory.CreateScope();varorders=scope.ServiceProvider.GetRequiredService<IOrderService>();// ... use orders within the scope}}// ⚠ AW009: 'DataSyncWorker' is a HostedService (effectively Singleton) and injects 'IOrderService' which is Scoped.[HostedService]publicclassDataSyncWorker:BackgroundService{publicDataSyncWorker(IOrderServiceorders){}// ← captive!}// ✅ Fix — inject IServiceScopeFactory (quick-fix available via Alt+Enter):[HostedService]publicclassDataSyncWorker:BackgroundService{privatereadonlyIServiceScopeFactory_scopeFactory;publicDataSyncWorker(IServiceScopeFactoryscopeFactory){_scopeFactory=scopeFactory;}protectedoverrideasyncTaskExecuteAsync(CancellationTokenstoppingToken){while(!stoppingToken.IsCancellationRequested){usingvarscope=_scopeFactory.CreateScope();varorders=scope.ServiceProvider.GetRequiredService<IOrderService>();awaitorders.SyncAsync(stoppingToken);awaitTask.Delay(TimeSpan.FromMinutes(5),stoppingToken);}}}// ⚠ AW013: 'OrderService' constructor parameter 'IPaymentGateway' is not registered with AutoWire.[Scoped]publicclassOrderService{publicOrderService(IPaymentGatewaygateway){}}// ✅ Fix — either register it manually in Program.cs...builder.Services.AddScoped<IPaymentGateway,StripePaymentGateway>();// ...or make the implementation discoverable by AutoWire:[Scoped]publicclassStripePaymentGateway:IPaymentGateway{}// ⚠ AW010: Multiple [Interceptor] attributes on 'LoggingInterceptor' target the same interface 'IOrderService'.[Interceptor(typeof(IOrderService))][Interceptor(typeof(IOrderService))]// ← duplicate!publicclassLoggingInterceptor:IAutoWireInterceptor{ ...}// ✅ Fix — remove the duplicate, or use separate interceptor classes:[Interceptor(typeof(IOrderService))]publicclassLoggingInterceptor:IAutoWireInterceptor{ ...}// ⚠ AW011: The interface 'IEmptyContract' passed to [Interceptor] on 'MyInterceptor'// has no non-generic, non-ref/out instance methods. The proxy will be empty.publicinterfaceIEmptyContract{}// no methods![Interceptor(typeof(IEmptyContract))]publicclassMyInterceptor:IAutoWireInterceptor{ ...}// ✅ Fix — pass an interface that has instance methods:publicinterfaceIOrderService{stringGetStatus();}[Interceptor(typeof(IOrderService))]publicclassMyInterceptor:IAutoWireInterceptor{ ...}AW004 covers both [Singleton] and [TrySingleton], and detects scoped services registered via [Scoped] or [TryScoped].
// ❌ AW016 Error: Circular dependency detected: OrderService -> InvoiceService -> OrderService.[Scoped]publicclassOrderService{publicOrderService(IInvoiceServiceinvoices){}}[Scoped]publicclassInvoiceService:IInvoiceService{publicInvoiceService(OrderServiceorders){}// ← cycle!}// ✅ Fix — break the cycle with IServiceScopeFactory, a Lazy<T> dependency, or an event/callback abstraction.// ℹ AW017 Info: Service 'IReportArchiver' is registered by AutoWire but does not appear to be used anywhere in this compilation.[Scoped]publicclassReportArchiver:IReportArchiver{}// No constructor anywhere takes an IReportArchiver, and no GetService<IReportArchiver>() call exists.// Often harmless (e.g. consumed by a separate assembly), but worth double-checking for dead code.Use Condition to gate a registration behind a preprocessor symbol. AutoWire wraps the generated line(s) in #if ... #endif:
// Only registered in DEBUG builds[Scoped(Condition="DEBUG")]publicclassMockEmailService:IEmailService{}// Only registered when FEATURE_REDIS is defined[Singleton(typeof(ICacheService),Condition="FEATURE_REDIS")]publicclassRedisCache:ICacheService{}Generated output:
#if DEBUGservices.AddScoped<IEmailService,MockEmailService>();
#endif
#if FEATURE_REDISservices.AddSingleton<ICacheService,RedisCache>();
#endifCondition combines naturally with Profile:
[Scoped(Profile="staging",Condition="DEBUG")]publicclassStagingMockService:IMyService{}// → only registered when profile == "staging" AND DEBUG is definedUse IncludeLazy = true to also register Lazy<T> so that services can take optional or deferred dependencies:
[Singleton(IncludeLazy=true)]publicclassHeavyService:IHeavyService{}// Generates:// services.AddSingleton<IHeavyService, HeavyService>();// services.AddTransient<Lazy<IHeavyService>>(sp => new Lazy<IHeavyService>(() => sp.GetRequiredService<IHeavyService>()));Consumers can then inject Lazy<IHeavyService> to defer instantiation until first use:
publicclassReportController(Lazy<IHeavyService>heavy){publicvoidOnDemand()=>heavy.Value.Load();}Use [Factory] when a service can't be directly instantiated by the DI container — for example when it needs runtime parameters, connection strings, or configuration values that require non-trivial construction logic.
[Factory(typeof(IDbConnection))]publicclassDbConnectionFactory{privatereadonlyIConfiguration_config;publicDbConnectionFactory(IConfigurationconfig){_config=config;}publicIDbConnectionCreate()=>newSqlConnection(_config.GetConnectionString("Default"));}AutoWire generates two registrations:
// The factory class itself — Singleton so it's created onceservices.AddSingleton<DbConnectionFactory>();// The product — resolved via the factory's Create() methodservices.AddScoped<IDbConnection>(sp =>sp.GetRequiredService<DbConnectionFactory>().Create());| Property | Default | What it controls |
|---|---|---|
Lifetime | "Scoped" | Lifetime of the product (IDbConnection) |
FactoryLifetime | "Singleton" | Lifetime of the factory class itself |
// Singleton product (e.g. read-once config reader)[Factory(typeof(IConfigReader),Lifetime="Singleton")]publicclassConfigReaderFactory{ ...}// Transient product with scoped factory[Factory(typeof(IToken),Lifetime="Transient",FactoryLifetime="Scoped")]publicclassTokenFactory{ ...}For large codebases where adding an attribute to every class is impractical, AutoWire supports convention-based scanning via an assembly-level attribute.
// At the top of any .cs file in your project (e.g. ScanConfig.cs)[assembly:AutoWire.AutoWireScan("MyApp.Services")]AutoWire will scan every non-abstract class in that namespace (and sub-namespaces by default) and register it as if you had written [Scoped] on each one.
- Concrete, non-abstract classes in the target namespace
- Classes without an existing
[Scoped],[Singleton],[Transient]etc. attribute (explicit always wins) - Classes without
[AutoWireExclude] - Open-generic classes are skipped (they need an explicit registration)
[AutoWireExclude]publicclassInternalHelper:IHelper{}// skipped by scanning// All classes in the namespace registered as Singleton[assembly:AutoWire.AutoWireScan("MyApp.Services",Lifetime="Singleton")]// Or Transient[assembly:AutoWire.AutoWireScan("MyApp.Adapters",Lifetime="Transient")]Sub-namespaces (MyApp.Services.Impl, MyApp.Services.Adapters, etc.) are included by default. Opt out with IncludeSubNamespaces = false:
[assembly:AutoWire.AutoWireScan("MyApp.Services",IncludeSubNamespaces=false)]Stack multiple [AutoWireScan] attributes for different lifetimes or namespaces:
[assembly:AutoWire.AutoWireScan("MyApp.Services")]// Scoped (default)[assembly:AutoWire.AutoWireScan("MyApp.Repositories",Lifetime="Singleton")]// Singletons[assembly:AutoWire.AutoWireScan("MyApp.Adapters",Lifetime="Transient")]// TransientsScanning and explicit attributes compose naturally. Explicit [Scoped] etc. on a class always takes priority — scanned registrations won't duplicate it:
[assembly:AutoWire.AutoWireScan("MyApp.Services")]// This class is in MyApp.Services but has an explicit key — the explicit wins.[Scoped(Key="v2")]publicclassOrderServiceV2:IOrderService{}// This class has no attribute — picked up by scanning.publicclassInvoiceService:IInvoiceService{}Use Profile to conditionally register services based on environment or deployment context:
// Always registered (no profile)[Scoped]publicclassInMemoryCache:ICache{}// Only registered when profile matches[Scoped(Profile="production")]publicclassRedisCache:ICache{}[Scoped(Profile="testing")]publicclassNullCache:ICache{}// Register all unprofiled services + "production" servicesbuilder.Services.AddAutoWireServices(profile:"production");// Register only unprofiled services (default — unchanged behaviour)builder.Services.AddAutoWireServices();The profile parameter defaults to null. All existing projects that call AddAutoWireServices() without arguments are unaffected.
publicstaticIServiceCollectionAddAutoWireServices(thisIServiceCollectionservices,string?profile=null){services.AddScoped<ICache,InMemoryCache>();// alwaysif(profile=="production")services.AddScoped<ICache,RedisCache>();if(profile=="testing")services.AddScoped<ICache,NullCache>();returnservices;}Note: Profile-specific services use last-registration-wins by default. Use
Duplicate = DuplicateStrategy.Replaceto explicitly remove the default before adding the profile service.
AutoWire fully supports open generic registrations. The correct typeof() overload is generated automatically — no reflection needed.
// Auto-discovers compatible generic interfaces[Scoped]publicclassRepository<T>:IRepository<T>{}// → services.AddScoped(typeof(IRepository<>), typeof(Repository<>));// Explicit service type[Singleton(typeof(IReadOnlyRepository<>))]publicclassCachedRepository<T>:IRepository<T>,IReadOnlyRepository<T>{}// → services.AddSingleton(typeof(IReadOnlyRepository<>), typeof(CachedRepository<>));// No interface — registers as concrete open generic[Transient]publicclassEventProcessor<T>{}// → services.AddTransient(typeof(EventProcessor<>));Resolving closed generics from DI works automatically:
// All of these work after a single AddAutoWireServices() call:provider.GetRequiredService<IRepository<Order>>();provider.GetRequiredService<IRepository<Product>>();provider.GetRequiredService<EventProcessor<EmailMessage>>();[DecorateScoped] / [DecorateSingleton] / [DecorateTransient] wraps an existing service registration with a decorator class at compile time — no Scrutor dependency required.
// Inner service — registered normally[Scoped]publicclassOrderService:IOrderService{publicstringGetStatus()=>"pending";}// Decorator — wraps IOrderService[DecorateScoped(typeof(IOrderService))]publicclassLoggingOrderService:IOrderService{privatereadonlyIOrderService_inner;// Constructor takes the SERVICE TYPE (IOrderService) — AutoWire injects the concrete innerpublicLoggingOrderService(IOrderServiceinner){_inner=inner;}publicstringGetStatus(){Console.WriteLine("GetStatus called");return_inner.GetStatus();}}// Program.cs — unchangedbuilder.Services.AddAutoWireServices();AutoWire generates:
// 1. Normal registration for inner serviceservices.AddScoped<IOrderService,OrderService>();// 2. Decorator wiring (generated at the end, after all normal registrations)services.RemoveAll<IOrderService>();services.AddScoped<OrderService>();// inner concrete self-registered — injectable directlyservices.AddScoped<IOrderService>(sp =>(IOrderService)ActivatorUtilities.CreateInstance(sp,typeof(LoggingOrderService),sp.GetRequiredService<OrderService>()));provider.GetRequiredService<IOrderService>()→LoggingOrderServicewrappingOrderService✓provider.GetRequiredService<OrderService>()→OrderServicedirectly (useful in tests) ✓- The decorator is the same lifetime as the
[DecorateScoped/Singleton/Transient]attribute - No reflection — the inner type is resolved at compile time from AutoWire's own registration map
- AW012 ensures the decorator actually implements the service it claims to wrap
Apply [Decorate*] to a class that decorates multiple service types using AllowMultiple:
[DecorateScoped(typeof(IOrderService))][DecorateScoped(typeof(IReadOnlyOrderService))]publicclassCachingOrderService:IOrderService,IReadOnlyOrderService{ ...}When multiple decorators target the same service type, use Order to control which is innermost (closest to the original) and which is outermost (what consumers receive):
[Scoped]publicclassOrderService:IOrderService{ ...}// Order = 1 → applied first, wraps OrderService directly (inner)[DecorateScoped(typeof(IOrderService),Order=1)]publicclassLoggingOrderService:IOrderService{publicLoggingOrderService(IOrderServiceinner){_inner=inner;}}// Order = 2 → applied second, wraps LoggingOrderService (outer — what you receive)[DecorateScoped(typeof(IOrderService),Order=2)]publicclassCachingOrderService:IOrderService{publicCachingOrderService(IOrderServiceinner){_inner=inner;}}Result: provider.GetRequiredService<IOrderService>() → CachingOrderService(LoggingOrderService(OrderService))
All intermediate types are self-registered so you can inject them directly:
provider.GetRequiredService<LoggingOrderService>();// ✓ inner layerprovider.GetRequiredService<OrderService>();// ✓ originalIf the inner service wasn't registered via AutoWire (e.g. registered manually in Program.cs), AutoWire generates a runtime fallback that scans the IServiceCollection to find and wrap the existing registration automatically.
[Options] generates the full AddOptions<T>().BindConfiguration().ValidateDataAnnotations().ValidateOnStart() chain — eliminating boilerplate for every configuration class.
Requires Microsoft.Extensions.Options.ConfigurationExtensions, Microsoft.Extensions.Options.DataAnnotations, and Microsoft.Extensions.Hosting.Abstractions.
// Section name is "Database" (explicit)[Options("Database")]publicclassDatabaseOptions{[Required]publicstringConnectionString{get;set;}="";publicintMaxConnections{get;set;}=10;}// Section name derived from class name: "EmailOptions" → "Email"[Options]publicclassEmailOptions{publicstringSmtpHost{get;set;}="localhost";}// Opt out of validation[Options("Minimal",ValidateDataAnnotations=false,ValidateOnStart=false)]publicclassMinimalOptions{}Generated:
services.AddOptions<DatabaseOptions>().BindConfiguration("Database").ValidateDataAnnotations().ValidateOnStart();| Property | Default | Description |
|---|---|---|
| Constructor arg | class name (sans "Options") | Configuration section key |
ValidateDataAnnotations | true | Chain .ValidateDataAnnotations() |
ValidateOnStart | true | Chain .ValidateOnStart() — throws on invalid config at startup |
[HttpClient] generates services.AddHttpClient<T>() with optional named-client configuration. Requires Microsoft.Extensions.Http.
// Simple typed client[HttpClient]publicclassWeatherApiClient{publicWeatherApiClient(HttpClienthttp){Http=http;}publicHttpClientHttp{get;}}// → services.AddHttpClient<WeatherApiClient>();// Named client with base address[HttpClient(Name="GitHub",BaseAddress="https://api.github.com")]publicclassGitHubApiClient{publicGitHubApiClient(HttpClienthttp){Http=http;}publicHttpClientHttp{get;}}// → services.AddHttpClient("GitHub", c => c.BaseAddress = new Uri("https://api.github.com"))// .AddTypedClient<GitHubApiClient>();Set Resilience = true to chain .AddStandardResilienceHandler(), which adds retry, circuit-breaker, and timeout policies backed by Microsoft.Extensions.Http.Resilience:
[HttpClient(Resilience=true)]publicclassPaymentApiClient{publicPaymentApiClient(HttpClienthttp){Http=http;}publicHttpClientHttp{get;}}// → services.AddHttpClient<PaymentApiClient>()// .AddStandardResilienceHandler();| Property | Default | Description |
|---|---|---|
Name | null (typed client) | Named-client name |
BaseAddress | null | Sets HttpClient.BaseAddress |
Resilience | false | Chains .AddStandardResilienceHandler() |
Timeout | 0 (no timeout set) | Sets HttpClient.Timeout via TimeSpan.FromSeconds(n) |
DefaultHeaders | null | string[] of "Key:Value" pairs — emits c.DefaultRequestHeaders.Add(...) |
UseFactory | false | Registers via IHttpClientFactory instead of typed client — requires Name to be set |
When multiple consumers share the same named client configuration, use UseFactory = true. AutoWire registers the named client once and emits a factory delegate that resolves it via IHttpClientFactory:
[HttpClient(Name="GitHub",BaseAddress="https://api.github.com",UseFactory=true)]publicclassGitHubReposService{ ...}// → services.AddHttpClient("GitHub", c => c.BaseAddress = new Uri("https://api.github.com"));// → services.AddScoped(sp =>// sp.GetRequiredService<IHttpClientFactory>().CreateClient("GitHub"));This lets other classes also call factory.CreateClient("GitHub") without duplicating the configuration.
// Timeout + default headers[HttpClient(BaseAddress="https://api.myservice.com",Timeout=30,DefaultHeaders=new[]{"Accept:application/json","X-App-Id:myapp"})]publicclassMyApiClient{publicMyApiClient(HttpClienthttp){Http=http;}publicHttpClientHttp{get;}}// → services.AddHttpClient<MyApiClient>(static c => {// c.BaseAddress = new Uri("https://api.myservice.com");// c.Timeout = TimeSpan.FromSeconds(30);// c.DefaultRequestHeaders.Add("Accept", "application/json");// c.DefaultRequestHeaders.Add("X-App-Id", "myapp");// });[Validate] auto-registers a FluentValidation validator with a single attribute — no manual services.AddScoped<IValidator<T>, MyValidator>() required.
Requires FluentValidation (or FluentValidation.DependencyInjectionExtensions). AutoWire only emits the registration code; it does not depend on FluentValidation itself.
usingFluentValidation;usingAutoWire;[Validate]publicclassCreateOrderRequestValidator:AbstractValidator<CreateOrderRequest>{publicCreateOrderRequestValidator(){RuleFor(x =>x.CustomerId).NotEmpty();RuleFor(x =>x.Items).NotEmpty();}}// → services.AddScoped<IValidator<CreateOrderRequest>, CreateOrderRequestValidator>();AutoWire walks the inheritance chain to find AbstractValidator<T> and extracts T automatically. Registration is Scoped (matching FluentValidation conventions).
Inject IValidator<T> normally:
publicclassOrderController(IValidator<CreateOrderRequest>validator,IOrderServiceorders){publicasyncTask<IActionResult>Create(CreateOrderRequestrequest){varresult=awaitvalidator.ValidateAsync(request);if(!result.IsValid)returnBadRequest(result.Errors);// ...}}[Interceptor(typeof(IMyService))] generates a compile-time proxy class that wraps every method of the target interface through an IAutoWireInterceptor implementation — no Castle.DynamicProxy, no Autofac required.
usingAutoWire;[Interceptor(typeof(IOrderService))]publicclassLoggingInterceptor:IAutoWireInterceptor{privatereadonlyILogger<LoggingInterceptor>_logger;publicLoggingInterceptor(ILogger<LoggingInterceptor>logger){_logger=logger;}publicvoidIntercept(IAutoWireInvocationinvocation){_logger.LogInformation("Calling {Method}",invocation.MethodName);// Proceed is implicit — the proxy calls this interceptor once per method.// To return a value: invocation.Result = /* computed value */;}}AutoWire generates a file sealed proxy class and registers it as the IOrderService implementation:
// Generated:services.AddScoped<IOrderService>(sp =>newAutoWire_Proxy_OrderService_with_LoggingInterceptor(sp.GetRequiredService<LoggingInterceptor>()));services.AddScoped<LoggingInterceptor>();filesealedclassAutoWire_Proxy_OrderService_with_LoggingInterceptor:IOrderService{privatereadonlyLoggingInterceptor_interceptor;// ... proxy methods that call _interceptor.Intercept(invocation)}publicinterfaceIAutoWireInterceptor{voidIntercept(IAutoWireInvocationinvocation);}publicinterfaceIAutoWireInvocation{stringMethodName{get;}object?[]Arguments{get;}object?Result{get;set;}// set this to return a value from a non-void method}The proxy is registered with the same lifetime as the interceptor. Override with Lifetime:
[Interceptor(typeof(IOrderService),Lifetime="Singleton")]publicclassCachingInterceptor:IAutoWireInterceptor{ ...}| Method kind | Supported |
|---|---|
void methods | ✅ |
| Value-returning methods | ✅ — set invocation.Result |
Task / ValueTask | ✅ |
Task<T> / ValueTask<T> | ✅ — set invocation.Result |
| Generic methods | ⛔ Skipped (proxy emits no wrapper) |
ref / out parameters | ⛔ Skipped |
AW010 warns when two [Interceptor] attributes on the same class target the same interface. Only the first is registered; the duplicate is dropped silently.
Use the Module property to group related services into an opt-in named module. Module services are excluded from AddAutoWireServices() and instead get their own generated extension method.
// These services are NOT in AddAutoWireServices() — they're in AddPaymentsModule()[Scoped(Module="Payments")]publicclassBankTransferService:IPaymentService{}[Scoped(Module="Payments")]publicclassStripePaymentService:IPaymentService{}// Different module[Singleton(Module="Notifications")]publicclassSmsChannel:INotificationChannel{}// Program.csbuilder.Services.AddAutoWireServices();// core services only// Enable modules you wantbuilder.Services.AddPaymentsModule();builder.Services.AddNotificationsModule();AutoWire generates a separate extension method for each unique module name:
publicstaticIServiceCollectionAddPaymentsModule(thisIServiceCollectionservices){services.AddScoped<IPaymentService,BankTransferService>();services.AddScoped<IPaymentService,StripePaymentService>();returnservices;}- Feature flags — ship code for a feature but only activate it when the module is registered
- Optional integrations — separate "core" from "Azure Storage", "Stripe", "SendGrid" modules
- Microservice extraction — move a module to its own project incrementally without breaking callers
Use [ScanAssembly] when your composition-root project needs to include services decorated in a different referenced assembly:
usingAutoWire;[assembly:ScanAssembly(typeof(MyApp.Core.MarkerType))][assembly:ScanAssembly(typeof(MyApp.Infrastructure.MarkerType))]AutoWire inspects the referenced assemblies' metadata at compile time and includes any public classes decorated with:
[Scoped][Singleton][Transient][TryScoped][TrySingleton][TryTransient]
This is ideal for layered solutions where the startup project wants to register services declared in another project without falling back to Scrutor runtime scanning.
- AW014 — marker type is not from a referenced assembly (for example, you passed a type from the current project)
- AW015 — the target assembly resolved, but AutoWire found no attributed public services in it
[AutoWireScan] can still scan a different assembly by pointing its AssemblyOf property at any public type from that assembly:
// Scan the "External.Services" namespace in the assembly that contains MarkerType[assembly:AutoWireScan("External.Services",AssemblyOf=typeof(External.MarkerType))]AutoWire scans only public non-abstract classes from the external assembly and respects [AutoWireExclude] for classes in your own assembly.
AutoWire generates AutoWireRegistrationSummary.g.cs alongside the extension method — a compile-time snapshot of every service count, useful for startup logging and diagnostics:
usingAutoWire;// In your startup code:logger.LogInformation("AutoWire registered {Total} services ({Scoped} scoped, {Singleton} singleton, {Transient} transient)",RegistrationSummary.TotalCount,RegistrationSummary.ScopedCount,RegistrationSummary.SingletonCount,RegistrationSummary.TransientCount);Available constants: TotalCount · ScopedCount · SingletonCount · TransientCount · HostedServiceCount · FactoryCount · HttpClientCount · ModuleServiceCount · RegisteredImplementations (string array).
AutoWire also generates AutoWireDependencyGraph.g.cs with a compile-time Mermaidgraph TD diagram of every registration and its constructor-dependency edges — paste it into mermaid.live or render it in any Markdown viewer that supports Mermaid:
usingAutoWire;File.WriteAllText("dependency-graph.mmd",AutoWireDependencyGraph.Mermaid);Set ConfigKey on any of the six registration attributes to let ops/config decide the lifetime at runtime instead of baking it in at compile time:
[Scoped(ConfigKey="OrderService")]publicclassOrderService:IOrderService{}// appsettings.json
{
"AutoWire": {
"Lifetime": {
"OrderService": "Singleton"
}
}
}// Pass IConfiguration through — the parameter is added automatically to AddAutoWireServices()// once your project references Microsoft.Extensions.Configuration.Abstractions.builder.Services.AddAutoWireServices(configuration:builder.Configuration);At runtime AutoWire reads configuration["AutoWire:Lifetime:OrderService"], matches it case-insensitively against "Scoped"/"Singleton"/"Transient", and falls back to the attribute's own declared lifetime (Scoped in the example above) when the key is absent or unrecognized. Services without a ConfigKey are completely unaffected and keep generating the exact same code as before.
Map a class with a public static Handle/HandleAsync method as a minimal API route without hand-wiring app.MapGet(...) calls:
usingAutoWire;usingMicrosoft.AspNetCore.Http;[Endpoint("GET","/orders/{id}")]publicstaticclassGetOrder{publicstaticIResultHandle(intid,IOrderServiceorders)=>Results.Ok(orders.GetById(id));}// Program.csvarapp=builder.Build();app.MapAutoWireEndpoints();// generated — maps every [Endpoint]-decorated classapp.Run();GET/POST/PUT/DELETE/PATCH map to MapGet/MapPost/MapPut/MapDelete/MapPatch; any other verb falls back to app.MapMethods(route, new[] { method }, handler). MapAutoWireEndpoints() is only generated when at least one [Endpoint] usage exists and your project references ASP.NET Core routing (Microsoft.AspNetCore.Routing.IEndpointRouteBuilder) — projects without ASP.NET Core are unaffected.
AutoWire ships IDE light-bulb fixes for four diagnostics — click the squiggle, press Alt+Enter, and the fix is applied automatically:
| Diagnostic | Fix |
|---|---|
| AW001 — abstract class with attribute | Remove the AutoWire attribute |
| AW003 — ServiceType not implemented | Remove the explicit ServiceType argument |
| AW006 — Transient disposable | Change [Transient] → [Scoped] |
| AW009 — Scoped in HostedService | Replace with IServiceScopeFactory (rewrites constructor + adds private field) |
AutoWire is a Roslyn incremental source generator. At build time it:
- Finds all non-abstract, non-generic classes decorated with
[Scoped],[Singleton], or[Transient] - Resolves the correct service type(s) — explicit or auto-discovered
- Emits
ServiceCollectionExtensions.AddAutoWireServices()into your compilation
The generated file lives in obj/ and looks exactly like hand-written code:
// <auto-generated by AutoWire/>publicstaticpartialclassServiceCollectionExtensions{publicstaticIServiceCollectionAddAutoWireServices(thisIServiceCollectionservices){services.AddScoped<IOrderService,OrderService>();services.AddScoped<IAuditableService,OrderService>();services.AddSingleton<ICache,RedisCache>();services.AddTransient<PdfExporter>();returnservices;}}No reflection. No assembly scanning. No startup cost.
| Approach | Registration | Runtime overhead | Refactor-safe |
|---|---|---|---|
| Manual | Write by hand | None | ❌ Easy to forget |
| Scrutor | Assembly scanning | Reflection at startup | ✅ |
| Injectio | Source generator | None | ✅ |
| AutoWire | Source generator | None | ✅ |
AutoWire differs from Scrutor in that registration happens at compile time — there is no assembly scanning, no reflection, and no startup cost. That also makes AutoWire Native AOT-friendly, whereas Scrutor-style runtime scanning is not. It also differs from Scrutor's convention-based scanning in that intent is expressed directly on the class, making it easy to understand what is registered without reading Startup.cs.
When no ServiceType is specified, AutoWire registers the class against every interface it implements, excluding System.* interfaces (e.g. IDisposable, IComparable). If the class has no non-system interfaces, it is registered as its own concrete type.
[Scoped]publicclassOrderService:IOrderService,IDisposable{publicvoidDispose(){}}// → services.AddScoped<IOrderService, OrderService>();// IDisposable is excluded — "System.IDisposable" is a System.* interfacepublicinterfaceIPaymentGateway{}[Scoped(Key="stripe")]publicclassStripeGateway:IPaymentGateway{}[Scoped(Key="paypal")]publicclassPayPalGateway:IPaymentGateway{}// Resolve by keyvarstripe=serviceProvider.GetRequiredKeyedService<IPaymentGateway>("stripe");// Or inject via [FromKeyedServices]publicclassCheckoutService([FromKeyedServices("stripe")]IPaymentGatewaygateway){}Note: Keyed services require
Microsoft.Extensions.DependencyInjection8.0+. The[Keyed]property is available on all frameworks; the generatedAddKeyedScoped/Singleton/Transientcalls only compile on .NET 8+.
The Key property accepts any enum value, not just strings. AutoWire emits the fully-qualified enum member expression so resolution is type-safe at compile time:
publicenumPaymentProvider{Stripe=1,PayPal=2}[Scoped(Key=PaymentProvider.Stripe)]publicclassStripeGateway:IPaymentGateway{}[Scoped(Key=PaymentProvider.PayPal)]publicclassPayPalGateway:IPaymentGateway{}// Generated:// services.AddKeyedScoped<IPaymentGateway, StripeGateway>(global::PaymentProvider.Stripe);// services.AddKeyedScoped<IPaymentGateway, PayPalGateway>(global::PaymentProvider.PayPal);// Resolve — no magic stringsvargateway=provider.GetKeyedService<IPaymentGateway>(PaymentProvider.Stripe);net6.0 · net7.0 · net8.0 · net9.0 · netstandard2.0 · netstandard2.1
Works with ASP.NET Core, Worker Services, MAUI, Blazor, console apps — any project using Microsoft.Extensions.DependencyInjection.
Q: Can I use [DecorateScoped] with open generic types like IRepository<>?
Not currently — Microsoft.Extensions.DependencyInjection doesn't support factory-based registrations for open generic types, which is required for the decorator pattern. Decorators work with closed generic types (e.g. [DecorateScoped(typeof(IRepository<Order>))]). Register the inner concrete type explicitly for open-generic scenarios.
Q: I'm getting AW004 — what is a captive dependency?
A captive dependency occurs when a Singleton holds a reference to a Scoped service. Since singletons live for the application's lifetime, the scoped service is never released when its scope ends. Fix it by injecting IServiceScopeFactory and creating a short-lived scope inside the method that needs the scoped service.
Q: Can I resolve a service by both its interface and its concrete type?
Yes — use IncludeSelf = true: [Scoped(IncludeSelf = true)]. AutoWire emits an extra services.AddScoped<ConcreteType>() in addition to the normal interface registrations. Useful for tests and the decorator pattern.
Q: Does it work with Worker Services and background jobs?
Yes — use [HostedService] on any class implementing IHostedService or extending BackgroundService. AutoWire generates services.AddHostedService<T>() and handles idempotency automatically.
Q: What if I accidentally specify the wrong service type?
AW003 catches it at compile time with a build error. [Scoped(typeof(IFoo))] on a class that doesn't implement IFoo will fail the build with a clear message rather than exploding at runtime.
Q: What if my decorator doesn't implement the service it's decorating?
AW012 catches that at compile time. [DecorateScoped(typeof(IFoo))] on a class that does not implement IFoo is a build error.
Q: What if I have two services implementing the same interface?
AutoWire registers each independently and emits an AW002 info diagnostic. Use DuplicateStrategy.Replace to make the winner explicit, DuplicateStrategy.Skip to keep the first, or keyed services to disambiguate.
Q: Can I register different implementations per environment (e.g. Production vs Testing)?
Yes — use Profile: [Scoped(Profile = "production")]. Call AddAutoWireServices(profile: "production") to activate profile-specific services alongside unprofiled ones. Services with no Profile are always registered regardless.
Q: Can I scan an entire namespace without adding attributes to every class?
Yes — use [assembly: AutoWire.AutoWireScan("MyApp.Services")]. AutoWire registers every non-abstract class in that namespace (Scoped by default). Use Lifetime = "Singleton" or "Transient" to change the lifetime. Use [AutoWireExclude] on individual classes to opt out. Explicit [Scoped] etc. attributes always take priority over scanning.
Q: Does it work with open generic types?
Yes — AutoWire auto-discovers compatible open generic interfaces and emits services.AddScoped(typeof(IRepo<>), typeof(Repo<>)). See the Open generic types section.
Q: Does it support the decorator pattern?
Yes — use [DecorateScoped(typeof(IService))], [DecorateSingleton], or [DecorateTransient] on a class to wrap an existing registration. AutoWire generates compile-time code that self-registers the inner type and wires the decorator. No Scrutor required.
Q: What if a constructor dependency is registered manually, not by AutoWire?
AW013 is intentionally a warning, not an error. It only means AutoWire could not see that dependency in its generated registration graph. If you register the dependency manually in Program.cs or another extension method, you're fine; otherwise add an AutoWire attribute to the implementation.
Q: I'm writing a NuGet library and don't want to override my consumer's registrations. What should I use?
Use [TryScoped], [TrySingleton], or [TryTransient]. These generate TryAddScoped/Singleton/Transient calls — the registration is silently skipped if the service type is already registered by the consumer.
Q: I'm getting AW011 — what does it mean?
AW011 fires when [Interceptor(typeof(IFoo))] is applied and IFoo has no methods that can be proxied (i.e., no non-generic, non-ref/out instance methods). The proxy class would be generated but would be empty — the interception has no effect. Check that you passed an interface type (not a concrete class), and that the interface declares at least one instance method.
Q: Can I register one class against multiple interfaces? Yes — three ways, in order of preference:
- Single attribute, multiple types (v1.18.0+):
[Scoped(typeof(IOrderReader), typeof(IOrderWriter))]— cleanest, one line. - Stacked attributes:
[Scoped(typeof(IOrderReader))]+[Scoped(typeof(IOrderWriter))]— more verbose, works in all versions. - Auto-discovery: just
[Scoped]— AutoWire registers against ALL non-system interfaces automatically.
AW003 validates all types in the multi-type constructor at compile time.
Q: My test project also uses AutoWire and I'm getting an ambiguous method error.
Add [assembly: AutoWire.AutoWireOptions(MethodName = "AddTestServices")] to your test project. This renames the generated method so each project has a unique one.
Q: Does it work with SpecFlow / xUnit / NUnit test fixtures?
Yes. Call AddAutoWireServices() first, then register stubs/fakes after — last registration wins. See the Testing & SpecFlow section for a full example.
Q: I'm getting CS1061 — 'AddAutoWireServices' not found. What's wrong?
The generated AddAutoWireServices() extension method lives in the AutoWire namespace. Add using AutoWire; to any file that calls it, or add a global using to your project:
// GlobalUsings.csglobalusingAutoWire;Q: Can I see the generated code?
Yes — look in obj/Debug/net9.0/generated/AutoWire/AutoWire.AutoWireGenerator/AutoWireServiceCollectionExtensions.g.cs.
Q: Does it slow down my build? Incremental source generators only re-run when a decorated class changes. The overhead on a clean build is negligible — far less than assembly scanning at runtime.
Q: Can I register FluentValidation validators automatically?
Yes — use [Validate] on your AbstractValidator<T> subclass. AutoWire walks the inheritance chain, extracts T, and emits services.AddScoped<IValidator<T>, MyValidator>(). No manual registration required. AutoWire itself doesn't depend on FluentValidation.
Q: Can I add AOP-style interception without a DI framework like Autofac?
Yes — use [Interceptor(typeof(IMyService))] on a class implementing IAutoWireInterceptor. AutoWire generates a compile-time proxy class (no reflection) that routes every non-generic instance method through Intercept(IAutoWireInvocation). Set invocation.Result to return a value from value-returning methods.
Q: I'm getting AW009 — what does it mean?[HostedService] classes run for the application's lifetime (equivalent to Singleton scope). Injecting a [Scoped] service directly creates a captive dependency — the scoped service is never released when its scope ends. Use the AW009 code fix (Alt+Enter) to automatically rewrite the constructor to accept IServiceScopeFactory instead, then create a short-lived scope inside ExecuteAsync.
Q: What frameworks are supported?net6.0 · net7.0 · net8.0 · net9.0 · netstandard2.0 · netstandard2.1 — any project using Microsoft.Extensions.DependencyInjection. Keyed services require .NET 8+.
Microsoft DI uses last-registration-wins, so you can always override production services after calling AddAutoWireServices():
// SpecFlow [BeforeScenario] hook or xUnit fixtureservices.AddAutoWireServices();// production registrationsservices.AddScoped<IOrderService,FakeOrderService>();// overrides — last winsservices.AddScoped<IEmailSender,NullEmailSender>();services.AddScoped<TService, TImpl>() after production registration works, but with Duplicate = DuplicateStrategy.Replace/keyed services/multiple registrations for the same interface it can silently register a second implementation instead of replacing the first. OverrideService (ships with AutoWire, no extra package) removes every existing registration for the service type first, guaranteeing your test double wins:
usingAutoWire;// OverrideService extension methodsservices.AddAutoWireServices();// Replace with a different implementation type:services.OverrideService<IOrderService,FakeOrderService>();// Scoped by defaultservices.OverrideService<IEmailSender,NullEmailSender>(ServiceLifetime.Singleton);// Or override with a ready-made instance:services.OverrideService<IClock>(newFixedClock(DateTimeOffset.UnixEpoch));If your test project also references AutoWire and decorates test doubles with [Scoped] etc., both assemblies generate AddAutoWireServices(), causing an ambiguous extension method error.
Fix it by adding one line to your test project:
// Any .cs file in the test project, e.g. GlobalUsings.cs[assembly:AutoWire.AutoWireOptions(MethodName="AddTestServices")]Now each project has a distinct method:
services.AddAutoWireServices();// production projectservices.AddTestServices();// test project — no ambiguity// SpecFlow startup classpublicclassTestDependencies:IDependencyInjectionContainerBuilder{publicIServiceCollectionCreateServiceCollection(){varservices=newServiceCollection();// Register all production servicesservices.AddAutoWireServices();// Swap specific services with test doublesservices.AddScoped<IPaymentGateway,StubPaymentGateway>();services.AddScoped<IEmailSender,SpyEmailSender>();returnservices;}}Runnable examples in the samples/ folder:
| Sample | What it shows |
|---|---|
| AutoWire.Sample.Api | Convention scanning, decorators, profiles, keyed services, Scalar UI |
| AutoWire.Sample.Worker | [HostedService], Singleton, Transient in a background Worker |
cd samples/Api && dotnet run
cd samples/Worker && dotnet runI'm the author of AutoWire, AutoMap.Generator (compile-time object mapping), and a suite of 28+ Polly v8 resilience packages. I'm available for consulting on Polly v8 resilience, Azure cloud architecture, and clean .NET design.
→ solidqualitysolutions.com · LinkedIn
🌐 Full suite overview: swevo.github.io
| Package | Description |
|---|---|
| AutoMap.Generator | Compile-time object mapping — [Map(typeof(Dto))] generates ToDto() extension methods. Zero reflection, AOT-safe. |
| AutoDispatch.Generator | Compile-time CQRS dispatcher — [Handler] generates a strongly-typed IDispatcher. No IRequest<T>, no reflection. |
| AutoValidate.Generator | Compile-time FluentValidation wiring — discovers AbstractValidator<T> subclasses and generates AddValidators(). |
| AutoResult.Generator | Compile-time Result<T> monad — [TryWrap] generates Try*() wrappers for sync, async and void methods. |
| AutoLog.Generator | Compile-time high-performance logging — [Log(Level, Message)] on a partial method generates LoggerMessage.Define. AOT-safe. |
| AutoHttpClient.Generator | Compile-time typed HTTP client — [HttpClient] on an interface generates a strongly-typed client. AOT-safe Refit alternative. |
| AutoGuard | Compile-time guard clauses — [AutoGuard] + [NotNull]/[InRange]/[NotEmpty] generates argument checks from constructor parameters. |
| AutoQuery.Generator | Compile-time LINQ query specs — [QuerySpec(typeof(T))] generates Apply(IQueryable<T>). |
MIT © Justin Bannister