Open LucHeart opened 2 weeks ago
Can you please submit a minimal, runnable code sample? Partial snippets are very rarely enough to understand exactly what a user is doing.
Here ya go, simple as that.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
const string connectionString = "Host=localhost;Port=5432;Database=root;Username=root;Password=root"; // This doesnt need to be up, just needs to be a valid conn string
builder.Services.AddDbContextPool<MyContext>(optionsBuilder =>
{
optionsBuilder.UseNpgsql(connectionString, npgsql =>
{
npgsql.MapEnum<FunnyEnum>(); // Map Enum
});
});
builder.Services.AddPooledDbContextFactory<MyContext>(optionsBuilder =>
{
optionsBuilder.UseNpgsql(connectionString, npgsql =>
{
npgsql.MapEnum<FunnyEnum>(); // Map Enum, but this seems to cause the issue
});
});
var app = builder.Build();
await using var scope = app.Services.CreateAsyncScope();
var myContext = scope.ServiceProvider.GetRequiredService<MyContext>(); // Request context to initialize
var tryToQuery = await myContext.Test.FirstOrDefaultAsync(); // This will throw an exception
app.Run();
public class MyContext : DbContext
{
public MyContext() { }
public MyContext(DbContextOptions<MyContext> options) : base(options) { }
public virtual DbSet<TestSet> Test { get; set; }
}
public class TestSet
{
public Guid Id { get; set; }
public FunnyEnum FunnyEnum { get; set; }
}
public enum FunnyEnum
{
Yes
}
csproj is just web sdk and npgsql efcore
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.1" />
</ItemGroup>
</Project>
Facing the same issue, it seems like the MapEnum options write to a options.EnumDefinitions
and options is a singleton INpgsqlSingletonOptions
so calling UseNpgsql twice ends up adding the enum twice to the list :/
In my config I'm reusing the same exact npgsql options - I get the same one via a method
services.AddDbContext<DbContext>((servicesProvider, options) => DbContextOptionBuilder(options, servicesProvider), ServiceLifetime.Transient);
services.AddPooledDbContextFactory<DbContext>((servicesProvider, options) => DbContextOptionBuilder(options, servicesProvider));
Sadly the old way to do enums don't seems to work anymore and this cause issues with the new way.
Is there anyway to load the config somehow outside of the dbcontext adds ?
something like
service.AddSingleton<INpgsqlSingletonOptions>(new NpgsqlSingletonOptionsBuilder().TheOptions());
services.AddDbContext<DbContext>(ServiceLifetime.Transient);
services.AddPooledDbContextFactory<DbContext>();
Thanks for the minimal repro @LucHeart - I can see the problem happening.
So the way EF configuration works, if you specify both AddDbContextPool() and AddPooledDbContextFactory(), the configuration lambdas in both calls incrementally configure the same options; the context instances you get from both injection methods (direct injection, injection of the context factory) have the same options and behave identically.
This means that your code is effectively the same as duplicate invocation of MapEnum, i.e.:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseNpgsql("Host=localhost;Username=test;Password=test", o =>
{
o.MapEnum<FunnyEnum>();
o.MapEnum<FunnyEnum>();
})
I'll think a bit and consult with other EF members to see what we think of this... I could of course add logic to the PG provider that detects duplicate and/or conflicting mapping (keep in mind that the 2nd MapEnum could specify a different store type, or name translator - that would be incompatible and have to throw).
But it seems wrong for users to duplicate the context configuration just because the want to use both AddDbContextPool() and AddPooledDbContextFactory(). So at least as a temporary workaround, I'd recommend doing the following:
services.AddDbContextPool<MyContext>(optionsBuilder =>
optionsBuilder.UseNpgsql(connectionString, npgsql => npgsql.MapEnum<FunnyEnum>()));
services.AddPooledDbContextFactory<MyContext>(_ => {});
In other words, configure only once - in the first lambda - and do nothing in the second. But as I wrote above, I'll consult with the EF team to understand this better.
/cc @ajcvickers
From discussion with the EF team: we agree that double configuration is a bad thing (i.e. repeating the same MapEnum twice); we may look into allowing both DbContext and IDbContextFactory to be registered in DI via a single call (https://github.com/dotnet/efcore/issues/26528).
Regardless, we think it's a good idea for the PG provider to handle this scenario better, i.e. detect and ignore duplicate MapEnum invocations, and throw (or possibly do last-one-wins) for incompatible invocations.
I agree..
In other words, configure only once - in the first lambda - and do nothing in the second
thats what I've been doing in the meantime, just looks kinda sketchy but works fine
FWIW I think repeating the same configuration twice is even more sketchy... :/
Yeah that too
When using multiple DbContexts with the same connection string or data source and trying to .MapEnum the enums present on both of them will throw an exception at startup. We ran into this issue while trying to upgrade from net8 to net9
We are using DbContextPool and PooledDbContextFactory in our application, because in same specific parts it makes more sense to use the factory.
The same is true when using a datasource. (It really doesnt make sense to me that .MapEnum on the datasource doesnt do anything btw. not sure if this is intended. I assumed just defining it on the data source was enough to begin with)
The error that is being thrown