Attribute-based service registration for Microsoft.Extensions.DependencyInjection with a compile-time source generator. Decorate classes with [Singleton], [Transient], or [Scoped] attributes and registrations are generated at build time — no runtime reflection required. Supports conditional registration, NativeAOT, and trimming.
$ dotnet add package X39.Util.DependencyInjectionAttribute-based service registration for Microsoft.Extensions.DependencyInjection with
compile-time source generation and NativeAOT support.
dotnet add package X39.Util.DependencyInjection
Or add directly to your .csproj:
<PackageReference Include="X39.Util.DependencyInjection" Version="*" />
The package includes a Roslyn source generator that runs automatically at compile time. No additional packages are required.
Decorate your service class with a lifetime attribute and call the generated AddDependencies
method during startup:
using X39.Util.DependencyInjection;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddDependencies(context.Configuration);
})
.Build();
host.Run();
using X39.Util.DependencyInjection.Attributes;
public interface IMyService
{
void DoWork();
}
[Singleton<MyService, IMyService>]
public class MyService : IMyService
{
public void DoWork() { /* ... */ }
}
The source generator discovers all attributed classes at compile time and generates the
AddDependencies extension method with explicit registration calls — no runtime reflection needed.
At compile time, the included Roslyn incremental source generator:
[Singleton], [Transient], or [Scoped] attributes.Dependencies.AddDependencies()) containing all
service registrations as direct IServiceCollection calls.The generated code is fully AOT-safe — all types are statically known and no reflection is used at runtime.
For a class like:
[Singleton<MyService, IMyService>]
public class MyService : IMyService
{
[DependencyInjectionCondition]
internal static bool IsEnabled(IConfiguration configuration)
=> configuration.GetValue<bool>("Features:MyService");
}
The generator produces:
// <auto-generated/>
public static class Dependencies
{
public static IServiceCollection AddDependencies(
this IServiceCollection services,
IConfiguration configuration)
{
RuntimeHelpers.RunClassConstructor(typeof(MyApp.MyService).TypeHandle);
if (MyApp.MyService.IsEnabled(configuration))
{
services.AddSingleton<MyApp.IMyService, MyApp.MyService>();
}
return services;
}
}
Because the source generator resolves all registrations at compile time, the generated code is fully compatible with NativeAOT publishing. There is no runtime reflection, no dynamic assembly scanning, and all types are explicitly referenced in the generated output.
By default, the generator creates a class named Dependencies in the
X39.Util.DependencyInjection namespace with an AddDependencies extension method. You can
customize both via MSBuild properties in your .csproj:
<PropertyGroup>
<X39_DependencyInjection_ClassName>MyServices</X39_DependencyInjection_ClassName>
<X39_DependencyInjection_Namespace>MyApp.DI</X39_DependencyInjection_Namespace>
</PropertyGroup>
This produces MyApp.DI.MyServices.AddMyServices(...) instead. The method name is always
Add + the class name.
The source generator reports errors at build time rather than at runtime:
| ID | Description |
|---|---|
X39DI001 | Condition method is private. Change to internal or public for source-generated registration. |
X39DI002 | Multiple dependency injection attributes on the same class. |
X39DI003 | Invalid condition method signature — must be static, return bool, and accept zero parameters or a single IConfiguration parameter. |
Each attribute corresponds to a standard DI lifetime. The generic forms require .NET 7+.
| Attribute | Lifetime | Equivalent call |
|---|---|---|
[Singleton<TService>] | Singleton | AddSingleton<TService>() |
[Singleton<TService, TAbstraction>] | Singleton | AddSingleton<TAbstraction, TService>() |
[Transient<TService>] | Transient | AddTransient<TService>() |
[Transient<TService, TAbstraction>] | Transient | AddTransient<TAbstraction, TService>() |
[Scoped<TService>] | Scoped | AddScoped<TService>() |
[Scoped<TService, TAbstraction>] | Scoped | AddScoped<TAbstraction, TService>() |
In the two-type-parameter form, TService is the implementation class and TAbstraction is the
interface or base class (TService : TAbstraction).
Pre-.NET 7: Non-generic versions are available using typeof(...). These are marked
[Obsolete] on .NET 7+ in favor of the generic forms.
// Without abstraction (registers as itself)
[Singleton(typeof(MyService))]
// With abstraction (note: parameter order is serviceType, actualType)
[Singleton(typeof(IMyService), typeof(MyService))]
Use [DependencyInjectionCondition] on a static method to control whether a service is registered.
The method must be static, return bool, and accept either no parameters or a single
IConfiguration parameter.
Important: Condition methods must be internal or public when using the source generator.
Private condition methods produce a compile-time error (X39DI001).
public interface IMyService
{
bool SomeFunc();
}
[Singleton<DebugService, IMyService>]
public class DebugService : IMyService
{
[DependencyInjectionCondition]
internal static bool Condition()
{
#if DEBUG
return true;
#else
return false;
#endif
}
public bool SomeFunc() => true;
}
[Singleton<ReleaseService, IMyService>]
public class ReleaseService : IMyService
{
[DependencyInjectionCondition]
internal static bool Condition()
{
#if DEBUG
return false;
#else
return true;
#endif
}
public bool SomeFunc() => true;
}
A condition method can also accept IConfiguration to make decisions based on app configuration:
[DependencyInjectionCondition]
internal static bool IsEnabled(IConfiguration configuration)
{
return configuration.GetValue<bool>("Features:MyService");
}
If a class has multiple condition methods, all must return true for the service to be
registered (AND logic).
The library also includes reflection-based registration methods that scan assemblies at runtime. These are useful when source generation is not available or when scanning assemblies outside your project.
All methods are extension methods on IServiceCollection.
AddAttributedServicesOf(IConfiguration, Assembly)Scans the given assembly for classes decorated with lifetime attributes and registers them.
services.AddAttributedServicesOf(configuration, typeof(Program).Assembly);
AddAttributedServicesFromAssemblyOf<T>(IConfiguration)Convenience overload that scans the assembly containing type T.
services.AddAttributedServicesFromAssemblyOf<Program>(configuration);
AddAttributedServicesOf(IConfiguration, AppDomain)Scans all assemblies loaded in the given AppDomain.
services.AddAttributedServicesOf(configuration, AppDomain.CurrentDomain);
Note: The reflection-based methods use runtime type scanning and are not compatible with NativeAOT. Prefer the source-generated
AddDependenciesmethod for new projects.
X39DI002 at
compile time; the reflection-based path throws MultipleDependencyInjectionAttributesPresentException.Inherited = false).Exceptions are thrown by the reflection-based registration path. The source generator reports equivalent issues as compile-time diagnostics instead.
All exceptions derive from DependencyInjectionException.
| Exception | Thrown when |
|---|---|
ActualTypeIsNotMatchingDecoratedTypeException | The TService type parameter does not match the class the attribute is applied to. |
ConditionMethodHasInvalidSignatureException | A [DependencyInjectionCondition] method is not static, does not return bool, or has unsupported parameters. |
MultipleDependencyInjectionAttributesPresentException | A class has more than one lifetime attribute ([Singleton], [Transient], [Scoped]). |
ServiceTypeIsNotImplementingDecoratedTypeException | The decorated class does not implement the TAbstraction type. |
This library follows the principles of Semantic Versioning.
Contributions are welcome! Please submit a pull request or create a discussion to discuss any changes you wish to make.
Be excellent to each other.
By submitting a contribution (pull request, patch, or any other form) to this project, you agree to the following terms:
License Grant. You grant the project maintainer ("Maintainer") and all recipients of the software a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license to use, reproduce, modify, distribute, sublicense, and otherwise exploit your contribution under the terms of the GNU Lesser General Public License v3.0 (LGPL-3.0-only). You additionally grant the Maintainer the right to relicense your contribution under any other open-source or proprietary license at the Maintainer's sole discretion.
Originality. You represent that your contribution is your original work, or that you have sufficient rights to grant the licenses above. If your contribution includes third-party material, you represent that its license is compatible with the LGPL-3.0-only and permits the grants made herein.
No Conflicting Obligations. You represent that your contribution is not subject to any agreement, obligation, or encumbrance (including but not limited to employment agreements or prior license grants) that would conflict with or restrict the rights granted under this agreement.
No Compensation. Your contribution is made voluntarily and without expectation of compensation, unless separately agreed in writing.
Right to Remove. The Maintainer may remove, modify, or replace your contribution at any time, for any reason, without notice or obligation to you.
Liability. To the maximum extent permitted by applicable law, your contribution is provided "as is", without warranty of any kind. You shall be solely liable for any damage arising from the inclusion of your contribution to the extent such damage is caused by a defect, rights violation, or other issue originating in your contribution.
Governing Law. This agreement is governed by the laws of the Federal Republic of Germany (Bundesrepublik Deutschland), in particular the German Civil Code (BGB), without regard to its conflict-of-laws provisions. For contributors outside Germany, this choice of law applies to the extent permitted by the contributor's local jurisdiction.
Please add yourself to the CONTRIBUTORS file when submitting your first pull request, and include the following statement in your pull request description:
I have read and agree to the Contributor License Agreement in this project's README.
This project is licensed under the GNU Lesser General Public License v3.0. See the LICENSE file for details.