Skip to content

Commit 1b36259

Browse files
Fix NRE in AddJsonNavigationBindings when combining DbFunction with OwnsOne/OwnsMany ToJson (#37855)
Fixes #37842 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
1 parent 63f9baa commit 1b36259

3 files changed

Lines changed: 118 additions & 34 deletions

File tree

src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -470,10 +470,20 @@ public static void SetSqlQuery(this IMutableEntityType entityType, string? query
470470
public static string? GetFunctionName(this IReadOnlyEntityType entityType)
471471
{
472472
var nameAnnotation = entityType.FindAnnotation(RelationalAnnotationNames.FunctionName);
473-
return nameAnnotation != null
474-
? (string?)nameAnnotation.Value
475-
: entityType.BaseType != null
476-
? entityType.GetRootType().GetFunctionName()
473+
if (nameAnnotation != null)
474+
{
475+
return (string?)nameAnnotation.Value;
476+
}
477+
478+
if (entityType.BaseType != null)
479+
{
480+
return entityType.GetRootType().GetFunctionName();
481+
}
482+
483+
var ownership = entityType.FindOwnership();
484+
return ownership != null
485+
&& (ownership.IsUnique || entityType.IsMappedToJson())
486+
? ownership.PrincipalEntityType.GetFunctionName()
477487
: null;
478488
}
479489

src/EFCore.Relational/Metadata/Internal/RelationalModel.cs

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,12 @@ public static IRelationalModel Create(
135135

136136
AddSqlQueries(databaseModel, entityType);
137137

138-
AddMappedFunctions(databaseModel, entityType);
138+
AddMappedFunctions(databaseModel, entityType, relationalTypeMappingSource);
139139

140140
AddStoredProcedures(databaseModel, entityType, relationalTypeMappingSource);
141141
}
142142

143-
AddTvfs(databaseModel);
143+
AddTvfs(databaseModel, relationalTypeMappingSource);
144144

145145
var tables = ((IRelationalModel)databaseModel).Tables;
146146
foreach (Table table in tables)
@@ -918,7 +918,7 @@ private static void AddSqlQueries(RelationalModel databaseModel, IEntityType ent
918918
queryMappings?.Reverse();
919919
}
920920

921-
private static void AddMappedFunctions(RelationalModel databaseModel, IEntityType entityType)
921+
private static void AddMappedFunctions(RelationalModel databaseModel, IEntityType entityType, IRelationalTypeMappingSource relationalTypeMappingSource)
922922
{
923923
var model = databaseModel.Model;
924924
var functionName = entityType.GetFunctionName();
@@ -940,7 +940,7 @@ private static void AddMappedFunctions(RelationalModel databaseModel, IEntityTyp
940940
}
941941

942942
var dbFunction = (IRuntimeDbFunction)model.FindDbFunction(mappedFunctionName)!;
943-
var functionMapping = CreateFunctionMapping(entityType, mappedType, dbFunction, databaseModel, @default: true);
943+
var functionMapping = CreateFunctionMapping(entityType, mappedType, dbFunction, databaseModel, relationalTypeMappingSource, @default: true);
944944

945945
mappedType = mappedType.BaseType;
946946

@@ -963,7 +963,7 @@ private static void AddMappedFunctions(RelationalModel databaseModel, IEntityTyp
963963
functionMappings?.Reverse();
964964
}
965965

966-
private static void AddTvfs(RelationalModel relationalModel)
966+
private static void AddTvfs(RelationalModel relationalModel, IRelationalTypeMappingSource relationalTypeMappingSource)
967967
{
968968
var model = relationalModel.Model;
969969
foreach (IRuntimeDbFunction function in model.GetDbFunctions())
@@ -982,25 +982,45 @@ private static void AddTvfs(RelationalModel relationalModel)
982982
continue;
983983
}
984984

985-
var functionMapping = CreateFunctionMapping(entityType, entityType, function, relationalModel, @default: false);
985+
AddTvfMapping(entityType, function, relationalModel, relationalTypeMappingSource);
986986

987-
if (entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionMappings)
988-
is not List<FunctionMapping> functionMappings)
987+
foreach (var ownedJsonNavigation in entityType.GetNavigationsInHierarchy()
988+
.Where(
989+
n => n.ForeignKey.IsOwnership
990+
&& n.TargetEntityType.IsMappedToJson()
991+
&& n.ForeignKey.PrincipalToDependent == n))
989992
{
990-
functionMappings = [];
991-
entityType.AddRuntimeAnnotation(RelationalAnnotationNames.FunctionMappings, functionMappings);
993+
AddTvfMapping(ownedJsonNavigation.TargetEntityType, function, relationalModel, relationalTypeMappingSource);
992994
}
995+
}
996+
}
993997

