The problem

We ran into an interesting bug last week at work. We have a simple ASP.NET Core API that talks to a Postgres database using EF Core (for writes) and Dapper (for reads).

The following code is just a minimal repro, hence directly returning the database dtos and stuff like that from the action method, don't do that IRL :).

AppointmentDto.cs

public class AppointmentDto
{
    public Guid Id { get; set; }
    public string ExternalId { get; set; } = null!;
    public Instant Start { get; set; }
    public Instant End { get; set; }
}
private readonly IDatabaseConnection _db;

[HttpGet]
public async Task<List<AppointmentDto>> List()
{
    var result = await _db.QueryAsync<AppointmentDto>("SELECT * From \"Appointment\"");
    return result.ToList();
}

When testing this code using an integration test, everything worked flawlessly.

// Not actual code, just some psuedo
[Fact]
    public async Task GET_ShouldReturnAppointmentsCorrectly()
    {
        var db = _fixture.CreateDbContext();
        db.Appointments.Add(new Appointment(.......))
        db.Appointments.Add(new Appointment(.......))
        db.Appointments.Add(new Appointment(.......))
        await db.SaveChangesAsync();
        var request = new HttpRequestMessage(HttpMethod.Get, "/api/"appointments");
        var client = _fixture.CreateClient();

        var jsonDocument = await client.ExecuteAsJsonDocument(request);

        jsonDocument.RootElement.GetProperty("data").EnumerateArray().Count.ShouldBe(3);
    }

The test populates a real Postgres database with some data using a DbContext.
It then sends a request towards the API and asserts that 3 items are returned, nothing fancy.

However, when calling the same method via Postman, it didn't work, we got the following error:

InvalidCastException: Invalid cast from 'System.DateTime' to 'NodaTime.Instant'.

Weird?

The solution

Since we are using NodaTime, our PostregressDbContext (🤣) is configured like this.

services.AddDbContext<PostregressDbContext>((provider, options) =>
{
    options.UseNpgsql(connectionString.Value, b => b.UseNodaTime());
});

This configures the DbContext (EF Core) for NodaTime. When injecting the PostregressDbContext somewhere, the factory method will be run. This is key here. This is why our test works but Postman fails.

During the arrange phase in our test we are populating the database via DbContext, so the method above will run.

Now, you might ask, why does that solve the problem? You still haven't configured DAPPER for NodaTime?.

Nope, that's true, I haven't configured Dapper for NodaTime. But the UseNodaTime method has!

When decompiling the UseNodaTime method we get the following code:

public static NpgsqlDbContextOptionsBuilder UseNodaTime(
    this NpgsqlDbContextOptionsBuilder optionsBuilder)
{
    Check.NotNull(optionsBuilder, nameof(optionsBuilder));

    // TODO: Global-only setup at the ADO.NET level for now, optionally allow per-connection?
    NpgsqlConnection.GlobalTypeMapper.UseNodaTime();

    var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder;

    var extension = coreOptionsBuilder.Options.FindExtension<NpgsqlNodaTimeOptionsExtension>()
                    ?? new NpgsqlNodaTimeOptionsExtension();

    ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension);

    return optionsBuilder;
}

The code that directly follows the TODO comment is the interesting bit.

NpgsqlConnection.GlobalTypeMapper.UseNodaTime()

This will configure the NpgsqlConnection to use NodaTime.

Test worked because

  1. We used DbContext to populate our database with test data
  2. Since we used DbContext, the AddDbContext ran and configured the DbContext (AND the NpsqlConnection used by Dapper).

Postman failed because

  1. We didn't use a DbContext anywhere so the AddDbContext method never fired. IF we had called any OTHER endpoint that used a DbContext and THEN called the endpoint, the code would've worked as-is.

The fix

Don't forget to configure Dapper for NodaTime... :)

// Registers a IDbConnection to be used by Dapper
services.AddScoped<IDbConnection>(_ => new NpgsqlConnection(connectionString.Value));
// Add this line to configure the NpgsqlConnection to use NodaTime
NpgsqlConnection.GlobalTypeMapper.UseNodaTime(); <-- The fix!