4

I'm attempting to override the default behavior of the SQL migrations generator so that I may specify a custom foreign key constraint name, as discussed here. I've wired up the configuration as advised.

Unfortunately, however, it's not going so well.

A quick logging statement reveals that the GetFkName() function is never hit.

I tried an alternate configuration construct, as discussed here and here, but I'm getting this error when I attempt to generate a migration:

More than one migrations configuration type was found in the assembly 'ConsoleApp1'. Specify the name of the one to use.

I find this result a bit odd, as I have only one configuration class, one SQL generation class, and one context class (the code below doesn't reflect this, but I commented out the extras for my actual tests). Specifying the configuration type on the command line, as indicated here, errors with this:

System.InvalidOperationException: The type 'ConsoleApp1.Db.CustomDbConfiguration2' does not inherit from 'System.Data.Entity.DbConfiguration'. Entity Framework code-based configuration classes must inherit from 'System.Data.Entity.DbConfiguration'.

All of this brings us back here, then, which doesn't work for the aforementioned reason (GetFkName() never gets hit). So it seems I'm chasing my tail (didn't know I had one until today).

What should I do to get this override to work correctly?


Configuration

Imports System.Data.Entity
Imports System.Data.Entity.Migrations
Imports System.Data.Entity.SqlServer

Namespace Db
  Friend Class CustomDbConfiguration
    Inherits DbConfiguration

    Public Sub New()
      Me.SetMigrationSqlGenerator(SqlProviderServices.ProviderInvariantName, Function() New CustomSqlGenerator)
    End Sub
  End Class

  Friend Class CustomDbConfiguration2
    Inherits DbMigrationsConfiguration(Of Context)

    Public Sub New()
      Me.SetSqlGenerator(SqlProviderServices.ProviderInvariantName, New CustomSqlGenerator2(Me.GetSqlGenerator(SqlProviderServices.ProviderInvariantName)))
      Me.ContextType = GetType(Context)
    End Sub
  End Class
End Namespace

SQL Generator

Imports System.Data.Entity.Migrations.Model
Imports System.Data.Entity.Migrations.Sql
Imports System.Data.Entity.SqlServer

Namespace Db
  Friend Class CustomSqlGenerator
    Inherits SqlServerMigrationSqlGenerator

    Protected Overrides Sub Generate(AddForeignKeyOperation As AddForeignKeyOperation)
      AddForeignKeyOperation.Name = GetFkName(AddForeignKeyOperation.PrincipalTable, AddForeignKeyOperation.DependentTable, AddForeignKeyOperation.DependentColumns.ToArray())
      MyBase.Generate(AddForeignKeyOperation)
    End Sub

    Protected Overrides Sub Generate(DropForeignKeyOperation As DropForeignKeyOperation)
      DropForeignKeyOperation.Name = GetFkName(DropForeignKeyOperation.PrincipalTable, DropForeignKeyOperation.DependentTable, DropForeignKeyOperation.DependentColumns.ToArray())
      MyBase.Generate(DropForeignKeyOperation)
    End Sub

    Private Shared Function GetFkName(PrimaryKeyTable As String, ForeignKeyTable As String, ParamArray ForeignTableFields As String()) As String
      IO.File.WriteAllText("D:\Logs\FkNameTest.log", $"{Now.ToString}{vbCrLf}")

      Return $"FK_{ForeignKeyTable}_{PrimaryKeyTable}"
    End Function
  End Class

  Friend Class CustomSqlGenerator2
    Inherits MigrationSqlGenerator

    Public Sub New(Generator As MigrationSqlGenerator)
      Me.Generator = Generator
    End Sub

    Public Overrides Function Generate(MigrationOperations As IEnumerable(Of MigrationOperation), ProviderManifestToken As String) As IEnumerable(Of MigrationStatement)
      Return Me.Generator.Generate(MigrationOperations, ProviderManifestToken)
    End Function

    Private ReadOnly Generator As MigrationSqlGenerator
  End Class
End Namespace

Context

Imports System.Data.Common
Imports System.Data.Entity
Imports System.Data.SqlClient
Imports System.Reflection

Namespace Db
  <DbConfigurationType(GetType(CustomDbConfiguration2))>
  Friend Class Context
    Inherits DbContext

    Public Sub New()
      MyBase.New(DbConnection.ConnectionString)
    End Sub

    Private Sub New(Connection As DbConnection)
      MyBase.New(Connection, True)

      Database.SetInitializer(New CreateDatabaseIfNotExists(Of Context))
      Database.SetInitializer(New MigrateDatabaseToLatestVersion(Of Context, Migrations.Configuration))

      Me.Database.Initialize(False)
    End Sub

    Public Shared Function Create() As Context
      Return New Context(DbConnection)
    End Function

    Private Shared ReadOnly Property DbConnection As SqlConnection
      Get
        Return New SqlConnection(Utils.DbConnectionString)
      End Get
    End Property

    Protected Overrides Sub OnModelCreating(Builder As DbModelBuilder)
      Builder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly)
      MyBase.OnModelCreating(Builder)
    End Sub

    Public Property Documents As DbSet(Of Document)
    Public Property Sections As DbSet(Of Section)
  End Class
End Namespace
timur
  • 14,239
  • 2
  • 11
  • 32
InteXX
  • 6,135
  • 6
  • 43
  • 80
  • It's easier to simply modify the migrations after they are generated. – David Browne - Microsoft Dec 27 '19 at 01:15
  • @DavidBrowne-Microsoft ~ Thanks, but not if there are a couple hundred of 'em ;-) – InteXX Dec 27 '19 at 01:35
  • @DavidBrowne-Microsoft ~ Do you know of a way to do this? – InteXX Jan 04 '20 at 14:40
  • I've never done this, and it's not something I would ever attempt in code-first. If you care about foreign key names, you should probably be maintaining your schema directly in the database, and then reverse-engineering the EF model from that. – David Browne - Microsoft Jan 04 '20 at 16:45
  • @DavidBrowne-Microsoft ~ Got it, thanks. – InteXX Jan 04 '20 at 17:54
  • Modifying the SQL generator is the _WRONG_ approach here, why? because the model doesn't know about the change or how to manage it when it needs to, for instance when it needs to drop keys and rebuild. So while you could do it, its a more effort than it is worth, and lets be honest, its pretty hard to debug. – Chris Schaller Jan 05 '20 at 04:19
  • To formulate a minimal answer, can you please pick one example, show your current FK declaration in the model (if any) and the FK that is created in the database, I ask this because my FKs, without doing anything different are generated with the same format you are trying to modify them to. `FK_{ForeignKeyTable}_{PrimaryKeyTable}` – Chris Schaller Jan 05 '20 at 04:22
  • @ChrisSchaller ~ `the model doesn't know about the change or how to manage it when it needs to, for instance when it needs to drop keys and rebuild` I considered this as well, but an examination of the generated migration code provides relief here. The `DropForeignKey()` method locates the key for drop not by the key's name but rather by its properties, e.g. `DependentTable`, `DependentColumn`, `PrincipalTable`. Hard to debug, I agree, but it's a single central construct and once it's working it shouldn't need debugging. – InteXX Jan 05 '20 at 13:07
  • @ChrisSchaller ~ I have no idea why it's suddenly working—all I did was move the code into a new project so as to be able to better provide the information you requested. I didn't change any of the architecture. But the log statement in `GetFkName()` was run, and a log file was created. I guess this is one Q that'll have to survive without an A. – InteXX Jan 05 '20 at 13:11
  • I'm happy to try and answer this with a different FK name for the purposes of the question, but I was looking into more information on _why_ you wanted to do it at all. I use custom conventions and custom migration logic a lot, just not for this. – Chris Schaller Jan 05 '20 at 15:10
  • @ChrisSchaller ~ `try and answer this` Thank you, but I don't see how we can... the question is nullified by my recent discovery. It'll be prudent to leave it up, though, for future researchers. `why you wanted to do it` Probably for a reason similar to why I choose to live in the deeply frozen land of Interior Alaska, where it's currently -35°F outside and I wouldn't trade it for the world :-) Seriously, though: 1) The `FK_{DependentTable}_{PrincipalTable}_{DependentColumn}` convention doesn't sit well with me (least important) and 2) I wanted to know that I could do it (most important). – InteXX Jan 05 '20 at 22:12
  • @ChrisSchaller ~ `I use custom conventions and custom migration logic a lot` I'm intrigued by that. Such things appeal to me. – InteXX Jan 05 '20 at 22:16
  • It's an epic response, will have it finished tonight after work ;) – Chris Schaller Jan 05 '20 at 23:33
  • @ChrisSchaller ~ I'm looking forward to it :-) – InteXX Jan 06 '20 at 00:12
  • @DavidBrowne-Microsoft ~ `it's not something I would ever attempt in code-first` I'm curious as to your reasons why. This sounds like something I ought to know about. – InteXX Jan 06 '20 at 00:14
  • At a certain point I believe it makes more sens to transition from code-first to database-first (aka Code-First from an Existing Database). EF Migrations is just not a simple and efficient tool for building and maintaining a complex database. So it makes sense to maintain the database in a database-specific tool, like SSDT/SSMS for SQL Server, or SqlDeveloper or Toad for Oracle. – David Browne - Microsoft Jan 06 '20 at 00:36
  • @DavidBrowne-Microsoft ~ I see. To clarify, my earlier allusion to "hundreds" of foreign key definitions doesn't mean they're all in one project or database. In fact they're spread out among many. For my part at least, with many—if not all—of my distributed apps I must stay with CFM for easy & reliable schema updates in production. It's either that or roll my own, and I trust yours a lot more than I'd trust mine ;-) BTW SQLCE is still very relevant :-) – InteXX Jan 06 '20 at 01:06
  • I find myself migrating more and more schemas into EF Code first, in larger teams and where the application is tightly bound to the DB schema this makes sense and is easier to enforce during testing and deployment without having to integrate any third party or external tools other than the application itself. EF really has finally evolved into a first class ORM. – Chris Schaller Jan 06 '20 at 13:55

