Steven Volckaert
Steven Volckaert

Reputation: 357

Unexpected TaskCanceledException in xUnit test that runs SQL Server Express LocalDB health check

I have an ASP.NET Core 8.0 REST API with a collection of xUnit unit, integration, and functional tests.

The REST API has some health checks registered with the IServiceProvider at application startup:

using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

public static IServiceCollection AddHealthCheckServices(
    this IServiceCollection serviceCollection,
    IConfiguration configuration)
{
    ArgumentNullException.ThrowIfNull(serviceCollection);
    ArgumentNullException.ThrowIfNull(configuration);

    serviceCollection.AddHealthChecks()
        .AddSqlServer(
            connectionString: configuration.GetConnectionStringOrThrow(nameof(ConnectionStrings.DatabaseConnection)),
            name: nameof(ConnectionStrings.DatabaseConnection)
        )
        .AddDbContextCheck<MyRestApplicationDbContext>(
            name: nameof(MyRestApplicationDbContext)
        );

    return serviceCollection;
}

The health check registered with IHealthChecksBuilder AddSqlServer(this IHealthChecksBuilder builder, string connectionString, string? name, TimeSpan? timeout) is provided by the AspNetCore.Diagnostics.HealthChecks version 8.0.2 NuGet package.

All health checks are executed when the GET /health endpoint receives an HTTP request.

The configured connection string in appsettings.json is connecting to SQL Server Express LocalDB:

"ConnectionStrings": {
  "DatabaseConnection": "Application Name='MyRestApplication.Local';Database='MyRestApplication';Server='(LocalDB)\\MSSQLLocalDB';Integrated Security=SSPI;MultipleActiveResultSets=False;Connection Timeout=15;"
}

However the test assembly is configured in Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint> to use the in-memory database using IServiceCollection UseInMemoryDatabase(this IServiceCollection serviceCollection), defined below like so:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public class TestApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder webHostBuilder)
    {
        webHostBuilder.ConfigureAppConfiguration(configurationBuilder =>
        {
            configurationBuilder
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
                .AddJsonFile(
                    path: Path.Combine(Directory.GetCurrentDirectory(), "appsettings.test.json"),
                    optional: false,
                    reloadOnChange: false
                )
                .AddUserSecrets<Program>();
        });
        webHostBuilder.ConfigureServices((webHostBuilderContext, serviceCollection) =>
        {
            serviceCollection
                .Configure<ConnectionStrings>(webHostBuilderContext.Configuration.GetSection(nameof(ConnectionStrings)))
                .UseInMemoryDatabase();

            var serviceProvider = serviceCollection.BuildServiceProvider();

            // Create a scope to obtain a reference to MyRestApplicationDbContext
            using var serviceScope = serviceProvider.CreateScope();
            var scopedServiceProvider = serviceScope.ServiceProvider;
            var scopedDbContext = scopedServiceProvider.GetRequiredService<MyRestApplicationDbContext>();
            var scopedLogger = scopedServiceProvider.GetRequiredService<ILogger<TestApplicationFactory>>();

            scopedDbContext.Database.EnsureCreated();

            try
            {
                SeedDbContext(scopedDbContext);
            }
            catch (Exception exception)
            {
                scopedLogger.LogError(
                    exception,
                    $"An unexpected exception of type '{scopedDbContext.GetType()}' " +
                    $"occurred while seeding the DbContext of type " +
                    $"'{typeof(MyRestApplicationDbContext)}': {exception}"
                );
            }
        });
    }

    /// <summary>
    ///     Seeds the specified <see cref="MyRestApplicationDbContext"/> with test data.
    /// </summary>
    /// <param name="dbContext"></param>
    private static void SeedDbContext(MyRestApplicationDbContext dbContext)
    {
        //dbContext.Sessions.Add(new Session { /* ... */ });
        //dbContext.SaveChanges();
    }
}

public static class IServiceCollectionExtensions
{
    public static IServiceCollection UseInMemoryDatabase(this IServiceCollection serviceCollection)
    {
        ArgumentNullException.ThrowIfNull(serviceCollection);

        // Remove DbContext registration, if it exist
        var serviceDescriptor = serviceCollection.SingleOrDefault(
            x => x.ServiceType == typeof(DbContextOptions<MyRestApplicationDbContext>)
        );

        if (serviceDescriptor is not null)
            serviceCollection.Remove(serviceDescriptor);

        // Register an in-memory database instead
        serviceCollection.AddDbContext<MyRestApplicationDbContext>((options, context) =>
        {
            context.UseInMemoryDatabase("MyRestApplication");
        });

        return serviceCollection;
    }
}

