From 9452f473cd074ab48b7406135d09f21e5dda7eb0 Mon Sep 17 00:00:00 2001 From: Tim Holzherr Date: Wed, 20 Jul 2022 14:47:58 +0200 Subject: [PATCH] Introduce MongoDB.Extensions.Migration (#62) Add fist version of Mongo.Extensions.Migration for performing migrations with MongoDB --- samples/Migration/Customer.cs | 8 ++ samples/Migration/ExampleMigration.cs | 19 +++ samples/Migration/Migration.csproj | 14 ++ samples/Migration/Program.cs | 21 +++ samples/Migration/Repository.cs | 20 +++ .../Migration/appsettings.Development.json | 8 ++ samples/Migration/appsettings.json | 9 ++ samples/MongoDB.Extensions.Samples.sln | 6 + .../Integration/Scenario1/MigrateDownTests.cs | 88 ++++++++++++ .../Integration/Scenario1/MigrateUpTests.cs | 98 ++++++++++++++ .../Scenario1/TestEntityForDown.cs | 8 ++ .../Integration/Scenario1/TestEntityForUp.cs | 8 ++ .../Integration/Scenario2/MigrateDownTests.cs | 71 ++++++++++ .../Integration/Scenario2/MigrateUpTests.cs | 71 ++++++++++ .../Scenario2/TestEntityForDown.cs | 8 ++ .../Integration/Scenario2/TestEntityForUp.cs | 8 ++ .../Integration/SharedMongoDbCollection.cs | 9 ++ src/Migration.Tests/Migration.Tests.csproj | 14 ++ src/Migration.Tests/TestMigration1.cs | 19 +++ src/Migration.Tests/TestMigration2.cs | 19 +++ src/Migration.Tests/TestMigration3.cs | 19 +++ .../Unit/EntityOptionBuilderTests.cs | 59 +++++++++ .../Unit/MigrationOptionBuilderTests.cs | 23 ++++ .../Unit/MigrationRunnerTests.cs | 48 +++++++ src/Migration.Tests/Unit/TestEntity.cs | 8 ++ src/Migration.Tests/Unit/TestMigration.cs | 20 +++ src/Migration/Builders/EntityOptionBuilder.cs | 67 ++++++++++ .../Builders/MigrationOptionBuilder.cs | 33 +++++ src/Migration/Contracts/IMigration.cs | 10 ++ src/Migration/Contracts/IVersioned.cs | 6 + .../InvalidConfigurationException.cs | 11 ++ src/Migration/Migration.csproj | 17 +++ src/Migration/MigrationExtensions.cs | 26 ++++ src/Migration/MigrationRunner.cs | 125 ++++++++++++++++++ src/Migration/MigrationSerializer.cs | 44 ++++++ src/Migration/MigrationSerializerProvider.cs | 29 ++++ src/Migration/Models/EntityContext.cs | 5 + src/Migration/Models/EntityOption.cs | 9 ++ src/Migration/Models/MigrationContext.cs | 5 + src/Migration/Models/MigrationOption.cs | 5 + src/Migration/Readme.md | 68 ++++++++++ src/MongoDB.Extensions.sln | 16 ++- src/TestProject.props | 6 +- 43 files changed, 1180 insertions(+), 5 deletions(-) create mode 100644 samples/Migration/Customer.cs create mode 100644 samples/Migration/ExampleMigration.cs create mode 100644 samples/Migration/Migration.csproj create mode 100644 samples/Migration/Program.cs create mode 100644 samples/Migration/Repository.cs create mode 100644 samples/Migration/appsettings.Development.json create mode 100644 samples/Migration/appsettings.json create mode 100644 src/Migration.Tests/Integration/Scenario1/MigrateDownTests.cs create mode 100644 src/Migration.Tests/Integration/Scenario1/MigrateUpTests.cs create mode 100644 src/Migration.Tests/Integration/Scenario1/TestEntityForDown.cs create mode 100644 src/Migration.Tests/Integration/Scenario1/TestEntityForUp.cs create mode 100644 src/Migration.Tests/Integration/Scenario2/MigrateDownTests.cs create mode 100644 src/Migration.Tests/Integration/Scenario2/MigrateUpTests.cs create mode 100644 src/Migration.Tests/Integration/Scenario2/TestEntityForDown.cs create mode 100644 src/Migration.Tests/Integration/Scenario2/TestEntityForUp.cs create mode 100644 src/Migration.Tests/Integration/SharedMongoDbCollection.cs create mode 100644 src/Migration.Tests/Migration.Tests.csproj create mode 100644 src/Migration.Tests/TestMigration1.cs create mode 100644 src/Migration.Tests/TestMigration2.cs create mode 100644 src/Migration.Tests/TestMigration3.cs create mode 100644 src/Migration.Tests/Unit/EntityOptionBuilderTests.cs create mode 100644 src/Migration.Tests/Unit/MigrationOptionBuilderTests.cs create mode 100644 src/Migration.Tests/Unit/MigrationRunnerTests.cs create mode 100644 src/Migration.Tests/Unit/TestEntity.cs create mode 100644 src/Migration.Tests/Unit/TestMigration.cs create mode 100644 src/Migration/Builders/EntityOptionBuilder.cs create mode 100644 src/Migration/Builders/MigrationOptionBuilder.cs create mode 100644 src/Migration/Contracts/IMigration.cs create mode 100644 src/Migration/Contracts/IVersioned.cs create mode 100644 src/Migration/Exceptions/InvalidConfigurationException.cs create mode 100644 src/Migration/Migration.csproj create mode 100644 src/Migration/MigrationExtensions.cs create mode 100644 src/Migration/MigrationRunner.cs create mode 100644 src/Migration/MigrationSerializer.cs create mode 100644 src/Migration/MigrationSerializerProvider.cs create mode 100644 src/Migration/Models/EntityContext.cs create mode 100644 src/Migration/Models/EntityOption.cs create mode 100644 src/Migration/Models/MigrationContext.cs create mode 100644 src/Migration/Models/MigrationOption.cs create mode 100644 src/Migration/Readme.md diff --git a/samples/Migration/Customer.cs b/samples/Migration/Customer.cs new file mode 100644 index 0000000..1e97b2c --- /dev/null +++ b/samples/Migration/Customer.cs @@ -0,0 +1,8 @@ +using MongoDB.Extensions.Migration; + +namespace Migration; + +public record Customer(string Id, string Name) : IVersioned +{ + public int Version { get; set; } +} diff --git a/samples/Migration/ExampleMigration.cs b/samples/Migration/ExampleMigration.cs new file mode 100644 index 0000000..e83d1ff --- /dev/null +++ b/samples/Migration/ExampleMigration.cs @@ -0,0 +1,19 @@ +using MongoDB.Bson; +using MongoDB.Extensions.Migration; + +namespace Migration; + +public class ExampleMigration : IMigration +{ + public int Version => 1; + + public void Up(BsonDocument document) + { + document["Name"] += " Migrated up to 1"; + } + + public void Down(BsonDocument document) + { + document["Name"] += " Migrated down to 0"; + } +} diff --git a/samples/Migration/Migration.csproj b/samples/Migration/Migration.csproj new file mode 100644 index 0000000..aaf72e5 --- /dev/null +++ b/samples/Migration/Migration.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/samples/Migration/Program.cs b/samples/Migration/Program.cs new file mode 100644 index 0000000..5248c1e --- /dev/null +++ b/samples/Migration/Program.cs @@ -0,0 +1,21 @@ +using Migration; +using MongoDB.Extensions.Migration; +using MongoDB.Driver; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddSingleton(_ => new MongoClient("mongodb://localhost:27017")) + .AddTransient(); + +var app = builder.Build(); + +app.UseMongoMigration(m => m + .ForEntity(e => e + .AtVersion(1) + .WithMigration(new ExampleMigration()))); + +app.MapGet("/customer/{id}", (string id, Repository repo) => repo.GetAsync(id)); +app.MapPost("/customer/", (Customer customer, Repository repo) => repo.AddAsync(customer)); + +app.Run(); diff --git a/samples/Migration/Repository.cs b/samples/Migration/Repository.cs new file mode 100644 index 0000000..2a49441 --- /dev/null +++ b/samples/Migration/Repository.cs @@ -0,0 +1,20 @@ +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace Migration; + +public class Repository +{ + private readonly IMongoCollection _collection; + + public Repository(MongoClient client) + { + var database = client.GetDatabase("Example1"); + _collection = database.GetCollection("customer"); + } + + public Task AddAsync(Customer customer) => _collection.InsertOneAsync(customer); + + public Task GetAsync(string id) => _collection.AsQueryable() + .SingleOrDefaultAsync(c => c.Id == id); +} diff --git a/samples/Migration/appsettings.Development.json b/samples/Migration/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/Migration/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/Migration/appsettings.json b/samples/Migration/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/samples/Migration/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/MongoDB.Extensions.Samples.sln b/samples/MongoDB.Extensions.Samples.sln index 8843c47..e8832b1 100644 --- a/samples/MongoDB.Extensions.Samples.sln +++ b/samples/MongoDB.Extensions.Samples.sln @@ -21,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SimpleBlog", "SimpleBlog", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Host", "Context\Host\Host.csproj", "{0CCED088-DBB6-4DA2-8DFC-D9968EEBB9FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Migration", "Migration\Migration.csproj", "{8226313B-FAC9-4D0F-AEE8-424DD310BBFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,10 @@ Global {0CCED088-DBB6-4DA2-8DFC-D9968EEBB9FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {0CCED088-DBB6-4DA2-8DFC-D9968EEBB9FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {0CCED088-DBB6-4DA2-8DFC-D9968EEBB9FA}.Release|Any CPU.Build.0 = Release|Any CPU + {8226313B-FAC9-4D0F-AEE8-424DD310BBFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8226313B-FAC9-4D0F-AEE8-424DD310BBFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8226313B-FAC9-4D0F-AEE8-424DD310BBFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8226313B-FAC9-4D0F-AEE8-424DD310BBFB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Migration.Tests/Integration/Scenario1/MigrateDownTests.cs b/src/Migration.Tests/Integration/Scenario1/MigrateDownTests.cs new file mode 100644 index 0000000..6ca0860 --- /dev/null +++ b/src/Migration.Tests/Integration/Scenario1/MigrateDownTests.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Extensions.Migration; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Migration.Tests; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Squadron; +using Xunit; + +namespace MongoMigrationTest.Integration.Scenario1; + +[Collection("SharedMongoDbCollection")] +public class MigrateDownTests +{ + readonly IMongoCollection _typedCollection; + readonly IMongoCollection _untypedCollection; + + public MigrateDownTests(MongoResource resource) + { + RegisterMongoMigrations(); + IMongoDatabase database = resource.Client.GetDatabase("Scenario1-down"); + _typedCollection = database.GetCollection("TestEntityForDown"); + _untypedCollection = database.GetCollection("TestEntityForDown"); + } + + static void RegisterMongoMigrations() + { + MigrationOption options = new MigrationOptionBuilder() + .ForEntity(o => o.AtVersion(0) + .WithMigration(new TestMigration1()) + .WithMigration(new TestMigration2()) + .WithMigration(new TestMigration3())) + .Build(); + var context = new MigrationContext(options, NullLoggerFactory.Instance); + + BsonSerializer.RegisterSerializationProvider(new MigrationSerializerProvider(context)); + } + + [Fact] + public async Task Scenario1_AddRetrieve_NoMigration() + { + // Arrange + const string input = "Bar"; + await _typedCollection.InsertOneAsync(new TestEntityForDown("1", input)); + + // Act + TestEntityForDown result = await _typedCollection.AsQueryable() + .SingleOrDefaultAsync(c => c.Id == "1"); + + // Assert + result.Foo.Should().Be(input); + } + + [Fact] + public async Task Scenario1_RetrieveAtVersion3_MigratedDownTo0() + { + // Arrange + await _untypedCollection.InsertOneAsync(new BsonDocument(new Dictionary + { ["_id"] = "id0", ["Foo"] = "Bar", ["Version"] = 3 })); + + // Act + TestEntityForDown result = await _typedCollection.AsQueryable() + .SingleOrDefaultAsync(c => c.Id == "id0"); + + // Assert + result.Foo.Should().Be("Bar Migrated Down to 2 Migrated Down to 1 Migrated Down to 0"); + } + + [Fact] + public async Task Scenario1_RetrieveAtVersion2_MigratedToVersion3() + { + // Arrange + await _untypedCollection.InsertOneAsync(new BsonDocument(new Dictionary + { ["_id"] = "id1", ["Foo"] = "Bar", ["Version"] = 1 })); + + // Act + TestEntityForDown result = await _typedCollection.AsQueryable() + .SingleOrDefaultAsync(c => c.Id == "id1"); + + // Assert + result.Foo.Should().Be("Bar Migrated Down to 0"); + } + +} diff --git a/src/Migration.Tests/Integration/Scenario1/MigrateUpTests.cs b/src/Migration.Tests/Integration/Scenario1/MigrateUpTests.cs new file mode 100644 index 0000000..971f033 --- /dev/null +++ b/src/Migration.Tests/Integration/Scenario1/MigrateUpTests.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Extensions.Migration; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Migration.Tests; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using Xunit; +using MongoDB.Driver.Linq; +using Squadron; + +namespace MongoMigrationTest.Integration.Scenario1; + +[Collection("SharedMongoDbCollection")] +public class MigrateUpTests +{ + readonly IMongoCollection _typedCollection; + readonly IMongoCollection _untypedCollection; + + public MigrateUpTests(MongoResource resource) + { + RegisterMongoMigrations(); + IMongoDatabase database = resource.Client.GetDatabase("Scenario1-up"); + _typedCollection = database.GetCollection("TestEntityForUp"); + _untypedCollection = database.GetCollection("TestEntityForUp"); + } + + static void RegisterMongoMigrations() + { + MigrationOption options = new MigrationOptionBuilder() + .ForEntity(o => o + .WithMigration(new TestMigration1()) + .WithMigration(new TestMigration2()) + .WithMigration(new TestMigration3())) + .Build(); + var context = new MigrationContext(options, NullLoggerFactory.Instance); + + BsonSerializer.RegisterSerializationProvider(new MigrationSerializerProvider(context)); + } + + [Fact] + public async Task Scenario1_AddRetrieve_NoMigration() + { + // Arrange + const string input = "Bar"; + await _typedCollection.InsertOneAsync(new TestEntityForUp("1", input)); + + // Act + var result = await _typedCollection.AsQueryable().SingleOrDefaultAsync(c => c.Id == "1"); + + // Assert + result.Foo.Should().Be(input); + } + + [Fact] + public async Task Scenario1_RetrieveWithoutVersion_MigratedToNewestVersion() + { + // Arrange + await _untypedCollection.InsertOneAsync(new BsonDocument(new Dictionary + { ["_id"] = "2", ["Foo"] = "Bar" })); + + // Act + TestEntityForUp result = await _typedCollection.AsQueryable().SingleOrDefaultAsync(c => c.Id == "2"); + + // Assert + result.Foo.Should().Be("Bar Migrated Up to 1 Migrated Up to 2 Migrated Up to 3"); + } + + [Fact] + public async Task Scenario1_RetrieveAtNewUnknownVersion_NoMigration() + { + // Arrange + await _untypedCollection.InsertOneAsync(new BsonDocument(new Dictionary + { ["_id"] = "3", ["Foo"] = "Bar", ["Version"] = 4 })); + + // Act + TestEntityForUp result = await _typedCollection.AsQueryable().SingleOrDefaultAsync(c => c.Id == "3"); + + // Assert + result.Foo.Should().Be("Bar"); + } + + [Fact] + public async Task Scenario1_RetrieveAtVersion2_MigratedToVersion3() + { + // Arrange + await _untypedCollection.InsertOneAsync(new BsonDocument(new Dictionary + { ["_id"] = "4", ["Foo"] = "Bar", ["Version"] = 2 })); + + // Act + TestEntityForUp result = await _typedCollection.AsQueryable().SingleOrDefaultAsync(c => c.Id == "4"); + + // Assert + result.Foo.Should().Be("Bar Migrated Up to 3"); + } +} diff --git a/src/Migration.Tests/Integration/Scenario1/TestEntityForDown.cs b/src/Migration.Tests/Integration/Scenario1/TestEntityForDown.cs new file mode 100644 index 0000000..e416f0d --- /dev/null +++ b/src/Migration.Tests/Integration/Scenario1/TestEntityForDown.cs @@ -0,0 +1,8 @@ +using MongoDB.Extensions.Migration; + +namespace MongoMigrationTest.Integration.Scenario1; + +public record TestEntityForDown(string Id, string Foo) : IVersioned +{ + public int Version { get; set; } +} diff --git a/src/Migration.Tests/Integration/Scenario1/TestEntityForUp.cs b/src/Migration.Tests/Integration/Scenario1/TestEntityForUp.cs new file mode 100644 index 0000000..98c7f61 --- /dev/null +++ b/src/Migration.Tests/Integration/Scenario1/TestEntityForUp.cs @@ -0,0 +1,8 @@ +using MongoDB.Extensions.Migration; + +namespace MongoMigrationTest.Integration.Scenario1; + +public record TestEntityForUp(string Id, string Foo) : IVersioned +{ + public int Version { get; set; } +} diff --git a/src/Migration.Tests/Integration/Scenario2/MigrateDownTests.cs b/src/Migration.Tests/Integration/Scenario2/MigrateDownTests.cs new file mode 100644 index 0000000..2dac5a0 --- /dev/null +++ b/src/Migration.Tests/Integration/Scenario2/MigrateDownTests.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Extensions.Migration; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Migration.Tests; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Squadron; +using Xunit; + +namespace MongoMigrationTest.Integration.Scenario2; + +[Collection("SharedMongoDbCollection")] +public class MigrateDownTests +{ + readonly IMongoCollection _typedCollection; + readonly IMongoCollection _untypedCollection; + + public MigrateDownTests(MongoResource resource) + { + RegisterMongoMigrations(); + IMongoDatabase database = resource.Client.GetDatabase("Scenario2-down"); + _typedCollection = database.GetCollection("TestEntityForDown"); + _untypedCollection = database.GetCollection("TestEntityForDown"); + } + + static void RegisterMongoMigrations() + { + MigrationOption options = new MigrationOptionBuilder() + .ForEntity(o => o + .WithMigration(new TestMigration1()) + .WithMigration(new TestMigration2()) + .WithMigration(new TestMigration3()) + .AtVersion(2)) + .Build(); + var context = new MigrationContext(options, NullLoggerFactory.Instance); + + BsonSerializer.RegisterSerializationProvider(new MigrationSerializerProvider(context)); + } + + [Fact] + public async Task Scenario2_RetrieveAtNewUnknownVersion_MigrateDownTo2() + { + // Arrange + await _untypedCollection.InsertOneAsync(new BsonDocument(new Dictionary + { ["_id"] = "3", ["Foo"] = "Bar", ["Version"] = 4 })); + + // Act + TestEntityForDown result = await _typedCollection.AsQueryable().SingleOrDefaultAsync(c => c.Id == "3"); + + // Assert + result.Foo.Should().Be("Bar Migrated Down to 2"); + } + + [Fact] + public async Task Scenario2_RetrieveAtVersion3_MigratedToVersion2() + { + // Arrange + await _untypedCollection.InsertOneAsync(new BsonDocument(new Dictionary + { ["_id"] = "4", ["Foo"] = "Bar", ["Version"] = 3 })); + + // Act + TestEntityForDown result = await _typedCollection.AsQueryable().SingleOrDefaultAsync(c => c.Id == "4"); + + // Assert + result.Foo.Should().Be("Bar Migrated Down to 2"); + } +} diff --git a/src/Migration.Tests/Integration/Scenario2/MigrateUpTests.cs b/src/Migration.Tests/Integration/Scenario2/MigrateUpTests.cs new file mode 100644 index 0000000..eefa708 --- /dev/null +++ b/src/Migration.Tests/Integration/Scenario2/MigrateUpTests.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Extensions.Migration; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Migration.Tests; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using Xunit; +using MongoDB.Driver.Linq; +using Squadron; + +namespace MongoMigrationTest.Integration.Scenario2; + +[Collection("SharedMongoDbCollection")] +public class MigrateUpTests +{ + readonly IMongoCollection _typedCollection; + readonly IMongoCollection _untypedCollection; + + public MigrateUpTests(MongoResource resource) + { + RegisterMongoMigrations(); + IMongoDatabase database = resource.Client.GetDatabase("Scenario2-up"); + _typedCollection = database.GetCollection("TestEntityForUp"); + _untypedCollection = database.GetCollection("TestEntityForUp"); + } + + static void RegisterMongoMigrations() + { + MigrationOption options = new MigrationOptionBuilder() + .ForEntity(o => o + .WithMigration(new TestMigration1()) + .WithMigration(new TestMigration2()) + .WithMigration(new TestMigration3()) + .AtVersion(2)) + .Build(); + var context = new MigrationContext(options, NullLoggerFactory.Instance); + + BsonSerializer.RegisterSerializationProvider(new MigrationSerializerProvider(context)); + } + + [Fact] + public async Task Scenario2_AddRetrieve_NoMigration() + { + // Arrange + const string input = "Bar"; + await _typedCollection.InsertOneAsync(new TestEntityForUp("1", input)); + + // Act + var result = await _typedCollection.AsQueryable().SingleOrDefaultAsync(c => c.Id == "1"); + + // Assert + result.Foo.Should().Be(input); + } + + [Fact] + public async Task Scenario2_RetrieveWithoutVersion_MigratedToCurrentVersion() + { + // Arrange + await _untypedCollection.InsertOneAsync(new BsonDocument(new Dictionary + { ["_id"] = "2", ["Foo"] = "Bar" })); + + // Act + TestEntityForUp result = await _typedCollection.AsQueryable().SingleOrDefaultAsync(c => c.Id == "2"); + + // Assert + result.Foo.Should().Be("Bar Migrated Up to 1 Migrated Up to 2"); + } +} diff --git a/src/Migration.Tests/Integration/Scenario2/TestEntityForDown.cs b/src/Migration.Tests/Integration/Scenario2/TestEntityForDown.cs new file mode 100644 index 0000000..2317186 --- /dev/null +++ b/src/Migration.Tests/Integration/Scenario2/TestEntityForDown.cs @@ -0,0 +1,8 @@ +using MongoDB.Extensions.Migration; + +namespace MongoMigrationTest.Integration.Scenario2; + +public record TestEntityForDown(string Id, string Foo) : IVersioned +{ + public int Version { get; set; } +} diff --git a/src/Migration.Tests/Integration/Scenario2/TestEntityForUp.cs b/src/Migration.Tests/Integration/Scenario2/TestEntityForUp.cs new file mode 100644 index 0000000..00e5f68 --- /dev/null +++ b/src/Migration.Tests/Integration/Scenario2/TestEntityForUp.cs @@ -0,0 +1,8 @@ +using MongoDB.Extensions.Migration; + +namespace MongoMigrationTest.Integration.Scenario2; + +public record TestEntityForUp(string Id, string Foo) : IVersioned +{ + public int Version { get; set; } +} diff --git a/src/Migration.Tests/Integration/SharedMongoDbCollection.cs b/src/Migration.Tests/Integration/SharedMongoDbCollection.cs new file mode 100644 index 0000000..1661c06 --- /dev/null +++ b/src/Migration.Tests/Integration/SharedMongoDbCollection.cs @@ -0,0 +1,9 @@ +using Squadron; +using Xunit; + +namespace MongoMigrationTest.Integration; + +[CollectionDefinition("SharedMongoDbCollection")] +public class SharedMongoDbCollection : ICollectionFixture +{ +} \ No newline at end of file diff --git a/src/Migration.Tests/Migration.Tests.csproj b/src/Migration.Tests/Migration.Tests.csproj new file mode 100644 index 0000000..ed77be6 --- /dev/null +++ b/src/Migration.Tests/Migration.Tests.csproj @@ -0,0 +1,14 @@ + + + + + Migration.Tests + Migration.Tests + net6.0 + + + + + + + diff --git a/src/Migration.Tests/TestMigration1.cs b/src/Migration.Tests/TestMigration1.cs new file mode 100644 index 0000000..dc0f510 --- /dev/null +++ b/src/Migration.Tests/TestMigration1.cs @@ -0,0 +1,19 @@ +using MongoDB.Bson; +using MongoDB.Extensions.Migration; + +namespace Migration.Tests; + +public class TestMigration1 : IMigration +{ + public int Version { get; } = 1; + + public void Up(BsonDocument document) + { + document["Foo"] += " Migrated Up to 1"; + } + + public void Down(BsonDocument document) + { + document["Foo"] += " Migrated Down to 0"; + } +} diff --git a/src/Migration.Tests/TestMigration2.cs b/src/Migration.Tests/TestMigration2.cs new file mode 100644 index 0000000..6190a20 --- /dev/null +++ b/src/Migration.Tests/TestMigration2.cs @@ -0,0 +1,19 @@ +using MongoDB.Bson; +using MongoDB.Extensions.Migration; + +namespace Migration.Tests; + +public class TestMigration2 : IMigration +{ + public int Version { get; } = 2; + + public void Up(BsonDocument document) + { + document["Foo"] += " Migrated Up to 2"; + } + + public void Down(BsonDocument document) + { + document["Foo"] += " Migrated Down to 1"; + } +} diff --git a/src/Migration.Tests/TestMigration3.cs b/src/Migration.Tests/TestMigration3.cs new file mode 100644 index 0000000..3988c6d --- /dev/null +++ b/src/Migration.Tests/TestMigration3.cs @@ -0,0 +1,19 @@ +using MongoDB.Bson; +using MongoDB.Extensions.Migration; + +namespace Migration.Tests; + +public class TestMigration3 : IMigration +{ + public int Version { get; } = 3; + + public void Up(BsonDocument document) + { + document["Foo"] += " Migrated Up to 3"; + } + + public void Down(BsonDocument document) + { + document["Foo"] += " Migrated Down to 2"; + } +} diff --git a/src/Migration.Tests/Unit/EntityOptionBuilderTests.cs b/src/Migration.Tests/Unit/EntityOptionBuilderTests.cs new file mode 100644 index 0000000..2ef9989 --- /dev/null +++ b/src/Migration.Tests/Unit/EntityOptionBuilderTests.cs @@ -0,0 +1,59 @@ +using System; +using FluentAssertions; +using MongoDB.Extensions.Migration; +using Xunit; + +namespace Migration.Tests.Unit; + +public class EntityOptionBuilderTests +{ + [Fact] + public void EntityOptionBuilder_AtVersionNotSet_SetToLatestVersion() + { + // Act + EntityOption entityOption = new EntityOptionBuilder() + .WithMigration(new TestMigration1()) + .WithMigration(new TestMigration2()) + .Build(); + + // Assert + entityOption.CurrentVersion.Should().Be(2); + } + + [Fact] + public void EntityOptionBuilder_NoMigrationRegistered_Throws() + { + // Act + Action action = () => new EntityOptionBuilder().Build(); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void EntityOptionBuilder_GabInMigrationVersions_Throws() + { + // Act + Action action = () => new EntityOptionBuilder() + .WithMigration(new TestMigration3()) + .WithMigration(new TestMigration1()) + .Build(); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void EntityOptionBuilder_AtVersionHasNoMigrationMigrationVersions_Throws() + { + // Act + Action action = () => new EntityOptionBuilder() + .WithMigration(new TestMigration2()) + .WithMigration(new TestMigration1()) + .AtVersion(3) + .Build(); + + // Assert + action.Should().Throw(); + } +} diff --git a/src/Migration.Tests/Unit/MigrationOptionBuilderTests.cs b/src/Migration.Tests/Unit/MigrationOptionBuilderTests.cs new file mode 100644 index 0000000..be83ede --- /dev/null +++ b/src/Migration.Tests/Unit/MigrationOptionBuilderTests.cs @@ -0,0 +1,23 @@ +using System; +using FluentAssertions; +using MongoDB.Extensions.Migration; +using Xunit; + +namespace Migration.Tests.Unit; + +public class MigrationOptionBuilderTests +{ + [Fact] + public void MigrationOptionBuilder_RegisterEntityTwice_Throws() + { + // Arrange + var builder = new MigrationOptionBuilder(); + builder.ForEntity(b => b.WithMigration(new TestMigration())); + + // Act + Action action = () => builder.ForEntity(b => b.WithMigration(new TestMigration())); + + // Assert + action.Should().Throw(); + } +} diff --git a/src/Migration.Tests/Unit/MigrationRunnerTests.cs b/src/Migration.Tests/Unit/MigrationRunnerTests.cs new file mode 100644 index 0000000..23da257 --- /dev/null +++ b/src/Migration.Tests/Unit/MigrationRunnerTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using MongoDB.Bson; +using MongoDB.Extensions.Migration; +using Xunit; + +namespace Migration.Tests.Unit; + +public class MigrationRunnerTests +{ + [Fact] + public void MigrationRunner_MigrationUpThrows_Catches() + { + // Arrange + var runner = new MigrationRunner(new EntityContext( + new EntityOptionBuilder().WithMigration(new TestMigration()) + .Build(), + new NullLoggerFactory())); + var document = new Dictionary { ["Version"] = 1, ["_id"] = 1 }; + + // Act + Action action = () => runner.Run(new BsonDocument(document)); + + // Assert + action.Should().NotThrow(); + } + + [Fact] + public void MigrationRunner_MigrationDownThrows_Catches() + { + // Arrange + var runner = new MigrationRunner(new EntityContext( + new EntityOptionBuilder().WithMigration(new TestMigration()).AtVersion(0) + .Build(), + new NullLoggerFactory())); + var document = new Dictionary { ["Version"] = 1, ["_id"] = 1 }; + + // Act + Action action = () => runner.Run(new BsonDocument(document)); + + + // Assert + action.Should().NotThrow(); + } + +} diff --git a/src/Migration.Tests/Unit/TestEntity.cs b/src/Migration.Tests/Unit/TestEntity.cs new file mode 100644 index 0000000..5a490a9 --- /dev/null +++ b/src/Migration.Tests/Unit/TestEntity.cs @@ -0,0 +1,8 @@ +using MongoDB.Extensions.Migration; + +namespace Migration.Tests.Unit; + +record TestEntity(int Id) : IVersioned +{ + public int Version { get; set; } +} diff --git a/src/Migration.Tests/Unit/TestMigration.cs b/src/Migration.Tests/Unit/TestMigration.cs new file mode 100644 index 0000000..05a9e91 --- /dev/null +++ b/src/Migration.Tests/Unit/TestMigration.cs @@ -0,0 +1,20 @@ +using System; +using MongoDB.Bson; +using MongoDB.Extensions.Migration; + +namespace Migration.Tests.Unit; + +public class TestMigration : IMigration +{ + public int Version { get; } = 1; + + public void Up(BsonDocument document) + { + throw new Exception(); + } + + public void Down(BsonDocument document) + { + throw new Exception(); + } +} diff --git a/src/Migration/Builders/EntityOptionBuilder.cs b/src/Migration/Builders/EntityOptionBuilder.cs new file mode 100644 index 0000000..62c24d1 --- /dev/null +++ b/src/Migration/Builders/EntityOptionBuilder.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; + +namespace MongoDB.Extensions.Migration; + +public class EntityOptionBuilder where T : IVersioned +{ + private int? _atVersion; + private readonly List _migrations = new(); + + /// + /// Set the current Version of the data which is suitable for the application. + /// If not set the newest Version is used. + /// + public EntityOptionBuilder AtVersion(int atVersion) + { + _atVersion = atVersion; + return this; + } + + /// + /// Register a migration for an entity. The versions of the migrations must start at 1 and be + /// continuously incremented without a gap + /// + public EntityOptionBuilder WithMigration(IMigration migration) + { + _migrations.Add(migration); + return this; + } + + /// + /// Builds the EntityOption + /// + public EntityOption Build() + { + if (!_migrations.Any()) + { + throw new InvalidConfigurationException( + $"There must be at least one migration registered for entity {typeof(T).Name}"); + } + + _migrations.Sort((x, y) => x.Version.CompareTo(y.Version)); + + _atVersion ??= _migrations.Last().Version; + + if (_atVersion != _migrations.First().Version - 1 && + _migrations.All(m => !Equals(m.Version, _atVersion))) + { + throw new InvalidConfigurationException( + $"There is no migration for version {_atVersion} for entity {typeof(T).Name}"); + } + + for (var i = 1; i < _migrations.Count; i++) + { + if (_migrations[i - 1].Version + 1 != _migrations[i].Version) + { + throw new InvalidConfigurationException( + $"{typeof(T).Name}: Migration Versions must be continuously incremented!"); + } + } + + return new EntityOption( + typeof(T), + _atVersion.Value, + _migrations); + } +} diff --git a/src/Migration/Builders/MigrationOptionBuilder.cs b/src/Migration/Builders/MigrationOptionBuilder.cs new file mode 100644 index 0000000..a1d9f3e --- /dev/null +++ b/src/Migration/Builders/MigrationOptionBuilder.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MongoDB.Extensions.Migration; + +public class MigrationOptionBuilder +{ + private readonly List _entityOptions = new(); + + /// + /// Builds the MigrationOption + /// + public MigrationOption Build() + { + return new MigrationOption(_entityOptions); + } + + /// + /// Register a migration for a given entity. + /// + public MigrationOptionBuilder ForEntity( + Func, EntityOptionBuilder> builderAction) where T : IVersioned + { + if (_entityOptions.Any(e => e.Type == typeof(T))) + { + throw new InvalidConfigurationException( + $"Migrations for entity of type {typeof(T).FullName} have already been registered"); + } + _entityOptions.Add(builderAction(new EntityOptionBuilder()).Build()); + return this; + } +} diff --git a/src/Migration/Contracts/IMigration.cs b/src/Migration/Contracts/IMigration.cs new file mode 100644 index 0000000..65760b6 --- /dev/null +++ b/src/Migration/Contracts/IMigration.cs @@ -0,0 +1,10 @@ +using MongoDB.Bson; + +namespace MongoDB.Extensions.Migration; + +public interface IMigration +{ + int Version { get; } + void Up(BsonDocument document); + void Down(BsonDocument document); +} diff --git a/src/Migration/Contracts/IVersioned.cs b/src/Migration/Contracts/IVersioned.cs new file mode 100644 index 0000000..669dc03 --- /dev/null +++ b/src/Migration/Contracts/IVersioned.cs @@ -0,0 +1,6 @@ +namespace MongoDB.Extensions.Migration; + +public interface IVersioned +{ + int Version { get; set; } +} \ No newline at end of file diff --git a/src/Migration/Exceptions/InvalidConfigurationException.cs b/src/Migration/Exceptions/InvalidConfigurationException.cs new file mode 100644 index 0000000..d4cdccc --- /dev/null +++ b/src/Migration/Exceptions/InvalidConfigurationException.cs @@ -0,0 +1,11 @@ +using System; + +namespace MongoDB.Extensions.Migration; + +public class InvalidConfigurationException : Exception +{ + public InvalidConfigurationException() { } + public InvalidConfigurationException(string message) : base(message) { } + public InvalidConfigurationException(string message, Exception innerException) : + base(message, innerException) { } +} diff --git a/src/Migration/Migration.csproj b/src/Migration/Migration.csproj new file mode 100644 index 0000000..d980e7a --- /dev/null +++ b/src/Migration/Migration.csproj @@ -0,0 +1,17 @@ + + + + + MongoDB.Extensions.Migration + MongoDB.Extensions.Migration + MongoDB.Extensions.Migration + net6.0 + + + + + + + + + diff --git a/src/Migration/MigrationExtensions.cs b/src/Migration/MigrationExtensions.cs new file mode 100644 index 0000000..e39cf47 --- /dev/null +++ b/src/Migration/MigrationExtensions.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson.Serialization; +using Microsoft.Extensions.Logging; + +namespace MongoDB.Extensions.Migration; + +public static class MigrationExtensions +{ + public static IApplicationBuilder UseMongoMigration( + this IApplicationBuilder app, + Func builderAction) + { + var builder = new MigrationOptionBuilder(); + MigrationOption options = builderAction(builder).Build(); + + MigrationContext context = new( + options, + app.ApplicationServices.GetRequiredService()); + + BsonSerializer.RegisterSerializationProvider(new MigrationSerializerProvider(context)); + + return app; + } +} diff --git a/src/Migration/MigrationRunner.cs b/src/Migration/MigrationRunner.cs new file mode 100644 index 0000000..8e26c51 --- /dev/null +++ b/src/Migration/MigrationRunner.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace MongoDB.Extensions.Migration; + +public class MigrationRunner +{ + private readonly ILogger> _logger; + private const string Version = "Version"; + private const string Id = "_id"; + private readonly Dictionary _migrationRegistry; + private readonly int _currentVersionOfApplication; + private readonly int _lastKey; + private readonly int _beforeFirstKey; + + public MigrationRunner(EntityContext context) + { + _currentVersionOfApplication = context.Option.CurrentVersion; + _migrationRegistry = context.Option.Migrations.ToDictionary(m => m.Version, m => m); + _logger = context.LoggerFactory.CreateLogger>(); + _lastKey = _migrationRegistry.Keys.Last(); + _beforeFirstKey = _migrationRegistry.Keys.First() - 1; + } + + public void Run(BsonDocument document) + { + var fromVersion = FindCurrentVersionOfDocument(document); + + MigrateUp(document, fromVersion, _currentVersionOfApplication); + MigrateDown(document, fromVersion, _currentVersionOfApplication); + } + + private int FindCurrentVersionOfDocument(BsonDocument document) + { + // Document is from before Migrations have been introduced + if (!(document.Contains(Version) && document[Version].IsInt32)) + { + return _beforeFirstKey; + } + + var fromVersion = document[Version].AsInt32; + if (_migrationRegistry.ContainsKey(fromVersion)) + { + return fromVersion; + } + + // Document is newer than any migration we know + if (fromVersion > _lastKey) + { + return _lastKey; + } + + // Document is older than any migration we know + return _beforeFirstKey; + + } + + private void MigrateUp( + BsonDocument document, + int fromVersion, + int toVersion) + { + for (var version = fromVersion + 1; version <= toVersion; version++) + { + try + { + IMigration migration = _migrationRegistry[version]; + migration.Up(document); + document.Set(Version, version); + _logger.LogDebug( + "Successfully Migrated {entity} with id {id} to version {version}", + typeof(T).Name, + GetIdOrEmpty(document), + version); + } + catch (Exception e) + { + _logger.LogError(e, + "Migration of {entity} with id {id} to version {version} failed", + typeof(T).Name, + GetIdOrEmpty(document), + version); + break; + } + } + } + + private void MigrateDown( + BsonDocument document, + int fromVersion, + int toVersion) + { + for (var version = fromVersion; version > toVersion; version--) + { + try + { + IMigration migration = _migrationRegistry[version]; + migration.Down(document); + document.Set(Version, version); + _logger.LogDebug( + "Successfully Migrated {entity} with id {id} to version {version}", + typeof(T).Name, + GetIdOrEmpty(document), + version); + } + catch (Exception e) + { + _logger.LogError(e, + "Migration of {entity} with id {id} to version {version} failed", + typeof(T).Name, + GetIdOrEmpty(document), + version); + break; + } + } + } + + private static string GetIdOrEmpty(BsonDocument document) + { + return document.Contains(Id) ? document[Id].ToString() ?? string.Empty : string.Empty; + } +} diff --git a/src/Migration/MigrationSerializer.cs b/src/Migration/MigrationSerializer.cs new file mode 100644 index 0000000..74e2913 --- /dev/null +++ b/src/Migration/MigrationSerializer.cs @@ -0,0 +1,44 @@ +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace MongoDB.Extensions.Migration; + +class MigrationSerializer : BsonClassMapSerializer where T : IVersioned +{ + private readonly EntityContext _context; + private readonly MigrationRunner _migrationRunner; + + public MigrationSerializer(EntityContext context) : base(BsonClassMap.LookupClassMap(typeof(T))) + { + _context = context; + _migrationRunner = new MigrationRunner(context); + } + + + public override void Serialize( + BsonSerializationContext context, + BsonSerializationArgs args, + T value) + { + value.Version = _context.Option.CurrentVersion; + base.Serialize(context, args, value); + } + + public override T Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + BsonDocument bsonDocument = BsonDocumentSerializer.Instance.Deserialize(context); + + _migrationRunner.Run(bsonDocument); + + var migratedContext = + BsonDeserializationContext.CreateRoot(new BsonDocumentReader(bsonDocument)); + + T entity = base.Deserialize(migratedContext, args); + + entity.Version = _context.Option.CurrentVersion; + + return entity; + } +} diff --git a/src/Migration/MigrationSerializerProvider.cs b/src/Migration/MigrationSerializerProvider.cs new file mode 100644 index 0000000..d286e87 --- /dev/null +++ b/src/Migration/MigrationSerializerProvider.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using MongoDB.Bson.Serialization; + +namespace MongoDB.Extensions.Migration; + +public class MigrationSerializerProvider : IBsonSerializationProvider +{ + private readonly MigrationContext _context; + + public MigrationSerializerProvider(MigrationContext context) + { + _context = context; + } + + public IBsonSerializer? GetSerializer(Type type) + { + EntityOption? option = _context.Option.EntityOptions.SingleOrDefault(e => e.Type == type); + if (option is null) + { + return null; + } + + EntityContext entityContext = new(option, _context.LoggerFactory); + Type migrationSerializerDefinition = typeof(MigrationSerializer<>); + Type migrationSerializerType = migrationSerializerDefinition.MakeGenericType(type); + return (IBsonSerializer?)Activator.CreateInstance(migrationSerializerType, entityContext); + } +} diff --git a/src/Migration/Models/EntityContext.cs b/src/Migration/Models/EntityContext.cs new file mode 100644 index 0000000..9f5e6cc --- /dev/null +++ b/src/Migration/Models/EntityContext.cs @@ -0,0 +1,5 @@ +using Microsoft.Extensions.Logging; + +namespace MongoDB.Extensions.Migration; + +public record EntityContext(EntityOption Option, ILoggerFactory LoggerFactory); diff --git a/src/Migration/Models/EntityOption.cs b/src/Migration/Models/EntityOption.cs new file mode 100644 index 0000000..4a0c363 --- /dev/null +++ b/src/Migration/Models/EntityOption.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; + +namespace MongoDB.Extensions.Migration; + +public record EntityOption( + Type Type, + int CurrentVersion, + List Migrations); diff --git a/src/Migration/Models/MigrationContext.cs b/src/Migration/Models/MigrationContext.cs new file mode 100644 index 0000000..7ff444d --- /dev/null +++ b/src/Migration/Models/MigrationContext.cs @@ -0,0 +1,5 @@ +using Microsoft.Extensions.Logging; + +namespace MongoDB.Extensions.Migration; + +public record MigrationContext(MigrationOption Option, ILoggerFactory LoggerFactory); diff --git a/src/Migration/Models/MigrationOption.cs b/src/Migration/Models/MigrationOption.cs new file mode 100644 index 0000000..d7f94c7 --- /dev/null +++ b/src/Migration/Models/MigrationOption.cs @@ -0,0 +1,5 @@ +using System.Collections.Generic; + +namespace MongoDB.Extensions.Migration; + +public record MigrationOption(List EntityOptions); diff --git a/src/Migration/Readme.md b/src/Migration/Readme.md new file mode 100644 index 0000000..b3d3af1 --- /dev/null +++ b/src/Migration/Readme.md @@ -0,0 +1,68 @@ +# MongoDB.Extensions.Migration + +MongoDB.Extensions.Migration is a library which supports writing migrations for MongoDB in c#. +Simple changes in the data model can often be accommodated by using data annotations from the MongoDB c# driver like `[BsonDefaultValue(...)]` but for more complicated changes the ability to write migrations is helpful. + +## Concept + +Traditionally in relational databases, migration have been applied to all documents at once and caused downtime. +With document databases which have a schema on read not on write we can do better. +Introducing a version field on each document and having migration logic which performs the needed migrations when reading a document allows for a downtime free migrations. +This pattern allows for multiple versions of an application to use the same database, for example in the case of a rolling update. + +## Getting Started + +1. Add MongoDB.Extensions.Migration to your project using `dotnet add package MongoDB.Extensions.Migration` +2. Make sure that your your domain entities for which you want to write a migration implement the interface IVersioned +3. Write a migration by creating a new class which implements IMigration, see below. +4. Regsiter your migrations with the `UseMongoMigration` extension method on the `IApplicationBuilder`. Either in Program.cs or in the Configure method of Startup.cs. + +```csharp +... +var app = builder.Build(); + +app.UseMongoMigration(m => m + .ForEntity(e => e + .AtVersion(1) + .WithMigration(new ExampleMigration()))); + + +public record Customer : IVersioned { + public int Version { get; set; } + public Guid Id {get; set;} + public string FirstName {get; set;} + public string LastName {get; set;} +} + +public class ExampleMigration : IMigration {...} +``` + +Entities for which a migration is needed must implement IVersioned and Migrations must implement IMigration. +The optional method AtVersion allows setting the version of the data the application currently supports. +This allows for scenarios where a migration down to a specific version is needed. + +### Writing Migrations + +Each migration has a version. +The versions of all registered migrations must be continuously incrementing without a gap. +We recommend to never change a already deployed migration. + +The up and down method of a migration act directly on the BsonDocument and allows making the needed changes to get from one version to another. +This example shows how a typo in a field name is fixed. + +```csharp +public class ExampleMigration : IMigration +{ + public int Version => 1; + + public void Up(BsonDocument document) + { + document["Name"] = document["Namee"]; + } + + public void Down(BsonDocument document) + { + document["Namee"] = document["Name"]; + } +} +``` diff --git a/src/MongoDB.Extensions.sln b/src/MongoDB.Extensions.sln index 6c46c20..168d8e3 100644 --- a/src/MongoDB.Extensions.sln +++ b/src/MongoDB.Extensions.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29503.13 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32421.90 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Context", "Context\Context.csproj", "{57EA6D91-FB1A-4E68-82B7-A9AA58A400BE}" EndProject @@ -21,6 +21,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Transactions.Tests", "Trans EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Context.InterferingTests", "Context.InterferingTests\Context.InterferingTests.csproj", "{6655AD69-6FD7-4AE2-BF09-53152911D7BE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migration", "Migration\Migration.csproj", "{696254EA-91B9-4809-9D7F-EDD731C74622}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migration.Tests", "Migration.Tests\Migration.Tests.csproj", "{A7D17D8A-99BC-40AC-92B4-996790F7F26E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +67,14 @@ Global {6655AD69-6FD7-4AE2-BF09-53152911D7BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {6655AD69-6FD7-4AE2-BF09-53152911D7BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {6655AD69-6FD7-4AE2-BF09-53152911D7BE}.Release|Any CPU.Build.0 = Release|Any CPU + {696254EA-91B9-4809-9D7F-EDD731C74622}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {696254EA-91B9-4809-9D7F-EDD731C74622}.Debug|Any CPU.Build.0 = Debug|Any CPU + {696254EA-91B9-4809-9D7F-EDD731C74622}.Release|Any CPU.ActiveCfg = Release|Any CPU + {696254EA-91B9-4809-9D7F-EDD731C74622}.Release|Any CPU.Build.0 = Release|Any CPU + {A7D17D8A-99BC-40AC-92B4-996790F7F26E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7D17D8A-99BC-40AC-92B4-996790F7F26E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7D17D8A-99BC-40AC-92B4-996790F7F26E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7D17D8A-99BC-40AC-92B4-996790F7F26E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/TestProject.props b/src/TestProject.props index 612ba0e..257f5d3 100644 --- a/src/TestProject.props +++ b/src/TestProject.props @@ -1,17 +1,17 @@ - $(TestProjectTargetFrameworks) + $(TestProjectTargetFrameworks) false 10.0 enable - + - +