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

Fix for 18063. Add missing concurrency token properties for table splitting #20748

Merged
merged 3 commits into from
Apr 28, 2020
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
Expand Up @@ -92,6 +92,12 @@ public override ConventionSet CreateConventionSet()
var dbFunctionAttributeConvention = new RelationalDbFunctionAttributeConvention(Dependencies, RelationalDependencies);
conventionSet.ModelInitializedConventions.Add(dbFunctionAttributeConvention);

// Use TypeMappingConvention to add the relational store type mapping
// to the generated concurrency token property
ConventionSet.AddBefore(
conventionSet.ModelFinalizingConventions,
new TableSharingConcurrencyTokenConvention(Dependencies, RelationalDependencies),
typeof(TypeMappingConvention));
// ModelCleanupConvention would remove the entity types added by QueryableDbFunctionConvention #15898
ConventionSet.AddAfter(
conventionSet.ModelFinalizingConventions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ private static void TryUniquifyColumnNames(
var usePrefix = property.DeclaringEntityType != otherProperty.DeclaringEntityType
|| property.IsPrimaryKey()
|| otherProperty.IsPrimaryKey();
if (!property.IsPrimaryKey())
if (!property.IsPrimaryKey()
&& !property.IsConcurrencyToken)
{
var newColumnName = TryUniquify(property, columnName, properties, usePrefix, maxLength);
if (newColumnName != null)
Expand All @@ -177,7 +178,8 @@ private static void TryUniquifyColumnNames(
}
}

if (!otherProperty.IsPrimaryKey())
if (!otherProperty.IsPrimaryKey()
&& !otherProperty.IsConcurrencyToken)
{
var newColumnName = TryUniquify(otherProperty, columnName, properties, usePrefix, maxLength);
if (newColumnName != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
{
/// <summary>
/// A convention that finds entity types which share a table
/// which has a concurrency token column where those entity types
/// do not have a property mapped to that column. It then
/// creates a shadow concurrency property mapped to that column
/// on the base-most entity type(s).
/// </summary>
public class TableSharingConcurrencyTokenConvention : IModelFinalizingConvention
{
/// <summary>
/// Creates a new instance of <see cref="TableSharingConcurrencyTokenConvention" />.
/// </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 TableSharingConcurrencyTokenConvention(
[NotNull] ProviderConventionSetBuilderDependencies dependencies,
[NotNull] RelationalConventionSetBuilderDependencies relationalDependencies)
{
Dependencies = dependencies;
}

/// <summary>
/// Parameter object containing service dependencies.
/// </summary>
protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }

public static readonly string ConcurrencyPropertyPrefix = "_TableSharingConcurrencyTokenConvention_";

/// <inheritdoc />
public virtual void ProcessModelFinalizing(
IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> context)
{
GetMappings(modelBuilder.Metadata,
out var tableToEntityTypes, out var concurrencyColumnsToProperties);

foreach (var tableToEntityType in tableToEntityTypes)
{
var table = tableToEntityType.Key;
if (!concurrencyColumnsToProperties.TryGetValue(table, out var concurrencyColumns))
{
continue; // this table has no mapped concurrency columns
}

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)
{
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() == concurrencyColumnName);

if (!foundMappedProperty)
{
// store the entity type which is missing the
// concurrency token property, mapped to an example
// property which _is_ mapped to this concurrency token
// column and which will be used later as a template
entityTypesMissingConcurrencyColumn.Add(
entityType, propertiesMappedToConcurrencyColumn.First());
}
}

RemoveDerivedEntityTypes(ref 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,
exampleProperty.ClrType,
!exampleProperty.IsNullable).Builder;
concurrencyShadowPropertyBuilder
.HasColumnName(concurrencyColumnName)
?.IsConcurrencyToken(true)
?.ValueGenerated(exampleProperty.ValueGenerated);
#pragma warning restore EF1001 // Internal EF Core API usage.
}
}
}
}

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)
{
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())
{
var tableName = entityType.GetTableName();
if (tableName == null)
{
continue; // unmapped entityType
}

var table = (tableName, entityType.GetSchema());

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

mappedTypes.Add(entityType);

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

var columnName = property.GetColumnName();
if (!columnToProperties.TryGetValue(columnName, out var properties))
{
properties = new List<IConventionProperty>();
columnToProperties[columnName] = properties;
}

properties.Add(property);
}
}
}
}

