Search code examples
c#asp.net-coredependency-injection

Can ASP.NET core dependency injection inject null references?


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 .Net 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 :)


Solution

  • Under 'normal' conditions (i.e. when solely depending on the use of Auto-Wiring) null values can't be injected.

    DEFINITION: Auto-Wiring is the ability to automatically compose an object graph from maps between Abstractions and concrete types by making use of type information supplied by the compiler and the Common Language Runtime (CLR).

    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 flaw 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, though, 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.

    You could also argue that it's not up to the class to 'know' whether it is constructed using a DI Container or using Pure DI. This means that the class should itself check its invariants and prevent it to be initialized in an invalid state.

    So there are multiple ways to look at this issue.