Skip to content

Commit d16aa4d

Browse files
committed
Add support for property bag complex types
Throw for shadow properties on complex types Throw for concurrency tokens mapped to JSON Throw for invalid JSON column type Throw for JSON complex properties nested in non-JSON properties Fixes #31237 Fixes #35934
1 parent 54033a4 commit d16aa4d

33 files changed

+1322
-302
lines changed

src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ protected override void ValidatePropertyMapping(
139139

140140
if (complexProperty.ComplexType.IsMappedToJson())
141141
{
142+
if (!complexProperty.DeclaringType.IsMappedToJson()
143+
&& complexProperty.DeclaringType is IComplexType)
144+
{
145+
// Issue #36558
146+
throw new InvalidOperationException(
147+
RelationalStrings.NestedComplexPropertyJsonWithTableSharing(
148+
$"{complexProperty.DeclaringType.DisplayName()}.{complexProperty.Name}",
149+
complexProperty.DeclaringType.DisplayName()));
150+
}
151+
142152
ValidateJsonProperties(complexProperty.ComplexType);
143153
}
144154
}
@@ -2702,7 +2712,7 @@ protected virtual void ValidateJsonEntities(
27022712
var nonJsonType = mappedTypes.Where(x => !x.IsMappedToJson()).First();
27032713

27042714
// must be an owned collection (mapped to a separate table) that owns a JSON type
2705-
// issue #28441
2715+
// Issue #28441
27062716
throw new InvalidOperationException(
27072717
RelationalStrings.JsonEntityOwnedByNonJsonOwnedType(
27082718
nonJsonType.DisplayName(), table.DisplayName()));
@@ -2711,7 +2721,7 @@ protected virtual void ValidateJsonEntities(
27112721
var distinctRootTypes = nonOwnedTypes.Select(x => x.GetRootType()).Distinct().ToList();
27122722
if (distinctRootTypes.Count > 1)
27132723
{
2714-
// issue #28442
2724+
// Issue #28442
27152725
throw new InvalidOperationException(
27162726
RelationalStrings.JsonEntityWithTableSplittingIsNotSupported);
27172727
}
@@ -2966,26 +2976,36 @@ private static Dictionary<string, string> ValidateJsonProperties(IConventionType
29662976
foreach (var property in typeBase.GetProperties())
29672977
{
29682978
var jsonPropertyName = property.GetJsonPropertyName();
2969-
if (!string.IsNullOrEmpty(jsonPropertyName))
2979+
if (string.IsNullOrEmpty(jsonPropertyName))
29702980
{
2971-
var columnNameAnnotation = property.FindAnnotation(RelationalAnnotationNames.ColumnName);
2972-
if (columnNameAnnotation != null && !string.IsNullOrEmpty((string?)columnNameAnnotation.Value))
2973-
{
2974-
throw new InvalidOperationException(
2975-
RelationalStrings.PropertyBothColumnNameAndJsonPropertyName(
2976-
$"{typeBase.DisplayName()}.{property.Name}",
2977-
(string)columnNameAnnotation.Value,
2978-
jsonPropertyName));
2979-
}
2981+
continue;
2982+
}
29802983

2981-
if (property.TryGetDefaultValue(out var _))
2982-
{
2983-
throw new InvalidOperationException(
2984-
RelationalStrings.JsonEntityWithDefaultValueSetOnItsProperty(
2985-
typeBase.DisplayName(), property.Name));
2986-
}
2984+
var columnNameAnnotation = property.FindAnnotation(RelationalAnnotationNames.ColumnName);
2985+
if (columnNameAnnotation != null && !string.IsNullOrEmpty((string?)columnNameAnnotation.Value))
2986+
{
2987+
throw new InvalidOperationException(
2988+
RelationalStrings.PropertyBothColumnNameAndJsonPropertyName(
2989+
$"{typeBase.DisplayName()}.{property.Name}",
2990+
(string)columnNameAnnotation.Value,
2991+
jsonPropertyName));
2992+
}
29872993

2988-
CheckUniqueness(jsonPropertyName, property.Name, typeBase, jsonPropertyNames);
2994+
if (property.TryGetDefaultValue(out var _))
2995+
{
2996+
// Issue #35934
2997+
throw new InvalidOperationException(
2998+
RelationalStrings.JsonEntityWithDefaultValueSetOnItsProperty(
2999+
typeBase.DisplayName(), property.Name));
3000+
}
3001+
3002+
CheckUniqueness(jsonPropertyName, property.Name, typeBase, jsonPropertyNames);
3003+
3004+
if (property.IsConcurrencyToken)
3005+
{
3006+
throw new InvalidOperationException(
3007+
RelationalStrings.ConcurrencyTokenOnJsonMappedProperty(
3008+
property.Name, typeBase.DisplayName()));
29893009
}
29903010
}
29913011

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,12 @@ private static void CreateContainerColumn<TColumnMappingBase>(
608608

609609
Check.DebugAssert(tableBase.FindColumn(containerColumnName) == null, $"Table '{tableBase.Name}' already has a '{containerColumnName}' column.");
610610

611-
var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonTypePlaceholder), storeTypeName: containerColumnType)!;
611+
var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonTypePlaceholder), storeTypeName: containerColumnType);
612+
if (jsonColumnTypeMapping == null)
613+
{
614+
throw new InvalidOperationException(RelationalStrings.UnsupportedJsonColumnType(containerColumnType ?? "null", containerColumnName, tableBase.Name));
615+
}
616+
612617
var jsonColumn = createColumn(containerColumnName, containerColumnType, tableBase, jsonColumnTypeMapping);
613618
tableBase.Columns.Add(containerColumnName, jsonColumn);
614619

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@
160160
<data name="ComputedColumnSqlUnspecified" xml:space="preserve">
161161
<value>The computed column SQL has not been specified for the column '{table}.{column}'. Specify the SQL before using Entity Framework to create the database schema.</value>
162162
</data>
163+
<data name="ConcurrencyTokenOnJsonMappedProperty" xml:space="preserve">
164+
<value>The property '{property}' on '{type}' is configured as a concurrency token, but the type is mapped to JSON. Concurrency tokens are not supported on JSON-mapped types.</value>
165+
</data>
163166
<data name="ConflictingAmbientTransaction" xml:space="preserve">
164167
<value>An ambient transaction has been detected. The ambient transaction needs to be completed before starting a new transaction on this connection.</value>
165168
</data>
@@ -1003,6 +1006,9 @@
10031006
<data name="NestedCollectionsNotSupported" xml:space="preserve">
10041007
<value>The property '{propertyType} {type}.{property}' is a primitive collection of a primitive collection. Nested primitive collections are not yet supported with relational database providers.</value>
10051008
</data>
1009+
<data name="NestedComplexPropertyJsonWithTableSharing" xml:space="preserve">
1010+
<value>Complex property '{complexProperty}' is mapped to JSON but its containing type '{containingType}' is not. Map the root complex type to JSON. See https://github.com/dotnet/efcore/issues/36558</value>
1011+
</data>
10061012
<data name="NoActiveTransaction" xml:space="preserve">
10071013
<value>The connection does not have any active transactions.</value>
10081014
</data>
@@ -1285,6 +1291,9 @@
12851291
<data name="UnsupportedDataOperationStoreType" xml:space="preserve">
12861292
<value>The store type '{type}' used for the column '{column}' in a migration data operation is not supported by the current provider.</value>
12871293
</data>
1294+
<data name="UnsupportedJsonColumnType" xml:space="preserve">
1295+
<value>The store type '{storeType}' specified for JSON column '{columnName}' in table '{tableName}' is not supported by the current provider. JSON columns require a provider-specific JSON store type.</value>
1296+
</data>
12881297
<data name="UnsupportedOperatorForSqlExpression" xml:space="preserve">
12891298
<value>Unsupported operator '{nodeType}' specified for expression of type '{expressionType}'.</value>
12901299
</data>

