learntogrow-growtolearn
learntogrow-growtolearn

Reputation: 1290

Facing issue mocking the IServiceProvider extensions method

I am aware this question might be repeated but I am facing an issue where I cant mock the non-static method as it is the static method that is calling the non-static method.

My controller logic calls the static ServiceProviderServiceExtensions method GetServices<T>(this IServiceProvider provider) which in turn seems to call the non-static method provider.GetService(serviceType).

Basically, I have dependency injection where one interface has two implementations as

services.AddSingleton<IProvider, CustomProvider1>();
services.AddSingleton<IProvider, CustomProvider2>();

Now, I have 2 controllers which directly take the dependency on this provider as:

 public Controller1(IProvider provider)
 public Controller2(IProvider provider)

In my controller, I resolve dependencies as

Controller1.cs

provider = serviceProvider.GetServices<IProvider>()
                .FirstOrDefault(lp => lp.GetType() == typeof(CustomeProvider1));
            and

Controller2.cs
provider = serviceProvider.GetServices<IProvider>()
                .FirstOrDefault(lp => lp.GetType() == typeof(CustomeProvider1));

Now, when I try to mock my unit tests as belows:

 serviceProviderMock
                .Setup(x => x.GetService(typeof(CustomeProvider2)))
                .Returns(a);

I get an error No service for type System.Collections.Generic.IEnumerable[IProvider]has been registered. and I cant directly mock theGetServicesmethod` as it is static.

Any clue how can I get my tests mocked? Thanks.

Upvotes: 0

Views: 3282

Answers (1)

Steven
Steven

Reputation: 172835

My controller logic calls the static ServiceProviderServiceExtensions method GetServices(this IServiceProvider provider)

This is where things will start to go wrong. Calling GetServices<T> from within your controller is an application of the Service Locator anti-pattern. All troubles you have are originating from this misuse.

Instead, you should:

  1. Solely use constructor injection
  2. Never inject IServiceProvider of another abstraction that represents the container in a constructor of any class outside your Composition Root

So this means you should have a constructor as follows:

public Controller1(IProvider provider)

Your IProvider is ambiguous, but let's assume for a moment that this is okay. What's not okay, however, is to let application code deal with this ambiguity. Instead, you should solely handle this ambiguity inside your Composition Root, which can be done as follows:

services.AddSingleton<CustomProvider1>();
services.AddTransient<Controller1>(c => new Controller1(
    c.GetRequiredService<CustomProvider1>()));

services.AddSingleton<CustomProvider2>();
services.AddTransient<Controller2>(c => new Controller2(
    c.GetRequiredService<CustomProvider2>()));

Do note that, by default, ASP.NET Core MVC does not resolve controllers from the DI Container (which is a really, really weird default). So to force MVC to use the DI Container to resolve controllers, and thereby using the above registrations, you will have to add the following code:

services.AddMvc()
   .AddControllersAsServices();

The above registration only works well in case those controllers only have one dependency, because this method effectively disables auto-wiring. In case the class has more dependencies, a more maintainable construct would be the following:

services.AddTransient<Controller1>(c =>
    ActivatorUtilities.CreateInstance<Controller1>(
        c,
        c.GetRequiredService<CustomProvider1>()));

services.AddTransient<Controller2>(c =>
    ActivatorUtilities.CreateInstance<Controller2>(
        c,
        c.GetRequiredService<CustomProvider1>()));

This makes use of .NET Core's ActivatorUtilities class, which allows a class to get auto-wired, while passing in certain dependencies.

Do note that ActivatorUtilities comes with certain downsides, such as the inability to detect cyclic dependencies. Instead, a stack overflow exception will be thrown (yuck).

But... as I noted previously, your IProvider abstraction is ambiguous, because there are two implementations and consumers require a different implementation. Although this isn't a bad thing per se, you should always verify whether or not you are not violating the Liskov Substitution Principle by doing so.

You can check to see whether you violate the LSP by swapping the two implementations around. So ask yourself: what happens to Controller1 when it gets injected with a CustomProvider2 and what will happen to Controller2 when it gets injected with a CustomProvider1. If the answer is that they will stop working, this means that you are violating the LSP and this a design problem.

If the controller breaks, it means that both implementations behave very differently while consumers should be able to assume all implementations behave according to their abstraction. Violating the LSP means adding complexity.

When you determined you are violating the LSP in this case, the solution is to give each implementation its own abstraction:

interface IProvider1 { }
interface IProvider2 { }

Whether or not those two interface have an identical signature is irrelevant, because the LSP violation signals that both interfaces are actually very different in behavior, because swapping out implementations breaks their clients.

Note, however, that even if the direct consumer keeps working, it could still mean that the application starts to behave incorrectly when the implementations are swapped. This doesn't mean you are violating the SRP. For instance, when provider1 logs to disk and provider2 logs to database, the expected behavior is that a call to controller1 will cause a log on disk to be appended. Having it the other way around is therefore not what you want to achieve, but that is something you wish to configure in your Composition Root. That is not an indication of a LSP violation; in that case the contract still behaves as consumers expect it to.

If swapping implementations has no notable effect on their consumers, you are not violating LSP and it means the given registrations are the way to go. Or you can simplify things by starting to us a 'real' DI Container ;-)

Upvotes: 3

Related Questions