Home > database >  C# MVC Integration Test - System.InvalidOperationException : Cannot resolve scoped service '<
C# MVC Integration Test - System.InvalidOperationException : Cannot resolve scoped service '<

Time:06-06

I am trying to access my in-memory database created for integration tests. My CustomWebApplicationFactory file looks like this:

using System;
using System.Linq;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using amaranth.Data;

namespace amaranth.Tests
{
    public class CustomWebApplicationFactory<TStartup>
        : WebApplicationFactory<TStartup> where TStartup: class
    {
        public ApplicationDbContext GetDb() 
        {
            return Services.GetService<ApplicationDbContext>() ?? throw new NullReferenceException("Unable to get DB from registered services");
        }

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));

                services.Remove(descriptor);

                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });
                services.AddAntiforgery(t =>
                {
                    t.Cookie.Name = AntiForgeryTokenExtractor.AntiForgeryCookieName;
                    t.FormFieldName = AntiForgeryTokenExtractor.AntiForgeryFieldName;
                });

                var sp = services.BuildServiceProvider();

                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();

                    db.Database.EnsureCreated();
                }
            });
        }
    }
}

And this is the test in question, from the file IntegrationTests/AuthTests.cs:

using System.Collections.Generic;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.Net.Http.Headers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using AngleSharp.Html.Dom;
using Xunit;
using amaranth.Tests.Helpers;
using Microsoft.EntityFrameworkCore;
using amaranth.Data;

namespace amaranth.Tests
{
    public class AuthTests : 
        IClassFixture<CustomWebApplicationFactory<amaranth.Startup>>
    {
        private readonly CustomWebApplicationFactory<amaranth.Startup> 
            _factory;

        public AuthTests(CustomWebApplicationFactory<amaranth.Startup> factory)
        {
            _factory = factory;
        }

        [Fact]
        public async Task Post_FirsttoRegisterClaimsAdmin()
        {
            // Arrange
            var client = _factory.CreateClient(
                new WebApplicationFactoryClientOptions
                {
                    AllowAutoRedirect = true
                });
            var initResponse = await client.GetAsync("/Identity/Account/Register");
            var antiForgeryValues = await AntiForgeryTokenExtractor.ExtractAntiForgeryValues(initResponse);

            var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Register");
            postRequest.Headers.Add("Cookie", new CookieHeaderValue(AntiForgeryTokenExtractor.AntiForgeryCookieName, antiForgeryValues.cookieValue).ToString());

            var formModel = new Dictionary<string, string>
            {
                { AntiForgeryTokenExtractor.AntiForgeryFieldName, antiForgeryValues.fieldValue },
                { "Email", "[email protected]" },
                { "Password", "pas3w0!rRd" },
                { "ConfirmPassword", "pas3w0!rRd" },
            };

            postRequest.Content = new FormUrlEncodedContent(formModel);
            var response = await client.SendAsync(postRequest);

            response.EnsureSuccessStatusCode();

            var postRequest2 = new HttpRequestMessage(HttpMethod.Post, "/Home/SetAdminToUser");
            postRequest2.Headers.Add("Cookie", new CookieHeaderValue(AntiForgeryTokenExtractor.AntiForgeryCookieName, antiForgeryValues.cookieValue).ToString());

            var formModel2 = new Dictionary<string, string>
            {
                { AntiForgeryTokenExtractor.AntiForgeryFieldName, antiForgeryValues.fieldValue },
                { "check1", "True" },
                { "check2", "True" },
                { "check3", "True" },
            };

            postRequest2.Content = new FormUrlEncodedContent(formModel2);
            var response2 = await client.SendAsync(postRequest2);
            response2.EnsureSuccessStatusCode();

            Assert.Equal("http://localhost/Home/ClaimAdmin", response.RequestMessage.RequestUri.ToString());
            Assert.Equal("http://localhost/Admin/MasterWalletList", response2.RequestMessage.RequestUri.ToString());

            //TODO check the db to see if the user is now admin
            var db = _factory.GetDb();

            var user = db.UserRoles.FirstOrDefault(ur => ur.RoleId == "admin");

            Assert.NotNull(user);
            Assert.Equal(user?.UserId, "TODO GET USER ID");
            #endregion
        }
    }

    public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, 
            ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
            : base(options, logger, encoder, clock)
        {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
            var identity = new ClaimsIdentity(claims, "Test");
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, "Test");

            var result = AuthenticateResult.Success(ticket);

            return Task.FromResult(result);
        }
    }
}

On this line var db = _factory.GetDb(); I get this error:

Request finished HTTP/1.1 GET http://localhost/Admin/MasterWalletList - - - 200 - text/html; charset=utf-8 11.1544ms
[xUnit.net 00:00:02.78] amaranth.Tests.AuthTests.Post_FirsttoRegisterClaimsAdmin [FAIL]
Failed amaranth.Tests.AuthTests.Post_FirsttoRegisterClaimsAdmin [1 s]

Error Message:

System.InvalidOperationException : Cannot resolve scoped service 'amaranth.Data.ApplicationDbContext' from root provider.

Stack Trace:
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider) at amaranth.Tests.CustomWebApplicationFactory`1.GetDb() in /path/to/project/directory/amaranth.Tests/CustomWebApplicationFactory.cs:line 17 at amaranth.Tests.AuthTests.Post_FirsttoRegisterClaimsAdmin() in /path/to/project/directory/amaranth.Tests/IntegrationTests/AuthTests.cs:line 128
--- End of stack trace from previous location ---
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]

What am I doing wrong? Why can't I access amaranth.Data.ApplicationDbContext?

CodePudding user response:

I solved this by making var db scoped. So you can fix this by replacing:

var db = _factory.GetDb();

var user = db.UserRoles.FirstOrDefault(ur => ur.RoleId == "admin");
Assert.NotNull(user);
Assert.Equal(user?.UserId, "TODO GET USER ID");

With this:

Microsoft.AspNetCore.Identity.IdentityUserRole<string>? userRl;
Microsoft.AspNetCore.Identity.IdentityUser? user;
using (var scope = _factory.Services.CreateScope())
{
    var scopedServices = scope.ServiceProvider;
    var db = scopedServices.GetRequiredService<ApplicationDbContext>();
    user = db.Users.FirstOrDefault();
    userRl = db.UserRoles.FirstOrDefault();
}

Assert.NotNull(user);
Assert.True(userRl?.UserId == user?.Id);
  • Related