Skip to content

Commit 5f149a0

Browse files
authored
Add basic update pipeline support for complex properties mapped to JSON (#36379)
Part of #31252
1 parent 1b05bed commit 5f149a0

File tree

8 files changed

+1026
-66
lines changed

8 files changed

+1026
-66
lines changed

src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static class RelationalComplexPropertyExtensions
2424
/// </returns>
2525
public static string? GetJsonPropertyName(this IReadOnlyComplexProperty complexProperty)
2626
=> (string?)complexProperty.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.Value
27-
?? (!complexProperty.DeclaringType.IsMappedToJson() ? null : complexProperty.Name);
27+
?? (complexProperty.DeclaringType.IsMappedToJson() ? complexProperty.Name : null);
2828

2929
/// <summary>
3030
/// Sets the value of JSON property name used for the given complex property of an entity mapped to a JSON column.

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

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -542,31 +542,31 @@ private static void CreateTableMapping(
542542

543543
CreateColumnMapping(column, property, tableMapping);
544544
}
545+
}
545546

546-
// TODO: Change this to call GetComplexProperties()
547-
// Issue #31248
548-
foreach (var complexProperty in mappedType.GetDeclaredComplexProperties())
549-
{
550-
var complexType = complexProperty.ComplexType;
551-
552-
var complexTableMappings =
553-
(List<TableMapping>?)complexType.FindRuntimeAnnotationValue(RelationalAnnotationNames.TableMappings);
554-
if (complexTableMappings == null)
555-
{
556-
complexTableMappings = [];
557-
complexType.AddRuntimeAnnotation(RelationalAnnotationNames.TableMappings, complexTableMappings);
558-
}
547+
// TODO: Change this to call GetComplexProperties()
548+
// Issue #31248
549+
foreach (var complexProperty in mappedType.GetDeclaredComplexProperties())
550+
{
551+
var complexType = complexProperty.ComplexType;
559552

560-
CreateTableMapping(
561-
relationalTypeMappingSource,
562-
complexType,
563-
complexType,
564-
mappedTable,
565-
databaseModel,
566-
complexTableMappings,
567-
includesDerivedTypes: true,
568-
isSplitEntityTypePrincipal: isSplitEntityTypePrincipal == true ? false : isSplitEntityTypePrincipal);
553+
var complexTableMappings =
554+
(List<TableMapping>?)complexType.FindRuntimeAnnotationValue(RelationalAnnotationNames.TableMappings);
555+
if (complexTableMappings == null)
556+
{
557+
complexTableMappings = [];
558+
complexType.AddRuntimeAnnotation(RelationalAnnotationNames.TableMappings, complexTableMappings);
569559
}
560+
561+
CreateTableMapping(
562+
relationalTypeMappingSource,
563+
complexType,
564+
complexType,
565+
mappedTable,
566+
databaseModel,
567+
complexTableMappings,
568+
includesDerivedTypes: true,
569+
isSplitEntityTypePrincipal: isSplitEntityTypePrincipal == true ? false : isSplitEntityTypePrincipal);
570570
}
571571

572572
if (((ITableMappingBase)tableMapping).ColumnMappings.Any()

src/EFCore.Relational/Update/ModificationCommand.cs

Lines changed: 103 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Text.Json;
88
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
99
using Microsoft.EntityFrameworkCore.Internal;
10+
using Microsoft.EntityFrameworkCore.Metadata;
1011
using Microsoft.EntityFrameworkCore.Metadata.Internal;
1112
using IColumnMapping = Microsoft.EntityFrameworkCore.Metadata.IColumnMapping;
1213
using ITableMapping = Microsoft.EntityFrameworkCore.Metadata.ITableMapping;
@@ -243,7 +244,7 @@ private sealed class JsonPartialUpdateInfo
243244
public object? PropertyValue { get; set; }
244245
}
245246

246-
private record struct JsonPartialUpdatePathEntry(string PropertyName, int? Ordinal, IUpdateEntry ParentEntry, INavigation Navigation);
247+
private record struct JsonPartialUpdatePathEntry(string PropertyName, int? Ordinal, IUpdateEntry ParentEntry, INavigation? Navigation, IComplexProperty? ComplexProperty = null);
247248

248249
private List<IColumnModification> GenerateColumnModifications()
249250
{
@@ -263,7 +264,7 @@ private List<IColumnModification> GenerateColumnModifications()
263264
{
264265
Check.DebugAssert(StoreStoredProcedure is null, "Multiple entries/shared identity not supported with stored procedures");
265266

266-
sharedTableColumnMap = new Dictionary<string, ColumnValuePropagator>();
267+
sharedTableColumnMap = [];
267268

268269
if (_comparer != null
269270
&& _entries.Count > 1)
@@ -297,6 +298,7 @@ private List<IColumnModification> GenerateColumnModifications()
297298
if (!jsonEntry)
298299
{
299300
if (entry.EntityType.IsMappedToJson()
301+
|| entry.EntityType.GetFlattenedComplexProperties().Any(cp => cp.ComplexType.IsMappedToJson())
300302
|| entry.EntityType.GetNavigations().Any(e => e.IsCollection && e.TargetEntityType.IsMappedToJson()))
301303
{
302304
jsonEntry = true;
@@ -654,7 +656,8 @@ static JsonPartialUpdateInfo FindCommonJsonPartialUpdateInfo(
654656
first.Path[i].PropertyName,
655657
null,
656658
first.Path[i].ParentEntry,
657-
first.Path[i].Navigation);
659+
Navigation: first.Path[i].Navigation,
660+
ComplexProperty: first.Path[i].ComplexProperty);
658661

659662
result.Path.Add(common);
660663

@@ -671,11 +674,15 @@ void HandleJson(List<IColumnModification> columnModifications)
671674
{
672675
var jsonColumnsUpdateMap = new Dictionary<IColumn, JsonPartialUpdateInfo>();
673676
var processedEntries = new List<IUpdateEntry>();
674-
foreach (var entry in _entries.Where(e => e.EntityType.IsMappedToJson()))
677+
foreach (var entry in _entries)
675678
{
679+
if (!entry.EntityType.IsMappedToJson())
680+
{
681+
continue;
682+
}
683+
676684
var jsonColumn = GetTableMapping(entry.EntityType)!.Table.FindColumn(entry.EntityType.GetContainerColumnName()!)!;
677685
var jsonPartialUpdateInfo = FindJsonPartialUpdateInfo(entry, processedEntries);
678-
679686
if (jsonPartialUpdateInfo == null)
680687
{
681688
continue;
@@ -691,8 +698,13 @@ void HandleJson(List<IColumnModification> columnModifications)
691698
jsonColumnsUpdateMap[jsonColumn] = jsonPartialUpdateInfo;
692699
}
693700

694-
foreach (var entry in _entries.Where(e => !e.EntityType.IsMappedToJson()))
701+
foreach (var entry in _entries)
695702
{
703+
if (entry.EntityType.IsMappedToJson())
704+
{
705+
continue;
706+
}
707+
696708
foreach (var jsonCollectionNavigation in entry.EntityType.GetNavigations()
697709
.Where(
698710
n => n.IsCollection
@@ -712,14 +724,32 @@ void HandleJson(List<IColumnModification> columnModifications)
712724
jsonColumnsUpdateMap[jsonCollectionColumn] = jsonPartialUpdateInfo;
713725
}
714726
}
727+
728+
foreach (var complexProperty in entry.EntityType.GetFlattenedComplexProperties()
729+
.Where(cp => cp.ComplexType.IsMappedToJson() && !cp.DeclaringType.IsMappedToJson()))
730+
{
731+
var complexType = complexProperty.ComplexType;
732+
var jsonColumn = GetTableMapping(entry.EntityType)!.Table.FindColumn(complexType.GetContainerColumnName()!)!;
733+
734+
if (!jsonColumnsUpdateMap.ContainsKey(jsonColumn))
735+
{
736+
var jsonPartialUpdateInfo = new JsonPartialUpdateInfo();
737+
jsonPartialUpdateInfo.Path.Insert(0, new JsonPartialUpdatePathEntry("$", null, entry, Navigation: null, ComplexProperty: complexProperty));
738+
jsonPartialUpdateInfo.PropertyValue = entry.GetCurrentValue(complexProperty);
739+
jsonColumnsUpdateMap[jsonColumn] = jsonPartialUpdateInfo;
740+
}
741+
}
715742
}
716743

717744
foreach (var (jsonColumn, updateInfo) in jsonColumnsUpdateMap)
718745
{
719746
var finalUpdatePathElement = updateInfo.Path.Last();
720747
var navigation = finalUpdatePathElement.Navigation;
748+
var complexProperty = finalUpdatePathElement.ComplexProperty;
721749
var jsonColumnTypeMapping = jsonColumn.StoreTypeMapping;
722-
var navigationValue = finalUpdatePathElement.ParentEntry.GetCurrentValue(navigation);
750+
var jsonContainerProperty = (IPropertyBase?)navigation ?? complexProperty;
751+
var navigationValue = finalUpdatePathElement.ParentEntry.GetCurrentValue(jsonContainerProperty!);
752+
723753
var jsonPathString = string.Join(
724754
".", updateInfo.Path.Select(x => x.PropertyName + (x.Ordinal != null ? "[" + x.Ordinal + "]" : "")));
725755
if (updateInfo.Property is IProperty property)
@@ -755,8 +785,8 @@ void HandleJson(List<IColumnModification> columnModifications)
755785
WriteJson(
756786
writer,
757787
navigationValueElement,
758-
finalUpdatePathElement.ParentEntry,
759-
navigation.TargetEntityType,
788+
(IInternalEntry)finalUpdatePathElement.ParentEntry,
789+
((IPropertyBase?)navigation) ?? complexProperty!,
760790
ordinal: null,
761791
isCollection: false,
762792
isTopLevel: true);
@@ -769,13 +799,14 @@ void HandleJson(List<IColumnModification> columnModifications)
769799
}
770800
else
771801
{
802+
var propertyBase = ((IPropertyBase?)navigation) ?? complexProperty!;
772803
WriteJson(
773804
writer,
774805
navigationValue,
775-
finalUpdatePathElement.ParentEntry,
776-
navigation.TargetEntityType,
806+
(IInternalEntry)finalUpdatePathElement.ParentEntry,
807+
((IPropertyBase?)navigation) ?? complexProperty!,
777808
ordinal: null,
778-
isCollection: navigation.IsCollection,
809+
isCollection: propertyBase.IsCollection,
779810
isTopLevel: true);
780811
}
781812

@@ -840,16 +871,20 @@ protected virtual void ProcessSinglePropertyJsonUpdate(ref ColumnModificationPar
840871
}
841872
}
842873

874+
#pragma warning disable EF1001 // Internal EF Core API usage.
843875
private void WriteJson(
844876
Utf8JsonWriter writer,
845-
object? navigationValue,
846-
IUpdateEntry parentEntry,
847-
IEntityType entityType,
877+
object? value,
878+
IInternalEntry parentEntry,
879+
IPropertyBase property,
848880
int? ordinal,
849881
bool isCollection,
850882
bool isTopLevel)
851883
{
852-
if (navigationValue == null)
884+
var structuralType = property is INavigation navigation
885+
? (ITypeBase)navigation.TargetEntityType
886+
: ((IComplexProperty)property).ComplexType;
887+
if (value is null)
853888
{
854889
if (!isTopLevel)
855890
{
@@ -861,15 +896,15 @@ private void WriteJson(
861896

862897
if (isCollection)
863898
{
864-
var i = 1;
899+
var i = 0;
865900
writer.WriteStartArray();
866-
foreach (var collectionElement in (IEnumerable)navigationValue)
901+
foreach (var collectionElement in (IEnumerable)value)
867902
{
868903
WriteJson(
869904
writer,
870905
collectionElement,
871906
parentEntry,
872-
entityType,
907+
property,
873908
i++,
874909
isCollection: false,
875910
isTopLevel: false);
@@ -879,63 +914,89 @@ private void WriteJson(
879914
return;
880915
}
881916

882-
#pragma warning disable EF1001 // Internal EF Core API usage.
883-
var entry = (IUpdateEntry)((InternalEntityEntry)parentEntry).StateManager.TryGetEntry(navigationValue, entityType)!;
884-
#pragma warning restore EF1001 // Internal EF Core API usage.
885-
886917
writer.WriteStartObject();
887-
foreach (var property in entityType.GetFlattenedProperties())
918+
919+
var entry = structuralType is IComplexType complexType
920+
? complexType.ComplexProperty.IsCollection
921+
? parentEntry.GetComplexCollectionEntry(complexType.ComplexProperty, ordinal!.Value)
922+
: parentEntry
923+
: ((InternalEntityEntry)parentEntry).StateManager.TryGetEntry(value, (IEntityType)structuralType)!;
924+
WriteJsonObject(writer, parentEntry, entry, structuralType, ordinal);
925+
926+
writer.WriteEndObject();
927+
}
928+
929+
private void WriteJsonObject(Utf8JsonWriter writer, IInternalEntry parentEntry, IInternalEntry entry, ITypeBase structuralType, int? ordinal)
930+
{
931+
foreach (var property in structuralType.GetProperties())
888932
{
889933
if (property.IsKey())
890934
{
891935
if (property.IsOrdinalKeyProperty() && ordinal != null)
892936
{
893-
entry.SetStoreGeneratedValue(property, ordinal.Value, setModified: false);
937+
entry.SetStoreGeneratedValue(property, ordinal.Value + 1, setModified: false);
894938
}
895939

896940
continue;
897941
}
898942

899943
// jsonPropertyName can only be null for key properties
900944
var jsonPropertyName = property.GetJsonPropertyName()!;
901-
var value = entry.GetCurrentValue(property);
945+
var propertyValue = entry.GetCurrentValue(property);
902946
writer.WritePropertyName(jsonPropertyName);
903947

904-
if (value is not null)
948+
if (propertyValue is not null)
905949
{
906950
var jsonValueReaderWriter = property.GetJsonValueReaderWriter() ?? property.GetTypeMapping().JsonValueReaderWriter;
907951
Check.DebugAssert(jsonValueReaderWriter is not null, "Missing JsonValueReaderWriter on JSON property");
908-
jsonValueReaderWriter.ToJson(writer, value);
952+
jsonValueReaderWriter.ToJson(writer, propertyValue);
909953
}
910954
else
911955
{
912956
writer.WriteNullValue();
913957
}
914958
}
915959

916-
foreach (var navigation in entityType.GetNavigations())
960+
foreach (var complexProperty in structuralType.GetComplexProperties())
917961
{
918-
// skip back-references to the parent
919-
if (navigation.IsOnDependent)
920-
{
921-
continue;
922-
}
923-
924-
var jsonPropertyName = navigation.TargetEntityType.GetJsonPropertyName()!;
925-
var ownedNavigationValue = entry.GetCurrentValue(navigation)!;
926-
962+
var jsonPropertyName = complexProperty.GetJsonPropertyName()!;
963+
var complexPropertyValue = entry.GetCurrentValue(complexProperty);
927964
writer.WritePropertyName(jsonPropertyName);
965+
928966
WriteJson(
929967
writer,
930-
ownedNavigationValue,
968+
complexPropertyValue,
931969
entry,
932-
navigation.TargetEntityType,
970+
complexProperty,
933971
ordinal: null,
934-
isCollection: navigation.IsCollection,
972+
isCollection: complexProperty.IsCollection,
935973
isTopLevel: false);
936974
}
937975

938-
writer.WriteEndObject();
976+
if (structuralType is IEntityType entityType)
977+
{
978+
foreach (var navigation in entityType.GetNavigations())
979+
{
980+
// skip back-references to the parent
981+
if (navigation.IsOnDependent)
982+
{
983+
continue;
984+
}
985+
986+
var jsonPropertyName = navigation.TargetEntityType.GetJsonPropertyName()!;
987+
var ownedNavigationValue = entry.GetCurrentValue(navigation)!;
988+
989+
writer.WritePropertyName(jsonPropertyName);
990+
WriteJson(
991+
writer,
992+
ownedNavigationValue,
993+
entry,
994+
navigation,
995+
ordinal: null,
996+
isCollection: navigation.IsCollection,
997+
isTopLevel: false);
998+
}
999+
}
9391000
}
9401001

9411002
private ITableMapping? GetTableMapping(ITypeBase structuralType)

0 commit comments

Comments
 (0)