Home > Software design >  Can ASP.NET core dependency injection inject null references?
Can ASP.NET core dependency injection inject null references?

Time:06-24

In my job we are developing ASP.NET core applications. I keep seeing the follwing pattern used with dependency-injection. In this example Logic depends on OtherLogic:

public class Logic {
    private readonly OtherLogic myOtherLogic;

    public Logic(OtherLogic myOtherLogic) {
        // I mean specifically this pattern of checking injected dependencies for null:
        this.myOtherLogic = myOtherLogic
            ?? throw new ArgumentNullException(nameof(myOtherLogic));
    }
}

All dependencies are added to an IServiceCollection during service configuration and both (Logic and OtherLogic) are only retrieved from dependency injection. In my current project I'm using DotNet 5 and I have enabled nullable reference types if that matters.

I'd like to drop this Guard Clause pattern, because I believe the injector will always throw an exception (during service configuration) when it's unable to resolve a non-optional service. My question is, in which circumstances the injector could inject a null-reference for a non-optional service? Is it even possible?

This article doesn't mention the possibility of null being injected, but as far as I can see, also doesn't explicitly deny it. Just want to make sure I don't run into trouble later on, thanks for your time :)

CodePudding user response:

Under 'normal' conditions (i.e. when solely depending on the use of Auto-Wiring) null values can't be injected. The following code snippets, however, show examples of when null references can be injected into Logic:

Case 1: Manual construction

new Logic(null);

It's always possible to create instances outside the context of the DI Container by manually invoking its constructor using plain-old C#. In most cases you wouldn't inject null directly, but there could be many reasons why this happens, for instance by calling a method to retrieve the dependency, while this method has a default case where null is returned, or when the dependency comes from a static field that wasn't initialized yet, which can happen if static fields have a dependency upon each other.

Case 2: Direct null injection through factory registration

services.AddTransient(c => new Logic(null));

This case is practically identical to case 1, but now you wrap the creation inside the factory delegate of a registration.

Case 3: Incorrect use of GetService opposed to GetRequiredService in factory

services.AddTransient(c => new Logic(c.GetService<OtherLogic>()));

The MS.DI container contains a GetService<T> method that allows retrieving the dependency, but returns null in case the registration for T does not exist. A misconfiguration could, therefore, allow null to be injected. Instead, you should almost always call the GetRequiredService<T> extension method, which ensures and exception is thrown and null is never returned. GetRequiredService<T> even throws an exception when the dependency is registered, but its factory delegate returns null, as can be seen in the next case.

Case 4: Auto-registration with dependency that returns null from its factory registration

services.AddTransient<Logic>();
services.AddTransient<OtherLogic>(c => null);

In this case a factory registration is made that directly or (more commonly) indirectly a null reference. MS.DI allows that null reference of OtherLogic to be injected into Logic.

In my opinion, option 4 should not be allowed, and I consider it a design flow in MS.DI to allow a factory registration to return null. But that still leaves the other three cases where the Logic constructor is called using plain-old C#.

Whether or not you want to be very safe and protect the class's pre conditions using a Guard Clause is up to you. Personally, for applications where I make use of a DI Container that doesn't allow case 4 and makes it hard to do case 3, I typically leave out the Guard Clauses. This even allows me to use the more concise record syntax:

// WARNING: No null checks are performed in the record constructor
public sealed record Logic(OtherLogic MyOtherLogic)
{
}

As a counter argument, its worth to mention that the addition of these null check Guard Clauses would not result in any maintainability issues, as they don't change after being written. They are part of the class's DI infrastructure, and although a bit noisy, won't cause sweeping changes in the long run.

CodePudding user response:

It should not be necessary to check parameters being passed into the constructor with dependency injection as (like you mentioned) exceptions will occur when registering the dependency as you mentioned.

I found an article here on this topic where someone suggests that future implementations of DI could results in null injections: https://stevetalkscode.co.uk/null-injection

However, the article is hypothetical and the scenarios are quite specific. I think it's okay to get rid of the null checks.

  • Related