Skip to content

Commit

Permalink
Validate that no properties on an entity type are mapped to the same …
Browse files Browse the repository at this point in the history
…column

Fixes #26263
  • Loading branch information
AndriySvyryd committed Aug 17, 2022
1 parent 53c43cc commit 35c5e17
Show file tree
Hide file tree
Showing 13 changed files with 107 additions and 149 deletions.
13 changes: 13 additions & 0 deletions src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,19 @@ protected virtual void ValidateSharedColumnsCompatibility(
continue;
}

if (property.DeclaringEntityType.IsAssignableFrom(duplicateProperty.DeclaringEntityType)
|| duplicateProperty.DeclaringEntityType.IsAssignableFrom(property.DeclaringEntityType))
{
throw new InvalidOperationException(
RelationalStrings.DuplicateColumnNameSameHierarchy(
duplicateProperty.DeclaringEntityType.DisplayName(),
duplicateProperty.Name,
property.DeclaringEntityType.DisplayName(),
property.Name,
columnName,
storeObject.DisplayName()));
}

ValidateCompatible(property, duplicateProperty, columnName, storeObject, logger);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public virtual void ProcessModelFinalizing(
continue;
}

RemoveDerivedEntityTypes(entityTypesMissingConcurrencyColumn);
RemoveDerivedEntityTypes(entityTypesMissingConcurrencyColumn, mappedTypes);

foreach (var (conventionEntityType, exampleProperty) in entityTypesMissingConcurrencyColumn)
{
Expand Down Expand Up @@ -194,10 +194,11 @@ public static bool IsConcurrencyTokenMissing(
{
var declaringEntityType = mappedProperty.DeclaringEntityType;
if (declaringEntityType.IsAssignableFrom(entityType)
|| entityType.IsAssignableFrom(declaringEntityType)
|| declaringEntityType.IsInOwnershipPath(entityType)
|| entityType.IsInOwnershipPath(declaringEntityType))
{
// The concurrency token is on the base type or in the same aggregate
// The concurrency token is on the base type, derived type or in the same aggregate
propertyMissing = false;
continue;
}
Expand All @@ -220,21 +221,31 @@ public static bool IsConcurrencyTokenMissing(
return propertyMissing;
}

private static void RemoveDerivedEntityTypes<T>(Dictionary<IConventionEntityType, T> entityTypeDictionary)
private static void RemoveDerivedEntityTypes(
Dictionary<IConventionEntityType, IReadOnlyProperty> entityTypeDictionary,
List<IConventionEntityType> mappedTypes)
{
foreach (var entityType in entityTypeDictionary.Keys)
foreach (var (entityType, property) in entityTypeDictionary)
{
var removed = false;
var baseType = entityType.BaseType;
while (baseType != null)
{
if (entityTypeDictionary.ContainsKey(baseType))
{
entityTypeDictionary.Remove(entityType);
removed = true;
break;
}

baseType = baseType.BaseType;
}

if (!removed
&& entityType.IsAssignableFrom(property.DeclaringEntityType))
{
entityTypeDictionary.Remove(entityType);
}
}
}
}
26 changes: 17 additions & 9 deletions src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@
<value>'{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different maximum lengths ('{maxLength1}' and '{maxLength2}').</value>
</data>
<data name="DuplicateColumnNameNullabilityMismatch" xml:space="preserve">
<value>'{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different nullability settings.</value>
<value>'{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different column nullability settings.</value>
</data>
<data name="DuplicateColumnNameOrderMismatch" xml:space="preserve">
<value>'{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured to use different column orders ('{columnOrder1}' and '{columnOrder2}').</value>
Expand All @@ -283,6 +283,9 @@
<data name="DuplicateColumnNameProviderTypeMismatch" xml:space="preserve">
<value>'{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured to use differing provider types ('{type1}' and '{type2}').</value>
</data>
<data name="DuplicateColumnNameSameHierarchy" xml:space="preserve">
<value>'{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but the properties are contained within the same hierarchy. All properties on an entity type must be mapped to unique different columns.</value>
</data>
<data name="DuplicateColumnNameScaleMismatch" xml:space="preserve">
<value>'{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different scales ('{scale1}' and '{scale2}').</value>
</data>
Expand Down Expand Up @@ -1008,9 +1011,6 @@
<data name="StoredProcedureOutputParameterNotGenerated" xml:space="preserve">
<value>The property '{entityType}.{property}' is mapped to an output parameter of the stored procedure '{sproc}', but it is not configured as store-generated. Either configure it as store-generated or don't configure the parameter as output.</value>
</data>
<data name="StoredProcedureRowsAffectedNotPopulated" xml:space="preserve">
<value>Stored procedure '{sproc}' was configured with a rows affected output parameter or return value, but a valid value was not found when executing the procedure.</value>
</data>
<data name="StoredProcedureOverrideMismatch" xml:space="preserve">
<value>The property '{propertySpecification}' has specific configuration for the stored procedure '{sproc}', but it isn't mapped to a parameter or a result column on that stored procedure. Remove the specific configuration, or map an entity type that contains this property to '{sproc}'.</value>
</data>
Expand Down Expand Up @@ -1038,6 +1038,9 @@
<data name="StoredProcedureResultColumnParameterConflict" xml:space="preserve">
<value>The property '{entityType}.{property}' is mapped to a result column of the stored procedure '{sproc}', but it is also mapped to an output parameter. A store-generated property can only be mapped to one of these.</value>
</data>
<data name="StoredProcedureRowsAffectedNotPopulated" xml:space="preserve">
<value>Stored procedure '{sproc}' was configured with a rows affected output parameter or return value, but a valid value was not found when executing the procedure.</value>
</data>
<data name="StoredProcedureRowsAffectedReturnConflictingParameter" xml:space="preserve">
<value>The stored procedure '{sproc}' cannot be configured to return the rows affected because a rows affected parameter or a rows affected result column for this stored procedure already exists.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,29 +308,11 @@ private static void ValidateTemporalPeriodProperty(IEntityType temporalEntityTyp
temporalEntityType.DisplayName(), periodProperty.Name));
}