src/EFCore/ChangeTracking/Internal/ChangeDetector.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ private void LogChangeDetected(IInternalEntry entry, IProperty property, object?
750750
}
751751
else
752752
{
753-
_logger.ComplexTypePropertyChangeDetectedSensitive((InternalComplexEntry)entry, property, original, current);
753+
_logger.ComplexElementPropertyChangeDetectedSensitive((InternalComplexEntry)entry, property, original, current);
754754
}
755755
}
756756
else
@@ -761,7 +761,7 @@ private void LogChangeDetected(IInternalEntry entry, IProperty property, object?
761761
}
762762
else
763763
{
764-
_logger.ComplexTypePropertyChangeDetected((InternalComplexEntry)entry, property, original, current);
764+
_logger.ComplexElementPropertyChangeDetected((InternalComplexEntry)entry, property, original, current);
765765
}
766766
}
767767
}

src/EFCore/Diagnostics/CoreEventId.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ private enum Id
139139
StateChanged,
140140
ValueGenerated,
141141
SkipCollectionChangeDetected,
142-
ComplexTypePropertyChangeDetected
142+
ComplexElementPropertyChangeDetected
143143
}
144144

145145
private static readonly string _updatePrefix = DbLoggerCategory.Update.Name + ".";
@@ -986,7 +986,7 @@ private static EventId MakeChangeTrackingId(Id id)
986986
/// <see cref="DiagnosticSource" />.
987987
/// </para>
988988
/// </remarks>
989-
public static readonly EventId ComplexTypePropertyChangeDetected = MakeChangeTrackingId(Id.ComplexTypePropertyChangeDetected);
989+
public static readonly EventId ComplexElementPropertyChangeDetected = MakeChangeTrackingId(Id.ComplexElementPropertyChangeDetected);
990990

