Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate that no properties on an entity type are mapped to the same column #28759

Merged
merged 3 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Metadata.Internal;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;

Expand Down
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
@@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;

/// <summary>
/// A convention that makes sure there is a trigger on all entity types.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see> for more information and examples.
/// </remarks>
public class BlankTriggerAddingConvention : IModelFinalizingConvention
{
/// <summary>
/// Creates a new instance of <see cref="BlankTriggerAddingConvention" />.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
/// <param name="relationalDependencies"> Parameter object containing relational dependencies for this convention.</param>
public BlankTriggerAddingConvention(
ProviderConventionSetBuilderDependencies dependencies,
RelationalConventionSetBuilderDependencies relationalDependencies)
{
Dependencies = dependencies;
RelationalDependencies = relationalDependencies;
}

/// <summary>
/// Dependencies for this convention.
/// </summary>
protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }

/// <summary>
/// Relational provider-specific dependencies for this service.
/// </summary>
protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }

/// <summary>
/// Called when a model is being finalized.
/// </summary>
/// <param name="modelBuilder">The builder for the model.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
var table = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table);
if (table != null
&& entityType.GetDeclaredTriggers().All(t => t.GetName(table.Value) == null))
{
entityType.Builder.HasTrigger(table.Value.Name + "_Trigger");
}

foreach (var fragment in entityType.GetMappingFragments(StoreObjectType.Table))
{
if (entityType.GetDeclaredTriggers().All(t => t.GetName(fragment.StoreObject) == null))
{
entityType.Builder.HasTrigger(fragment.StoreObject.Name + "_Trigger");
}
}
}
}
}
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