10

I use .NET Core WebAPI with dependency injection and multiple authentication schemas (http basic, access keys, JWT). I inject some business services which require some authenticated user data. If user is authenticated by any of auth middleware, DI works fine. If the user is not authenticated, DI cannot resolve some services. I need DI to return null.

How is that possible? Code bellow will result in exception, null is not allowed as result.

services.AddTransient<IMasterRepository>(serviceProvider =>
        {
            var _serviceFactory = new RepositoriesFactory(Configuration);

            if (!Authenticated)
            {
                return null;
            }

            return _serviceFactory.CreateMasterRepository();
        });

Also, I cannot return 401 in auth middleware, because another middleware may success (expl: cannot return 401 in http basic auth middleware because next one, JWT, may success)

Also, I cannot add "authentication required" check after all auth middlewares because some controllers are public (no authentication / dependency injection required).

Any advice? Thanks!

1
  • Does it work when explicitly stating the generic type as nullable using ? (C# 8)? Commented Dec 31, 2019 at 0:12

4 Answers 4

7
+50

There's no problem in registering an implementation as null. It's only in resolving that you will have a problem.

In other words, if you register:

services.AddTransient<IMasterRepository>(provider => null);

And then try:

private readonly IMasterRepository _repository;

public SomeController(IMasterRepository repository)
{
    _repository = repository;
}

You will get an InvalidOperationException at runtime, with a message similar to:

Unable to resolve service for type 'MyApp.IMasterRepository' while attempting to activate 'MyApp.Controllers.SomeController'

However, there is a simple workaround. Rather than injecting the interface, inject an IEnumerable of that interface:

private readonly IMasterRepository _repository;

public SomeController(IEnumerable<IMasterRepository> repositories)
{
    _repository = repositories.First();  // (using System.Linq)
}

You might think it should be FirstOrDefault, however there will indeed be a single item containing the null you registered.

This approach works because DI in ASP.Net Core supports registering multiple implementations of a given type and doesn't distinguish between null and object instances at time of registration.

Do keep in mind that even though this works, it's not recommended because now the _repository variable is potentially nullable, and a null check must be used every time it is accessed. For example: if (_repository != null) { _repository.DoSomething(); } or _repository?.DoSomething();. Most people do not expect to write code like that.

This covers the DI part of the question. But if indeed the issue is strictly with auth then ste-fu's answer describes a more appropriate approach.

Sign up to request clarification or add additional context in comments.

2 Comments

I can't watch this :o, if I see this in a code-base, I'd stop whatever I was doing and refactor this out.
@huysentruitw - Indeed. I added a paragraph to echo that this is not a recommended practice. Thanks.
5

The default DI framework does not allow for the factory delegate to return null by design.

Consider null object pattern by creating a NullObject derived from the interface

public class NullRepository : IMasterRepository {
    public static readonly IMasterRepository Empty = new NullRepository();

    public NullRepository () { }

    //...implement members that do nothing and may return empty collections.
}

that does nothing when invoked.

services.AddTransient<IMasterRepository>(serviceProvider => {
    IMasterRepository result = NullRepository.Empty;
    var _serviceFactory = new RepositoriesFactory(Configuration);
    if (Authenticated) {
        result = _serviceFactory.CreateMasterRepository();
    }
    return result;
});

Checking for null now becomes

//ctor
public SomeClass(IMasterRepository repository) {

    if(repository == NullRepository.Empty)
        //...throw

    //...
}

1 Comment

Null Object pattern addresses the problem. However, the patterns is implemented in this answer incorrectly. SomeClass should not know with which interface implementation it's working - that's the principle of interfaces that the user of the interface does not check what's behind it.
3

To my mind, this sounds like a problem with your dependency setup.

All your auth Middleware should be setting the ClaimsPrincipal on the HttpContext as part of the Invoke method if the authentication is successful.

Whilst a service may need to be able to access the ClaimsPrincipal in order to function correctly, you can do this by injecting IHttpContextAccessor in the constructor and enabling it in the ConfigureServices method in Startup.cs.

Getting the DI container to return null would just mean that you have to make lots of null checks all over your code. Ensuring that the ClaimsPrincipal is correctly set means that you can leverage the [Authorize] attribute to control access to particular Controllers or methods or setup Policy-based authorization which should return the correct status codes after all the auth middleware has run.

Comments

2

While you could create a wrapper class that is able to hold the service instance or null -OR- you could inject a dummy service in case the user is not authenticated, it's not recommended from a clean-code perspective. Both solutions would be a code smell as for the first: you would have to place null checks everywhere, even in places where you'd expect the service. And the latter: it seems like the code could actually use the service, but it's not clear a dummy service will be provided.

To keep your code clean, I'd just move the public routes, that are not requiring those services, to a separate controller class (with the same route as they have now) that does not depend on the service. This way, you can just register the repository as-is and avoid magic trickery.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.