'xUnit - Unable to resolve service from Service Scope (NullReferenceException)

Without wanting to immediately depend on another nuget package (Xunit.DependencyInjection), I tried to setup my IoC container manually by implementing a TestsBase class, which every single unit test class will inherit from. Throughout my project I'm using a CQRS pattern with Mediatr and FluentValidation. For both the ValidationTests and HandlingTests I'm respectively resolving the implemented IValidator type and the implemented IMediator type.

As every test class individually was running as expected I initially thought I worked out the setup of my TestsBase correctly. However, when trying to do a 'Run all Tests', it seems something goes wrong during the configuration of my container OR the disposal of my ServiceScope. When trying a 'Run all Tests' only 1 testclass will run correctly, and every other testclass will throw NullReferenceExceptions on every testmethod, particularly on the ServiceScope property.

I've already tried some different approaches, up to the point where multiple test classes would run correctly, but until now I've never managed to get a fully succesful 'Run all Tests'. I'm up to date with the concept of Fixtures within xUnit, but I don't feel I would get a lot of benefit from them trying to solve this issue.

Am I doing something completely wrong regarding the concepts of a ServiceContainer? Or is the locking mechanism not working as I would expect it to?

My current setup:

TestsBase:

public class TestsBase : IDisposable
    {
        private IServiceScope ServiceScope { get; set; }
        protected readonly IUnitOfWork Uow;

        protected TestsBase()
        {
            ConfigureServiceContainer();

            ConfigureNBuilder();

            Uow = ServiceScope.ServiceProvider.GetService<IUnitOfWork>();

            // Wrap test with transaction
            Uow.BeginTransactionAsync().GetAwaiter().GetResult();
        }

        private static bool _containerIsConfigured;
        private static readonly object ContainerSyncObject = new object();
        private void ConfigureServiceContainer()
        {
            lock (ContainerSyncObject)
            {
                if (!_containerIsConfigured)
                {
                    var services = new ServiceCollection();

                    // FLUENTVALIDATION
                    services.AddValidatorsFromAssembly(typeof(ValidationBehavior<,>).Assembly);
                    ValidatorOptions.Global.LanguageManager = new CustomLanguageManager();  // Override the default validation messages

                    // AUTOMAPPER
                    services.AddSingleton<CrossCutting.Interfaces.IMapper, AutoMapperMapper>();
                    services.AddSingleton<AutoMapper.IConfigurationProvider>(AutoMapperConfiguration.ConfigureAutoMapper());
                    services.AddSingleton<AutoMapper.IMapper>(sp =>
                        new Mapper(sp.GetRequiredService<AutoMapper.IConfigurationProvider>(), sp.GetRequiredService));

                    // MEDIATR
                    // AddMediatR scans the assembly for potential handlers and registers them through DI
                    services.AddMediatR(typeof(UnitOfWorkHandler<,>).Assembly);

                    // Have MediatR call the Transaction Behavior on every request
                    services.AddTransient(typeof(IPipelineBehavior<,>), typeof(TransactionBehaviorForTests<,>));

                    // BUSINESSMANAGERS
                    services.AddTransient(typeof(IContactManager), typeof(ContactManager));

                    // DATABASE
                    var connectionString = "Data Source=(localdb)\\MSSQLLocalDb;Initial Catalog=brbsalon;Integrated Security=True;MultipleActiveResultSets=True";
                    services.AddScoped<IEntitiesDbContext>(x => new EntityFrameworkEntitiesDbContext(connectionString, true));
                    services.AddScoped(typeof(IGenericRepository<>), typeof(EntityFrameworkGenericRepository<>));
                    services.AddScoped<IUnitOfWork, EntityFrameworkUnitOfWork>();

                    ServiceScope = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true }).CreateScope();

                    _containerIsConfigured = true;
                }
            }
        }

        protected IValidator<T> ValidatorFor<T>()
        {
            try
            {
                Uow.DetachAllChangeTrackerEntitiesForTests();   // Clear dbContext BEFORE action

                //return ServiceProvider.GetService<IValidator<T>>();
                return ServiceScope.ServiceProvider.GetService<IValidator<T>>();
            }
            finally
            {
                Uow.DetachAllChangeTrackerEntitiesForTests();   // Clear dbContext AFTER action
            }
        }

        protected IMediator GetMediator()
        {
            //return ServiceProvider.GetService<IMediator>();
            return ServiceScope.ServiceProvider.GetService<IMediator>();
        }

        /// <summary>
        /// Inserts an entity directly in the database (not through manager)
        /// </summary>
        public async Task<TEntity> InsertEntityAsync<TEntity>(TEntity entity)
            where TEntity : class, IEntityBase, new()
        {
            await Uow.ExecuteActionWithSaveDbContextChangesAsync(() =>
            {
                Uow.RepositoryFor<TEntity>().Insert(entity);
            });

            return entity;
        }

        protected virtual void Dispose(bool disposing)
        {
            if (Uow != null)
            {
                // Rollback the transaction to keep the database clean
                Uow.CloseTransactionAsync(new Exception("Dummy exception which will simulate rollback"));
                Uow.Dispose();
            }

            lock (ContainerSyncObject)
            {
                if (_containerIsConfigured)
                {
                    ServiceScope.Dispose();
                    _containerIsConfigured = false;
                }
            }
        }

        public void Dispose()
        {
            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }

Example of one of my unit test classes:

public class GetUserDetailsValidatorTests : TestsBase
    {
        private readonly IValidator<GetUserDetails> _validator;

        public GetUserDetailsValidatorTests()    // constructor gets called BEFORE each test
        {
            _validator = ValidatorFor<GetUserDetails>();
        }

        [Fact]
        public async Task Valid_For_ValidDto()
        {
            // Arrange
            var dto = new GetUserDetails { UserId = Guid.NewGuid() };

            // Act
            var validationResult = await _validator.ValidateAsync(dto);

            // Assert
            validationResult.IsValid.ShouldBeTrue();
        }

        [Fact]
        public async Task Invalid_For_EmptyDto()
        {
            // Arrange
            var dto = new GetUserDetails();

            // Act
            var validationResult = await _validator.ValidateAsync(dto);

            // Assert
            validationResult.IsValid.ShouldBeFalse();
            validationResult.Errors.ShouldContainValidationMessage(nameof(User), VALIDATION_NOT_EMPTY_VALIDATOR);
            validationResult.Errors.Count.ShouldBe(1);
        }
    }


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source