Dependency injection by setting attributes and using a source generator to register service types.
$ dotnet add package Mdk.DISourceGeneratorThe DISourceGenerator package is designed to help clean up your service registration code when using the default Dependency Injection (DI) container in .NET. It allows you to use custom attributes to register your services, keeping the DI metadata close to the implementation classes.
[AddScoped] class MyClass { ... } is equivalent to services.AddScoped<MyClass>();.If you have a lot of services registered in the default DI container, your registration code can become some sort of a mess.
Using custom attributes can make your registration much cleaner. Attributes with registration information keep DI metadata close to the implementation classes the attributes are assigned to.
DISourceGenerator generates service registration source code based on attributes assigned to classes. Adding one line of code in ConfigureServices registers all services both in the host assembly as in directly and transitively referenced assemblies.
The examples section of the GitHub repository contains a Blazor application and a Minimal API project, in which this registration strategy is implemented.
The source generator is available as a NuGet package: Mdk.DISourceGenerator
Following examples focus on scoped registration. Use AddSingleton or AddTransient for other lifetimes.
[AddScoped]
class MyClass { ... }
corresponds to services.AddScoped<MyClass>();
[AddScoped<IMyInterface>]
class MyClass: IMyInterface { ... }
corresponds to services.AddScoped<IMyInterface, MyClass>();
Generic attributes require C# 11. If you are still on a earlier version use [AddScoped(typeof(IMyInterface))]
[AddScoped<IMyInterface1>]
[AddScoped<IMyInterface2>]
class MyClass: IMyInterface1, IMyInterface2 { ... }
corresponds to
services.AddScoped<IMyInterface1, MyClass>();
services.AddScoped<IMyInterface2, MyClass>();
[AddScoped]
class MyClass<T> { ... }
corresponds to services.AddScoped(typeof(MyClass<>));
[AddScoped(typeof(IMyInterface<>))]
class MyClass<T>: IMyInterface<T> { ... }
corresponds to services.AddScoped(typeof(IMyInterface<>), typeof(MyClass<>));
[AddScoped<MyClass<int>>]
class MyClass<T> { ... }
corresponds to services.AddScoped<MyClass<int>>();
[AddScoped<IMyInterface<int>>]
class MyClass<T>: IMyInterface<T> { ... }
corresponds to services.AddScoped<IMyInterface<int>, MyClass<int>>();
Multiple generic type parameters are also supported, for example:
[AddScoped]
class MyClass<T, U> { ... }
corresponds to services.AddScoped(typeof(MyClass<,>));
If there's a direct or transitive reference to the DISourceGenerator package and DIAttributes are used in an assembly, a registration method is generated per assembly.
This method registers all services in an assembly and also includes static method calls to all generated methods in referenced assemblies, both direct and transitive:
// <auto-generated />
using Microsoft.Extensions.DependencyInjection;
namespace Mdk.DISourceGenerator;
/// <summary>Dependency injection registrations for MinimalApi.</summary>
public static partial class DIRegistrations
{
/// <summary>Registers the services for MinimalApi and referenced assemblies.</summary>
public static IServiceCollection RegisterServicesMinimalApi(this IServiceCollection services)
{
if (registeredServicesMinimalApi)
return services;
services.RegisterServicesBusinessBaseLogic();
services.RegisterServicesBusinessLogic();
services.AddScoped<global::MinimalApi.MinimalApiService>();
registeredServicesMinimalApi = true;
return services;
}
private static bool registeredServicesMinimalApi;
}
The naming convention for the static method is DIRegistrations.RegisterServices{AssemblyName}(...).
So in startup only one method call is needed to register all services based on DIAttributes:
using Mdk.DISourceGenerator;
builder.Services.RegisterServicesMinimalApi();
In the Solution Explorer of Visual Studio generated source can be found in:
{AssemblyName} > Dependencies > Analyzers > Mdk.DISourceGenerator > Mdk.DISourceGenerator.DISourceGenerator > DISourceGenerator.{AssemblyName}.g.cs
You can make the generated source more visible in your project and even add the generated files to source control. Copy and paste following PropertyGroup and ItemGroup into the .csproj-file.
<PropertyGroup>
<!-- Save the generated DI registrations to file. -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<!-- Define the root folder for the generated files within the project. -->
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<!-- Don't use the persisted generated files for compilation, the in-memory generated source code is used. -->
<Compile Remove="$(CompilerGeneratedFilesOutputPath)/**/*.cs" />
<!-- Include the persisted generated files in the project, so they are visible in the Solution Explorer. -->
<None Include="$(CompilerGeneratedFilesOutputPath)/**/*.cs" />
</ItemGroup>
This configuration is for all source generators in the project. Use git ignore to prevent output of (other) source generators to be included in source control.
The package also contains analyzers to help you use the attributes correctly. No registration is generated for attributes for which analyzers generate errors. This prevents compiler errors in the generated code.
If the service type is a bound generic type en the class type is generic as well, the implementation type is mandatory.
Severity: Error
Example:
[AddScoped<IGenericType<int>>]
class GenericType<T> : IGenericType<T> { }
Should be:
[AddScoped<IGenericType<int>, GenericType<int>>]
class GenericType<T> : IGenericType<T> { }
If the service type is an interface, the class type must implement this interface.
Severity: Error
Example:
[AddScoped<IInterface>]
class Implementation { }
Should be:
[AddScoped<IInterface>]
class Implementation : IInterface { }
If the implementation type is non generic and is set on the attribute, it must be the same as the class type.
Severity: Error
Example:
[AddScoped<IInterface, Implementation>]
class ClassType : IInterface { }
typeof(Implementation) == typeof(ClassType)
The class type must be the same as (or subclass of) the service type, if the service type is a non-generic class type.
Severity: Error
Example:
[AddScoped<SubClassType>]
class ClassType { }
class SubClassType { }
Should be:
[AddScoped<SubClassType>]
class ClassType : SubClassType { }
class SubClassType { }