991991
/// <summary>
992992
/// DetectChanges has detected a change in a foreign key property value.

src/EFCore/Diagnostics/CoreLoggerExtensions.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2290,21 +2290,21 @@ private static string PropertyChangeDetectedSensitive(EventDefinitionBase defini
22902290
}
22912291

22922292
/// <summary>
2293-
/// Logs for the <see cref="CoreEventId.ComplexTypePropertyChangeDetected" /> event.
2293+
/// Logs for the <see cref="CoreEventId.ComplexElementPropertyChangeDetected" /> event.
22942294
/// </summary>
22952295
/// <param name="diagnostics">The diagnostics logger to use.</param>
22962296
/// <param name="internalComplexEntry">The internal complex entry.</param>
22972297
/// <param name="property">The property.</param>
22982298
/// <param name="oldValue">The old value.</param>
22992299
/// <param name="newValue">The new value.</param>
2300-
public static void ComplexTypePropertyChangeDetected(
2300+
public static void ComplexElementPropertyChangeDetected(
23012301
this IDiagnosticsLogger<DbLoggerCategory.ChangeTracking> diagnostics,
23022302
InternalComplexEntry internalComplexEntry,
23032303
IProperty property,
23042304
object? oldValue,
23052305
object? newValue)
23062306
{
2307-
var definition = CoreResources.LogComplexTypePropertyChangeDetected(diagnostics);
2307+
var definition = CoreResources.LogComplexElementPropertyChangeDetected(diagnostics);
23082308

23092309
if (diagnostics.ShouldLog(definition))
23102310
{
@@ -2315,7 +2315,7 @@ public static void ComplexTypePropertyChangeDetected(
23152315
{
23162316
var eventData = new ComplexTypePropertyChangedEventData(
23172317
definition,
2318-
ComplexTypePropertyChangeDetected,
2318+
ComplexElementPropertyChangeDetected,
23192319
new ComplexElementEntry(internalComplexEntry),
23202320
property,
23212321
oldValue,
@@ -2325,7 +2325,7 @@ public static void ComplexTypePropertyChangeDetected(
23252325
}
23262326
}
23272327

2328-
private static string ComplexTypePropertyChangeDetected(EventDefinitionBase definition, EventData payload)
2328+
private static string ComplexElementPropertyChangeDetected(EventDefinitionBase definition, EventData payload)
23292329
{
23302330
var d = (EventDefinition<string, string>)definition;
23312331
var p = (ComplexTypePropertyChangedEventData)payload;
@@ -2335,21 +2335,21 @@ private static string ComplexTypePropertyChangeDetected(EventDefinitionBase defi
23352335
}
23362336

23372337
/// <summary>
2338-
/// Logs for the <see cref="CoreEventId.ComplexTypePropertyChangeDetected" /> event.
2338+
/// Logs for the <see cref="CoreEventId.ComplexElementPropertyChangeDetected" /> event.
23392339
/// </summary>
23402340
/// <param name="diagnostics">The diagnostics logger to use.</param>
23412341
/// <param name="internalComplexEntry">The internal complex entry.</param>
23422342
/// <param name="property">The property.</param>
23432343
/// <param name="oldValue">The old value.</param>
23442344
/// <param name="newValue">The new value.</param>
2345-
public static void ComplexTypePropertyChangeDetectedSensitive(
2345+
public static void ComplexElementPropertyChangeDetectedSensitive(
23462346
this IDiagnosticsLogger<DbLoggerCategory.ChangeTracking> diagnostics,
23472347
InternalComplexEntry internalComplexEntry,
23482348
IProperty property,
23492349
object? oldValue,
23502350
object? newValue)
23512351
{
2352-
var definition = CoreResources.LogComplexTypePropertyChangeDetectedSensitive(diagnostics);
2352+
var definition = CoreResources.LogComplexElementPropertyChangeDetectedSensitive(diagnostics);
23532353

23542354
if (diagnostics.ShouldLog(definition))
23552355
{
@@ -2366,7 +2366,7 @@ public static void ComplexTypePropertyChangeDetectedSensitive(
23662366
{
23672367
var eventData = new ComplexTypePropertyChangedEventData(
23682368
definition,
2369-
ComplexTypePropertyChangeDetectedSensitive,
2369+
ComplexElementPropertyChangeDetectedSensitive,
23702370
new ComplexElementEntry(internalComplexEntry),
23712371
property,
23722372
oldValue,
@@ -2376,7 +2376,7 @@ public static void ComplexTypePropertyChangeDetectedSensitive(
23762376
}
23772377
}
23782378

2379-
private static string ComplexTypePropertyChangeDetectedSensitive(EventDefinitionBase definition, EventData payload)
2379+
private static string ComplexElementPropertyChangeDetectedSensitive(EventDefinitionBase definition, EventData payload)
23802380
{
23812381
var d = (EventDefinition<string, string, object?, object?, string>)definition;
23822382
var p = (ComplexTypePropertyChangedEventData)payload;

src/EFCore/Diagnostics/LoggingDefinitions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ public abstract class LoggingDefinitions
527527
/// doing so can result in application failures when updating to a new Entity Framework Core release.
528528
/// </summary>
529529
[EntityFrameworkInternal]
530-
public EventDefinitionBase? LogComplexTypePropertyChangeDetected;
530+
public EventDefinitionBase? LogComplexElementPropertyChangeDetected;
531531

532532
/// <summary>
533533
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -536,7 +536,7 @@ public abstract class LoggingDefinitions
536536
/// doing so can result in application failures when updating to a new Entity Framework Core release.
537537
/// </summary>
538538
[EntityFrameworkInternal]
539-
public EventDefinitionBase? LogComplexTypePropertyChangeDetectedSensitive;
539+
public EventDefinitionBase? LogComplexElementPropertyChangeDetectedSensitive;
540540

541541
/// <summary>
542542
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to

src/EFCore/Infrastructure/ModelValidator.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,14 @@ protected virtual void ValidatePropertyMapping(IConventionComplexProperty comple
354354
CoreStrings.ComplexValueTypeShadowProperty(complexProperty.ComplexType.DisplayName(), shadowProperty.Name));
355355
}
356356
}
357+
358+
// Issue #35613: Shadow properties on all complex types are not supported
359+
var shadowPropertyOnComplexType = complexProperty.ComplexType.GetDeclaredProperties().FirstOrDefault(p => p.IsShadowProperty());
360+
if (shadowPropertyOnComplexType != null)
361+
{
362+
throw new InvalidOperationException(
363+
CoreStrings.ComplexTypeShadowProperty(complexProperty.ComplexType.DisplayName(), shadowPropertyOnComplexType.Name));
364+
}
357365
}
358366

359367
/// <summary>

src/EFCore/Metadata/IConventionNavigationBase.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ namespace Microsoft.EntityFrameworkCore.Metadata;
1919
/// </remarks>
2020
public interface IConventionNavigationBase : IReadOnlyNavigationBase, IConventionPropertyBase
2121
{
22+
/// <summary>
23+
/// Gets the entity type that this navigation property will hold an instance(s) of.
24+
/// </summary>
25+
new IConventionEntityType TargetEntityType
26+
{
27+
[DebuggerStepThrough]
28+
get => (IConventionEntityType)((IReadOnlyNavigationBase)this).TargetEntityType;
29+
}
30+
2231
/// <summary>
2332
/// Sets a value indicating whether this navigation should be eager loaded by default.
2433
/// </summary>

0 commit comments

Comments
 (0)