// 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)
{
var toRemove = new HashSet<KeyValuePair<IConventionEntityType, T>>();
var entityTypesWithDerivedTypes =
entityTypeDictionary.Where(e => e.Key.GetDirectlyDerivedTypes().Any()).ToList();
foreach (var entityType in entityTypeDictionary.Where(e => e.Key.BaseType != null))
{
foreach (var otherEntityType in entityTypesWithDerivedTypes)
{
if (toRemove.Contains(otherEntityType)
|| otherEntityType.Equals(entityType))
{
continue;
}

if (otherEntityType.Key.IsAssignableFrom(entityType.Key))
{
toRemove.Add(entityType);
break;
}
}
}

foreach (var entityType in toRemove)
{
entityTypeDictionary.Remove(entityType.Key);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
Expand Down Expand Up @@ -962,9 +963,9 @@ public virtual void Passes_for_compatible_duplicate_index_names_within_hierarchy
}

[ConditionalFact]
public virtual void Detects_missing_concurrency_token_on_the_base_type()
public virtual void Detects_missing_concurrency_token_on_the_base_type_without_convention()
{
var modelBuilder = CreateConventionalModelBuilder();
lajones marked this conversation as resolved.
Show resolved Hide resolved
var modelBuilder = CreateModelBuilderWithoutConvention();
modelBuilder.Entity<Person>().ToTable(nameof(Animal))
.Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");
modelBuilder.Entity<Animal>().HasOne(a => a.FavoritePerson).WithOne().HasForeignKey<Person>(p => p.Id);
Expand All @@ -977,9 +978,9 @@ public virtual void Detects_missing_concurrency_token_on_the_base_type()
}

[ConditionalFact]
public virtual void Detects_missing_concurrency_token_on_the_sharing_type()
public virtual void Detects_missing_concurrency_token_on_the_sharing_type_without_convention()
{
var modelBuilder = CreateConventionalModelBuilder();
var modelBuilder = CreateModelBuilderWithoutConvention();
modelBuilder.Entity<Person>().ToTable(nameof(Animal));
modelBuilder.Entity<Animal>().HasOne(a => a.FavoritePerson).WithOne().HasForeignKey<Person>(p => p.Id);
modelBuilder.Entity<Animal>().Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");
Expand All @@ -989,6 +990,30 @@ public virtual void Detects_missing_concurrency_token_on_the_sharing_type()
modelBuilder.Model);
}

[ConditionalFact]
public virtual void Passes_with_missing_concurrency_token_property_on_the_base_type()
{
var modelBuilder = CreateConventionalModelBuilder();
modelBuilder.Entity<Person>().ToTable(nameof(Animal))
.Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");
modelBuilder.Entity<Animal>().HasOne(a => a.FavoritePerson).WithOne().HasForeignKey<Person>(p => p.Id);
modelBuilder.Entity<Cat>()
.Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");

Validate(modelBuilder.Model);
}

[ConditionalFact]
public virtual void Passes_with_missing_concurrency_token_property_on_the_sharing_type()
{
var modelBuilder = CreateConventionalModelBuilder();
modelBuilder.Entity<Person>().ToTable(nameof(Animal));
modelBuilder.Entity<Animal>().HasOne(a => a.FavoritePerson).WithOne().HasForeignKey<Person>(p => p.Id);
modelBuilder.Entity<Animal>().Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");

Validate(modelBuilder.Model);
}

[ConditionalFact]
public virtual void Passes_for_correctly_mapped_concurrency_tokens_with_table_sharing()
{
Expand Down Expand Up @@ -1171,6 +1196,18 @@ public TestDecimalToDecimalConverter()
}
}

protected virtual ModelBuilder CreateModelBuilderWithoutConvention(bool sensitiveDataLoggingEnabled = false)
{
var conventionSet = TestHelpers.CreateConventionalConventionSet(
CreateModelLogger(sensitiveDataLoggingEnabled), CreateValidationLogger(sensitiveDataLoggingEnabled));

ConventionSet.Remove(
conventionSet.ModelFinalizingConventions,
typeof(TableSharingConcurrencyTokenConvention));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a generic argument


return new ModelBuilder(conventionSet);
}

protected override TestHelpers TestHelpers => RelationalTestHelpers.Instance;
}
}
Loading