Skip to content

Commit

Permalink
Fix for 18063.
Browse files Browse the repository at this point in the history
Added new convention to create a shadow concurrency property on the base-most entity
type(s) in table-splitting scenarios where the property would otherwise be
missing.
  • Loading branch information
lajones committed Apr 25, 2020
1 parent 1d01584 commit a4a0209
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// 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.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;

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 AddConcurrencyTokenPropertiesConvention : IModelFinalizingConvention
{
/// <summary>
/// Creates a new instance of <see cref="AddConcurrencyTokenPropertiesConvention" />.
/// </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 AddConcurrencyTokenPropertiesConvention(
[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 = "_concurrency_";

/// <inheritdoc />
public virtual void ProcessModelFinalizing(
IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> context)
{
var maxIdentifierLength = modelBuilder.Metadata.GetMaxIdentifierLength();

GetMappings(modelBuilder.Metadata,
out var tableToEntityTypes, out var concurrencyColumnsToProperties);

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

var entityTypesMappedToTable = table.Value;

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

var entityTypesMissingConcurrencyColumn =
new Dictionary<IConventionEntityType, IProperty>();
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());
}
}

foreach(var entityTypeToExampleProperty in
BasestEntities(entityTypesMissingConcurrencyColumn))
{
var entityType = entityTypeToExampleProperty.Key;
var exampleProperty = entityTypeToExampleProperty.Value;
var allExistingProperties =
entityType.GetProperties().Select(p => p.Name)
.Union(entityType.GetNavigations().Select(p => p.Name))
.ToDictionary(s => s, s => 0);
var concurrencyShadowPropertyName =
Uniquifier.Uniquify(ConcurrencyPropertyPrefix + concurrencyColumnName, allExistingProperties, maxIdentifierLength);
var concurrencyShadowProperty =
entityType.AddProperty(concurrencyShadowPropertyName, exampleProperty.ClrType, null);
concurrencyShadowProperty.SetColumnName(concurrencyColumnName);
concurrencyShadowProperty.SetIsConcurrencyToken(true);
concurrencyShadowProperty.SetValueGenerated(exampleProperty.ValueGenerated);
}
}
}
}

private void GetMappings(IConventionModel model,
out Dictionary<string, IList<IConventionEntityType>> tableToEntityTypes,
out Dictionary<string, Dictionary<string, IList<IProperty>>> concurrencyColumnsToProperties)
{
tableToEntityTypes = new Dictionary<string, IList<IConventionEntityType>>();
concurrencyColumnsToProperties = new Dictionary<string, Dictionary<string, IList<IProperty>>>();
foreach (var entityType in model.GetEntityTypes())
{
var tableName = entityType.GetSchemaQualifiedTableName();
if (tableName == null)
{
continue; // unmapped entityType
}

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

mappedTypes.Add(entityType);

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

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

properties.Add(property);
}
}
}
}

// Given a (distinct) IEnumerable of EntityTypes (mapped to T),
// return only the mappings where the EntityType does not inherit
// from any other EntityType in the list.
private static IEnumerable<KeyValuePair<IConventionEntityType, T>> BasestEntities<T>(
IEnumerable<KeyValuePair<IConventionEntityType, T>> entityTypeDictionary)
{
var toRemove = new HashSet<KeyValuePair<IConventionEntityType, T>>();
foreach (var entityType in entityTypeDictionary)
{
var otherEntityTypes = entityTypeDictionary
.Except(new[] { entityType }).Except(toRemove);
foreach (var otherEntityType in otherEntityTypes)
{
if (otherEntityType.Key.IsAssignableFrom(entityType.Key))
{
toRemove.Add(entityType);
break;
}
}
}

return entityTypeDictionary.Except(toRemove);
}
}
}
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 AddConcurrencyTokenPropertiesConvention(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
Expand Up @@ -962,7 +962,7 @@ 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 Missing_concurrency_token_property_is_created_on_the_base_type()
{
var modelBuilder = CreateConventionalModelBuilder();
modelBuilder.Entity<Person>().ToTable(nameof(Animal))
Expand All @@ -971,22 +971,70 @@ public virtual void Detects_missing_concurrency_token_on_the_base_type()
modelBuilder.Entity<Cat>()
.Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");

VerifyError(
RelationalStrings.MissingConcurrencyColumn(nameof(Animal), "Version", nameof(Animal)),
modelBuilder.Model);
var model = modelBuilder.Model;
Validate(model);

var animal = model.FindEntityType(typeof(Animal));
var concurrencyProperty = animal.FindProperty("_concurrency_Version");
Assert.True(concurrencyProperty.IsConcurrencyToken);
Assert.True(concurrencyProperty.IsShadowProperty());
Assert.Equal("Version", concurrencyProperty.GetColumnName());
Assert.Equal(ValueGenerated.OnAddOrUpdate, concurrencyProperty.ValueGenerated);
}

[ConditionalFact]
public virtual void Missing_concurrency_token_properties_are_created_on_the_base_most_types()
{
var modelBuilder = CreateConventionalModelBuilder();
modelBuilder.Entity<Person>().ToTable(nameof(Animal)).Property<byte[]>("Version")
.HasColumnName("Version").ValueGeneratedOnUpdate().IsConcurrencyToken(true);
modelBuilder.Entity<Animal>().HasOne(a => a.FavoritePerson).WithOne().HasForeignKey<Person>(p => p.Id);
modelBuilder.Entity<Animal>().HasOne(a => a.Dwelling).WithOne().HasForeignKey<AnimalHouse>(p => p.Id);
modelBuilder.Entity<Cat>();
modelBuilder.Entity<AnimalHouse>().ToTable(nameof(Animal));
modelBuilder.Entity<TheMovie>();

var model = modelBuilder.Model;
Validate(model);

var animal = model.FindEntityType(typeof(Animal));
var concurrencyProperty = animal.FindProperty("_concurrency_Version");
Assert.True(concurrencyProperty.IsConcurrencyToken);
Assert.True(concurrencyProperty.IsShadowProperty());
Assert.Equal("Version", concurrencyProperty.GetColumnName());
Assert.Equal(ValueGenerated.OnUpdate, concurrencyProperty.ValueGenerated);

var cat = model.FindEntityType(typeof(Cat));
Assert.DoesNotContain(cat.GetDeclaredProperties(), p => p.Name == "_concurrency_Version");

var animalHouse = model.FindEntityType(typeof(AnimalHouse));
concurrencyProperty = animalHouse.FindProperty("_concurrency_Version");
Assert.True(concurrencyProperty.IsConcurrencyToken);
Assert.True(concurrencyProperty.IsShadowProperty());
Assert.Equal("Version", concurrencyProperty.GetColumnName());
Assert.Equal(ValueGenerated.OnUpdate, concurrencyProperty.ValueGenerated);

var theMovie = model.FindEntityType(typeof(TheMovie));
Assert.DoesNotContain(theMovie.GetDeclaredProperties(), p => p.Name == "_concurrency_Version");
}

[ConditionalFact]
public virtual void Detects_missing_concurrency_token_on_the_sharing_type()
public virtual void Missing_concurrency_token_property_is_created_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);
var model = modelBuilder.Model;
Validate(model);

var personEntityType = model.FindEntityType(typeof(Person));
var concurrencyProperty = personEntityType.FindProperty("_concurrency_Version");
Assert.True(concurrencyProperty.IsConcurrencyToken);
Assert.True(concurrencyProperty.IsShadowProperty());
Assert.Equal("Version", concurrencyProperty.GetColumnName());
Assert.Equal(ValueGenerated.OnAddOrUpdate, concurrencyProperty.ValueGenerated);
}

[ConditionalFact]
Expand Down Expand Up @@ -1116,6 +1164,7 @@ protected class Animal
public string Name { get; set; }

public Person FavoritePerson { get; set; }
public AnimalHouse Dwelling { get; set; }
}

protected class Cat : Animal
Expand All @@ -1138,6 +1187,16 @@ protected class Dog : Animal
public int Identity { get; set; }
}

protected class AnimalHouse
{
public int Id { get; set; }
}

protected class TheMovie : AnimalHouse
{
public bool CanHaveAnother { get; set; }
}

protected class Person
{
public int Id { get; set; }
Expand Down

0 comments on commit a4a0209

Please sign in to comment.