Search code examples
c#asp.net-coreentity-framework-coreasp.net-core-identity

Count number of queries in EF Core backed Identity system


I'm running ASP.NET Core Identity (UserManager<TUser> and that jazz), backed by EF Core stores. I recently had a bug in my application where a controller endpoint caused way too many calls to UserManager<T>.UpdateAsync(...).

Since I have integration tests in place (with a WebApplicationFactory<TStartup> setup and an UseInMemoryDatabase(...)) I would love to write an integration/smoke test would warn me if the controller endpoint would go over its query budget.

Basically, I would love to write (but cannot write) this test:

public class FooControllerTests : IClassFixture<WebApplicationFactory>
{
    private readonly WebApplicationFactory _factory;

    public FooControllerTests(WebApplicationFactory factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task Bar_WhenCalled_StaysWithinQueryBudget()
    {
        using var scope = _factory.Server.Host.Services.CreateScope();
        var services = scope.ServiceProvider;
        var dbContext = services.GetRequiredService<MyDbContext>();

        var countBefore = dbContext.SomehowGetCurrentQueryCount(); // ??

        var httpClient = _factory.CreateClient();
        var _ = httpClient.PostAsJsonAsync("/foo/bar", new { data = "something" });

        var countAfter = dbContext.SomehowGetCurrentQueryCount(); // ??

        var budgetForEndpoint = 3;
        Assert.True(countAfter - countBefore > budgetForEndpoint);
    }
}

But what matters is the spirit: I want a smoke test to check if I won't make the same mistake in the future and have a bug that causes way too many (update) queries.

I've tried writing a DbCommandInterceptor and added it to my AddDbContext call, but it seems the Store that was added via AddEntityFrameworkStores<TDbContext>(...) is isolated from that, as my CommandCreating handler was never called.

How can I check that my endpoints stay within a query budget if the queries run via Identity EF Core Stores?

I'm currently using ASP.NET Core 3.1 and EF Core 3.1, but am willing to upgrade to get this feature.


Solution

  • I've tried writing a DbCommandInterceptor and added it to my AddDbContext call, but it seems the Store that was added via AddEntityFrameworkStores(...) is isolated from that, as my CommandCreating handler was never called.

    That is the way to do it. The following is taken from Introduction to Identity on ASP.NET Core: Configure Identity services:

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    

    Just adding the interceptor should do it:

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
               .AddInterceptors(new QueryCountingCommandInterceptor())
    );
    

    Note that you mention using UseInMemoryDatabase(), which you might do only in the WebApplicationFactory<TStartup> class. If that's the case, you need to add the interceptor there too:

    public class WebApplicationFactory : WebApplicationFactory<Startup>
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(s => s.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseInMemoryDatabase("integration-tests");
                options.AddInterceptors(new MyInterceptor());
            }));
        }
    }
    

    This is nice, because you could add the interceptor only for tests, and not have it run for production code.