Skip to content

Commit 34ee81a

Browse files
authored
Add support for optional complex types to model building (#35614)
Breaking change: uniquify and validate complex type column uniqueness Part of #31376
1 parent 59fc95b commit 34ee81a

File tree

35 files changed

+657
-407
lines changed

35 files changed

+657
-407
lines changed

Diff for: src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,7 @@ public virtual void Generate(IStoredProcedureMapping sprocMapping, CSharpRuntime
14981498
private void GenerateAddMapping(
14991499
ITableMappingBase tableMapping,
15001500
string tableVariable,
1501-
string entityTypeVariable,
1501+
string structuralTypeVariable,
15021502
string tableMappingsVariable,
15031503
string tableMappingVariable,
15041504
string mappingType,
@@ -1510,7 +1510,7 @@ private void GenerateAddMapping(
15101510
var typeBase = tableMapping.TypeBase;
15111511

15121512
mainBuilder
1513-
.Append($"var {tableMappingVariable} = new {mappingType}({entityTypeVariable}, ")
1513+
.Append($"var {tableMappingVariable} = new {mappingType}({structuralTypeVariable}, ")
15141514
.Append($"{tableVariable}, {additionalParameter ?? ""}{code.Literal(tableMapping.IncludesDerivedTypes)}");
15151515

15161516
if (tableMapping.IsSharedTablePrincipal.HasValue
@@ -1549,7 +1549,7 @@ private void GenerateAddMapping(
15491549
foreach (var internalForeignKey in table.GetRowInternalForeignKeys(entityType))
15501550
{
15511551
mainBuilder
1552-
.Append(tableVariable).Append($".AddRowInternalForeignKey({entityTypeVariable}, ")
1552+
.Append(tableVariable).Append($".AddRowInternalForeignKey({structuralTypeVariable}, ")
15531553
.AppendLine("RelationalModel.GetForeignKey(this,").IncrementIndent()
15541554
.AppendLine($"{code.Literal(internalForeignKey.DeclaringEntityType.Name)},")
15551555
.AppendLine($"{code.Literal(internalForeignKey.Properties.Select(p => p.Name).ToArray())},")

Diff for: src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs

+74-41
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Globalization;
56
using System.Runtime.CompilerServices;
67
using System.Text;
@@ -230,21 +231,54 @@ public static string GetDefaultColumnName(this IReadOnlyProperty property)
230231
StringBuilder? builder = null;
231232
var currentStoreObject = storeObject;
232233
if (property.DeclaringType is IReadOnlyEntityType entityType)
234+
{
235+
builder = CreateOwnershipPrefix(entityType, storeObject, builder);
236+
}
237+
else if (StoreObjectIdentifier.Create(property.DeclaringType, currentStoreObject.StoreObjectType) == currentStoreObject
238+
|| property.DeclaringType.GetMappingFragments(storeObject.StoreObjectType)
239+
.Any(f => f.StoreObject == currentStoreObject))
240+
{
241+
builder = CreateComplexPrefix((IReadOnlyComplexType)property.DeclaringType, storeObject, builder);
242+
}
243+
244+
var baseName = storeObject.StoreObjectType == StoreObjectType.Table ? property.GetDefaultColumnName() : property.Name;
245+
if (builder == null)
246+
{
247+
return baseName;
248+
}
249+
250+
builder.Append(baseName);
251+
baseName = builder.ToString();
252+
253+
return Uniquifier.Truncate(baseName, property.DeclaringType.Model.GetMaxIdentifierLength());
254+
255+
[return: NotNullIfNotNull("builder")]
256+
static StringBuilder? CreateOwnershipPrefix(IReadOnlyEntityType entityType, in StoreObjectIdentifier storeObject, StringBuilder? builder)
233257
{
234258
while (true)
235259
{
236260
var ownership = entityType.GetForeignKeys().SingleOrDefault(fk => fk.IsOwnership);
237261
if (ownership == null)
238262
{
239-
break;
263+
return builder;
240264
}
241265

242266
var ownerType = ownership.PrincipalEntityType;
243-
if (StoreObjectIdentifier.Create(ownerType, currentStoreObject.StoreObjectType) != currentStoreObject
244-
&& ownerType.GetMappingFragments(storeObject.StoreObjectType)
245-
.All(f => f.StoreObject != currentStoreObject))
267+
if (StoreObjectIdentifier.Create(ownerType, storeObject.StoreObjectType) != storeObject)
246268
{
247-
break;
269+
var foundMappedFragment = false;
270+
foreach (var fragment in ownerType.GetMappingFragments(storeObject.StoreObjectType))
271+
{
272+
if (fragment.StoreObject == storeObject)
273+
{
274+
foundMappedFragment = true;
275+
}
276+
}
277+
278+
if (!foundMappedFragment)
279+
{
280+
return builder;
281+
}
248282
}
249283

250284
builder ??= new StringBuilder();
@@ -254,31 +288,27 @@ public static string GetDefaultColumnName(this IReadOnlyProperty property)
254288
entityType = ownerType;
255289
}
256290
}
257-
else if (StoreObjectIdentifier.Create(property.DeclaringType, currentStoreObject.StoreObjectType) == currentStoreObject
258-
|| property.DeclaringType.GetMappingFragments(storeObject.StoreObjectType)
259-
.Any(f => f.StoreObject == currentStoreObject))
291+
292+
static StringBuilder CreateComplexPrefix(IReadOnlyComplexType complexType, in StoreObjectIdentifier storeObject, StringBuilder? builder)
260293
{
261-
var complexType = (IReadOnlyComplexType)property.DeclaringType;
262294
builder ??= new StringBuilder();
263-
while (complexType != null)
295+
while (true)
264296
{
265297
builder.Insert(0, "_");
266298
builder.Insert(0, complexType.ComplexProperty.Name);
267299

268-
complexType = complexType.ComplexProperty.DeclaringType as IReadOnlyComplexType;
300+
switch (complexType.ComplexProperty.DeclaringType)
301+
{
302+
case IReadOnlyComplexType declaringComplexType:
303+
complexType = declaringComplexType;
304+
break;
305+
case IReadOnlyEntityType declaringEntityType:
306+
return CreateOwnershipPrefix(declaringEntityType, storeObject, builder);
307+
default:
308+
return builder;
309+
}
269310
}
270311
}
271-
272-
var baseName = storeObject.StoreObjectType == StoreObjectType.Table ? property.GetDefaultColumnName() : property.Name;
273-
if (builder == null)
274-
{
275-
return baseName;
276-
}
277-
278-
builder.Append(baseName);
279-
baseName = builder.ToString();
280-
281-
return Uniquifier.Truncate(baseName, property.DeclaringType.Model.GetMaxIdentifierLength());
282312
}
283313

284314
/// <summary>
@@ -1183,15 +1213,21 @@ public static void SetIsFixedLength(this IMutableProperty property, bool? fixedL
11831213
/// <remarks>
11841214
/// This depends on the property itself and also how it is mapped. For example,
11851215
/// derived non-nullable properties in a TPH type hierarchy will be mapped to nullable columns.
1186-
/// As well as properties on optional types sharing the same table.
11871216
/// </remarks>
11881217
/// <param name="property">The <see cref="IReadOnlyProperty" />.</param>
11891218
/// <returns><see langword="true" /> if the mapped column is nullable; <see langword="false" /> otherwise.</returns>
11901219
public static bool IsColumnNullable(this IReadOnlyProperty property)
11911220
=> property.IsNullable
11921221
|| (property.DeclaringType.ContainingEntityType is IReadOnlyEntityType entityType
11931222
&& entityType.BaseType != null
1194-
&& entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy);
1223+
&& entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy)
1224+
|| (property.DeclaringType is IReadOnlyComplexType complexType
1225+
&& IsNullable(complexType.ComplexProperty));
1226+
1227+
private static bool IsNullable(IReadOnlyComplexProperty complexProperty)
1228+
=> complexProperty.IsNullable
1229+
|| (complexProperty.DeclaringType is IReadOnlyComplexType complexType
1230+
&& IsNullable(complexType.ComplexProperty));
11951231

11961232
/// <summary>
11971233
/// Checks whether the column mapped to the given property will be nullable
@@ -1222,7 +1258,9 @@ public static bool IsColumnNullable(this IReadOnlyProperty property, in StoreObj
12221258
|| (property.DeclaringType.ContainingEntityType is IReadOnlyEntityType entityType
12231259
&& ((entityType.BaseType != null
12241260
&& entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy)
1225-
|| IsOptionalSharingDependent(entityType, storeObject, 0)));
1261+
|| IsOptionalSharingDependent(entityType, storeObject, 0)))
1262+
|| (property.DeclaringType is IReadOnlyComplexType complexType
1263+
&& IsNullable(complexType.ComplexProperty));
12261264
}
12271265

12281266
private static bool IsOptionalSharingDependent(
@@ -1476,7 +1514,7 @@ public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyPrope
14761514
if (property.DeclaringType.IsMappedToJson())
14771515
{
14781516
//JSON-splitting is not supported
1479-
//issue #28574
1517+
//Issue #28574
14801518
return null;
14811519
}
14821520

@@ -1494,20 +1532,15 @@ public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyPrope
14941532
// Using a hashset is detrimental to the perf when there are no cycles
14951533
for (var i = 0; i < Metadata.Internal.RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++)
14961534
{
1497-
var entityType = rootProperty.DeclaringType as IReadOnlyEntityType;
1498-
if (entityType == null)
1499-
{
1500-
break;
1501-
}
1502-
1535+
var entityType = rootProperty.DeclaringType.ContainingEntityType;
15031536
IReadOnlyProperty? linkedProperty = null;
1504-
foreach (var p in entityType
1537+
foreach (var principalProperty in entityType
15051538
.FindRowInternalForeignKeys(storeObject)
1506-
.SelectMany(fk => fk.PrincipalEntityType.GetProperties()))
1539+
.SelectMany(static fk => fk.PrincipalEntityType.GetProperties()))
15071540
{
1508-
if (p.GetColumnName(storeObject) == column)
1541+
if (principalProperty.GetColumnName(storeObject) == column)
15091542
{
1510-
linkedProperty = p;
1543+
linkedProperty = principalProperty;
15111544
break;
15121545
}
15131546
}
@@ -1538,8 +1571,8 @@ public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyPrope
15381571
// Using a hashset is detrimental to the perf when there are no cycles
15391572
for (var i = 0; i < Metadata.Internal.RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++)
15401573
{
1541-
var entityType = principalProperty.DeclaringType as IReadOnlyEntityType;
1542-
var linkingRelationship = entityType?.FindRowInternalForeignKeys(storeObject).FirstOrDefault();
1574+
var entityType = principalProperty.DeclaringType.ContainingEntityType;
1575+
var linkingRelationship = entityType.FindRowInternalForeignKeys(storeObject).FirstOrDefault();
15431576
if (linkingRelationship == null)
15441577
{
15451578
break;
@@ -1566,8 +1599,8 @@ public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyPrope
15661599
// Using a hashset is detrimental to the perf when there are no cycles
15671600
for (var i = 0; i < Metadata.Internal.RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++)
15681601
{
1569-
var entityType = principalProperty.DeclaringType as IReadOnlyEntityType;
1570-
var linkingRelationship = entityType?.FindRowInternalForeignKeys(storeObject).FirstOrDefault();
1602+
var entityType = principalProperty.DeclaringType.ContainingEntityType;
1603+
var linkingRelationship = entityType.FindRowInternalForeignKeys(storeObject).FirstOrDefault();
15711604
if (linkingRelationship == null)
15721605
{
15731606
break;
@@ -1595,7 +1628,7 @@ public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyPrope
15951628
/// <param name="property">The property.</param>
15961629
/// <returns>The property facet overrides.</returns>
15971630
public static IEnumerable<IReadOnlyRelationalPropertyOverrides> GetOverrides(this IReadOnlyProperty property)
1598-
=> RelationalPropertyOverrides.Get(property) ?? Enumerable.Empty<IReadOnlyRelationalPropertyOverrides>();
1631+
=> RelationalPropertyOverrides.Get(property) ?? [];
15991632

16001633
/// <summary>
16011634
/// <para>

Diff for: src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs

+64-43
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@ static void ValidateType(ITypeBase typeBase)
9797
}
9898
}
9999

100+
/// <inheritdoc/>
101+
protected override void ValidatePropertyMapping(IConventionComplexProperty complexProperty)
102+
{
103+
base.ValidatePropertyMapping(complexProperty);
104+
105+
var typeBase = complexProperty.DeclaringType;
106+
107+
if (!typeBase.IsMappedToJson()
108+
&& complexProperty.IsNullable
109+
&& complexProperty.ComplexType.GetProperties().All(m => m.IsNullable))
110+
{
111+
throw new InvalidOperationException(
112+
RelationalStrings.ComplexPropertyOptionalTableSharing(typeBase.DisplayName(), complexProperty.Name));
113+
}
114+
}
115+
100116
/// <summary>
101117
/// Validates the mapping/configuration of SQL queries in the model.
102118
/// </summary>
@@ -1239,16 +1255,56 @@ protected virtual void ValidateSharedColumnsCompatibility(
12391255
if (missingConcurrencyTokens != null)
12401256
{
12411257
missingConcurrencyTokens.Clear();
1242-
foreach (var (key, readOnlyProperties) in concurrencyColumns!)
1258+
foreach (var (concurrencyColumn, concurrencyProperties) in concurrencyColumns!)
12431259
{
1244-
if (TableSharingConcurrencyTokenConvention.IsConcurrencyTokenMissing(readOnlyProperties, entityType, mappedTypes))
1260+
if (TableSharingConcurrencyTokenConvention.IsConcurrencyTokenMissing(concurrencyProperties, entityType, mappedTypes))
12451261
{
1246-
missingConcurrencyTokens.Add(key);
1262+
missingConcurrencyTokens.Add(concurrencyColumn);
12471263
}
12481264
}
12491265
}
12501266

1251-
foreach (var property in entityType.GetDeclaredProperties())
1267+
ValidateCompatible(entityType, storeObject, propertyMappings, missingConcurrencyTokens, logger);
1268+
1269+
if (missingConcurrencyTokens != null)
1270+
{
1271+
foreach (var concurrencyColumn in missingConcurrencyTokens)
1272+
{
1273+
throw new InvalidOperationException(
1274+
RelationalStrings.MissingConcurrencyColumn(
1275+
entityType.DisplayName(), concurrencyColumn, storeObject.DisplayName()));
1276+
}
1277+
}
1278+
}
1279+
1280+
var columnOrders = new Dictionary<int, List<string>>();
1281+
foreach (var property in propertyMappings.Values)
1282+
{
1283+
var columnOrder = property.GetColumnOrder(storeObject);
1284+
if (!columnOrder.HasValue)
1285+
{
1286+
continue;
1287+
}
1288+
1289+
var columns = columnOrders.GetOrAddNew(columnOrder.Value);
1290+
columns.Add(property.GetColumnName(storeObject)!);
1291+
}
1292+
1293+
if (columnOrders.Any(g => g.Value.Count > 1))
1294+
{
1295+
logger.DuplicateColumnOrders(
1296+
storeObject,
1297+
columnOrders.Where(g => g.Value.Count > 1).SelectMany(g => g.Value).ToList());
1298+
}
1299+
1300+
void ValidateCompatible(
1301+
ITypeBase structuralType,
1302+
in StoreObjectIdentifier storeObject,
1303+
Dictionary<string, IProperty> propertyMappings,
1304+
HashSet<string>? missingConcurrencyTokens,
1305+
IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
1306+
{
1307+
foreach (var property in structuralType.GetDeclaredProperties())
12521308
{
12531309
var columnName = property.GetColumnName(storeObject);
12541310
if (columnName == null)
@@ -1276,39 +1332,14 @@ protected virtual void ValidateSharedColumnsCompatibility(
12761332
storeObject.DisplayName()));
12771333
}
12781334

1279-
ValidateCompatible(property, duplicateProperty, columnName, storeObject, logger);
1335+
this.ValidateCompatible(property, duplicateProperty, columnName, storeObject, logger);
12801336
}
12811337

1282-
if (missingConcurrencyTokens != null)
1338+
foreach (var complexProperty in structuralType.GetDeclaredComplexProperties())
12831339
{
1284-
foreach (var missingColumn in missingConcurrencyTokens)
1285-
{
1286-
throw new InvalidOperationException(
1287-
RelationalStrings.MissingConcurrencyColumn(
1288-
entityType.DisplayName(), missingColumn, storeObject.DisplayName()));
1289-
}
1340+
ValidateCompatible(complexProperty.ComplexType, storeObject, propertyMappings, missingConcurrencyTokens, logger);
12901341
}
12911342
}
1292-
1293-
var columnOrders = new Dictionary<int, List<string>>();
1294-
foreach (var property in propertyMappings.Values)
1295-
{
1296-
var columnOrder = property.GetColumnOrder(storeObject);
1297-
if (!columnOrder.HasValue)
1298-
{
1299-
continue;
1300-
}
1301-
1302-
var columns = columnOrders.GetOrAddNew(columnOrder.Value);
1303-
columns.Add(property.GetColumnName(storeObject)!);
1304-
}
1305-
1306-
if (columnOrders.Any(g => g.Value.Count > 1))
1307-
{
1308-
logger.DuplicateColumnOrders(
1309-
storeObject,
1310-
columnOrders.Where(g => g.Value.Count > 1).SelectMany(g => g.Value).ToList());
1311-
}
13121343
}
13131344

13141345
/// <summary>
@@ -1326,17 +1357,7 @@ protected virtual void ValidateCompatible(
13261357
in StoreObjectIdentifier storeObject,
13271358
IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
13281359
{
1329-
if (property.IsColumnNullable(storeObject) != duplicateProperty.IsColumnNullable(storeObject))
1330-
{
1331-
throw new InvalidOperationException(
1332-
RelationalStrings.DuplicateColumnNameNullabilityMismatch(
1333-
duplicateProperty.DeclaringType.DisplayName(),
1334-
duplicateProperty.Name,
1335-
property.DeclaringType.DisplayName(),
1336-
property.Name,
1337-
columnName,
1338-
storeObject.DisplayName()));
1339-
}
1360+
// NB: Properties can have different nullability, the resulting column will be non-nullable if any of the properties is non-nullable
13401361

13411362
var currentMaxLength = property.GetMaxLength(storeObject);
13421363
var previousMaxLength = duplicateProperty.GetMaxLength(storeObject);

0 commit comments

Comments
 (0)