diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index d1ed7791b15..8c4905ccf1e 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -106,6 +106,7 @@ public override ConventionSet CreateConventionSet() conventionSet.ModelFinalizingConventions.Add(dbFunctionAttributeConvention); conventionSet.ModelFinalizingConventions.Add(tableNameFromDbSetConvention); conventionSet.ModelFinalizingConventions.Add(storeGenerationConvention); + conventionSet.ModelFinalizingConventions.Add(new SequenceUniquificationConvention(Dependencies, RelationalDependencies)); conventionSet.ModelFinalizingConventions.Add(new SharedTableConvention(Dependencies, RelationalDependencies)); conventionSet.ModelFinalizingConventions.Add(new DbFunctionTypeMappingConvention(Dependencies, RelationalDependencies)); ReplaceConvention( diff --git a/src/EFCore.Relational/Metadata/Conventions/SequenceUniquificationConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SequenceUniquificationConvention.cs new file mode 100644 index 00000000000..a2dfbd000d0 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/SequenceUniquificationConvention.cs @@ -0,0 +1,61 @@ +// 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; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// A convention which ensures that all sequences in the model have unique names + /// within a schema when truncated to the maximum identifier length for the model. + /// + public class SequenceUniquificationConvention : IModelFinalizingConvention + { + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public SequenceUniquificationConvention( + [NotNull] ProviderConventionSetBuilderDependencies dependencies, + [NotNull] RelationalConventionSetBuilderDependencies relationalDependencies) + { + Dependencies = dependencies; + } + + /// + /// Parameter object containing service dependencies. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + { + var model = modelBuilder.Metadata; + var modelSequences = + (SortedDictionary<(string Name, string Schema), Sequence>)model[RelationalAnnotationNames.Sequences]; + + if (modelSequences != null) + { + var maxLength = model.GetMaxIdentifierLength(); + var toReplace = modelSequences + .Where(s => s.Key.Name.Length > maxLength).ToList(); + + foreach (var sequence in toReplace) + { + var schemaName = sequence.Key.Schema; + var newSequenceName = Uniquifier.Uniquify( + sequence.Key.Name, modelSequences, + sequenceName => (sequenceName, schemaName), maxLength); + Sequence.SetName((IMutableModel)model, sequence.Value, newSequenceName); + } + } + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/Sequence.cs b/src/EFCore.Relational/Metadata/Internal/Sequence.cs index e4b336119f3..24f746b6137 100644 --- a/src/EFCore.Relational/Metadata/Internal/Sequence.cs +++ b/src/EFCore.Relational/Metadata/Internal/Sequence.cs @@ -24,7 +24,6 @@ public class Sequence : ConventionAnnotatable, IMutableSequence, IConventionSequ { private readonly IModel _model; - private readonly string _name; private readonly string _schema; private long? _startValue; private int? _incrementBy; @@ -105,7 +104,7 @@ public Sequence( Check.NullButNotEmpty(schema, nameof(schema)); _model = model; - _name = name; + Name = name; _schema = schema; _configurationSource = configurationSource; Builder = new InternalSequenceBuilder(this, ((IConventionModel)model).Builder); @@ -127,7 +126,7 @@ public Sequence([NotNull] IModel model, [NotNull] string annotationName) _configurationSource = ConfigurationSource.Explicit; var data = SequenceData.Deserialize((string)model[annotationName]); - _name = data.Name; + Name = data.Name; _schema = data.Schema; _startValue = data.StartValue; _incrementBy = data.IncrementBy; @@ -187,6 +186,36 @@ public static Sequence AddSequence( return sequence; } + /// + /// 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. + /// + public static Sequence SetName( + [NotNull] IMutableModel model, [NotNull] Sequence sequence, [NotNull] string name) + { + Check.NotNull(model, nameof(model)); + Check.NotNull(sequence, nameof(sequence)); + Check.NotEmpty(name, nameof(name)); + + var sequences = (SortedDictionary<(string, string), Sequence>)model[RelationalAnnotationNames.Sequences]; + var tuple = (sequence.Name, sequence.Schema); + if (sequences == null + || !sequences.ContainsKey(tuple)) + { + return null; + } + + sequences.Remove(tuple); + + sequence.Name = name; + + sequences.Add((name, sequence.Schema), sequence); + + return sequence; + } + /// /// 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 @@ -230,7 +259,7 @@ public static Sequence RemoveSequence([NotNull] IMutableModel model, [NotNull] s /// 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. /// - public virtual string Name => _name; + public virtual string Name { get; [param: NotNull] set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/SequenceUniquificationConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/SequenceUniquificationConventionTest.cs new file mode 100644 index 00000000000..9cc85ee33e5 --- /dev/null +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/SequenceUniquificationConventionTest.cs @@ -0,0 +1,85 @@ +// 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 Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +// ReSharper disable InconsistentNaming +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + public class SequenceUniquificationConventionTest + { + [ConditionalFact] + public virtual void Sequence_names_are_truncated_and_uniquified() + { + var modelBuilder = GetModelBuilder(); + modelBuilder.GetInfrastructure().HasMaxIdentifierLength(10); + modelBuilder.HasSequence("UniquifyMeToo", (string)null); + modelBuilder.HasSequence("UniquifyMeToo", "TestSchema"); + modelBuilder.HasSequence("UniquifyM!Too", (string)null); + modelBuilder.HasSequence("UniquifyM!Too", "TestSchema"); + // the below ensure we deal with clashes with existing + // sequence names that look like candidate uniquified names + modelBuilder.HasSequence("UniquifyM~", (string)null); + modelBuilder.HasSequence("UniquifyM~", "TestSchema"); + + var model = modelBuilder.Model; + model.FinalizeModel(); + + Assert.Collection(model.GetSequences(), + s0 => + { + Assert.Equal("Uniquify~1", s0.Name); + Assert.Null(s0.Schema); + }, + s1 => + { + Assert.Equal("Uniquify~1", s1.Name); + Assert.Equal("TestSchema", s1.Schema); + }, + s2 => + { + Assert.Equal("Uniquify~2", s2.Name); + Assert.Null(s2.Schema); + }, + s3 => + { + Assert.Equal("Uniquify~2", s3.Name); + Assert.Equal("TestSchema", s3.Schema); + }, + s4 => + { + Assert.Equal("UniquifyM~", s4.Name); + Assert.Null(s4.Schema); + }, + s5 => + { + Assert.Equal("UniquifyM~", s5.Name); + Assert.Equal("TestSchema", s5.Schema); + }); + } + + private ModelBuilder GetModelBuilder() + { + var conventionSet = new ConventionSet(); + + var dependencies = CreateDependencies() + .With(new CurrentDbContext(new DbContext(new DbContextOptions()))); + var relationalDependencies = CreateRelationalDependencies(); + conventionSet.ModelFinalizingConventions.Add( + new SequenceUniquificationConvention(dependencies, relationalDependencies)); + + return new ModelBuilder(conventionSet); + } + + private ProviderConventionSetBuilderDependencies CreateDependencies() + => RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + + private RelationalConventionSetBuilderDependencies CreateRelationalDependencies() + => RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + } +}