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 82e8d0b5e89..c2cc06d124c 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,18 +982,37 @@ private static void AddTvfs(RelationalModel relationalModel) continue; } - var functionMapping = CreateFunctionMapping(entityType, entityType, function, relationalModel, @default: false); + AddTvfMapping(entityType, function, relationalModel, relationalTypeMappingSource); - if (entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionMappings) - is not List functionMappings) + foreach (var ownedJsonNavigation in entityType.GetNavigationsInHierarchy() + .Where( + n => n.ForeignKey.IsOwnership + && n.TargetEntityType.IsMappedToJson() + && n.ForeignKey.PrincipalToDependent == n)) { - functionMappings = []; - entityType.AddRuntimeAnnotation(RelationalAnnotationNames.FunctionMappings, functionMappings); + AddTvfMapping(ownedJsonNavigation.TargetEntityType, function, relationalModel, relationalTypeMappingSource); } + } + } - functionMappings.Add(functionMapping); - ((StoreFunction)functionMapping.StoreFunction).EntityTypeMappings.Add(functionMapping); + private static void AddTvfMapping( + IEntityType entityType, + IRuntimeDbFunction function, + RelationalModel relationalModel, + IRelationalTypeMappingSource relationalTypeMappingSource) + { + 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); } private static FunctionMapping CreateFunctionMapping( @@ -1001,6 +1020,7 @@ private static FunctionMapping CreateFunctionMapping( IEntityType mappedType, IRuntimeDbFunction dbFunction, RelationalModel model, + IRelationalTypeMappingSource relationalTypeMappingSource, bool @default) { var storeFunction = GetOrCreateStoreFunction(dbFunction, model); @@ -1010,29 +1030,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) + 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()) { - continue; - } + var columnName = property.GetColumnName(mappedFunction); + if (columnName == null) + { + continue; + } - var column = storeFunction.FindColumn(columnName); - if (column == null) - { - column = new FunctionColumn(columnName, property.GetColumnType(mappedFunction), storeFunction) + var column = storeFunction.FindColumn(columnName); + if (column == null) { - IsNullable = property.IsColumnNullable(mappedFunction) - }; - storeFunction.Columns.Add(columnName, column); - } - else if (!property.IsColumnNullable(mappedFunction)) - { - column.IsNullable = false; - } + 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; + } - CreateFunctionColumnMapping(column, property, functionMapping); + CreateFunctionColumnMapping(column, property, functionMapping); + } } return functionMapping; diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 21c3f3d9cbc..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(); @@ -3264,6 +3268,42 @@ 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.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(c => c.DateDetails, o => o.ToJson("date_details")); + cb.OwnsMany(c => c.Addresses, o => o.ToJson("addresses")); +#pragma warning restore EF8001 + }); + + modelBuilder.HasDbFunction( + typeof(RelationalModelTest).GetMethod( + nameof(GetOrdersForCustomer), BindingFlags.NonPublic | BindingFlags.Static, [typeof(int)])); + + var model = Finalize(modelBuilder); + + var orderType = model.Model.FindEntityType(typeof(Order)); + + var functionMappings = orderType.GetFunctionMappings().ToList(); + Assert.Single(functionMappings); + + var storeFunction = functionMappings[0].StoreFunction; + Assert.Equal( + [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) => modelBuilder.FinalizeModel(designTime: true).GetRelationalModel(); @@ -3350,6 +3390,8 @@ private class Order public OrderDetails Details { get; set; } public ComplexData ComplexProperty { get; set; } + + public List
Addresses { get; set; } } private class OrderDetails