I’m facing this issue on almost every new project in .net core. The most annoying for me are IServiceCollection and IConfigurationBuilder. These interfaces have many static extension methods. For example the IServiceCollection has AddScoped, AddDbContext, AddSingleton, AddLogging, AddOptions and so on. They are all static extensions and Moq will not work on them.
There are a few approaches to solve this. In this article I will discuss the combination of delegate and facade pattern to simplify the Moq for the objects with extensions.
The first step is to create a Facade file for example, ServiceCollectionFacade.cs. I wrapped all calls to ServiceCollection. First, derive the class from an interface:
public class ServiceCollectionFacade : IServiceCollectionFacade
I will describe the interface later. Next, create private serviceCollection:
private readonly IServiceCollection serviceCollection;
Create a class constructor that will take parameter as IServiceCollection:
public ServiceCollectionFacade(IServiceCollection serviceCollection) { this.serviceCollection = serviceCollection; }
And finally add all your extension method as the following:
... public IServiceCollection AddDbContext<TContext>(Action<DbContextOptionsBuilder> optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext { return this.serviceCollection.AddDbContext<TContext>(optionsAction, contextLifetime, optionsLifetime); } public IServiceCollection AddScoped<TService, TImplementation>() where TService : class where TImplementation : class, TService { return this.serviceCollection.AddScoped<TService, TImplementation>(); } public IServiceCollection AddScoped<TService, TImplementation>(Func<IServiceProvider, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService { return this.serviceCollection.AddScoped<TService, TImplementation>(implementationFactory); } public IServiceProvider BuildServiceProvider() { return this.serviceCollection.BuildServiceProvider(); } ...
You may want to add the IServiceCollection interface method as well.
... public int Count => this.serviceCollection.Count; public bool IsReadOnly => this.serviceCollection.IsReadOnly; public ServiceDescriptor this[int index] { get => this.serviceCollection[index]; set => this.serviceCollection[index] = value; } public void Add(ServiceDescriptor item) { this.serviceCollection.Add(item); } public void Clear() { this.serviceCollection.Clear(); } public bool Contains(ServiceDescriptor item) { return this.serviceCollection.Contains(item); } public void CopyTo(ServiceDescriptor[] array, int arrayIndex) { this.serviceCollection.CopyTo(array, arrayIndex); } ...
The IServiceCollectionFacade I derived from the IServiceCollection and add extension methods:
public interface IServiceCollectionFacade : IServiceCollection { IServiceCollection AddSingleton(Type serviceType, Type implementationType); IServiceCollection AddLogging(Action<ILoggingBuilder> configure); IServiceCollection AddOptions(); IServiceCollection Configure<TOptions>(IConfiguration config) where TOptions : class; IServiceCollection AddDbContext<TContext>(Action<DbContextOptionsBuilder> optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext; IServiceCollection AddScoped<TService, TImplementation>() where TService : class where TImplementation : class, TService; IServiceCollection AddScoped<TService, TImplementation>(Func<IServiceProvider, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService; IServiceProvider BuildServiceProvider(); }
Now, let go to the class where I will use my new Facade. First, I created static ServiceCollectionFunc:
internal static Func<IServiceCollectionFacade> ServiceCollectionFunc = () => new ServiceCollectionFacade(new ServiceCollection());
In my method I just call the delegate as:
public static IServiceProvider CreateServiceBuilder(string[] args) { IServiceCollectionFacade services = ServiceCollectionFunc(); services.AddSingleton(typeof(ILoggerAdapter<>), typeof(LoggerAdapter<>)); services.AddOptions(); services.AddScoped<IGlobalRepository, GlobalRepository>(); ... IServiceProvider provider = services.BuildServiceProvider(); return provider; }
I do not use the method chaining. I found it more difficult for mocking and unit testing. If you want to have method chaining feature, change the facade method to return the IServiceCollectionFacade (“return this;”).
Now, when we are done with the code, let create a unit test for the CreateServiceBuilder method. I will use Moq and Xunit, but this approach compatible with other mocking and testing frameworks.
First, create the serviceCollectionFacadeMock and setup all it methods:
Mock<IServiceCollectionFacade> serviceCollectionFacadeMock = new Mock<IServiceCollectionFacade>(); serviceCollectionFacadeMock.Setup(s => s.AddSingleton(typeof(ILoggerAdapter<>), typeof(LoggerAdapter<>))).Returns(serviceCollectionFacadeMock.Object); serviceCollectionFacadeMock.Setup(s => s.AddLogging(It.IsAny<Action<ILoggingBuilder>>())).Returns(serviceCollectionFacadeMock.Object); serviceCollectionFacadeMock.Setup(s => s.AddOptions()).Returns(serviceCollectionFacadeMock.Object); ... Mock<IServiceProvider> serviceProviderMock = new Mock<IServiceProvider>(); serviceCollectionFacadeMock.Setup(s => s.BuildServiceProvider()).Returns(serviceProviderMock.Object); ...
In the example above, I ignore parameters, however you may try to validate them. Now, I set up my delegate and call the method:
MyService.ServiceCollectionFunc = () => serviceCollectionFacadeMock.Object; var result = MyService.CreateServiceBuilder(args); Assert.NotNull(result); Assert.Equal(serviceProviderMock.Object, result); Mock.VerifyAll();
You may also want to create unit tests for facade as well. Here is an example how to test the Constructor
public class ServiceCollectionFacadeFixture { [Fact] public void ServiceCollectionFacadeTest_ValidateConstructor() { const int count= 10; var mockServiceCollection = new Mock<IServiceCollection>(); mockServiceCollection.Setup(s => s.Count).Returns(count); var actual = new ServiceCollectionFacade(mockServiceCollection.Object); Assert.NotNull(actual); Assert.Equal(count, actual.Count); } }