Skip to content

Commit

Permalink
Only create additional concurrency token properties when needed
Browse files Browse the repository at this point in the history
Expose CreateUniqueProperty on IConventionEntityTypeBuilder

Fixes #21316
  • Loading branch information
AndriySvyryd committed Jul 23, 2020
1 parent f39c3af commit 46365f2
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 148 deletions.
40 changes: 7 additions & 33 deletions src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -463,33 +462,11 @@ protected virtual void ValidateSharedColumnsCompatibility(
StoreObjectIdentifier storeObject,
[NotNull] IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
{
Dictionary<string, IProperty> storeConcurrencyTokens = null;
var concurrencyColumns = TableSharingConcurrencyTokenConvention.GetConcurrencyTokensMap(storeObject, mappedTypes);
HashSet<string> missingConcurrencyTokens = null;
if (mappedTypes.Count > 1)
if (concurrencyColumns != null)
{
foreach (var property in mappedTypes.SelectMany(et => et.GetDeclaredProperties()))
{
if (property.IsConcurrencyToken
&& (property.ValueGenerated & ValueGenerated.OnUpdate) != 0)
{
if (storeConcurrencyTokens == null)
{
storeConcurrencyTokens = new Dictionary<string, IProperty>();
}

var columnName = property.GetColumnName(storeObject);
if (columnName == null)
{
continue;
}

storeConcurrencyTokens[columnName] = property;
if (missingConcurrencyTokens == null)
{
missingConcurrencyTokens = new HashSet<string>();
}
}
}
missingConcurrencyTokens = new HashSet<string>();
}

var propertyMappings = new Dictionary<string, IProperty>();
Expand All @@ -498,12 +475,9 @@ protected virtual void ValidateSharedColumnsCompatibility(
if (missingConcurrencyTokens != null)
{
missingConcurrencyTokens.Clear();
foreach (var tokenPair in storeConcurrencyTokens)
foreach (var tokenPair in concurrencyColumns)
{
var declaringType = tokenPair.Value.DeclaringEntityType;
if (!declaringType.IsAssignableFrom(entityType)
&& !declaringType.IsInOwnershipPath(entityType)
&& !entityType.IsInOwnershipPath(declaringType))
if (TableSharingConcurrencyTokenConvention.IsConcurrencyTokenMissing(tokenPair.Value, entityType, mappedTypes))
{
missingConcurrencyTokens.Add(tokenPair.Key);
}
Expand All @@ -527,7 +501,7 @@ protected virtual void ValidateSharedColumnsCompatibility(
{
foreach (var missingColumn in missingConcurrencyTokens)
{
if (entityType.GetAllBaseTypes().SelectMany(t => t.GetDeclaredProperties())
if (entityType.GetAllBaseTypesAscending().SelectMany(t => t.GetDeclaredProperties())
.All(p => p.GetColumnName(storeObject) != missingColumn))
{
throw new InvalidOperationException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
{
Expand Down Expand Up @@ -43,47 +43,55 @@ public virtual void ProcessModelFinalizing(
IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> context)
{
GetMappings(modelBuilder.Metadata,
out var tableToEntityTypes, out var concurrencyColumnsToProperties);
var tableToEntityTypes = new Dictionary<(string Name, string Schema), List<IConventionEntityType>>();
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
var tableName = entityType.GetTableName();
if (tableName == null)
{
continue;
}

var table = (tableName, entityType.GetSchema());
if (!tableToEntityTypes.TryGetValue(table, out var mappedTypes))
{
mappedTypes = new List<IConventionEntityType>();
tableToEntityTypes[table] = mappedTypes;
}

mappedTypes.Add(entityType);
}

foreach (var tableToEntityType in tableToEntityTypes)
{
var table = tableToEntityType.Key;
if (!concurrencyColumnsToProperties.TryGetValue(table, out var concurrencyColumns))
var mappedTypes = tableToEntityType.Value;

var concurrencyColumns = GetConcurrencyTokensMap(StoreObjectIdentifier.Table(table.Name, table.Schema), mappedTypes);
if (concurrencyColumns == null)
{
continue; // this table has no mapped concurrency columns
continue;
}

var entityTypesMappedToTable = tableToEntityType.Value;

foreach (var concurrencyColumn in concurrencyColumns)
{
var concurrencyColumnName = concurrencyColumn.Key;
var propertiesMappedToConcurrencyColumn = concurrencyColumn.Value;

var entityTypesMissingConcurrencyColumn =
new Dictionary<IConventionEntityType, IConventionProperty>();
foreach (var entityType in entityTypesMappedToTable)
Dictionary<IConventionEntityType, IProperty> entityTypesMissingConcurrencyColumn = null;
foreach (var entityType in mappedTypes)
{
var foundMappedProperty = false;
foreach (var mappedProperty in propertiesMappedToConcurrencyColumn)
{
var declaringEntityType = mappedProperty.DeclaringEntityType;
if (declaringEntityType.IsAssignableFrom(entityType)
|| declaringEntityType.IsInOwnershipPath(entityType)
|| entityType.IsInOwnershipPath(declaringEntityType))
{
foundMappedProperty = true;
break;
}
}

foundMappedProperty = foundMappedProperty
|| entityType.GetAllBaseTypes().SelectMany(t => t.GetDeclaredProperties())
.Any(p => p.GetColumnName(StoreObjectIdentifier.Table(table.Table, table.Schema)) == concurrencyColumnName);
var foundMappedProperty = !IsConcurrencyTokenMissing(propertiesMappedToConcurrencyColumn, entityType, mappedTypes)
|| entityType.GetDeclaredProperties()
.Concat(entityType.GetAllBaseTypes().SelectMany(t => t.GetDeclaredProperties()))
.Any(p => p.GetColumnName(StoreObjectIdentifier.Table(table.Name, table.Schema)) == concurrencyColumnName);

if (!foundMappedProperty)
{
if (entityTypesMissingConcurrencyColumn == null)
{
entityTypesMissingConcurrencyColumn = new Dictionary<IConventionEntityType, IProperty>();
}
// store the entity type which is missing the
// concurrency token property, mapped to an example
// property which _is_ mapped to this concurrency token
Expand All @@ -93,86 +101,136 @@ public virtual void ProcessModelFinalizing(
}
}

RemoveDerivedEntityTypes(ref entityTypesMissingConcurrencyColumn);
if (entityTypesMissingConcurrencyColumn == null)
{
continue;
}

RemoveDerivedEntityTypes(entityTypesMissingConcurrencyColumn);

foreach(var entityTypeToExampleProperty in entityTypesMissingConcurrencyColumn)
foreach (var entityTypeToExampleProperty in entityTypesMissingConcurrencyColumn)
{
var entityType = entityTypeToExampleProperty.Key;
var exampleProperty = entityTypeToExampleProperty.Value;
var concurrencyShadowPropertyBuilder =
#pragma warning disable EF1001 // Internal EF Core API usage.
((InternalEntityTypeBuilder)entityType.Builder).CreateUniqueProperty(
ConcurrencyPropertyPrefix + exampleProperty.Name,
entityTypeToExampleProperty.Key.Builder.CreateUniqueProperty(
exampleProperty.ClrType,
!exampleProperty.IsNullable).Builder;
concurrencyShadowPropertyBuilder
ConcurrencyPropertyPrefix + exampleProperty.Name,
!exampleProperty.IsNullable)
.HasColumnName(concurrencyColumnName)
.HasColumnType(exampleProperty.GetColumnType())
?.IsConcurrencyToken(true)
?.ValueGenerated(exampleProperty.ValueGenerated);
#pragma warning restore EF1001 // Internal EF Core API usage.
.IsConcurrencyToken(true)
.ValueGenerated(exampleProperty.ValueGenerated);
}
}
}
}

private void GetMappings(IConventionModel model,
out Dictionary<(string Table, string Schema), IList<IConventionEntityType>> tableToEntityTypes,
out Dictionary<(string Table, string Schema), Dictionary<string, IList<IConventionProperty>>> concurrencyColumnsToProperties)
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static Dictionary<string, List<IProperty>> GetConcurrencyTokensMap(
StoreObjectIdentifier table, [NotNull] IReadOnlyList<IEntityType> mappedTypes)
{
tableToEntityTypes = new Dictionary<(string Table, string Schema), IList<IConventionEntityType>>();
concurrencyColumnsToProperties = new Dictionary<(string Table, string Schema), Dictionary<string, IList<IConventionProperty>>>();
foreach (var entityType in model.GetEntityTypes())
if (mappedTypes.Count < 2)
{
var tableName = entityType.GetTableName();
if (tableName == null)
{
continue; // unmapped entityType
}

var table = (Name: tableName, Schema: entityType.GetSchema());
return null;
}

if (!tableToEntityTypes.TryGetValue(table, out var mappedTypes))
Dictionary<string, List<IProperty>> concurrencyColumns = null;
var nonHierarchyTypesCount = 0;
foreach (var entityType in mappedTypes)
{
if (entityType.BaseType == null
|| !mappedTypes.Contains(entityType.BaseType))
{
mappedTypes = new List<IConventionEntityType>();
tableToEntityTypes[table] = mappedTypes;
nonHierarchyTypesCount++;
}

mappedTypes.Add(entityType);

foreach (var property in entityType.GetDeclaredProperties())
{
if (property.IsConcurrencyToken)
if (!property.IsConcurrencyToken
|| (property.ValueGenerated & ValueGenerated.OnUpdate) == 0)
{
if (!concurrencyColumnsToProperties.TryGetValue(table, out var columnToProperties))
{
columnToProperties = new Dictionary<string, IList<IConventionProperty>>();
concurrencyColumnsToProperties[table] = columnToProperties;
}
continue;
}

var columnName = property.GetColumnName(StoreObjectIdentifier.Table(tableName, table.Schema));
if (columnName == null)
{
continue;
}
var columnName = property.GetColumnName(table);
if (columnName == null)
{
continue;
}

if (!columnToProperties.TryGetValue(columnName, out var properties))
{
properties = new List<IConventionProperty>();
columnToProperties[columnName] = properties;
}
if (concurrencyColumns == null)
{
concurrencyColumns = new Dictionary<string, List<IProperty>>();
}

properties.Add(property);
if (!concurrencyColumns.TryGetValue(columnName, out var properties))
{
properties = new List<IProperty>();
concurrencyColumns[columnName] = properties;
}

properties.Add(property);
}
}

return nonHierarchyTypesCount < 2 ? null : concurrencyColumns;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public static bool IsConcurrencyTokenMissing(
[NotNull] List<IProperty> propertiesMappedToConcurrencyColumn,
[NotNull] IEntityType entityType,
[NotNull] IReadOnlyList<IEntityType> mappedTypes)
{
if (entityType.FindPrimaryKey() == null)
{
return false;
}

var propertyMissing = false;
foreach (var mappedProperty in propertiesMappedToConcurrencyColumn)
{
var declaringEntityType = mappedProperty.DeclaringEntityType;
if (declaringEntityType.IsAssignableFrom(entityType)
|| entityType.IsAssignableFrom(declaringEntityType)
|| declaringEntityType.IsInOwnershipPath(entityType)
|| entityType.IsInOwnershipPath(declaringEntityType))
{
// The concurrency token is in the same hierarchy or in the same aggregate
continue;
}

var linkingFks = declaringEntityType.FindForeignKeys(declaringEntityType.FindPrimaryKey().Properties)
.Where(fk => fk.PrincipalKey.IsPrimaryKey()
&& mappedTypes.Contains(fk.PrincipalEntityType)).ToList();
if (linkingFks.Count > 0
&& !linkingFks.Any(fk => fk.PrincipalEntityType == entityType)
&& linkingFks.Any(fk => fk.PrincipalEntityType.IsAssignableFrom(entityType)
|| entityType.IsAssignableFrom(fk.PrincipalEntityType)))
{
// The concurrency token is on a type that shares the row with a base or derived type
continue;
}

propertyMissing = true;
break;
}

return propertyMissing;
}

// Given a Dictionary of EntityTypes (mapped to T), remove
// any mappings where the EntityType inherits from any
// other EntityType in the Dictionary.
private static void RemoveDerivedEntityTypes<T>(
ref Dictionary<IConventionEntityType, T> entityTypeDictionary)
private static void RemoveDerivedEntityTypes<T>(Dictionary<IConventionEntityType, T> entityTypeDictionary)
{
var toRemove = new HashSet<KeyValuePair<IConventionEntityType, T>>();
var entityTypesWithDerivedTypes =
Expand Down
2 changes: 0 additions & 2 deletions src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;
using Microsoft.Extensions.DependencyInjection;
Expand Down
12 changes: 12 additions & 0 deletions src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ IConventionPropertyBuilder Property(
/// </returns>
IConventionPropertyBuilder Property([NotNull] MemberInfo memberInfo, bool fromDataAnnotation = false);

/// <summary>
/// Creates a property with a name that's different from any existing properties.
/// </summary>
/// <param name="basePropertyName"> The desired property name. </param>
/// <param name="propertyType"> The type of value the property will hold. </param>
/// <param name="isRequired"> A value indicating whether the property is required. </param>
/// <returns>
/// An object that can be used to configure the property if it exists on the entity type,
/// <see langword="null" /> otherwise.
/// </returns>
IConventionPropertyBuilder CreateUniqueProperty([NotNull] Type propertyType, [NotNull] string basePropertyName, bool isRequired);

/// <summary>
/// Returns the existing properties with the given names or creates them if matching CLR members are found.
/// </summary>
Expand Down
Loading

0 comments on commit 46365f2

Please sign in to comment.