994-
functionMappings.Add(functionMapping);
995-
((StoreFunction)functionMapping.StoreFunction).EntityTypeMappings.Add(functionMapping);
998+
private static void AddTvfMapping(
999+
IEntityType entityType,
1000+
IRuntimeDbFunction function,
1001+
RelationalModel relationalModel,
1002+
IRelationalTypeMappingSource relationalTypeMappingSource)
1003+
{
1004+
var functionMapping = CreateFunctionMapping(
1005+
entityType, entityType, function, relationalModel, relationalTypeMappingSource, @default: false);
1006+
1007+
if (entityType.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionMappings)
1008+
is not List<FunctionMapping> functionMappings)
1009+
{
1010+
functionMappings = [];
1011+
entityType.AddRuntimeAnnotation(RelationalAnnotationNames.FunctionMappings, functionMappings);
9961012
}
1013+
1014+
functionMappings.Add(functionMapping);
1015+
((StoreFunction)functionMapping.StoreFunction).EntityTypeMappings.Add(functionMapping);
9971016
}
9981017

9991018
private static FunctionMapping CreateFunctionMapping(
10001019
IEntityType entityType,
10011020
IEntityType mappedType,
10021021
IRuntimeDbFunction dbFunction,
10031022
RelationalModel model,
1023+
IRelationalTypeMappingSource relationalTypeMappingSource,
10041024
bool @default)
10051025
{
10061026
var storeFunction = GetOrCreateStoreFunction(dbFunction, model);
@@ -1010,29 +1030,41 @@ private static FunctionMapping CreateFunctionMapping(
10101030
entityType, storeFunction, dbFunction,
10111031
includesDerivedTypes: entityType.GetDirectlyDerivedTypes().Any() ? true : null) { IsDefaultFunctionMapping = @default };
10121032

1013-
foreach (var property in mappedType.GetProperties())
1033+
var containerColumnName = mappedType.GetContainerColumnName();
1034+
var containerColumnType = mappedType.GetContainerColumnType();
1035+
if (!string.IsNullOrEmpty(containerColumnName))
10141036
{
1015-
var columnName = property.GetColumnName(mappedFunction);
1016-
if (columnName == null)
1037+
CreateContainerColumn(
1038+
storeFunction, containerColumnName, containerColumnType, mappedType, relationalTypeMappingSource,
1039+
static (colName, colType, table, mapping)
1040+
=> new FunctionColumn(colName, colType ?? mapping.StoreType, (StoreFunction)table, mapping));
1041+
}
1042+
else
1043+
{
1044+
foreach (var property in mappedType.GetProperties())
10171045
{
1018-
continue;
1019-
}
1046+
var columnName = property.GetColumnName(mappedFunction);
1047+
if (columnName == null)
1048+
{
1049+
continue;
1050+
}
10201051

1021-
var column = storeFunction.FindColumn(columnName);
1022-
if (column == null)
1023-
{
1024-
column = new FunctionColumn(columnName, property.GetColumnType(mappedFunction), storeFunction)
1052+
var column = storeFunction.FindColumn(columnName);
1053+
if (column == null)
10251054
{
1026-
IsNullable = property.IsColumnNullable(mappedFunction)
1027-
};
1028-
storeFunction.Columns.Add(columnName, column);
1029-
}
1030-
else if (!property.IsColumnNullable(mappedFunction))
1031-
{
1032-
column.IsNullable = false;
1033-
}
1055+
column = new FunctionColumn(columnName, property.GetColumnType(mappedFunction), storeFunction)
1056+
{
1057+
IsNullable = property.IsColumnNullable(mappedFunction)
1058+
};
1059+
storeFunction.Columns.Add(columnName, column);
1060+
}
1061+
else if (!property.IsColumnNullable(mappedFunction))
1062+
{
1063+
column.IsNullable = false;
1064+
}
10341065

1035-
CreateFunctionColumnMapping(column, property, functionMapping);
1066+
CreateFunctionColumnMapping(column, property, functionMapping);
1067+
}
10361068
}
10371069

10381070
return functionMapping;