Now, all tests pass, except the test that calls the GET /health endpoint. When debugging, the failure is caused by the health check registered with IHealthChecksBuilder AddSqlServer(this IHealthChecksBuilder builder, string connectionString, string? name, TimeSpan? timeout), which throws a Microsoft.Data.SqlClient.SqlException:

Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot open database "MyRestApplication" requested by the login. The login failed.
Login failed for user 'AzureAD\StevenVolckaert'.
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, SqlCommand command, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at Microsoft.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.CompleteLogin(Boolean enlistOK)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.AttemptOneLogin(ServerInfo serverInfo, String newPassword, SecureString newSecurePassword, TimeoutTimer timeout, Boolean withFailover)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.LoginNoFailover(ServerInfo serverInfo, String newPassword, SecureString newSecurePassword, Boolean redirectedUserInstance, SqlConnectionString connectionOptions, SqlCredential credential, TimeoutTimer timeout)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.OpenLoginEnlist(TimeoutTimer timeout, SqlConnectionString connectionOptions, SqlCredential credential, String newPassword, SecureString newSecurePassword, Boolean redirectedUserInstance)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds..ctor(DbConnectionPoolIdentity identity, SqlConnectionString connectionOptions, SqlCredential credential, Object providerInfo, String newPassword, SecureString newSecurePassword, Boolean redirectedUserInstance, SqlConnectionString userConnectionOptions, SessionData reconnectSessionData, Boolean applyTransientFaultHandling, String accessToken, DbConnectionPool pool, Func`3 accessTokenCallback)
   at Microsoft.Data.SqlClient.SqlConnectionFactory.CreateConnection(DbConnectionOptions options, DbConnectionPoolKey poolKey, Object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection, DbConnectionOptions userOptions)
   at Microsoft.Data.ProviderBase.DbConnectionFactory.CreatePooledConnection(DbConnectionPool pool, DbConnection owningObject, DbConnectionOptions options, DbConnectionPoolKey poolKey, DbConnectionOptions userOptions)
   at Microsoft.Data.ProviderBase.DbConnectionPool.CreateObject(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection)
   at Microsoft.Data.ProviderBase.DbConnectionPool.UserCreateRequest(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection)
   at Microsoft.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection)
   at Microsoft.Data.ProviderBase.DbConnectionPool.WaitForPendingOpen()
--- End of stack trace from previous location ---
   at HealthChecks.SqlServer.SqlServerHealthCheck.CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken) in /_/src/HealthChecks.SqlServer/SqlServerHealthCheck.cs:line 28
ClientConnectionId:f1289a1e-0e0b-4b5f-b389-d715e7214424
Error Number:4060,State:1,Class:11

User AzureAD\StevenVolckaert is het Windows account I'm currently signed in with.

I'm sure the health check attempts to connect to LocalDB and not to the in-memory database provider, because when I change the connection string in appsettings.json to connect to a SQL Server instance, the test passes.

Any ideas what could be the cause of this exception, and how to resolve it?

Below is the xUnit test that fails:

using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

public class DatabaseConnectionHealthCheck_CheckHealthAsync : IClassFixture<TestApplicationFactory>
{
    private readonly TestApplicationFactory factory;

    public DatabaseConnectionHealthCheck_CheckHealthAsync(TestApplicationFactory factory)
    {
        this.factory = factory;
    }

    [Fact]
    public async Task ReturnsHealthy()
    {
        // Arrange
        var healthCheckService = this.factory.Services.GetRequiredService<HealthCheckService>();

        // Act
        var healthReport = await healthCheckService.CheckHealthAsync(
            predicate: registration => registration.Name == nameof(ConnectionStrings.DatabaseConnection)
        );

        // Assert
        healthReport.Should().NotBeNull();
        healthReport.Status.Should().Be(HealthStatus.Healthy); // Fails (healthReport.Status is HealthStatus.Unhealthy)
    }
}

Upvotes: 0

Views: 58

Answers (0)

Related Questions