if (temporalEntityType.GetTableName() is string tableName)
if (periodProperty.ValueGenerated != ValueGenerated.OnAddOrUpdate)
{
var storeObjectIdentifier = StoreObjectIdentifier.Table(tableName, temporalEntityType.GetSchema());
var periodColumnName = periodProperty.GetColumnName(storeObjectIdentifier);

var propertiesMappedToPeriodColumn = temporalEntityType.GetProperties().Where(
p => p.Name != periodProperty.Name && p.GetColumnName(storeObjectIdentifier) == periodColumnName).ToList();
foreach (var propertyMappedToPeriodColumn in propertiesMappedToPeriodColumn)
{
if (propertyMappedToPeriodColumn.ValueGenerated != ValueGenerated.OnAddOrUpdate)
{
throw new InvalidOperationException(
SqlServerStrings.TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate(
temporalEntityType.DisplayName(), propertyMappedToPeriodColumn.Name, nameof(ValueGenerated.OnAddOrUpdate)));
}

if (propertyMappedToPeriodColumn.TryGetDefaultValue(out var _))
{
throw new InvalidOperationException(
SqlServerStrings.TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue(
temporalEntityType.DisplayName(), propertyMappedToPeriodColumn.Name));
}
}
throw new InvalidOperationException(
SqlServerStrings.TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate(
temporalEntityType.DisplayName(), periodProperty.Name, nameof(ValueGenerated.OnAddOrUpdate)));
}

// TODO: check that period property is excluded from query (once the annotation is added)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Builders;
/// Instances of this class are returned from methods when using the <see cref="ModelBuilder" /> API
/// and it is not designed to be directly constructed in your application code.
/// </summary>
public class TemporalPeriodPropertyBuilder
public class TemporalPeriodPropertyBuilder : IInfrastructure<PropertyBuilder>
{
private readonly PropertyBuilder _propertyBuilder;

Expand Down Expand Up @@ -59,6 +59,9 @@ public virtual TemporalPeriodPropertyBuilder HasPrecision(int precision)
return this;
}

PropertyBuilder IInfrastructure<PropertyBuilder>.Instance
=> _propertyBuilder;

#region Hidden System.Object members

/// <summary>
Expand Down
8 changes: 0 additions & 8 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -332,9 +332,6 @@
<data name="TemporalPeriodPropertyMustBeNonNullableDateTime" xml:space="preserve">
<value>Period property '{entityType}.{propertyName}' must be non-nullable and of type '{dateTimeType}'.</value>
</data>
<data name="TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue" xml:space="preserve">
<value>Property '{entityType}.{propertyName}' is mapped to the period column and can't have default value specified.</value>
</data>
<data name="TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate" xml:space="preserve">
<value>Property '{entityType}.{propertyName}' is mapped to the period column and must have ValueGenerated set to '{valueGeneratedValue}'.</value>
</data>
Expand Down
Loading

0 comments on commit 35c5e17

Please sign in to comment.