test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public void Both_design_and_runtime_RelationalModels_are_built_for_external_mode
4444
modelBuilder.Ignore<OrderDetails>();
4545
modelBuilder.Ignore<DateDetails>();
4646
modelBuilder.Ignore<Customer>();
47+
modelBuilder.Ignore<Address>();
4748
modelBuilder.Entity<Order>().ToTable(tb => tb.HasCheckConstraint("OrderCK", "[Id] > 0"));
4849

4950
var options = FakeRelationalTestHelpers.Instance.CreateOptions((IModel)modelBuilder.Model);
@@ -2275,6 +2276,7 @@ private IRelationalModel CreateTestModel(
22752276

22762277
modelBuilder.Entity<Order>(ob =>
22772278
{
2279+
ob.Ignore(o => o.Addresses);
22782280
ob.Property(o => o.OrderDate).HasColumnName("OrderDate");
22792281
ob.Property(o => o.AlternateId).HasColumnName("AlternateId");
22802282

@@ -2934,6 +2936,7 @@ public void Can_use_relational_model_with_SQL_queries()
29342936
cb.Ignore(c => c.Customer);
29352937
cb.Ignore(c => c.Details);
29362938
cb.Ignore(c => c.DateDetails);
2939+
cb.Ignore(c => c.Addresses);
29372940

29382941
cb.Property(c => c.AlternateId).HasColumnName("SomeName");
29392942
cb.HasNoKey();
@@ -3063,6 +3066,7 @@ public void Can_use_relational_model_with_functions()
30633066
cb.Ignore(c => c.Customer);
30643067
cb.Ignore(c => c.Details);
30653068
cb.Ignore(c => c.DateDetails);
3069+
cb.Ignore(c => c.Addresses);
30663070

30673071
cb.Property(c => c.AlternateId).HasColumnName("SomeName");
30683072
cb.HasNoKey();
@@ -3264,6 +3268,42 @@ public void Complex_property_json_column_is_nullable_in_TPH_hierarchy()
32643268
Assert.IsType<JsonColumn>(jsonColumn);
32653269
}
32663270

3271+
[ConditionalFact]
3272+
public void Can_use_relational_model_with_functions_and_json_owned_types()
3273+
{
3274+
var modelBuilder = CreateConventionModelBuilder();
3275+
3276+
modelBuilder.Entity<Order>(cb =>
3277+
{
3278+
cb.Ignore(c => c.Customer);
3279+
cb.Ignore(c => c.Details);
3280+
cb.Ignore(c => c.ComplexProperty);
3281+
3282+
#pragma warning disable EF8001 // Owned JSON entities are obsolete
3283+
cb.OwnsOne(c => c.DateDetails, o => o.ToJson("date_details"));
3284+
cb.OwnsMany(c => c.Addresses, o => o.ToJson("addresses"));
3285+
#pragma warning restore EF8001
3286+
});
3287+
3288+
modelBuilder.HasDbFunction(
3289+
typeof(RelationalModelTest).GetMethod(
3290+
nameof(GetOrdersForCustomer), BindingFlags.NonPublic | BindingFlags.Static, [typeof(int)]));
3291+
3292+
var model = Finalize(modelBuilder);
3293+
3294+
var orderType = model.Model.FindEntityType(typeof(Order));
3295+
3296+
var functionMappings = orderType.GetFunctionMappings().ToList();
3297+
Assert.Single(functionMappings);
3298+
3299+
var storeFunction = functionMappings[0].StoreFunction;
3300+
Assert.Equal(
3301+
[nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.Id), nameof(Order.OrderDate), "addresses", "date_details"],
3302+
storeFunction.Columns.Select(m => m.Name));
3303+
Assert.NotNull(storeFunction.FindColumn("date_details"));
3304+
Assert.NotNull(storeFunction.FindColumn("addresses"));
3305+
}
3306+
32673307
private static IRelationalModel Finalize(TestHelpers.TestModelBuilder modelBuilder)
32683308
=> modelBuilder.FinalizeModel(designTime: true).GetRelationalModel();
32693309

@@ -3350,6 +3390,8 @@ private class Order
33503390
public OrderDetails Details { get; set; }
33513391

33523392
public ComplexData ComplexProperty { get; set; }
3393+
3394+
public List<Address> Addresses { get; set; }
33533395
}
33543396

33553397
private class OrderDetails

0 commit comments

Comments
 (0)