From 1ddc7e7bd5e808688d7bbe255f98d7afe1770d49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 06:50:04 +0000 Subject: [PATCH 1/5] Initial plan From c55a40945551af8303ffc7335cc1337d3fede6e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 07:09:35 +0000 Subject: [PATCH 2/5] Fix NullReferenceException in AddJsonNavigationBindings when combining DbFunction with OwnsOne/OwnsMany ToJson Add JSON container column creation to CreateFunctionMapping in RelationalModel, matching how tables and views already handle JSON container columns. Pass IRelationalTypeMappingSource through AddMappedFunctions, AddTvfs and CreateFunctionMapping to enable container column creation. Co-authored-by: roji <1862641+roji@users.noreply.github.com> --- .../Metadata/Internal/RelationalModel.cs | 30 ++++++++--- .../Metadata/RelationalModelTest.cs | 51 +++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 82e8d0b5e89..1f9825a9529 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -135,12 +135,12 @@ public static IRelationalModel Create( AddSqlQueries(databaseModel, entityType); - AddMappedFunctions(databaseModel, entityType); + AddMappedFunctions(databaseModel, entityType, relationalTypeMappingSource); AddStoredProcedures(databaseModel, entityType, relationalTypeMappingSource); } - AddTvfs(databaseModel); + AddTvfs(databaseModel, relationalTypeMappingSource); var tables = ((IRelationalModel)databaseModel).Tables; foreach (Table table in tables) @@ -918,7 +918,7 @@ private static void AddSqlQueries(RelationalModel databaseModel, IEntityType ent queryMappings?.Reverse(); } - private static void AddMappedFunctions(RelationalModel databaseModel, IEntityType entityType) + private static void AddMappedFunctions(RelationalModel databaseModel, IEntityType entityType, IRelationalTypeMappingSource relationalTypeMappingSource) { var model = databaseModel.Model; var functionName = entityType.GetFunctionName(); @@ -940,7 +940,7 @@ private static void AddMappedFunctions(RelationalModel databaseModel, IEntityTyp } var dbFunction = (IRuntimeDbFunction)model.FindDbFunction(mappedFunctionName)!; - var functionMapping = CreateFunctionMapping(entityType, mappedType, dbFunction, databaseModel, @default: true); + var functionMapping = CreateFunctionMapping(entityType, mappedType, dbFunction, databaseModel, relationalTypeMappingSource, @default: true); mappedType = mappedType.BaseType; @@ -963,7 +963,7 @@ private static void AddMappedFunctions(RelationalModel databaseModel, IEntityTyp functionMappings?.Reverse(); } - private static void AddTvfs(RelationalModel relationalModel) + private static void AddTvfs(RelationalModel relationalModel, IRelationalTypeMappingSource relationalTypeMappingSource) { var model = relationalModel.Model; foreach (IRuntimeDbFunction function in model.GetDbFunctions()) @@ -982,7 +982,7 @@ private static void AddTvfs(RelationalModel relationalModel) continue; } - var functionMapping = CreateFunctionMapping(entityType, entityType, function, relationalModel, @default: false); + var functionMapping = CreateFunctionMapping(entityType, entityType, function, relationalModel, relationalTypeMappingSource, @default: false); if (entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionMappings) is not List functionMappings) @@ -1001,6 +1001,7 @@ private static FunctionMapping CreateFunctionMapping( IEntityType mappedType, IRuntimeDbFunction dbFunction, RelationalModel model, + IRelationalTypeMappingSource relationalTypeMappingSource, bool @default) { var storeFunction = GetOrCreateStoreFunction(dbFunction, model); @@ -1035,6 +1036,23 @@ private static FunctionMapping CreateFunctionMapping( CreateFunctionColumnMapping(column, property, functionMapping); } + foreach (var navigation in entityType.GetNavigationsInHierarchy() + .Where( + n => n.ForeignKey.IsOwnership + && n.TargetEntityType.IsMappedToJson() + && n.ForeignKey.PrincipalToDependent == n)) + { + var targetEntityType = navigation.TargetEntityType; + CreateContainerColumn( + storeFunction, + targetEntityType.GetContainerColumnName()!, + targetEntityType.GetContainerColumnType(), + targetEntityType, + relationalTypeMappingSource, + static (colName, colType, table, mapping) + => new FunctionColumn(colName, colType ?? mapping.StoreType, (StoreFunction)table, mapping)); + } + return functionMapping; } diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 21c3f3d9cbc..10b015b6d95 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -2997,6 +2997,9 @@ private static IQueryable GetOrdersForCustomer(int id) private static IQueryable GetOrdersForCustomer(string name) => throw new NotImplementedException(); + private static IQueryable GetEntitiesWithJsonOwnedType() + => throw new NotImplementedException(); + [ConditionalFact] public void Complex_property_container_column_type_is_used_in_relational_model() { @@ -3264,6 +3267,36 @@ public void Complex_property_json_column_is_nullable_in_TPH_hierarchy() Assert.IsType(jsonColumn); } + [ConditionalFact] + public void Can_use_relational_model_with_functions_and_json_owned_types() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(cb => + { + cb.HasKey(x => x.Id); +#pragma warning disable EF8001 // Owned JSON entities are obsolete + cb.OwnsOne(x => x.OwnedData, o => o.ToJson("owned_data")); + cb.OwnsMany(x => x.OwnedItems, o => o.ToJson("owned_items")); +#pragma warning restore EF8001 + }); + + modelBuilder.HasDbFunction( + typeof(RelationalModelTest).GetMethod( + nameof(GetEntitiesWithJsonOwnedType), BindingFlags.NonPublic | BindingFlags.Static)); + + var model = Finalize(modelBuilder); + + var entityType = model.Model.FindEntityType(typeof(EntityWithJsonOwnedType)); + + var functionMappings = entityType.GetFunctionMappings().ToList(); + Assert.Single(functionMappings); + + var storeFunction = functionMappings[0].StoreFunction; + Assert.NotNull(storeFunction.FindColumn("owned_data")); + Assert.NotNull(storeFunction.FindColumn("owned_items")); + } + private static IRelationalModel Finalize(TestHelpers.TestModelBuilder modelBuilder) => modelBuilder.FinalizeModel(designTime: true).GetRelationalModel(); @@ -3407,6 +3440,24 @@ private class ComplexData public string Value { get; set; } public int Number { get; set; } } + + private class EntityWithJsonOwnedType + { + public int Id { get; set; } + public string Name { get; set; } + public OwnedJsonData OwnedData { get; set; } + public ICollection OwnedItems { get; set; } + } + + private class OwnedJsonData + { + public string Value { get; set; } + } + + private class OwnedJsonItem + { + public string Description { get; set; } + } } } From 1c902a770c102efd1f6335cd540224528ad813b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:03:31 +0000 Subject: [PATCH 3/5] Address review feedback: simplify CreateFunctionMapping and fix GetFunctionName ownership fallback Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../RelationalEntityTypeExtensions.cs | 18 +++- .../Metadata/Internal/RelationalModel.cs | 86 +++++++++++-------- 2 files changed, 65 insertions(+), 39 deletions(-) diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index 1f746b06c2f..7b7ca39ac61 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -470,10 +470,20 @@ public static void SetSqlQuery(this IMutableEntityType entityType, string? query public static string? GetFunctionName(this IReadOnlyEntityType entityType) { var nameAnnotation = entityType.FindAnnotation(RelationalAnnotationNames.FunctionName); - return nameAnnotation != null - ? (string?)nameAnnotation.Value - : entityType.BaseType != null - ? entityType.GetRootType().GetFunctionName() + if (nameAnnotation != null) + { + return (string?)nameAnnotation.Value; + } + + if (entityType.BaseType != null) + { + return entityType.GetRootType().GetFunctionName(); + } + + var ownership = entityType.FindOwnership(); + return ownership != null + && (ownership.IsUnique || entityType.IsMappedToJson()) + ? ownership.PrincipalEntityType.GetFunctionName() : null; } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 1f9825a9529..0c0a0ba1d4e 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -993,6 +993,27 @@ private static void AddTvfs(RelationalModel relationalModel, IRelationalTypeMapp functionMappings.Add(functionMapping); ((StoreFunction)functionMapping.StoreFunction).EntityTypeMappings.Add(functionMapping); + + foreach (var ownedJsonNavigation in entityType.GetNavigationsInHierarchy() + .Where( + n => n.ForeignKey.IsOwnership + && n.TargetEntityType.IsMappedToJson() + && n.ForeignKey.PrincipalToDependent == n)) + { + var ownedType = ownedJsonNavigation.TargetEntityType; + var ownedFunctionMapping = CreateFunctionMapping( + ownedType, ownedType, function, relationalModel, relationalTypeMappingSource, @default: false); + + if (ownedType.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionMappings) + is not List ownedFunctionMappings) + { + ownedFunctionMappings = []; + ownedType.AddRuntimeAnnotation(RelationalAnnotationNames.FunctionMappings, ownedFunctionMappings); + } + + ownedFunctionMappings.Add(ownedFunctionMapping); + ((StoreFunction)ownedFunctionMapping.StoreFunction).EntityTypeMappings.Add(ownedFunctionMapping); + } } } @@ -1011,46 +1032,41 @@ private static FunctionMapping CreateFunctionMapping( entityType, storeFunction, dbFunction, includesDerivedTypes: entityType.GetDirectlyDerivedTypes().Any() ? true : null) { IsDefaultFunctionMapping = @default }; - foreach (var property in mappedType.GetProperties()) + var containerColumnName = mappedType.GetContainerColumnName(); + var containerColumnType = mappedType.GetContainerColumnType(); + if (!string.IsNullOrEmpty(containerColumnName)) { - var columnName = property.GetColumnName(mappedFunction); - if (columnName == null) - { - continue; - } - - var column = storeFunction.FindColumn(columnName); - if (column == null) + CreateContainerColumn( + storeFunction, containerColumnName, containerColumnType, mappedType, relationalTypeMappingSource, + static (colName, colType, table, mapping) + => new FunctionColumn(colName, colType ?? mapping.StoreType, (StoreFunction)table, mapping)); + } + else + { + foreach (var property in mappedType.GetProperties()) { - column = new FunctionColumn(columnName, property.GetColumnType(mappedFunction), storeFunction) + var columnName = property.GetColumnName(mappedFunction); + if (columnName == null) { - IsNullable = property.IsColumnNullable(mappedFunction) - }; - storeFunction.Columns.Add(columnName, column); - } - else if (!property.IsColumnNullable(mappedFunction)) - { - column.IsNullable = false; - } + continue; + } - CreateFunctionColumnMapping(column, property, functionMapping); - } + var column = storeFunction.FindColumn(columnName); + if (column == null) + { + column = new FunctionColumn(columnName, property.GetColumnType(mappedFunction), storeFunction) + { + IsNullable = property.IsColumnNullable(mappedFunction) + }; + storeFunction.Columns.Add(columnName, column); + } + else if (!property.IsColumnNullable(mappedFunction)) + { + column.IsNullable = false; + } - foreach (var navigation in entityType.GetNavigationsInHierarchy() - .Where( - n => n.ForeignKey.IsOwnership - && n.TargetEntityType.IsMappedToJson() - && n.ForeignKey.PrincipalToDependent == n)) - { - var targetEntityType = navigation.TargetEntityType; - CreateContainerColumn( - storeFunction, - targetEntityType.GetContainerColumnName()!, - targetEntityType.GetContainerColumnType(), - targetEntityType, - relationalTypeMappingSource, - static (colName, colType, table, mapping) - => new FunctionColumn(colName, colType ?? mapping.StoreType, (StoreFunction)table, mapping)); + CreateFunctionColumnMapping(column, property, functionMapping); + } } return functionMapping; From 4bfd2db545b4161c802fdfe0a3dec4104d0f9a7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:45:00 +0000 Subject: [PATCH 4/5] Use existing Order class in function+JSON test and assert column names Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Metadata/RelationalModelTest.cs | 42 ++++++------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 10b015b6d95..63dc16fc56f 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -2997,9 +2997,6 @@ private static IQueryable GetOrdersForCustomer(int id) private static IQueryable GetOrdersForCustomer(string name) => throw new NotImplementedException(); - private static IQueryable GetEntitiesWithJsonOwnedType() - => throw new NotImplementedException(); - [ConditionalFact] public void Complex_property_container_column_type_is_used_in_relational_model() { @@ -3272,29 +3269,33 @@ public void Can_use_relational_model_with_functions_and_json_owned_types() { var modelBuilder = CreateConventionModelBuilder(); - modelBuilder.Entity(cb => + modelBuilder.Entity(cb => { - cb.HasKey(x => x.Id); + cb.Ignore(c => c.Customer); + cb.Ignore(c => c.Details); + cb.Ignore(c => c.ComplexProperty); + #pragma warning disable EF8001 // Owned JSON entities are obsolete - cb.OwnsOne(x => x.OwnedData, o => o.ToJson("owned_data")); - cb.OwnsMany(x => x.OwnedItems, o => o.ToJson("owned_items")); + cb.OwnsOne(c => c.DateDetails, o => o.ToJson("date_details")); #pragma warning restore EF8001 }); modelBuilder.HasDbFunction( typeof(RelationalModelTest).GetMethod( - nameof(GetEntitiesWithJsonOwnedType), BindingFlags.NonPublic | BindingFlags.Static)); + nameof(GetOrdersForCustomer), BindingFlags.NonPublic | BindingFlags.Static, [typeof(int)])); var model = Finalize(modelBuilder); - var entityType = model.Model.FindEntityType(typeof(EntityWithJsonOwnedType)); + var orderType = model.Model.FindEntityType(typeof(Order)); - var functionMappings = entityType.GetFunctionMappings().ToList(); + var functionMappings = orderType.GetFunctionMappings().ToList(); Assert.Single(functionMappings); var storeFunction = functionMappings[0].StoreFunction; - Assert.NotNull(storeFunction.FindColumn("owned_data")); - Assert.NotNull(storeFunction.FindColumn("owned_items")); + Assert.Equal( + [nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.Id), nameof(Order.OrderDate), "date_details"], + storeFunction.Columns.Select(m => m.Name)); + Assert.NotNull(storeFunction.FindColumn("date_details")); } private static IRelationalModel Finalize(TestHelpers.TestModelBuilder modelBuilder) @@ -3441,23 +3442,6 @@ private class ComplexData public int Number { get; set; } } - private class EntityWithJsonOwnedType - { - public int Id { get; set; } - public string Name { get; set; } - public OwnedJsonData OwnedData { get; set; } - public ICollection OwnedItems { get; set; } - } - - private class OwnedJsonData - { - public string Value { get; set; } - } - - private class OwnedJsonItem - { - public string Description { get; set; } - } } } From e3ee4f0fb608623a5f57e822ea48e00ddcf2e281 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:32:24 +0000 Subject: [PATCH 5/5] Extract AddTvfMapping helper, add OwnsMany test, remove blank line Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Metadata/Internal/RelationalModel.cs | 44 +++++++++---------- .../Metadata/RelationalModelTest.cs | 11 ++++- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 0c0a0ba1d4e..c2cc06d124c 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -982,17 +982,7 @@ private static void AddTvfs(RelationalModel relationalModel, IRelationalTypeMapp continue; } - var functionMapping = CreateFunctionMapping(entityType, entityType, function, relationalModel, relationalTypeMappingSource, @default: false); - - if (entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionMappings) - is not List functionMappings) - { - functionMappings = []; - entityType.AddRuntimeAnnotation(RelationalAnnotationNames.FunctionMappings, functionMappings); - } - - functionMappings.Add(functionMapping); - ((StoreFunction)functionMapping.StoreFunction).EntityTypeMappings.Add(functionMapping); + AddTvfMapping(entityType, function, relationalModel, relationalTypeMappingSource); foreach (var ownedJsonNavigation in entityType.GetNavigationsInHierarchy() .Where( @@ -1000,21 +990,29 @@ private static void AddTvfs(RelationalModel relationalModel, IRelationalTypeMapp && n.TargetEntityType.IsMappedToJson() && n.ForeignKey.PrincipalToDependent == n)) { - var ownedType = ownedJsonNavigation.TargetEntityType; - var ownedFunctionMapping = CreateFunctionMapping( - ownedType, ownedType, function, relationalModel, relationalTypeMappingSource, @default: false); + AddTvfMapping(ownedJsonNavigation.TargetEntityType, function, relationalModel, relationalTypeMappingSource); + } + } + } - if (ownedType.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionMappings) - is not List ownedFunctionMappings) - { - ownedFunctionMappings = []; - ownedType.AddRuntimeAnnotation(RelationalAnnotationNames.FunctionMappings, ownedFunctionMappings); - } + private static void AddTvfMapping( + IEntityType entityType, + IRuntimeDbFunction function, + RelationalModel relationalModel, + IRelationalTypeMappingSource relationalTypeMappingSource) + { + var functionMapping = CreateFunctionMapping( + entityType, entityType, function, relationalModel, relationalTypeMappingSource, @default: false); - ownedFunctionMappings.Add(ownedFunctionMapping); - ((StoreFunction)ownedFunctionMapping.StoreFunction).EntityTypeMappings.Add(ownedFunctionMapping); - } + if (entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionMappings) + is not List functionMappings) + { + functionMappings = []; + entityType.AddRuntimeAnnotation(RelationalAnnotationNames.FunctionMappings, functionMappings); } + + functionMappings.Add(functionMapping); + ((StoreFunction)functionMapping.StoreFunction).EntityTypeMappings.Add(functionMapping); } private static FunctionMapping CreateFunctionMapping( diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 63dc16fc56f..13ad7cbaba1 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -44,6 +44,7 @@ public void Both_design_and_runtime_RelationalModels_are_built_for_external_mode modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); + modelBuilder.Ignore
(); modelBuilder.Entity().ToTable(tb => tb.HasCheckConstraint("OrderCK", "[Id] > 0")); var options = FakeRelationalTestHelpers.Instance.CreateOptions((IModel)modelBuilder.Model); @@ -2275,6 +2276,7 @@ private IRelationalModel CreateTestModel( modelBuilder.Entity(ob => { + ob.Ignore(o => o.Addresses); ob.Property(o => o.OrderDate).HasColumnName("OrderDate"); ob.Property(o => o.AlternateId).HasColumnName("AlternateId"); @@ -2934,6 +2936,7 @@ public void Can_use_relational_model_with_SQL_queries() cb.Ignore(c => c.Customer); cb.Ignore(c => c.Details); cb.Ignore(c => c.DateDetails); + cb.Ignore(c => c.Addresses); cb.Property(c => c.AlternateId).HasColumnName("SomeName"); cb.HasNoKey(); @@ -3063,6 +3066,7 @@ public void Can_use_relational_model_with_functions() cb.Ignore(c => c.Customer); cb.Ignore(c => c.Details); cb.Ignore(c => c.DateDetails); + cb.Ignore(c => c.Addresses); cb.Property(c => c.AlternateId).HasColumnName("SomeName"); cb.HasNoKey(); @@ -3277,6 +3281,7 @@ public void Can_use_relational_model_with_functions_and_json_owned_types() #pragma warning disable EF8001 // Owned JSON entities are obsolete cb.OwnsOne(c => c.DateDetails, o => o.ToJson("date_details")); + cb.OwnsMany(c => c.Addresses, o => o.ToJson("addresses")); #pragma warning restore EF8001 }); @@ -3293,9 +3298,10 @@ public void Can_use_relational_model_with_functions_and_json_owned_types() var storeFunction = functionMappings[0].StoreFunction; Assert.Equal( - [nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.Id), nameof(Order.OrderDate), "date_details"], + [nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.Id), nameof(Order.OrderDate), "addresses", "date_details"], storeFunction.Columns.Select(m => m.Name)); Assert.NotNull(storeFunction.FindColumn("date_details")); + Assert.NotNull(storeFunction.FindColumn("addresses")); } private static IRelationalModel Finalize(TestHelpers.TestModelBuilder modelBuilder) @@ -3384,6 +3390,8 @@ private class Order public OrderDetails Details { get; set; } public ComplexData ComplexProperty { get; set; } + + public List
Addresses { get; set; } } private class OrderDetails @@ -3441,7 +3449,6 @@ private class ComplexData public string Value { get; set; } public int Number { get; set; } } - } }