Dependency Injection improvements in Asp.net Core

Putting injection statements directly in Startup.cs is the initial approach.
As the project grows, the number of lines will increase to a level that makes the maintenance effort harder.

In this article, I will provide some improvements so that the class stays small and easier to maintain.

As an introduction, let’s see how Startup.cs might look like:

Startup.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class Startup {
.
.

public void ConfigureServices(IServiceCollection services) {
.
.
.
.

services.AddSingleton<IInterfaceA, ImplementationClassA>();
services.AddSingleton<IInterfaceB, ImplementationClassB>();
services.AddSingleton<IInterfaceC, ImplementationClassC>();
services.AddSingleton<IInterfaceD, ImplementationClassD>();
services.AddSingleton<IInterfaceE, ImplementationClassE>();
services.AddSingleton<IInterfaceG, ImplementationClassG>();
services.AddSingleton<IInterfaceH, ImplementationClassH>();
services.AddSingleton<IInterfaceI, ImplementationClassI>();
services.AddSingleton<IInterfaceJ, ImplementationClassJ>();
services.AddSingleton<IInterfaceK, ImplementationClassK>();
services.AddSingleton<IInterfaceL, ImplementationClassL>();
services.AddSingleton<IInterfaceM, ImplementationClassM>();


services.AddScoped<IInterfaceN, ImplementationClassN>();
services.AddScoped<IInterfaceO, ImplementationClassO>();
services.AddScoped<IInterfaceP, ImplementationClassP>();
services.AddScoped<IInterfaceQ, ImplementationClassQ>();
services.AddScoped<IInterfaceR, ImplementationClassR>();
services.AddScoped<IInterfaceS, ImplementationClassS>();
services.AddScoped<IInterfaceT, ImplementationClassT>();
services.AddScoped<IInterfaceU, ImplementationClassU>();

services.AddTransient<IInterfaceV, ImplementationClassV>();
services.AddTransient<IInterfaceW, ImplementationClassW>();
services.AddTransient<IInterfaceX, ImplementationClassX>();
services.AddTransient<IInterfaceY, ImplementationClassY>();
services.AddTransient<IInterfaceZ, ImplementationClassZ>();

services.Configure<StrongCongiruationDto1>(Configuration.GetSection("Section1"));
services.Configure<StrongCongiruationDto2>(Configuration.GetSection("Section2"));
services.Configure<StrongCongiruationDto3>(Configuration.GetSection("Section3"));
services.Configure<StrongCongiruationDto4>(Configuration.GetSection("Section4"));
services.Configure<StrongCongiruationDto5>(Configuration.GetSection("Section5"));
services.Configure<StrongCongiruationDto6>(Configuration.GetSection("Section6"));
services.Configure<StrongCongiruationDto7>(Configuration.GetSection("Section7"));
services.Configure<StrongCongiruationDto8>(Configuration.GetSection("Section8"));


.
.
.
.
}

.
.
}

To tackle this problem, the main idea is using custom injection attributes.
So, instead of explicitly declaring an injection statement inside the Starup.cs class, it will be coupled implicitly inside the service class or the configuration section.

The Startup.cs, service and strongly-typed configuration section classes will look like:

Startup.cs
1
2
3
public void ConfigureServices(IServiceCollection services) {            
Injection.InjectionFactory.StartInjection(services, Configuration);
}

SomeService.cs
1
2
3
4
5
6
7
    
[InjectAs(ServiceLifetime.Singleton)]
public class SomeService {
.
.
.
}

SomeConfigSection.cs
1
2
3
4
5
6
7
    
[InjectAsConfigureSection(someKeyNameHere)]
public class SomeConfigSection {
.
.
.
}

Evidently, this will be less verbose as well as easier to maintain.

If that piques your interest, please read on.

Solution Overview

The solution classes, for that purpose, will be:
    1. InjectionFactory will:
        1. Scan the application for any custom injection attribute.
        2. Inject the service classes as well as the strongly-typed configuration ones.
    2. Injection Attributes:
        1. InjectAs: to register any class based on the desired lifetime. You can also declare the implemented interface. Default value is null.
        2. InjectAsConfigureSection: to bind any strongly-typed configuration class.

Implementation Details:

How do services get injected indirectly?

The method InjectionFactory.InjectServices will do the trick. It scans the provided dll (if not provided, it will default to the app dll) for any class that has the attribute: InjectAs.
Then it iterates over the result and performs the injection using IServiceCollection of the app.

The relevant phase for this process is the startup. Please note that although I have used Reflection to scan the dll, I have avoided any instantiation through the Activator. Otherwise, the idea of “Dependency Injection“ is ruined.

Here is the final version of the InjectServices:

InjectionFactory.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void InjectServices(IServiceCollection services, Assembly assembly = null)
{
var types = ReflectionExtension.GetTypesWithAttribute<InjectAsAttribute>(assembly);
var serviceTypeMap = new Dictionary<ServiceLifetime, Action<Type, Type>>
{
[ServiceLifetime.Singleton] = (type, implementedInterface) => services.AddSingleton(implementedInterface ?? type, type),
[ServiceLifetime.Transient] = (type, implementedInterface) => services.AddTransient(implementedInterface ?? type, type),
[ServiceLifetime.Scoped] = (type, implementedInterface) => services.AddScoped(implementedInterface ?? type, type)
};

foreach (var type in types)
{
var injectAsAttribute = type.GetCustomAttribute<InjectAsAttribute>();
if (injectAsAttribute != null)
{
serviceTypeMap[injectAsAttribute.ServiceLifetime](type, injectAsAttribute.ImplementedInterface);
}
}
}

How configuration sections are bound to strongly-typed classes:

Our target is invoking the method:

1
services.Configure<someStronglyTypedClass>(Configuration.GetSection(someKeyNameFromAppSetting));

Talking about implementation details, this method is found inside the extension class: OptionsConfigurationServiceCollectionExtensions.

With the aid of reflection: we need to access the method, choose the version of two parameters, and provide the strongly-typed class to build the final callable version prior to calling it eventually.

In order to do that:

  1. The provided app dll is scanned for any class that implements the interface ConfigurationSectionAttribute.

  2. Iterate over the result and perform the following:
    2.1 grab the configuration section. Remember that we store the key name that contains the configuration section inside configurationSectionAttribute.

    2.2 use the code:

    InjectionFactory.cs
    1
    2
    var configureMethodType = typeof(OptionsConfigurationServiceCollectionExtensions);
    var configureMethod = configureMethodType.GetMethods().First(x => x.Name == "Configure" && x.GetParameters().Length == 2);

    Now, the method with generic type T have been grabbed.

    2.3 provide the strongly-typed class as a generic argument:

    InjectionFactory.cs
    1
    MethodInfo method = configureMethod.MakeGenericMethod(type);

    The method is now ready to be called.

    2.4 call the method:

    InjectionFactory.cs
    1
    method.Invoke(services, new object[] {services, configurationSection});

The binding process between the strongly-typed class and the Json Configuration should be triggered.

Here is the final version of InjectConfigurationSections:

InjectionFactory.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static void InjectConfigurationSections(IServiceCollection services, IConfiguration configuration, Assembly assembly = null)
{
var types = ReflectionExtension.GetTypesWithAttribute<ConfigurationSectionAttribute>(assembly);
foreach (var type in types)
{
var sectionAttribute = type.GetCustomAttribute<ConfigurationSectionAttribute>();
if (sectionAttribute != null)
{
var configurationSection = configuration.GetSection(sectionAttribute.KeyName);
var configureMethodType = typeof(OptionsConfigurationServiceCollectionExtensions);
var configureMethod = configureMethodType.GetMethods().First(x => x.Name == "Configure" && x.GetParameters().Length == 2);
MethodInfo method = configureMethod.MakeGenericMethod(type);
method.Invoke(services, new object[] {services, configurationSection});
}
}
}

And that’s the entire process!

We have seen how direct invocation of injection statements can be avoided and implicitly declared within the service or configuration class. Much code can be easily deleted following this approach. Moreover, you can elaborate this idea further. For example, it is possible to impose some priority between services, if that is really required.

The source code can be found here.

Feel free to comment, send an email to mahmoud.alsati@gmail.com or fork the code.

Dependency Injection improvements in Asp.net Core

http://sattinos.com/2021/08/21/dependency-injection/

Author

Mahmoud AlSati

Posted on

2021-08-21

Updated on

2021-09-01

Licensed under