1 Answers1

3

Disclaimer: I haven't coded in VB for many years, these code examples are my feeble attempt to translate my working example in C# into OPs native VB. Please feel free to update my syntax ;)

You can manually edit the migration scripts to specify a custom name for each ForeignKey by specifying a value for the optional Name parameter in the call to Tablebuilder.ForeignKey as part of a create table statement:

CreateTable(
  "dbo.CorporationVariety",
  Function(c) New With
    {
      .Id = c.Int(nullable: false, identity:= true),
      .CorporationId = c.Int(nullable:= false),
      .VarietyId = c.Int(nullable:= false),
    }) _
  .PrimaryKey(Function(t) t.Id)
  .ForeignKey("dbo.Corporation", Function(t) t.CorporationId, name := "FKCorporatationCorporationVarietyCorporationId")
  .ForeignKey("dbo.Variety", Function(t) t.VarietyId, name := "FKVarietyCorporationVarietyVarietyId")
  .Index(Function(t) t.CorporationId)
  .Index(Function(t) t.VarietyId)

Or as part of a DbMigration.AddForeignKey statement:

AddForeignKey("dbo.CorporationVariety", "CorporationId", "dbo.Corporation", name := "FKCorporatationCorporationVarietyCorporationId")
AddForeignKey("dbo.CorporationVariety", "VarietyId", "dbo.Variety", name := "FKVarietyCorporationVarietyVarietyId")

If you have a lot of keys in your Model, and you want to implement a specific convention, (as in a standard rule or sequence of code that you want to apply in given scenarios) across all keys, then Normally the first place to look for a solution is EF Code First Conventions.

Unfortunately, there is neither a standard convention that can help you here nor can you define a custom name for a foreign key using fluent notation...

Normally we would go ahead and create a Custom Code First Convention to define your custom logic, this works in generally 2 ways:

  1. Your convention executes standard configuration via Fluent Notation
    • we already noted that this option is not available to us...
  2. Your convention logic stores custom metadata to the model via annotations

Primary and Foreign keys seem to be an anomaly in the EF Code First Runtime, there does not seem to be a way to easily access the annotations from the associations even though they are relatively easy to define.

I was surprised to find this and stumbled across this post that further confirms this: https://stackoverflow.com/a/54369685/1690217

Update I started this post assuming that Conventions was the right way to go, because I use it for many other customisations that I've needed to apply over the years. If you are looking to implement other similar types of customisations, look to Conventions first.

We can still easily override the standard VisualBasicMigrationCodeGenerator that generates the migration code files, so lets jump straight into that. coapply to custom name for your ForeignKey and then implement a custom MigrationCodeGenerator to process the output from your convention.

  1. Create a custom VisualBasicMigrationCodeGenerator
  2. Register the Code Generator so that it is used by EF to generate the next migration

NOTE: This will not force existing keys in your database to be renamed. To do that you would need to force each key to be dropped and re-added back. For a large model Consider using a T4 template to create custom once-off migration logic to achieve this, once the above steps are in place.

Think of your Custom VisualBasicMigrationCodeGenerator as your personal EF code first sour dough culture, you can share this and re-use it for every new application, adding new functionality and improvements with each iteration. But Conventions are the configuration options that you may not want in every project, (which is why using _Conventions_ for OPs solution was my first direction.)

1. Create a custom VisualBasicMigrationCodeGenerator

Create a new class that inherits from the EF VisualBasicMigrationCodeGenerator, the minimal we need to do is override the AddForeignKeyOperation and modify the Name of the key and call the base implementation. This will affect all new keys added to the model.

To target keys added as part of CreateTable we will have to override GenerateInline(AddForeignKeyOperation...), however the base implemention (in the C# Generator...) doesn't obey the custom Name so instead we have to replace the implementation entirely.

  • When doing this, goto the EF project on GitHub and start with the original implementation, then inject your customizations as needed.

Please excuse this C#, I didn't have time to translate it, it does generate the correct VB code though ;)

public class CustomVBMigrationCodeGenerator : System.Data.Entity.Migrations.Design.VisualBasicMigrationCodeGenerator
{

    protected override void Generate(AddForeignKeyOperation addForeignKeyOperation, IndentedTextWriter writer)
    {
        ApplyCustomFKName(addForeignKeyOperation);
        base.Generate(addForeignKeyOperation, writer);
    }

    private void ApplyCustomFKName(ForeignKeyOperation operation)
    {
        // expecting FK without scheme or underscores: "FK{DependentTable}{PrincipalTable}{FKField}"
        operation.Name = $"FK{StripSchemeFromName(operation.DependentTable)}{StripSchemeFromName(operation.PrincipalTable)}{String.Join("", operation.DependentColumns)}";
    }
    private string StripSchemeFromName(string dbObjectName)
    {
        return dbObjectName.Split(new[] { '.' }, 2).Last();
    }

    /// <summary>
    /// Generates code to perform an <see cref="AddForeignKeyOperation" /> as part of a <see cref="CreateTableOperation" />.
    /// </summary>
    /// <param name="addForeignKeyOperation"> The operation to generate code for. </param>
    /// <param name="writer"> Text writer to add the generated code to. </param>
    protected virtual void GenerateInline(AddForeignKeyOperation addForeignKeyOperation, IndentedTextWriter writer)
    {
        // sourced from https://github.com/aspnet/EntityFramework6/blob/master/src/EntityFramework/Migrations/Design/VisualBasicMigrationCodeGenerator.cs
        Check.NotNull(addForeignKeyOperation, "addForeignKeyOperation");
        Check.NotNull(writer, "writer");

        writer.WriteLine(" _");
        writer.Write(".ForeignKey(" + Quote(addForeignKeyOperation.PrincipalTable) + ", ");
        Generate(addForeignKeyOperation.DependentColumns, writer);

        // Our Custom logic
        ApplyCustomFKName(addForeignKeyOperation);

        // Insert our custom name if provided
        if (!addForeignKeyOperation.HasDefaultName)
        {
            writer.Write(", name := " + Quote(addForeignKeyOperation.Name));
        }

        if (addForeignKeyOperation.CascadeDelete)
        {
            writer.Write(", cascadeDelete := True");
        }

        writer.Write(")");
    }
}

2. Register the Code Generator so that it is used by EF to generate the next migration

Locate Configuration.vb in your project, int the constructor set the CodeGenerator to an instance of your CustomVBMigrationCodeGenerator :

Public Sub New()

  AutomaticMigrationsEnabled = false
  CodeGenerator = new CustomVBMigrationCodeGenerator()

End Sub

Now execute the add-migration to generate a new migration you will see you new custom name defined in the migration script.

You may be required to similarly override the Generate(DropForeignKeyOperation...) methods as well if you need to downgrade from this configuration _or_ if alter table commands require the key to be dropped first.

Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
  • Yes, this is epic :-) FYI I really *really* like doing this in the generated migration, rather than in the generated SQL. Nice touch. (BTW no worries about VB/C#. Either one is fine with me. Your conversions look pretty good.) – InteXX Jan 06 '20 at 15:04
  • I *finally* got around to implementing this. It works great. Thank you so very much. I wish I could upvote twice. – InteXX Apr 15 '22 at 09:34