diff --git a/EFCore.NamingConventions.Test/NameRewritingConventionTest.cs b/EFCore.NamingConventions.Test/NameRewritingConventionTest.cs index 505bb11..3a332b7 100644 --- a/EFCore.NamingConventions.Test/NameRewritingConventionTest.cs +++ b/EFCore.NamingConventions.Test/NameRewritingConventionTest.cs @@ -8,6 +8,8 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; @@ -32,6 +34,25 @@ public void Column() .GetColumnName(StoreObjectIdentifier.Create(entityType, StoreObjectType.Table)!.Value)); } + [Theory] + [InlineData(true, "__EFMigrationsHistory", "MigrationId", "ProductVersion")] + [InlineData(false, "__EFMigrationsHistory", "migration_id", "product_version")] + public void ColumnInMigrationTable(bool ignoreMigrationTable, string tableName, string migrationIdName, string productVersionName) + { + var entityType = BuildEntityType(b => b.Entity(e => { + e.ToTable("__EFMigrationsHistory"); + e.HasKey(h => h.MigrationId); + e.Property(h => h.MigrationId).HasMaxLength(150); + e.Property(h => h.ProductVersion).HasMaxLength(32).IsRequired(); + }), ignoreMigrationTable: ignoreMigrationTable); + + Assert.Equal(tableName, entityType.GetTableName()); + Assert.Equal(migrationIdName, entityType.FindProperty(nameof(HistoryRow.MigrationId)) + .GetColumnName(StoreObjectIdentifier.Create(entityType, StoreObjectType.Table)!.Value)); + Assert.Equal(productVersionName, entityType.FindProperty(nameof(HistoryRow.ProductVersion)) + .GetColumnName(StoreObjectIdentifier.Create(entityType, StoreObjectType.Table)!.Value)); + } + [Fact] public void Column_with_turkish_culture() { @@ -724,10 +745,10 @@ public void Foreign_key_on_key_without_setter() Assert.Equal("fk_card_board_board_id", entityType.GetForeignKeys().Single().GetConstraintName()); } - private IEntityType BuildEntityType(Action builderAction, CultureInfo? culture = null) - => BuildModel(builderAction, culture).GetEntityTypes().Single(); + private IEntityType BuildEntityType(Action builderAction, CultureInfo? culture = null, bool ignoreMigrationTable = false) + => BuildModel(builderAction, culture, ignoreMigrationTable).GetEntityTypes().Single(); - private IModel BuildModel(Action builderAction, CultureInfo? culture = null) + private IModel BuildModel(Action builderAction, CultureInfo? culture = null, bool ignoreMigrationTable = false) { var services = SqlServerTestHelpers .Instance @@ -741,7 +762,7 @@ private IModel BuildModel(Action builderAction, CultureInfo? cultu var optionsBuilder = new DbContextOptionsBuilder(); SqlServerTestHelpers.Instance.UseProviderOptions(optionsBuilder); - optionsBuilder.UseSnakeCaseNamingConvention(culture); + optionsBuilder.UseSnakeCaseNamingConvention(culture, ignoreMigrationTable); var plugin = new NamingConventionSetPlugin(dependencies, optionsBuilder.Options); plugin.ModifyConventions(conventionSet); diff --git a/EFCore.NamingConventions/Internal/NameRewritingConvention.cs b/EFCore.NamingConventions/Internal/NameRewritingConvention.cs index e02de78..a2b4f6e 100644 --- a/EFCore.NamingConventions/Internal/NameRewritingConvention.cs +++ b/EFCore.NamingConventions/Internal/NameRewritingConvention.cs @@ -27,10 +27,12 @@ private static readonly StoreObjectType[] _storeObjectTypes private readonly IDictionary _sets; private readonly INameRewriter _namingNameRewriter; + private readonly bool _ignoreMigrationTable; - public NameRewritingConvention(ProviderConventionSetBuilderDependencies dependencies, INameRewriter nameRewriter) + public NameRewritingConvention(ProviderConventionSetBuilderDependencies dependencies, INameRewriter nameRewriter, bool ignoreMigrationTable = false) { _namingNameRewriter = nameRewriter; + _ignoreMigrationTable = ignoreMigrationTable; // Copied from TableNameFromDbSetConvention _sets = new Dictionary(); @@ -56,6 +58,7 @@ public NameRewritingConvention(ProviderConventionSetBuilderDependencies dependen _sets.Remove(type); } } + } public virtual void ProcessEntityTypeAdded( @@ -474,6 +477,11 @@ private void RewriteColumnName(IConventionPropertyBuilder propertyBuilder) // Remove any previous setting of the column name we may have done, so we can get the default recalculated below. property.Builder.HasNoAnnotation(RelationalAnnotationNames.ColumnName); + if (_ignoreMigrationTable && structuralType.ClrType.FullName == "Microsoft.EntityFrameworkCore.Migrations.HistoryRow") + { + return; + } + // TODO: The following is a temporary hack. We should probably just always set the relational override below, // but https://github.com/dotnet/efcore/pull/23834 var baseColumnName = StoreObjectIdentifier.Create(structuralType, StoreObjectType.Table) is { } tableIdentifier diff --git a/EFCore.NamingConventions/Internal/NamingConventionSetPlugin.cs b/EFCore.NamingConventions/Internal/NamingConventionSetPlugin.cs index c96622c..93e89d0 100644 --- a/EFCore.NamingConventions/Internal/NamingConventionSetPlugin.cs +++ b/EFCore.NamingConventions/Internal/NamingConventionSetPlugin.cs @@ -26,6 +26,7 @@ public ConventionSet ModifyConventions(ConventionSet conventionSet) new NamingConventionsOptionsExtension().WithoutNaming(); var namingStyle = extension.NamingConvention; var culture = extension.Culture; + var ignoreMigrationTable = extension.IgnoreMigrationTable; if (namingStyle == NamingConvention.None) { return conventionSet; @@ -39,7 +40,7 @@ public ConventionSet ModifyConventions(ConventionSet conventionSet) NamingConvention.UpperCase => new UpperCaseNameRewriter(culture ?? CultureInfo.InvariantCulture), NamingConvention.UpperSnakeCase => new UpperSnakeCaseNameRewriter(culture ?? CultureInfo.InvariantCulture), _ => throw new ArgumentOutOfRangeException("Unhandled enum value: " + namingStyle) - }); + }, ignoreMigrationTable); conventionSet.EntityTypeAddedConventions.Add(convention); conventionSet.EntityTypeAnnotationChangedConventions.Add(convention); diff --git a/EFCore.NamingConventions/Internal/NamingConventionsOptionsExtension.cs b/EFCore.NamingConventions/Internal/NamingConventionsOptionsExtension.cs index 8dffcf5..55b7a52 100644 --- a/EFCore.NamingConventions/Internal/NamingConventionsOptionsExtension.cs +++ b/EFCore.NamingConventions/Internal/NamingConventionsOptionsExtension.cs @@ -12,12 +12,14 @@ public class NamingConventionsOptionsExtension : IDbContextOptionsExtension private DbContextOptionsExtensionInfo? _info; private NamingConvention _namingConvention; private CultureInfo? _culture; + private bool _ignoreMigrationTable; public NamingConventionsOptionsExtension() {} protected NamingConventionsOptionsExtension(NamingConventionsOptionsExtension copyFrom) { _namingConvention = copyFrom._namingConvention; _culture = copyFrom._culture; + _ignoreMigrationTable = copyFrom._ignoreMigrationTable; } public virtual DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this); @@ -26,6 +28,7 @@ protected NamingConventionsOptionsExtension(NamingConventionsOptionsExtension co internal virtual NamingConvention NamingConvention => _namingConvention; internal virtual CultureInfo? Culture => _culture; + internal virtual bool IgnoreMigrationTable => _ignoreMigrationTable; public virtual NamingConventionsOptionsExtension WithoutNaming() { @@ -34,43 +37,48 @@ public virtual NamingConventionsOptionsExtension WithoutNaming() return clone; } - public virtual NamingConventionsOptionsExtension WithSnakeCaseNamingConvention(CultureInfo? culture = null) + public virtual NamingConventionsOptionsExtension WithSnakeCaseNamingConvention(CultureInfo? culture = null, bool ignoreMigrationTable = false) { var clone = Clone(); clone._namingConvention = NamingConvention.SnakeCase; clone._culture = culture; + clone._ignoreMigrationTable = ignoreMigrationTable; return clone; } - public virtual NamingConventionsOptionsExtension WithLowerCaseNamingConvention(CultureInfo? culture = null) + public virtual NamingConventionsOptionsExtension WithLowerCaseNamingConvention(CultureInfo? culture = null, bool ignoreMigrationTable = false) { var clone = Clone(); clone._namingConvention = NamingConvention.LowerCase; clone._culture = culture; + clone._ignoreMigrationTable = ignoreMigrationTable; return clone; } - public virtual NamingConventionsOptionsExtension WithUpperCaseNamingConvention(CultureInfo? culture = null) + public virtual NamingConventionsOptionsExtension WithUpperCaseNamingConvention(CultureInfo? culture = null, bool ignoreMigrationTable = false) { var clone = Clone(); clone._namingConvention = NamingConvention.UpperCase; clone._culture = culture; + clone._ignoreMigrationTable = ignoreMigrationTable; return clone; } - public virtual NamingConventionsOptionsExtension WithUpperSnakeCaseNamingConvention(CultureInfo? culture = null) + public virtual NamingConventionsOptionsExtension WithUpperSnakeCaseNamingConvention(CultureInfo? culture = null, bool ignoreMigrationTable = false) { var clone = Clone(); clone._namingConvention = NamingConvention.UpperSnakeCase; clone._culture = culture; + clone._ignoreMigrationTable = ignoreMigrationTable; return clone; } - public virtual NamingConventionsOptionsExtension WithCamelCaseNamingConvention(CultureInfo? culture = null) + public virtual NamingConventionsOptionsExtension WithCamelCaseNamingConvention(CultureInfo? culture = null, bool ignoreMigrationTable = false) { var clone = Clone(); clone._namingConvention = NamingConvention.CamelCase; clone._culture = culture; + clone._ignoreMigrationTable = ignoreMigrationTable; return clone; } @@ -109,6 +117,12 @@ public override string LogFragment _ => throw new ArgumentOutOfRangeException("Unhandled enum value: " + Extension._namingConvention) }); + if (Extension._ignoreMigrationTable) + { + builder + .Append(" ignoring the migrations table"); + } + if (Extension._culture is null) { builder @@ -128,6 +142,7 @@ public override int GetServiceProviderHashCode() { var hashCode = Extension._namingConvention.GetHashCode(); hashCode = (hashCode * 3) ^ (Extension._culture?.GetHashCode() ?? 0); + hashCode = (hashCode * 7) ^ (Extension._ignoreMigrationTable.GetHashCode()); return hashCode; } @@ -138,6 +153,10 @@ public override void PopulateDebugInfo(IDictionary debugInfo) { debugInfo["Naming:UseNamingConvention"] = Extension._namingConvention.GetHashCode().ToString(CultureInfo.InvariantCulture); + + debugInfo["Naming:IgnoreMigrationTable"] + = Extension._ignoreMigrationTable.GetHashCode().ToString(CultureInfo.InvariantCulture); + if (Extension._culture != null) { debugInfo["Naming:Culture"] diff --git a/EFCore.NamingConventions/NamingConventionsExtensions.cs b/EFCore.NamingConventions/NamingConventionsExtensions.cs index 6f173f9..52f04b2 100644 --- a/EFCore.NamingConventions/NamingConventionsExtensions.cs +++ b/EFCore.NamingConventions/NamingConventionsExtensions.cs @@ -9,14 +9,15 @@ namespace Microsoft.EntityFrameworkCore; public static class NamingConventionsExtensions { public static DbContextOptionsBuilder UseSnakeCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder, - CultureInfo? culture = null) + [NotNull] this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null, + bool ignoreMigrationTable = false) { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); var extension = (optionsBuilder.Options.FindExtension() ?? new NamingConventionsOptionsExtension()) - .WithSnakeCaseNamingConvention(culture); + .WithSnakeCaseNamingConvention(culture, ignoreMigrationTable); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); @@ -24,19 +25,20 @@ public static DbContextOptionsBuilder UseSnakeCaseNamingConvention( } public static DbContextOptionsBuilder UseSnakeCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder , CultureInfo? culture = null) + [NotNull] this DbContextOptionsBuilder optionsBuilder , CultureInfo? culture = null, bool ignoreMigrationTable = false) where TContext : DbContext - => (DbContextOptionsBuilder)UseSnakeCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture); + => (DbContextOptionsBuilder)UseSnakeCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture, ignoreMigrationTable); public static DbContextOptionsBuilder UseLowerCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder, - CultureInfo? culture = null) + [NotNull] this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null, + bool ignoreMigrationTable = false) { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); var extension = (optionsBuilder.Options.FindExtension() ?? new NamingConventionsOptionsExtension()) - .WithLowerCaseNamingConvention(culture); + .WithLowerCaseNamingConvention(culture, ignoreMigrationTable); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); @@ -44,20 +46,22 @@ public static DbContextOptionsBuilder UseLowerCaseNamingConvention( } public static DbContextOptionsBuilder UseLowerCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder, - CultureInfo? culture = null) + [NotNull] this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null, + bool ignoreMigrationTable = false) where TContext : DbContext - => (DbContextOptionsBuilder)UseLowerCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder ,culture); + => (DbContextOptionsBuilder)UseLowerCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder ,culture, ignoreMigrationTable); public static DbContextOptionsBuilder UseUpperCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder, - CultureInfo? culture = null) + [NotNull] this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null, + bool ignoreMigrationTable = false) { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); var extension = (optionsBuilder.Options.FindExtension() ?? new NamingConventionsOptionsExtension()) - .WithUpperCaseNamingConvention(culture); + .WithUpperCaseNamingConvention(culture, ignoreMigrationTable); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); @@ -65,20 +69,22 @@ public static DbContextOptionsBuilder UseUpperCaseNamingConvention( } public static DbContextOptionsBuilder UseUpperCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder, - CultureInfo? culture = null) + [NotNull] this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null, + bool ignoreMigrationTable = false) where TContext : DbContext - => (DbContextOptionsBuilder)UseUpperCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture); + => (DbContextOptionsBuilder)UseUpperCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture, ignoreMigrationTable); public static DbContextOptionsBuilder UseUpperSnakeCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder, - CultureInfo? culture = null) + [NotNull] this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null, + bool ignoreMigrationTable = false) { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); var extension = (optionsBuilder.Options.FindExtension() ?? new NamingConventionsOptionsExtension()) - .WithUpperSnakeCaseNamingConvention(culture); + .WithUpperSnakeCaseNamingConvention(culture, ignoreMigrationTable); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); @@ -86,20 +92,22 @@ public static DbContextOptionsBuilder UseUpperSnakeCaseNamingConvention( } public static DbContextOptionsBuilder UseUpperSnakeCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder, - CultureInfo? culture = null) + [NotNull] this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null, + bool ignoreMigrationTable = false) where TContext : DbContext - => (DbContextOptionsBuilder)UseUpperSnakeCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture); + => (DbContextOptionsBuilder)UseUpperSnakeCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture, ignoreMigrationTable); public static DbContextOptionsBuilder UseCamelCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder, - CultureInfo? culture = null) + [NotNull] this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null, + bool ignoreMigrationTable = false) { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); var extension = (optionsBuilder.Options.FindExtension() ?? new NamingConventionsOptionsExtension()) - .WithCamelCaseNamingConvention(culture); + .WithCamelCaseNamingConvention(culture, ignoreMigrationTable); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); @@ -107,8 +115,9 @@ public static DbContextOptionsBuilder UseCamelCaseNamingConvention( } public static DbContextOptionsBuilder UseCamelCaseNamingConvention( - this DbContextOptionsBuilder optionsBuilder, - CultureInfo? culture = null) + [NotNull] this DbContextOptionsBuilder optionsBuilder, + CultureInfo? culture = null, + bool ignoreMigrationTable = false) where TContext : DbContext - => (DbContextOptionsBuilder)UseCamelCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture); + => (DbContextOptionsBuilder)UseCamelCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture, ignoreMigrationTable); } diff --git a/README.md b/README.md index afb887e..9c2b9b3 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,17 @@ SELECT c.id, c.full_name WHERE c.full_name = 'John Doe'; ``` +## Ignoring the Migration Table `__EFMigrationsHistory` + +To make migrations of existing databases more robust one might want to leave the migrations table out of the naming conventions. + +```c# +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseNpgsql(...) + .UseSnakeCaseNamingConvention(ignoreMigrationTable: true); +``` + ## Supported naming conventions * UseSnakeCaseNamingConvention: `FullName` becomes `full_name`