Skip to content

Commit

Permalink
Add flag to ignore the MigrationHistory table from the naming convent…
Browse files Browse the repository at this point in the history
…ions
  • Loading branch information
EraYaN committed Feb 2, 2024
1 parent 6d16226 commit 4908075
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 40 deletions.
29 changes: 25 additions & 4 deletions EFCore.NamingConventions.Test/NameRewritingConventionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<HistoryRow>(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))

Check warning on line 50 in EFCore.NamingConventions.Test/NameRewritingConventionTest.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'property' in 'string? RelationalPropertyExtensions.GetColumnName(IReadOnlyProperty property, in StoreObjectIdentifier storeObject)'.
.GetColumnName(StoreObjectIdentifier.Create(entityType, StoreObjectType.Table)!.Value));
Assert.Equal(productVersionName, entityType.FindProperty(nameof(HistoryRow.ProductVersion))

Check warning on line 52 in EFCore.NamingConventions.Test/NameRewritingConventionTest.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'property' in 'string? RelationalPropertyExtensions.GetColumnName(IReadOnlyProperty property, in StoreObjectIdentifier storeObject)'.
.GetColumnName(StoreObjectIdentifier.Create(entityType, StoreObjectType.Table)!.Value));
}

[Fact]
public void Column_with_turkish_culture()
{
Expand Down Expand Up @@ -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<ModelBuilder> builderAction, CultureInfo? culture = null)
=> BuildModel(builderAction, culture).GetEntityTypes().Single();
private IEntityType BuildEntityType(Action<ModelBuilder> builderAction, CultureInfo? culture = null, bool ignoreMigrationTable = false)
=> BuildModel(builderAction, culture, ignoreMigrationTable).GetEntityTypes().Single();

private IModel BuildModel(Action<ModelBuilder> builderAction, CultureInfo? culture = null)
private IModel BuildModel(Action<ModelBuilder> builderAction, CultureInfo? culture = null, bool ignoreMigrationTable = false)
{
var services = SqlServerTestHelpers
.Instance
Expand All @@ -741,7 +762,7 @@ private IModel BuildModel(Action<ModelBuilder> 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);

Expand Down
10 changes: 9 additions & 1 deletion EFCore.NamingConventions/Internal/NameRewritingConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ private static readonly StoreObjectType[] _storeObjectTypes

private readonly IDictionary<Type, string> _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<Type, string>();
Expand All @@ -56,6 +58,7 @@ public NameRewritingConvention(ProviderConventionSetBuilderDependencies dependen
_sets.Remove(type);
}
}

}

public virtual void ProcessEntityTypeAdded(
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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()
{
Expand All @@ -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;
}

Expand Down Expand Up @@ -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
Expand All @@ -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;
}

Expand All @@ -138,6 +153,10 @@ public override void PopulateDebugInfo(IDictionary<string, string> 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"]
Expand Down
67 changes: 38 additions & 29 deletions EFCore.NamingConventions/NamingConventionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,106 +9,115 @@ 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<NamingConventionsOptionsExtension>()
?? new NamingConventionsOptionsExtension())
.WithSnakeCaseNamingConvention(culture);
.WithSnakeCaseNamingConvention(culture, ignoreMigrationTable);

((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

return optionsBuilder;
}

public static DbContextOptionsBuilder<TContext> UseSnakeCaseNamingConvention<TContext>(
this DbContextOptionsBuilder<TContext> optionsBuilder , CultureInfo? culture = null)
[NotNull] this DbContextOptionsBuilder<TContext> optionsBuilder , CultureInfo? culture = null, bool ignoreMigrationTable = false)
where TContext : DbContext
=> (DbContextOptionsBuilder<TContext>)UseSnakeCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture);
=> (DbContextOptionsBuilder<TContext>)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<NamingConventionsOptionsExtension>()
?? new NamingConventionsOptionsExtension())
.WithLowerCaseNamingConvention(culture);
.WithLowerCaseNamingConvention(culture, ignoreMigrationTable);

((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

return optionsBuilder;
}

public static DbContextOptionsBuilder<TContext> UseLowerCaseNamingConvention<TContext>(
this DbContextOptionsBuilder<TContext> optionsBuilder,
CultureInfo? culture = null)
[NotNull] this DbContextOptionsBuilder<TContext> optionsBuilder,
CultureInfo? culture = null,
bool ignoreMigrationTable = false)
where TContext : DbContext
=> (DbContextOptionsBuilder<TContext>)UseLowerCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder ,culture);
=> (DbContextOptionsBuilder<TContext>)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<NamingConventionsOptionsExtension>()
?? new NamingConventionsOptionsExtension())
.WithUpperCaseNamingConvention(culture);
.WithUpperCaseNamingConvention(culture, ignoreMigrationTable);

((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

return optionsBuilder;
}

public static DbContextOptionsBuilder<TContext> UseUpperCaseNamingConvention<TContext>(
this DbContextOptionsBuilder<TContext> optionsBuilder,
CultureInfo? culture = null)
[NotNull] this DbContextOptionsBuilder<TContext> optionsBuilder,
CultureInfo? culture = null,
bool ignoreMigrationTable = false)
where TContext : DbContext
=> (DbContextOptionsBuilder<TContext>)UseUpperCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture);
=> (DbContextOptionsBuilder<TContext>)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<NamingConventionsOptionsExtension>()
?? new NamingConventionsOptionsExtension())
.WithUpperSnakeCaseNamingConvention(culture);
.WithUpperSnakeCaseNamingConvention(culture, ignoreMigrationTable);

((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

return optionsBuilder;
}

public static DbContextOptionsBuilder<TContext> UseUpperSnakeCaseNamingConvention<TContext>(
this DbContextOptionsBuilder<TContext> optionsBuilder,
CultureInfo? culture = null)
[NotNull] this DbContextOptionsBuilder<TContext> optionsBuilder,
CultureInfo? culture = null,
bool ignoreMigrationTable = false)
where TContext : DbContext
=> (DbContextOptionsBuilder<TContext>)UseUpperSnakeCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture);
=> (DbContextOptionsBuilder<TContext>)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<NamingConventionsOptionsExtension>()
?? new NamingConventionsOptionsExtension())
.WithCamelCaseNamingConvention(culture);
.WithCamelCaseNamingConvention(culture, ignoreMigrationTable);

((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

return optionsBuilder;
}

public static DbContextOptionsBuilder<TContext> UseCamelCaseNamingConvention<TContext>(
this DbContextOptionsBuilder<TContext> optionsBuilder,
CultureInfo? culture = null)
[NotNull] this DbContextOptionsBuilder<TContext> optionsBuilder,
CultureInfo? culture = null,
bool ignoreMigrationTable = false)
where TContext : DbContext
=> (DbContextOptionsBuilder<TContext>)UseCamelCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture);
=> (DbContextOptionsBuilder<TContext>)UseCamelCaseNamingConvention((DbContextOptionsBuilder)optionsBuilder, culture, ignoreMigrationTable);
}
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down

0 comments on commit 4908075

Please sign in to comment.