Mdk.DIAttributes 1.0.1

dotnet add package Mdk.DIAttributes --version 1.0.1                
NuGet\Install-Package Mdk.DIAttributes -Version 1.0.1                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Mdk.DIAttributes" Version="1.0.1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Mdk.DIAttributes --version 1.0.1                
#r "nuget: Mdk.DIAttributes, 1.0.1"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Mdk.DIAttributes as a Cake Addin
#addin nuget:?package=Mdk.DIAttributes&version=1.0.1

// Install Mdk.DIAttributes as a Cake Tool
#tool nuget:?package=Mdk.DIAttributes&version=1.0.1                

Build Build, pack and publish

Summary

The DIAttributes 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. The package includes a reflection-based strategy to register services using these attributes. However, if you prefer a source generator strategy, you can use the Mdk.DISourceGenerator package.

  • Installation: The package is available on NuGet as Mdk.DIAttributes.
  • Attribute Usage: The package provides attributes like AddScoped, AddSingleton, and AddTransient for different lifetimes. You can use these attributes on your classes to register them with the DI container. For example, [AddScoped] class MyClass { ... } is equivalent to services.AddScoped<MyClass>();.
  • Registration using Reflection: The attributes need to be translated to actual registrations in the DI container. This can be done using reflection. The reflection method involves iterating over assemblies, types, and attributes of the application domain. However, this method has some downsides.
  • Registration using a Source Generator: The Mdk.DISourceGenerator package provides a source generator that translates the attributes to registration code for the default DI container. This solves the issues associated with the reflection strategy.

DIAttributes

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. This also cleans up the list of registrations in startup code. All that is left is a method call for translating the attributes to actual registrations in the default DI container.

This package includes a reflection code strategy to register services using attributes. If you want a source generator as a better alternative strategy, go to: Mdk.DISourceGenerator

Installation

The custom attributes and registration extension methods are available as a NuGet package: Mdk.DIAttributes

Attribute usage

Following examples focus on scoped registration. Use AddSingleton or AddTransient for other lifetimes.

Simple classes and interfaces

[AddScoped]
class MyClass { ... }

equals to services.AddScoped<MyClass>();

[AddScoped<IMyInterface>]
class MyClass: IMyInterface { ... }

equals to services.AddScoped<IMyInterface, MyClass>();

Generic attributes require C# 11. If you are still on a earlier version use [AddScoped(typeof(IMyInterface))]

Multiple attributes on one class
[AddScoped<IMyInterface1>]
[AddScoped<IMyInterface2>]
class MyClass: IMyInterface1, IMyInterface2 { ... }

equals to

services.AddScoped<IMyInterface1, MyClass>();
services.AddScoped<IMyInterface2, MyClass>();

Generic classes and interfaces

Unbound generic registration:
[AddScoped]
class MyClass<T> { ... }

equals to services.AddScoped(typeof(MyClass<>));

[AddScoped(typeof(IMyInterface<>))]
class MyClass<T>: IMyInterface<T> { ... }

equals to services.AddScoped(typeof(IMyInterface<>), typeof(MyClass<>));

Bound generic registration:
[AddScoped<MyClass<int>>]
class MyClass<T> { ... }

equals to services.AddScoped<MyClass<int>>();

[AddScoped<IMyInterface<int>>]
class MyClass<T>: IMyInterface<T> { ... }

equals to services.AddScoped<IMyInterface<int>, MyClass<int>>();

Multiple generic type parameters

Multiple generic type parameters are also supported, for example:

[AddScoped]
class MyClass<T, U> { ... }

equals to services.AddScoped(typeof(MyClass<,>));

Attribute to registration translation

The assigned attributes need to be translated to actual registrations in the default DI container.

Using reflection

A common way to query for attributes at run time is using reflection.

Following extension method iterates over assemblies, types and attributes of the application domain, but this does NOT always work:

public static IServiceCollection RegisterByAttributes(this IServiceCollection services)
{
    foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
        foreach (Type type in assembly.GetTypes())
            foreach (DIAttribute attribute in type.GetCustomAttributes<DIAttribute>(false))
            {
                ... Register service based on attribute found.
            }

    return services;
}

AppDomain.GetAssemblies gets the assemblies that have been loaded into the execution context of the application domain. At startup possibly not all assemblies are loaded yet, so possibly not all attributes are found. So unfortunately this method is not reliable for doing runtime registrations. Loading all assemblies at startup is also not a good idea, because it can impact the startup time of your application.

A better solution is targeting just the assemblies we know of that contain the attributes we are looking for:

public static IServiceCollection RegisterByAttributes<T>(this IServiceCollection services)
{
    Assembly assembly = typeof(T).Assembly;

    foreach (Type type in assembly.GetTypes())
        foreach (DIAttribute attribute in type.GetCustomAttributes<DIAttribute>(false))
        {
            ... Register service based on attribute found.
        }

    return services;
}

For every assembly containing attributes we need to call this extension method, where T is a type in the assembly. T can be any type, but you could also create a empty class in the assembly just for this purpose:

public static class DependencyInjections
{
    public static IServiceCollection AddBusinessLogicServices(this IServiceCollection services)
        => services
            .RegisterByAttributes<BusinessLogicServices>()
            .AddBusinessBaseLogicServices();
}

internal sealed class BusinessLogicServices { }

In the examples section of this repository a Blazor application and a Minimal API project are added, in which this registration strategy is implemented.

A solution using reflection is not ideal because:

  • We moved from a compile time solution to a runtime solution that impacts startup.
  • We still need some code for every assembly containing the custom attributes.
  • Registration code is replaced by code using reflection, which makes is less direct.

Using a source generator

Source generators are a good alternative for the reflection strategy. All issues mentioned above are solved by using a source generator.

Mdk.DISourceGenerator is a GitHub repository that contains a source generator, that translates the attributes to registration code for the default DI container. The source generator is also available as a NuGet package: Mdk.DISourceGenerator on NuGet

References

Product 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. 
.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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.1 2 1/4/2024
1.0.0 2 1/4/2024