Skip to content

Commit 64afeec

Browse files
committed
Add support for optional complex types to model building
Part of #31376
1 parent 028980e commit 64afeec

File tree

27 files changed

+534
-361
lines changed

27 files changed

+534
-361
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

+56-26
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>
@@ -1501,13 +1531,13 @@ public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyPrope
15011531
}
15021532

15031533
IReadOnlyProperty? linkedProperty = null;
1504-
foreach (var p in entityType
1534+
foreach (var principalProperty in entityType
15051535
.FindRowInternalForeignKeys(storeObject)
1506-
.SelectMany(fk => fk.PrincipalEntityType.GetProperties()))
1536+
.SelectMany(static fk => fk.PrincipalEntityType.GetProperties()))
15071537
{
1508-
if (p.GetColumnName(storeObject) == column)
1538+
if (principalProperty.GetColumnName(storeObject) == column)
15091539
{
1510-
linkedProperty = p;
1540+
linkedProperty = principalProperty;
15111541
break;
15121542
}
15131543
}

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

+65-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,18 +1357,9 @@ 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

1362+
// TODO: allow conflicts if one of them is null
13411363
var currentMaxLength = property.GetMaxLength(storeObject);
13421364
var previousMaxLength = duplicateProperty.GetMaxLength(storeObject);
13431365
if (currentMaxLength != previousMaxLength)

Diff for: src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs

+13-4
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,18 @@ private static void UniquifyColumnNames(
235235
continue;
236236
}
237237

238-
var identifyingMemberInfo = property.PropertyInfo ?? (MemberInfo?)property.FieldInfo;
239-
if ((identifyingMemberInfo != null
240-
&& identifyingMemberInfo.IsSameAs(otherProperty.PropertyInfo ?? (MemberInfo?)otherProperty.FieldInfo))
238+
var declaringEntityType = property.DeclaringType as IConventionEntityType;
239+
#pragma warning disable EF1001 // Internal EF Core API usage.
240+
var identifyingMemberInfo = property.GetIdentifyingMemberInfo();
241+
var isInheritedSharedMember = identifyingMemberInfo != null
242+
&& ((declaringEntityType != null && identifyingMemberInfo.DeclaringType != type.ClrType)
243+
|| (declaringEntityType == null
244+
&& otherProperty.DeclaringType is IConventionComplexType otherDeclaringComplexType
245+
&& ((IConventionComplexType)property.DeclaringType).ComplexProperty.GetIdentifyingMemberInfo()
246+
.IsSameAs(otherDeclaringComplexType.ComplexProperty.GetIdentifyingMemberInfo())))
247+
&& identifyingMemberInfo.IsSameAs(otherProperty.GetIdentifyingMemberInfo());
248+
#pragma warning restore EF1001 // Internal EF Core API usage.
249+
if (isInheritedSharedMember
241250
|| (property.IsPrimaryKey() && otherProperty.IsPrimaryKey())
242251
|| (property.IsConcurrencyToken && otherProperty.IsConcurrencyToken)
243252
|| (!property.Builder.CanSetColumnName(null) && !otherProperty.Builder.CanSetColumnName(null)))
@@ -262,7 +271,7 @@ private static void UniquifyColumnNames(
262271
if (!usePrefix
263272
|| (!property.DeclaringType.IsStrictlyDerivedFrom(otherProperty.DeclaringType)
264273
&& !otherProperty.DeclaringType.IsStrictlyDerivedFrom(property.DeclaringType))
265-
|| (property.DeclaringType as IConventionEntityType)?.FindRowInternalForeignKeys(storeObject).Any() == true)
274+
|| declaringEntityType?.FindRowInternalForeignKeys(storeObject).Any() == true)
266275
{
267276
var newColumnName = TryUniquify(property, columnName, columns, storeObject, usePrefix, maxLength);
268277
if (newColumnName != null)

0 commit comments

Comments
 (0)