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 2 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,198 @@
// 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);
concurrencyShadowPropertyBuilder.SetColumnName(concurrencyColumnName);
_ = concurrencyShadowPropertyBuilder.SetIsConcurrencyToken(true, ConfigurationSource.Convention);
_ = concurrencyShadowPropertyBuilder.SetValueGenerated(exampleProperty.ValueGenerated, ConfigurationSource.Convention);
lajones marked this conversation as resolved.
Show resolved Hide resolved
#pragma warning restore EF1001 // Internal EF Core API usage.
}
}
}
}

private void GetMappings(IConventionModel model,
out Dictionary<Tuple<string, string>, IList<IConventionEntityType>> tableToEntityTypes,
out Dictionary<Tuple<string, string>, Dictionary<string, IList<IConventionProperty>>> concurrencyColumnsToProperties)
{
tableToEntityTypes = new Dictionary<Tuple<string, string>, IList<IConventionEntityType>>();
lajones marked this conversation as resolved.
Show resolved Hide resolved
concurrencyColumnsToProperties = new Dictionary<Tuple<string, string>, Dictionary<string, IList<IConventionProperty>>>();
foreach (var entityType in model.GetEntityTypes())
{
var tableName = entityType.GetTableName();
if (tableName == null)
{
continue; // unmapped entityType
}

var table = Tuple.Create(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());
lajones marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -961,34 +961,6 @@ public virtual void Passes_for_compatible_duplicate_index_names_within_hierarchy
Assert.Equal(index1.GetName(), index2.GetName());
}

[ConditionalFact]
public virtual void Detects_missing_concurrency_token_on_the_base_type()
{
var modelBuilder = CreateConventionalModelBuilder();
lajones marked this conversation as resolved.
Show resolved Hide resolved
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");

VerifyError(
RelationalStrings.MissingConcurrencyColumn(nameof(Animal), "Version", nameof(Animal)),
modelBuilder.Model);
}

[ConditionalFact]
public virtual void Detects_missing_concurrency_token_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");

VerifyError(
RelationalStrings.MissingConcurrencyColumn(nameof(Person), "Version", nameof(Animal)),
modelBuilder.Model);
}

[ConditionalFact]
public virtual void Passes_for_correctly_mapped_concurrency_tokens_with_table_sharing()
{
Expand Down
Loading