Airflow.MSDI.AutoRegister
1.2.0
dotnet add package Airflow.MSDI.AutoRegister --version 1.2.0
NuGet\Install-Package Airflow.MSDI.AutoRegister -Version 1.2.0
<PackageReference Include="Airflow.MSDI.AutoRegister" Version="1.2.0" />
<PackageVersion Include="Airflow.MSDI.AutoRegister" Version="1.2.0" />
<PackageReference Include="Airflow.MSDI.AutoRegister" />
paket add Airflow.MSDI.AutoRegister --version 1.2.0
#r "nuget: Airflow.MSDI.AutoRegister, 1.2.0"
#:package Airflow.MSDI.AutoRegister@1.2.0
#addin nuget:?package=Airflow.MSDI.AutoRegister&version=1.2.0
#tool nuget:?package=Airflow.MSDI.AutoRegister&version=1.2.0
Airflow.MSDI.AutoRegister
A source generator for Microsoft.Extensions.DependencyInjection (MSDI) that automatically registers your services — no more hand-written AddSingleton/AddScoped/AddTransient boilerplate, no reflection, zero runtime overhead.
中文文档: README.zh-CN.md
Why?
Manually registering dozens of services is tedious and error-prone. Reflection-based scanners (like Scrutor) work, but they cost runtime performance and can't be trimmed. This library moves registration to compile time:
- ✅ Zero runtime reflection — all registrations are emitted as plain C# code
- ✅ AOT / trimming friendly
- ✅ Fast IDE experience with full type safety
- ✅ Works against the MSDI 1.0 API surface (
Microsoft.Extensions.DependencyInjection.Abstractions1.0.0)
Features
[SingletonService],[ScopedService],[TransientService]— the three built-in registration attributes- Register a class by itself, by interface, or both — one attribute = one registration
- Multiple attributes on one class: all singleton registrations resolve the same instance via factory
Ordersupport: when several implementations compete for the same service type, the largest Order wins (registered last); unspecified Order falls back to type-name ordering- Factory method registration: annotate a method to register its return value as a service
- Extensible: write your own attribute by deriving from
BaseServiceAttributeand describing it with[RegistrationOptions] [AutoRegister]on apartialmethod triggers code generation; multiple[AutoRegister]methods each get their own implementation
Quick Start
1. Install
dotnet add package Airflow.MSDI.AutoRegister
2. Decorate your services
[SingletonService]
class ServiceA { }
[ScopedService]
class ServiceB { }
[TransientService]
class ServiceC { }
[SingletonService(typeof(InterfaceD))]
class ServiceD : InterfaceD { }
// Self registration + interface registration share the same singleton instance
[SingletonService]
[SingletonService(typeof(InterfaceE))]
class ServiceE : InterfaceE { }
// Order decides who wins when several types register the same service type
[SingletonService(typeof(InterfaceF), 2)]
class ServiceF : InterfaceF { }
[SingletonService(typeof(InterfaceF), 1)]
class ServiceG : InterfaceF { }
// Factory method registration
class ServiceH
{
private readonly InterfaceF _serviceG;
public ServiceH(InterfaceF serviceG) => _serviceG = serviceG;
[SingletonService]
public ServiceI GetServiceI() => new ServiceI(_serviceG);
[ScopedService]
public ServiceJ GetServiceJ(IServiceProvider sp) => new ServiceJ(_serviceG);
}
3. Generate the registrations
internal static partial class Register
{
[AutoRegister]
public static partial void AddServicesFromCurrentAssembly(IServiceCollection services);
public static IServiceCollection AddServices(this IServiceCollection services)
{
AddServicesFromCurrentAssembly(services);
return services;
}
}
4. Wire it up
var services = new ServiceCollection();
services.AddServices();
The generator emits the body of AddServicesFromCurrentAssembly, roughly equivalent to:
services.AddSingleton<ServiceA>();
services.AddScoped<ServiceB>();
services.AddTransient<ServiceC>();
services.AddSingleton<InterfaceD, ServiceD>();
services.AddSingleton<ServiceE>();
services.AddSingleton<InterfaceE>(sp => sp.GetRequiredService<ServiceE>());
services.AddSingleton<InterfaceF, ServiceG>(); // order 1 first
services.AddSingleton<InterfaceF, ServiceF>(); // order 2 last → wins
services.AddSingleton<ServiceH>(); // factory owner registered as singleton
services.AddSingleton<ServiceI>(sp => sp.GetRequiredService<ServiceH>().GetServiceI());
services.AddScoped<ServiceJ>(sp => sp.GetRequiredService<ServiceH>().GetServiceJ(sp));
5. Viewing the generated code
By default the generated code is not written to disk — it only exists in memory during compilation. If you want to see the generated file, you have two options:
Option A — view it in the IDE (nothing written to disk):
In Solution Explorer, expand Dependencies → Analyzers → Airflow.MSDI.AutoRegister.AutoRegisterGenerator and double-click AutoRegister.Register.AddServicesFromCurrentAssembly.g.cs.
Option B — persist it to disk, refreshed to the latest on every build:
Add the following to your .csproj:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(MSBuildProjectDirectory)/Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Generated/**/*.cs" />
<None Include="Generated/**/*.cs" Visible="false" />
</ItemGroup>
- Whenever the generator runs (i.e. on every real compilation), the
.g.csunderGenerated/is rewritten in full, so its content always matches the current build. - The file is a read-only reference — editing it doesn't affect compilation.
- Keep the
<Compile Remove>line: without it the on-disk file would be compiled again alongside the in-memory copy, causing duplicate-definition errors. - Configure this in whichever project wants the output; the generator project itself needs nothing.
How It Works
Class registration
| Attribute usage | Generated code |
|---|---|
[SingletonService] on C |
services.AddSingleton<C>(); |
[SingletonService(typeof(I))] on C : I |
services.AddSingleton<I, C>(); |
[SingletonService] + [SingletonService(typeof(I))] on C : I |
services.AddSingleton<C>(); + services.AddSingleton<I>(sp => sp.GetRequiredService<C>()); |
The last row is the same-instance rule: when a class registers itself and an interface, the interface resolution points back to the class registration, so I and C resolve to the exact same instance. The same pattern applies to Scoped and Transient — inside a scope, I and C are the same scoped instance.
Closed generic service types are supported as-is:
| Attribute usage | Generated code |
|---|---|
[SingletonService(typeof(I<TA>))] on C : I<TA> |
services.AddSingleton<I<TA>, C>(); |
[SingletonService] + [SingletonService(typeof(I<TA>))] on C : I<TA> |
services.AddSingleton<C>(); + services.AddSingleton<I<TA>>(sp => sp.GetRequiredService<C>()); |
The type arguments are filled in verbatim. Open/unbound generics (typeof(I<>)) and open-constructed generics are rejected with an error. A generic class itself cannot be registered (e.g. class G<T> : I<T> — an error is reported); close the type parameters first by deriving a concrete class: class StringG : G<string> and register StringG — "just wrap it one more layer".
Order
Registrations are sorted by (Order, implementation type name):
- A larger
Orderis registered later, and in MSDI the last registration wins for a service type. SoOrderacts as a priority. - When
Orderis not specified (default0), implementations are ordered by their type name — the user simply doesn't care about the winner.
Factory method registration
- The owning class is automatically registered as a singleton (once per class).
- The method must be a
publicinstance method on a plain class, with no parameters or exactly oneIServiceProviderparameter. - Return type becomes the service type. The generated registration omits the explicit type argument — the service type is inferred from the factory's return type (lambda natural-type inference), so closed generics are registered as-is (
List<TypeA>works, no repetition). Open/unbound generics and arrays are rejected:
// parameterless
services.AddSingleton(sp => sp.GetRequiredService<ServiceH>().GetServiceI());
// with IServiceProvider
services.AddScoped(sp => sp.GetRequiredService<ServiceH>().GetServiceJ(sp));
// closed generic return type, inferred the same way
services.AddSingleton(sp => sp.GetRequiredService<ServiceH>().GetList()); // service type = List<TypeA>
Invalid shapes (static method, non-public, wrong parameters, non-plain owner class, etc.) are silently skipped in v1.0. Since v1.1 the generator reports
AMAR-prefixed diagnostics (errors block compilation, warnings hint).
Custom attributes
Any subclass of BaseServiceAttribute works. Describe your constructor layout with [RegistrationOptions] so the generator knows which argument is the lifetime, the service type, and the order:
[RegistrationOptions(
LifetimeIndex = -1, // no lifetime ctor arg → use DefaultLifetime
ServiceTypeIndex = 0, // ctor arg #0 is the service type
OrderIndex = 1, // ctor arg #1 is the order
DefaultLifetime = ServiceLifetime.Transient,
DefaultServiceType = null,
DefaultOrder = 0)]
public sealed class CustomServiceAttribute : BaseServiceAttribute
{
public CustomServiceAttribute(Type? serviceType = null, double order = 0)
: base(ServiceLifetime.Transient, serviceType, order) { }
}
[CustomService(typeof(InterfaceX))]
public class ServiceX : InterfaceX { }
// → services.AddTransient<InterfaceX, ServiceX>();
Diagnostics
Since v1.1 the generator reports AMAR-prefixed diagnostics for invalid usage — errors block compilation, warnings are hints. Messages are localized: English by default, simplified Chinese via satellite resources; the language follows the IDE/compiler UI language.
| Rule ID | Severity | Description |
|---|---|---|
| AMAR1001 | Error | Factory method owner must be a plain class |
| AMAR1002 | Error | A class containing factory methods cannot have registration attributes |
| AMAR1003 | Error | Factory method cannot be static |
| AMAR1004 | Error | Factory method must be public |
| AMAR1005 | Error | Factory method cannot be generic |
| AMAR1006 | Error | Factory method cannot return void |
| AMAR1007 | Error | Factory method return type must be a resolvable named type |
| AMAR1008 | Error | Invalid number of factory method parameters |
| AMAR1009 | Error | Single parameter must be IServiceProvider |
| AMAR1010 | Error | Singleton factory method cannot take IServiceProvider |
| AMAR2001 | Error | Registration attribute on an unsupported target |
| AMAR2002 | Error | Service type must be an interface or class |
| AMAR2003 | Error | Generic classes cannot be registered |
| AMAR2005 | Warning | Duplicate registration of the same (ServiceType, ImplType) |
| AMAR2006 | Warning | Inconsistent lifetimes between self and interface registration |
| AMAR3001 | Error | [AutoRegister] method must be declared partial |
| AMAR3002 | Error | [AutoRegister] method's containing class must be partial |
| AMAR3003 | Error | [AutoRegister] method cannot have a manual implementation |
| AMAR3004 | Error | [AutoRegister] method must be static |
| AMAR3005 | Error | [AutoRegister] method signature must be (IServiceCollection services) |
| AMAR3006 | Warning | No registerable services found in the assembly |
| AMAR3007 | Error | Only one [AutoRegister] method is allowed per assembly |
| AMAR4001 | Error | Missing [RegistrationOptions] |
| AMAR4002 | Error | [RegistrationOptions] index exceeds constructor parameter count |
| AMAR4003 | Error | [RegistrationOptions] index points to a parameter of the wrong type |
| AMAR4004 | Error | Invalid lifetime value |
| AMAR4005 | Warning | Multiple [RegistrationOptions] on the inheritance chain |
Compatibility
- The package depends only on
Microsoft.Extensions.DependencyInjection.Abstractions1.0.0, the oldest MSDI API surface, so the generated code compiles against any modern MSDI container. - The generator targets
netstandard2.0and runs on the .NET SDK's Roslyn (C# 9+ partial methods with accessibility are required for[AutoRegister]).
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.