Using multiple FluentMigrator assemblies on same database
Asked Answered
T

3

8

Let's say I have two independent tables in the same database, tables Book and Cup. I create both with just primary keys (int), called Id. Then, to keep things tidy, I separate them into different projects and create FluentMigrations for both, which will reside in Book.Migrations.dll and Cup.Migrations.dll.

Now I realize that maybe my book should be able to have a name, and create a new migration to add a column called name. I set the version for this to be 201408111220 (so the timestamp as of writing this), and call it AddNameToBook. I apply this migration and the database is updated accordingly.

Then I realize that maybe a Cup should have a color. So I create a new migration, in the other project, with a version 201408111221 and call it AddColorToCup. Again I run the migration, and the database is updated.

As far as I know, so far everything should work just fine. What I am unsure about, is if I now add another migration to Book, say 201408111224, apply it, and then try to rollback. Since now the version 201408111221 from the other assembly exists in the VersionInfo -table, how will FluentMigrator handle this? Will it throw an error in my face, or just ignore the row since the current assembly doesn't know anything about it?

Also other comments regarding usage of FluentMigrator this way (with multiple assemblies for one database) are welcome.

Tropophilous answered 11/8, 2014 at 9:28 Comment(0)
H
7

I wouldn't recommend this approach. When rolling back or migrating up, the migration versions saved in the VersionInfo table will not match the migration versions in the FluentMigrator assembly. With a bit of bad luck, this will crash.

If you want to have two separate projects, then you need two VersionInfo tables. One of the projects can use the default VersionInfo table but the other project will have to create a custom one. See here on how to create custom meta data for the VersionInfo table.

For FluentMigrator this is the same as having two databases. This approach is common when combined with schemas. In your example, you would create a Book schema and a Cup schema in the database and keep them separate from each other.

In my experience, most databases are not big enough to justify this extra overhead but if you have hundreds or thousands of migrations then you should absolutely divide it up into different projects (and divide your database into different schemas).

Hygroscope answered 24/8, 2014 at 19:55 Comment(1)
this should be the answerTramway
D
5

I wrote a migration loader to help me do this

public class MultiAssemblyMigrationLoader : IMigrationInformationLoader
{
    public MultiAssemblyMigrationLoader(IMigrationConventions conventions, IEnumerable<Assembly> assemblies, string @namespace, IEnumerable<string> tagsToMatch)
        : this(conventions, assemblies, @namespace, false, tagsToMatch)
    {
    }

    public MultiAssemblyMigrationLoader(IMigrationConventions conventions, IEnumerable<Assembly> assemblies, string @namespace, bool loadNestedNamespaces, IEnumerable<string> tagsToMatch)
    {
        this.Conventions = conventions;
        this.Assemblies = assemblies;
        this.Namespace = @namespace;
        this.LoadNestedNamespaces = loadNestedNamespaces;
        this.TagsToMatch = tagsToMatch ?? new string[0];
    }

    public IMigrationConventions Conventions { get; private set; }

    public IEnumerable<Assembly> Assemblies { get; private set; }

    public string Namespace { get; private set; }

    public bool LoadNestedNamespaces { get; private set; }

    public IEnumerable<string> TagsToMatch { get; private set; }

    public SortedList<long, IMigrationInfo> LoadMigrations()
    {
        var sortedList = new SortedList<long, IMigrationInfo>();

        IEnumerable<IMigration> migrations = this.FindMigrations();
        if (migrations == null) return sortedList;

        foreach (IMigration migration in migrations)
        {
            IMigrationInfo migrationInfo = this.Conventions.GetMigrationInfo(migration);
            if (sortedList.ContainsKey(migrationInfo.Version))
                throw new DuplicateMigrationException(string.Format("Duplicate migration version {0}.", migrationInfo.Version));
            sortedList.Add(migrationInfo.Version, migrationInfo);
        }
        return sortedList;
    }

    private IEnumerable<IMigration> FindMigrations()
    {
        IEnumerable<Type> types = new Type[] { };
        foreach (var assembly in Assemblies)
        {
            types = types.Concat(assembly.GetExportedTypes());
        }

        IEnumerable<Type> source = types.Where(t =>
                                                    {
                                                        if (!Conventions.TypeIsMigration(t))
                                                            return false;
                                                        if (!Conventions.TypeHasMatchingTags(t, this.TagsToMatch))
                                                            return !Conventions.TypeHasTags(t);
                                                        return true;
                                                    });
        if (!string.IsNullOrEmpty(Namespace))
        {
            Func<Type, bool> predicate = t => t.Namespace == Namespace;
            if (LoadNestedNamespaces)
            {
                string matchNested = Namespace + ".";
                predicate = t =>
                                {
                                    if (t.Namespace != Namespace)
                                        return t.Namespace.StartsWith(matchNested);
                                    return true;
                                };
            }
            source = source.Where(predicate);
        }
        return source.Select(matchedType => (IMigration)matchedType.Assembly.CreateInstance(matchedType.FullName));
    }

}

to use this class you just need to hook it up to your MigrationRunner

var runner = new MigrationRunner(mainAssembly, runnerContext, processor);
runner.MigrationLoader = new MultiAssemblyMigrationLoader(runner.Conventions, assemblies, runnerContext.Namespace, runnerContext.NestedNamespaces, runnerContext.Tags);
runner.MigrateUp(true);
Dogbane answered 18/2, 2015 at 3:26 Comment(0)
K
0

You must look at @DanielLee's answer above it contains all the needed information to that correctly, but that answer lacks a code example.
Basically what he is saying is that we need to customize VersionInfo tables in each assembly separately.

here is how it should be done in the code

using FluentMigrator.Runner.Conventions;
using FluentMigrator.Runner.Initialization;
using FluentMigrator.Runner.VersionTableInfo;
using Microsoft.Extensions.Options;

[VersionTableMetaData]
public class SmsVersionTable : DefaultVersionTableMetaData
{
    public SmsVersionTable(IConventionSet conventionSet, IOptions<RunnerOptions> runnerOptions) :
        base(conventionSet, runnerOptions)
    {
    }

    public override string SchemaName => "YourSchemaName";
    public override string TableName => "YourTableInfoName";
}

public static class VersionTableExtensions
{
    public static void AddTableMetaData(this IServiceCollection services)
    {
        services.AddScoped<IVersionTableMetaData, SmsVersionTable>();
    }
}

here is the documentation for more details

Kneedeep answered 4/5 at 5:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.