Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trying to scaffold jsonb column throws "An item with the same key has already been added" #35338

Open
nejcerker opened this issue Dec 17, 2024 · 0 comments

Comments

@nejcerker
Copy link

Description

Hi. We are trying to scaffold a postgres DB in our C# Blazor app. The DB is pretty simple, but there is a number of jsonb columns on different tables that we are trying to bind to our POCOs.
E.g. we have a test_result table, with 2 jsonb columns runtime_info and test_output.

EF successfully scaffolded them into:

[Table("test_results", Schema = "fit3")]
public partial class TestResult
{
    [Column("runtime_info", TypeName = "jsonb")]
    public string? RuntimeInfo { get; set; }

    [Column("test_output", TypeName = "jsonb")]
    public string? TestOutput { get; set; }
}

Then, we created another partial class, like so:

public partial class TestResult
{
    public RuntimeInfo? RuntimeInfoJSON { get; set; }
    public TestOutput? TestOutputJSON { get; set; }
}

//along with the defined JSON structures
public class RuntimeInfo
{
	[JsonPropertyName("sw_resource_id")]
	public string? SwResourceId { get; set; }
}

//etc.

And finally, OnModelCreatingPartial:

partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
{
	modelBuilder.Entity<TestResult>(entity =>
	{
		entity.OwnsOne(x => x.RuntimeInfoJSON, t =>
		{
			t.ToJson("runtime_info");
		});

		entity.OwnsOne(x => x.TestOutputJSON, t =>
		{
			t.ToJson("test_output");
			t.OwnsMany(y => y.Measurements);
		});
	});
}

Up until here, everything works as expected, classes are populated with the values from the DB.

Then, following the same procedure, we wanted to add another jsonb field custom_config from another table fit_fixtures. We used exactly the same approach:

//EF auto generated code:
[Table("fit_fixtures", Schema = "fit3")]
public partial class FitFixture
{
    [Column("custom_config", TypeName = "jsonb")]
    public string? CustomConfig { get; set; }
}

//manually created partial class
public partial class FitFixture
{
    public CustomConfig? CustomConfigJSON { get; set; }
}

public class CustomConfig
{
    [JsonPropertyName("bb_communication")]
    public BbCommunication? BbCommunication { get; set; }
}
//etc.

//OnModelCreatingPartial
modelBuilder.Entity<FitFixture>(entity =>
{
	entity.OwnsOne(x => x.CustomConfigJSON, cc =>
	{
		cc.ToJson("custom_config");
		cc.OwnsOne(t => t.BbCommunication);
	});
});

Bug?

On the first DB query, in the EF initialization stage, we get an exception:

System.ArgumentException: 'An item with the same key has already been added. Key: [custom_config, Column: fit_fixtures.custom_config (jsonb) NonNullable)]'
   at System.Collections.Generic.TreeSet`1.AddIfNotPresent(T item)
   at System.Collections.Generic.SortedSet`1.Add(T item)
   at System.Collections.Generic.SortedDictionary`2.Add(TKey key, TValue value)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.CreateContainerColumn[TColumnMappingBase](TableBase tableBase, String containerColumnName, IEntityType mappedType, IRelationalTypeMappingSource relationalTypeMappingSource, Func`4 createColumn)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.CreateTableMapping(IRelationalTypeMappingSource relationalTypeMappingSource, ITypeBase typeBase, ITypeBase mappedType, StoreObjectIdentifier mappedTable, RelationalModel databaseModel, List`1 tableMappings, Boolean includesDerivedTypes, Nullable`1 isSplitEntityTypePrincipal)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.AddTables(RelationalModel databaseModel, IEntityType entityType, IRelationalTypeMappingSource relationalTypeMappingSource)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.Create(IModel model, IRelationalAnnotationProvider relationalAnnotationProvider, IRelationalTypeMappingSource relationalTypeMappingSource, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.Add(IModel model, IRelationalAnnotationProvider relationalAnnotationProvider, IRelationalTypeMappingSource relationalTypeMappingSource, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelRuntimeInitializer.InitializeModel(IModel model, Boolean designTime, Boolean prevalidation)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel(Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__8_4(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   etc...

Seems like it's a problem with the "two" columns custom_config being defined. If I remove the auto-generated scaffolded column, then it works, there is no exception, the values from the DB are correctly populated. But surely I don't want to fiddle with the auto-generated code...

So, the question is why doesn't the same thing happen with the other columns runtime_info and test_output?

I went looking down the stack and found this piece of code:

public static IRelationalModel Create(IModel model, IRelationalAnnotationProvider relationalAnnotationProvider, IRelationalTypeMappingSource relationalTypeMappingSource, bool designTime)
{
    RelationalModel relationalModel = new RelationalModel(model);
    foreach (IEntityType entityType in model.GetEntityTypes())
    { ... }

When I checked model.GetEntityTypes(), I saw that most of the Owned JSON types stuck together, TestResult entity type (that works fine) came after them, but the problematic FitFixture entity type came before them.
Image

Just for fun, I tried renaming FitFixture -> ZFitFixture, so it came after the owned JSON types and to my surprise it worked.

Please, shed some light onto this conundrum. Is our setup OK? Or is there actually a bug in EF?

Provider and version information

EF Core version: 8.0.10
Database provider: Npgsql.EntityFrameworkCore.PostgreSQL 8.0.10
Database: PostgreSQL 10.23
Target framework: .NET 8
Operating system: Windows 11
IDE: Visual Studio 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants