diff --git a/NDB.Test.Api/NDB.Test.Api.csproj b/NDB.Test.Api/NDB.Test.Api.csproj index e913ef3..38f3053 100644 --- a/NDB.Test.Api/NDB.Test.Api.csproj +++ b/NDB.Test.Api/NDB.Test.Api.csproj @@ -4,6 +4,10 @@ net5.0 + + + + @@ -15,4 +19,22 @@ + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + diff --git a/NDB.Test.Api/Scripts/3.3.3/01.Test.sql b/NDB.Test.Api/Scripts/3.3.3/01.Test.sql new file mode 100644 index 0000000..9f62ad7 --- /dev/null +++ b/NDB.Test.Api/Scripts/3.3.3/01.Test.sql @@ -0,0 +1 @@ +select 'Test script!' \ No newline at end of file diff --git a/NDB.Test.Api/Scripts/3.3.4/01.Test-new-ver.sql b/NDB.Test.Api/Scripts/3.3.4/01.Test-new-ver.sql new file mode 100644 index 0000000..9f62ad7 --- /dev/null +++ b/NDB.Test.Api/Scripts/3.3.4/01.Test-new-ver.sql @@ -0,0 +1 @@ +select 'Test script!' \ No newline at end of file diff --git a/NDB.Test.Api/Scripts/3.3.4/02.Test-new-ver-2.sql b/NDB.Test.Api/Scripts/3.3.4/02.Test-new-ver-2.sql new file mode 100644 index 0000000..9f62ad7 --- /dev/null +++ b/NDB.Test.Api/Scripts/3.3.4/02.Test-new-ver-2.sql @@ -0,0 +1 @@ +select 'Test script!' \ No newline at end of file diff --git a/NDB.Test.Api/Scripts/4.0.0/01.Major changes.sql b/NDB.Test.Api/Scripts/4.0.0/01.Major changes.sql new file mode 100644 index 0000000..9f62ad7 --- /dev/null +++ b/NDB.Test.Api/Scripts/4.0.0/01.Major changes.sql @@ -0,0 +1 @@ +select 'Test script!' \ No newline at end of file diff --git a/NDB.Test.Api/Scripts/4.0.0/02.Last script.sql b/NDB.Test.Api/Scripts/4.0.0/02.Last script.sql new file mode 100644 index 0000000..9f62ad7 --- /dev/null +++ b/NDB.Test.Api/Scripts/4.0.0/02.Last script.sql @@ -0,0 +1 @@ +select 'Test script!' \ No newline at end of file diff --git a/NDB.Test.Api/Startup.cs b/NDB.Test.Api/Startup.cs index 7351f3d..9946f5e 100644 --- a/NDB.Test.Api/Startup.cs +++ b/NDB.Test.Api/Startup.cs @@ -3,10 +3,10 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.OpenApi.Models; using NDB.Extensions.Swagger; using NDB.Extensions.Swagger.Constants; using NDB.Infrastructure.DatabaseMigration; +using NDB.Infrastructure.DatabaseMigration.Constants; using NDB.Test.Api.Extensions; namespace NDB.Test.Api @@ -29,7 +29,7 @@ namespace NDB.Test.Api services.AddControllers(); services.AddSwagger("NDB.Test.Api", AuthorizationType.InhouseIdentity); - services.AddMigration(); + services.AddMigration(DatabaseType.SQLite, MetadataLocation.Database); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/NDB.Test.Api/appsettings.json b/NDB.Test.Api/appsettings.json index c2c97f5..49bb0c0 100644 --- a/NDB.Test.Api/appsettings.json +++ b/NDB.Test.Api/appsettings.json @@ -1,7 +1,8 @@ { "ConnectionStrings": { - "DatabaseConnection": "Data Source={Workspace}\\TesterDb.db" //"DatabaseConnection": "***REMOVED***" + //"DatabaseConnection": "***REMOVED***" + "DatabaseConnection": "Data Source={Workspace}\\NDB_TESTER.db" }, "Logging": { "LogLevel": { diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Constants/ManifestResources.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Constants/ManifestResources.cs new file mode 100644 index 0000000..ab22360 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Constants/ManifestResources.cs @@ -0,0 +1,16 @@ +namespace NDB.Infrastructure.DatabaseMigration.Constants +{ + internal struct ManifestResourcesPath + { + public const string + SqlServer = "NDB.Infrastructure.DatabaseMigration.Scripts.SqlServer.", + Sqlite = "NDB.Infrastructure.DatabaseMigration.Scripts.Sqlite."; + } + + internal struct ManifestResources + { + public static string[] + SqlServer = new string[] { "01.CreateMigrationSchema.sql", "02.MigrationTables.sql" }, + Sqlite = new string[] { "01.MigrationSignatureTable.sql", "02.MigratedVersionTable.sql", "03.MigratedScriptTable.sql" }; + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Constants/MetadataLocation.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Constants/MetadataLocation.cs new file mode 100644 index 0000000..33b5843 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Constants/MetadataLocation.cs @@ -0,0 +1,8 @@ +namespace NDB.Infrastructure.DatabaseMigration.Constants +{ + public enum MetadataLocation + { + XmlFile, + Database + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/DbContexts/MigrationDbContext.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/DbContexts/MigrationDbContext.cs index 7407ece..766bcd9 100644 --- a/infrastructure/NDB.Infrastructure.DatabaseMigration/DbContexts/MigrationDbContext.cs +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/DbContexts/MigrationDbContext.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using NDB.Infrastructure.DatabaseMigration.Entities; +using NDB.Infrastructure.DatabaseMigration.Entities.Configurations; namespace NDB.Infrastructure.DatabaseMigration.DbContexts { @@ -8,9 +10,15 @@ namespace NDB.Infrastructure.DatabaseMigration.DbContexts { } + public DbSet MigrationSignatures { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); + + modelBuilder.ApplyConfiguration(new MigratedScriptConfiguration()); + modelBuilder.ApplyConfiguration(new MigratedVersionConfiguration()); + modelBuilder.ApplyConfiguration(new MigrationSignatureConfiguration()); } } } diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/DependencyInjectionExtensions.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/DependencyInjectionExtensions.cs index ee89c47..2831485 100644 --- a/infrastructure/NDB.Infrastructure.DatabaseMigration/DependencyInjectionExtensions.cs +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/DependencyInjectionExtensions.cs @@ -7,6 +7,7 @@ using NDB.Infrastructure.DatabaseMigration.DbContexts; using NDB.Infrastructure.DatabaseMigration.Models; using NDB.Infrastructure.DatabaseMigration.Repositories; using NDB.Infrastructure.DatabaseMigration.Services; +using NDB.Infrastructure.DatabaseMigration.Services.Abstractions; using System; namespace NDB.Infrastructure.DatabaseMigration @@ -17,13 +18,16 @@ namespace NDB.Infrastructure.DatabaseMigration public static void AddMigration(this IServiceCollection services, DatabaseType databaseType = DatabaseType.SQLite, + MetadataLocation metadataLocation = MetadataLocation.XmlFile, string connectionName = "DatabaseConnection", string workspace = "Workspace", string scriptsDirectoryPath = "Scripts") { - var serviceConfiguration = new ServiceConfiguration(databaseType, connectionName, workspace, scriptsDirectoryPath); + var serviceConfiguration = new ServiceConfiguration(databaseType, metadataLocation, connectionName, workspace, scriptsDirectoryPath); services.AddSingleton(serviceConfiguration); services.AddDataAccess(serviceConfiguration); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); } diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/Configurations/MigratedScriptConfiguration.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/Configurations/MigratedScriptConfiguration.cs new file mode 100644 index 0000000..2f39818 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/Configurations/MigratedScriptConfiguration.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace NDB.Infrastructure.DatabaseMigration.Entities.Configurations +{ + internal class MigratedScriptConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("MigratedScript", "migration").HasKey(z => z.Id); + builder.Property(z => z.Id).ValueGeneratedOnAdd(); + } + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/Configurations/MigratedVersionConfiguration.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/Configurations/MigratedVersionConfiguration.cs new file mode 100644 index 0000000..81d46ba --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/Configurations/MigratedVersionConfiguration.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace NDB.Infrastructure.DatabaseMigration.Entities.Configurations +{ + internal class MigratedVersionConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("MigratedVersion", "migration").HasKey(z => z.Id); + builder.Property(z => z.Id).ValueGeneratedOnAdd(); + builder.HasMany(z => z.Scripts).WithOne().HasForeignKey(z => z.VersionId); + } + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/Configurations/MigrationSignatureConfiguration.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/Configurations/MigrationSignatureConfiguration.cs new file mode 100644 index 0000000..dbf9a1e --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/Configurations/MigrationSignatureConfiguration.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace NDB.Infrastructure.DatabaseMigration.Entities.Configurations +{ + internal class MigrationSignatureConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("MigrationSignature", "migration").HasKey(z => z.Id); + builder.Property(z => z.Id).ValueGeneratedOnAdd(); + builder.HasMany(z => z.MigratedVersions).WithOne().HasForeignKey(z => z.SignatureId); + } + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/MigratedScript.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/MigratedScript.cs new file mode 100644 index 0000000..2f9ea93 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/MigratedScript.cs @@ -0,0 +1,9 @@ +namespace NDB.Infrastructure.DatabaseMigration.Entities +{ + internal class MigratedScript + { + public int Id { get; set; } + public int VersionId { get; set; } + public string Script { get; set; } + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/MigratedVersion.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/MigratedVersion.cs new file mode 100644 index 0000000..35c016a --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/MigratedVersion.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace NDB.Infrastructure.DatabaseMigration.Entities +{ + internal class MigratedVersion + { + public int Id { get; set; } + public int SignatureId { get; set; } + public string Version { get; set; } + + public ICollection Scripts { get; set; } + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/MigrationSignature.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/MigrationSignature.cs new file mode 100644 index 0000000..4f7471f --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Entities/MigrationSignature.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace NDB.Infrastructure.DatabaseMigration.Entities +{ + internal class MigrationSignature + { + public int Id { get; set; } + public DateTime MigrationDate { get; set; } + public string MachineName { get; set; } + public string LastVersion { get; set; } + + public ICollection MigratedVersions { get; set; } + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Extensions/Mappings.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Extensions/Mappings.cs new file mode 100644 index 0000000..30f2aae --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Extensions/Mappings.cs @@ -0,0 +1,49 @@ +using System.Linq; +using e = NDB.Infrastructure.DatabaseMigration.Entities; +using m = NDB.Infrastructure.DatabaseMigration.Models; + +namespace NDB.Infrastructure.DatabaseMigration.Extensions +{ + internal static class Mappings + { + public static m.MigratedVersion ToModel(this e.MigratedVersion migratedVersion) + { + return new m.MigratedVersion() + { + Version = migratedVersion.Version, + Scripts = migratedVersion.Scripts.Select(z => z.Script).ToArray() + }; + } + + public static m.MigrationSignature ToModel(this e.MigrationSignature migrationSignature) + { + return new m.MigrationSignature() + { + MigrationDate = migrationSignature.MigrationDate, + MachineName = migrationSignature.MachineName, + LastVersion = migrationSignature.LastVersion, + MigratedVersions = migrationSignature.MigratedVersions.Select(z => z.ToModel()).ToArray() + }; + } + + public static e.MigratedVersion ToEntity(this m.MigratedVersion migratedVersion) + { + return new e.MigratedVersion() + { + Version = migratedVersion.Version, + Scripts = migratedVersion.Scripts.Select(z => new e.MigratedScript() { Script = z }).ToArray() + }; + } + + public static e.MigrationSignature ToEntity(this m.MigrationSignature migrationSignature) + { + return new e.MigrationSignature() + { + MigrationDate = migrationSignature.MigrationDate, + MachineName = migrationSignature.MachineName, + LastVersion = migrationSignature.LastVersion, + MigratedVersions = migrationSignature.MigratedVersions.Select(z => z.ToEntity()).ToArray() + }; + } + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Items/Examples/MigrationSignatures.xml b/infrastructure/NDB.Infrastructure.DatabaseMigration/Items/Examples/MigrationSignatures.xml new file mode 100644 index 0000000..3385fa5 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Items/Examples/MigrationSignatures.xml @@ -0,0 +1,29 @@ + + + + + 2022-02-10T20:20:42.2487339+02:00 + TS1926 + + + 1.0.0 + + 01.UserStatus table.sql + 02.AppUser table.sql + 03.IDX_AppUser_Email_NOTNULL.sql + + + + 1.0.1 + + 01.UserClaim table.sql + 02.UserToken table.sql + 03.Add admin user.sql + 04.Add my user.sql + + + + 1.0.1 + + + \ No newline at end of file diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Models/ServiceConfiguration.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Models/ServiceConfiguration.cs index 48a47c6..fdb6d73 100644 --- a/infrastructure/NDB.Infrastructure.DatabaseMigration/Models/ServiceConfiguration.cs +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Models/ServiceConfiguration.cs @@ -5,13 +5,15 @@ namespace NDB.Infrastructure.DatabaseMigration.Models internal class ServiceConfiguration { public DatabaseType DatabaseType { get; } + public MetadataLocation MetadataLocation { get; } public string ConnectionName { get; } public string Workspace { get; } public string ScriptsDirectory { get; } - public ServiceConfiguration(DatabaseType databaseType, string connectionName, string workspace, string scriptsDirectory) + public ServiceConfiguration(DatabaseType databaseType, MetadataLocation metadataLocation, string connectionName, string workspace, string scriptsDirectory) { DatabaseType = databaseType; + MetadataLocation = metadataLocation; ConnectionName = connectionName; Workspace = workspace; ScriptsDirectory = scriptsDirectory; diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/NDB.Infrastructure.DatabaseMigration.csproj b/infrastructure/NDB.Infrastructure.DatabaseMigration/NDB.Infrastructure.DatabaseMigration.csproj index bd4ee51..8b37f46 100644 --- a/infrastructure/NDB.Infrastructure.DatabaseMigration/NDB.Infrastructure.DatabaseMigration.csproj +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/NDB.Infrastructure.DatabaseMigration.csproj @@ -10,6 +10,22 @@ 1.0.2 + + + + + + + + + + + + + + + + diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Repositories/IMigrationRepository.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Repositories/IMigrationRepository.cs index 76ef0df..6de68bd 100644 --- a/infrastructure/NDB.Infrastructure.DatabaseMigration/Repositories/IMigrationRepository.cs +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Repositories/IMigrationRepository.cs @@ -1,9 +1,14 @@ -using System.Threading.Tasks; +using NDB.Infrastructure.DatabaseMigration.Constants; +using NDB.Infrastructure.DatabaseMigration.Entities; +using System.Threading.Tasks; namespace NDB.Infrastructure.DatabaseMigration.Repositories { - public interface IMigrationRepository + internal interface IMigrationRepository { Task ExecuteSqlRaw(string sqlRaw); + Task MigrationTablesAreSet(DatabaseType databaseType); + Task GetLastMigrationSignature(); + Task AddMigrationSignature(MigrationSignature migrationSignature); } } diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Repositories/MigrationRepository.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Repositories/MigrationRepository.cs index 97aa65c..329b48b 100644 --- a/infrastructure/NDB.Infrastructure.DatabaseMigration/Repositories/MigrationRepository.cs +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Repositories/MigrationRepository.cs @@ -1,5 +1,9 @@ using Microsoft.EntityFrameworkCore; +using NDB.Infrastructure.DatabaseMigration.Constants; using NDB.Infrastructure.DatabaseMigration.DbContexts; +using NDB.Infrastructure.DatabaseMigration.Entities; +using System; +using System.Linq; using System.Threading.Tasks; namespace NDB.Infrastructure.DatabaseMigration.Repositories @@ -17,5 +21,40 @@ namespace NDB.Infrastructure.DatabaseMigration.Repositories { await _dbContext.Database.ExecuteSqlRawAsync(sqlRaw); } + + public async Task MigrationTablesAreSet(DatabaseType databaseType) + { + var query = databaseType switch + { + DatabaseType.SQLServer => "select count(1) from sys.objects where name = 'MigrationSignature' and type = 'U' and SCHEMA_NAME(schema_id)='migration'", + DatabaseType.SQLite => "select count(1) from sqlite_master where type='table' and name='MigrationSignature';", + _ => throw new NotImplementedException($"DatabaseMigration type {databaseType} is not implemented"), + }; + + using (var command = _dbContext.Database.GetDbConnection().CreateCommand()) + { + command.CommandText = query; + await _dbContext.Database.OpenConnectionAsync(); + var result = await command.ExecuteScalarAsync(); + + return result != null && result != DBNull.Value && Convert.ToInt32(result) > 0; + } + } + + public Task GetLastMigrationSignature() + { + var query = _dbContext.MigrationSignatures + .Include(z => z.MigratedVersions).ThenInclude(z => z.Scripts) + .OrderByDescending(z => z.MigrationDate) + .AsSplitQuery(); + + return query.FirstOrDefaultAsync(); + } + + public async Task AddMigrationSignature(MigrationSignature migrationSignature) + { + await _dbContext.MigrationSignatures.AddAsync(migrationSignature); + await _dbContext.SaveChangesAsync(); + } } } diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/SqlServer/01.CreateMigrationSchema.sql b/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/SqlServer/01.CreateMigrationSchema.sql new file mode 100644 index 0000000..93ecca7 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/SqlServer/01.CreateMigrationSchema.sql @@ -0,0 +1,4 @@ +if not exists (select top 1 1 from sys.schemas where name = 'migration') +begin + EXEC ('CREATE SCHEMA [migration] AUTHORIZATION [dbo]') +end \ No newline at end of file diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/SqlServer/02.MigrationTables.sql b/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/SqlServer/02.MigrationTables.sql new file mode 100644 index 0000000..5a76d52 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/SqlServer/02.MigrationTables.sql @@ -0,0 +1,30 @@ +if not exists (select top 1 1 from sys.objects where name = 'MigrationSignature' and type = 'U' and SCHEMA_NAME(schema_id) = 'migration') +begin + create table migration.MigrationSignature + ( + Id int identity(1, 1) constraint PK_MigrationSignature primary key, + MigrationDate Datetime not null, + MachineName varchar(30) not null, + LastVersion varchar(20) not null + ) +end + +if not exists (select top 1 1 from sys.objects where name = 'MigratedVersion' and type = 'U' and SCHEMA_NAME(schema_id) = 'migration') +begin + create table migration.MigratedVersion + ( + Id int identity(1, 1) constraint PK_MigratedVersion primary key, + SignatureId int constraint FK_MigratedVersion_MigrationSignature foreign key references migration.MigrationSignature(Id), + [Version] varchar(20) not null + ) +end + +if not exists (select top 1 1 from sys.objects where name = 'MigratedScript' and type = 'U' and SCHEMA_NAME(schema_id) = 'migration') +begin + create table migration.MigratedScript + ( + Id int identity(1, 1) constraint PK_MigratedScript primary key, + VersionId int constraint FK_MigratedScript_MigratedVersion foreign key references migration.MigratedVersion(Id), + Script varchar(250) not null + ) +end \ No newline at end of file diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/Sqlite/01.MigrationSignatureTable.sql b/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/Sqlite/01.MigrationSignatureTable.sql new file mode 100644 index 0000000..fa68826 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/Sqlite/01.MigrationSignatureTable.sql @@ -0,0 +1,7 @@ +CREATE TABLE "MigrationSignature" ( + "Id" INTEGER NOT NULL, + "MigrationDate" TEXT NOT NULL, + "MachineName" TEXT NOT NULL, + "LastVersion" TEXT NOT NULL, + CONSTRAINT "PK_MigrationSignature" PRIMARY KEY("Id" AUTOINCREMENT) +); \ No newline at end of file diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/Sqlite/02.MigratedVersionTable.sql b/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/Sqlite/02.MigratedVersionTable.sql new file mode 100644 index 0000000..e799cd7 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/Sqlite/02.MigratedVersionTable.sql @@ -0,0 +1,7 @@ +CREATE TABLE "MigratedVersion" ( + "Id" INTEGER NOT NULL, + "SignatureId" INTEGER, + "Version" TEXT NOT NULL, + CONSTRAINT "PK_MigratedVersion" PRIMARY KEY("Id" AUTOINCREMENT), + CONSTRAINT "FK_MigratedVersion_MigrationSignature" FOREIGN KEY("SignatureId") REFERENCES "MigrationSignature"("Id") +); \ No newline at end of file diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/Sqlite/03.MigratedScriptTable.sql b/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/Sqlite/03.MigratedScriptTable.sql new file mode 100644 index 0000000..3dd59e8 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Scripts/Sqlite/03.MigratedScriptTable.sql @@ -0,0 +1,7 @@ +CREATE TABLE "MigratedScript" ( + "Id" INTEGER NOT NULL, + "VersionId" INTEGER, + "Script" TEXT NOT NULL, + CONSTRAINT "PK_MigratedScript" PRIMARY KEY("Id" AUTOINCREMENT), + CONSTRAINT "FK_MigratedScript_MigratedVersion" FOREIGN KEY("VersionId") REFERENCES "MigratedVersion"("Id") +); \ No newline at end of file diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/Abstractions/IMetadataLocationService.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/Abstractions/IMetadataLocationService.cs new file mode 100644 index 0000000..f3ac067 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/Abstractions/IMetadataLocationService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace NDB.Infrastructure.DatabaseMigration.Services.Abstractions +{ + internal interface IMetadataLocationService + { + Task Check(); + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/IMigrationService.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/Abstractions/IMigrationService.cs similarity index 54% rename from infrastructure/NDB.Infrastructure.DatabaseMigration/Services/IMigrationService.cs rename to infrastructure/NDB.Infrastructure.DatabaseMigration/Services/Abstractions/IMigrationService.cs index 0c69a61..463875a 100644 --- a/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/IMigrationService.cs +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/Abstractions/IMigrationService.cs @@ -1,4 +1,4 @@ -namespace NDB.Infrastructure.DatabaseMigration.Services +namespace NDB.Infrastructure.DatabaseMigration.Services.Abstractions { public interface IMigrationService { diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/Abstractions/IMigrationSignaturesService.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/Abstractions/IMigrationSignaturesService.cs new file mode 100644 index 0000000..7868b5e --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/Abstractions/IMigrationSignaturesService.cs @@ -0,0 +1,11 @@ +using NDB.Infrastructure.DatabaseMigration.Models; +using System.Threading.Tasks; + +namespace NDB.Infrastructure.DatabaseMigration.Services.Abstractions +{ + internal interface IMigrationSignaturesService + { + Task GetLastMigrationSignature(); + Task SaveMigrationSignature(MigrationSignature migrationSignature); + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/MetadataLocationService.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/MetadataLocationService.cs new file mode 100644 index 0000000..13012f3 --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/MetadataLocationService.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NDB.Infrastructure.DatabaseMigration.Constants; +using NDB.Infrastructure.DatabaseMigration.Models; +using NDB.Infrastructure.DatabaseMigration.Repositories; +using NDB.Infrastructure.DatabaseMigration.Services.Abstractions; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace NDB.Infrastructure.DatabaseMigration.Services +{ + internal class MetadataLocationService : IMetadataLocationService + { + private readonly ServiceConfiguration _configuration; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public MetadataLocationService(ServiceConfiguration configuration, IServiceProvider serviceProvider, ILogger logger) + { + _configuration=configuration; + _serviceProvider=serviceProvider; + _logger=logger; + } + + public async Task Check() + { + switch (_configuration.MetadataLocation) + { + case MetadataLocation.XmlFile: + CheckWorkspace(); + break; + case MetadataLocation.Database: + await CheckMigrationTables(); + break; + default: + throw new NotImplementedException($"Metadata location {_configuration.MetadataLocation} is not implemented."); + } + } + + private void CheckWorkspace() + { + if (string.IsNullOrEmpty(_configuration.Workspace)) + throw new Exception($"Workspace path is empty! Check 'Workspace' parameter."); + + if (!Directory.Exists(_configuration.Workspace)) + Directory.CreateDirectory(_configuration.Workspace); + } + + private async Task CheckMigrationTables() + { + using (var scope = _serviceProvider.CreateScope()) + { + var _repository = scope.ServiceProvider.GetRequiredService(); + var migrationTablesAreSet = await _repository.MigrationTablesAreSet(_configuration.DatabaseType); + if (migrationTablesAreSet) + return; + + var assembly = Assembly.GetExecutingAssembly(); + var allEmbeddedResources = assembly.GetManifestResourceNames(); + + var sqlResources = GetSqlResources(); + foreach (var resource in sqlResources) + { + if (!allEmbeddedResources.Contains(resource)) + { + _logger.LogWarning($"Manifest resource {resource} does not exists in assembly {assembly.FullName} and will be ignored."); + continue; + } + + var resourceContent = GetManifestResourceContent(assembly, resource); + await _repository.ExecuteSqlRaw(resourceContent); + } + } + } + + private string[] GetSqlResources() + { + var result = _configuration.DatabaseType switch + { + DatabaseType.SQLServer => ManifestResources.SqlServer.Select(resource => $"{ManifestResourcesPath.SqlServer}{resource}"), + DatabaseType.SQLite => ManifestResources.Sqlite.Select(resource => $"{ManifestResourcesPath.Sqlite}{resource}"), + _ => throw new NotImplementedException($"DatabaseMigration type {_configuration.DatabaseType} is not implemented"), + }; + return result.ToArray(); + } + + private string GetManifestResourceContent(Assembly assembly, string resourceName) + { + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + using (StreamReader reader = new StreamReader(stream)) + { + string result = reader.ReadToEnd(); + return result; + } + } + } +} diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/MigrationService.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/MigrationService.cs index 4efd680..de09dea 100644 --- a/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/MigrationService.cs +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/MigrationService.cs @@ -2,73 +2,42 @@ using Microsoft.Extensions.Logging; using NDB.Infrastructure.DatabaseMigration.Models; using NDB.Infrastructure.DatabaseMigration.Repositories; +using NDB.Infrastructure.DatabaseMigration.Services.Abstractions; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using System.Xml; -using System.Xml.Serialization; namespace NDB.Infrastructure.DatabaseMigration.Services { internal class MigrationService : IMigrationService { - private readonly string _migrationSignaturesFilePath; - private const string _migrationSignaturesFileName = "MigrationSignatures.xml"; private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly ServiceConfiguration _configuration; + private readonly IMetadataLocationService _metadataLocationService; + private readonly IMigrationSignaturesService _migrationSignaturesService; - public MigrationService(ILogger logger, IServiceProvider serviceProvider, ServiceConfiguration configuration) + public MigrationService(ILogger logger, IServiceProvider serviceProvider, ServiceConfiguration configuration, IMetadataLocationService metadataLocationService, IMigrationSignaturesService migrationSignaturesService) { - _migrationSignaturesFilePath = Path.Combine(configuration.Workspace, _migrationSignaturesFileName); _logger = logger; _serviceProvider = serviceProvider; _configuration = configuration; + _metadataLocationService = metadataLocationService; + _migrationSignaturesService = migrationSignaturesService; } - private void CheckWorkspace() - { - if (string.IsNullOrEmpty(_configuration.Workspace)) - throw new Exception($"Workspace path is empty! Check 'Workspace' parameter."); + public void Execute() => ExecuteAsync().GetAwaiter().GetResult(); - if (!Directory.Exists(_configuration.Workspace)) - Directory.CreateDirectory(_configuration.Workspace); - } - - private MigrationSignature[] GetMigrationSignatures() - { - if (!File.Exists(_migrationSignaturesFilePath)) - return null; - - var serializer = new XmlSerializer(typeof(MigrationThumbprint)); - using (var reader = XmlReader.Create(_migrationSignaturesFilePath)) - { - var migrationSignatureRoot = (MigrationThumbprint)serializer.Deserialize(reader); - return migrationSignatureRoot.MigrationSignatures; - } - } - - private void SaveMigrationSignatures(MigrationSignature[] migrationSignatures) - { - var root = new MigrationThumbprint() { MigrationSignatures = migrationSignatures }; - var serializer = new XmlSerializer(root.GetType()); - var settings = new XmlWriterSettings() { Indent = true }; - using (var writer = XmlWriter.Create(_migrationSignaturesFilePath, settings)) - { - serializer.Serialize(writer, root); - } - } - - public void Execute() + private async Task ExecuteAsync() { _logger.LogInformation("Starting migration..."); - CheckWorkspace(); + await _metadataLocationService.Check(); - var localSignatures = GetMigrationSignatures(); - var lastInstalledVersion = localSignatures?.OrderByDescending(z => z.MigrationDate).FirstOrDefault()?.LastVersion ?? "0.0.0"; + var lastSignature = await _migrationSignaturesService.GetLastMigrationSignature(); + var lastInstalledVersion = lastSignature?.LastVersion ?? "0.0.0"; var targetVersion = new Version(lastInstalledVersion); var scriptPacks = GetScriptPacks(); var packsToInstall = scriptPacks.Where(p => p.Version > targetVersion); @@ -90,7 +59,9 @@ namespace NDB.Infrastructure.DatabaseMigration.Services _logger.LogInformation($"Running script pack: '{pack.Version}'"); - Array.ForEach(scripts, s => RunScript(s)); + foreach (var script in scripts) + await RunScript(script); + var migratedVersion = new MigratedVersion() { Version = pack.Version.ToString(), Scripts = scripts.Select(z => Path.GetFileName(z)).ToArray() }; migratedVersions.Add(migratedVersion); } @@ -98,10 +69,7 @@ namespace NDB.Infrastructure.DatabaseMigration.Services signature.MigratedVersions = migratedVersions.ToArray(); signature.LastVersion = packsToInstall.OrderByDescending(z => z.Version).First().Version.ToString(); - var signatures = localSignatures != null ? new List(localSignatures) : new List(); - signatures.Add(signature); - - SaveMigrationSignatures(signatures.ToArray()); + await _migrationSignaturesService.SaveMigrationSignature(signature); } private ScriptPack[] GetScriptPacks() @@ -112,10 +80,7 @@ namespace NDB.Infrastructure.DatabaseMigration.Services return packs.ToArray(); } - private void RunScript(string path) - => RunScriptAsync(path).GetAwaiter().GetResult(); - - private async Task RunScriptAsync(string path) + private async Task RunScript(string path) { _logger.LogInformation($"Running sql script: '{path}'"); var sqlContent = File.ReadAllText(path); diff --git a/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/MigrationSignaturesService.cs b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/MigrationSignaturesService.cs new file mode 100644 index 0000000..85a17ab --- /dev/null +++ b/infrastructure/NDB.Infrastructure.DatabaseMigration/Services/MigrationSignaturesService.cs @@ -0,0 +1,125 @@ +using Microsoft.Extensions.DependencyInjection; +using NDB.Infrastructure.DatabaseMigration.Constants; +using NDB.Infrastructure.DatabaseMigration.Extensions; +using NDB.Infrastructure.DatabaseMigration.Models; +using NDB.Infrastructure.DatabaseMigration.Repositories; +using NDB.Infrastructure.DatabaseMigration.Services.Abstractions; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; + +namespace NDB.Infrastructure.DatabaseMigration.Services +{ + internal class MigrationSignaturesService : IMigrationSignaturesService + { + private const string _migrationSignaturesFileName = "MigrationSignatures.xml"; + private readonly string _migrationSignaturesFilePath; + private readonly ServiceConfiguration _configuration; + private readonly IServiceProvider _serviceProvider; + + private MigrationThumbprint Thumbprint; + + public MigrationSignaturesService(ServiceConfiguration configuration, IServiceProvider serviceProvider) + { + _migrationSignaturesFilePath = Path.Combine(configuration.Workspace, _migrationSignaturesFileName); + _configuration = configuration; + _serviceProvider = serviceProvider; + } + + public async Task GetLastMigrationSignature() + { + switch (_configuration.MetadataLocation) + { + case MetadataLocation.XmlFile: + return GetLastMigrationSignatureFromFile(); + case MetadataLocation.Database: + return await GetLastMigrationSignatureFromDatabase(); + default: + throw new NotImplementedException($"Metadata location {_configuration.MetadataLocation} is not implemented."); + } + } + + public async Task SaveMigrationSignature(MigrationSignature migrationSignature) + { + switch (_configuration.MetadataLocation) + { + case MetadataLocation.XmlFile: + SaveMigrationSignatureToFile(migrationSignature); + break; + case MetadataLocation.Database: + await SaveMigrationSignatureToDatabase(migrationSignature); + break; + default: + throw new NotImplementedException($"Metadata location {_configuration.MetadataLocation} is not implemented."); + } + } + + private MigrationSignature GetLastMigrationSignatureFromFile() + { + Thumbprint = GetMigrationThumbprintFromFile(); + var lastSignature = Thumbprint?.MigrationSignatures?.OrderByDescending(z => z.MigrationDate).FirstOrDefault(); + return lastSignature; + } + + private MigrationThumbprint GetMigrationThumbprintFromFile() + { + if (!File.Exists(_migrationSignaturesFilePath)) + return null; + + var serializer = new XmlSerializer(typeof(MigrationThumbprint)); + using (var reader = XmlReader.Create(_migrationSignaturesFilePath)) + { + var migrationSignatureRoot = (MigrationThumbprint)serializer.Deserialize(reader); + return migrationSignatureRoot; + } + } + + private void SaveMigrationSignatureToFile(MigrationSignature migrationSignature) + { + if (Thumbprint != null) + { + var migrationSignatures = new List(Thumbprint.MigrationSignatures) { migrationSignature }; + Thumbprint.MigrationSignatures = migrationSignatures.ToArray(); + } + else + { + Thumbprint = new MigrationThumbprint() + { + MigrationSignatures = new List() { migrationSignature }.ToArray() + }; + } + + var serializer = new XmlSerializer(Thumbprint.GetType()); + var settings = new XmlWriterSettings() { Indent = true }; + using (var writer = XmlWriter.Create(_migrationSignaturesFilePath, settings)) + { + serializer.Serialize(writer, Thumbprint); + } + } + + private async Task GetLastMigrationSignatureFromDatabase() + { + using (var scope = _serviceProvider.CreateScope()) + { + var _repository = scope.ServiceProvider.GetRequiredService(); + var lastMigrationSignature = await _repository.GetLastMigrationSignature(); + if (lastMigrationSignature == null) + return null; + return lastMigrationSignature.ToModel(); + } + } + + private async Task SaveMigrationSignatureToDatabase(MigrationSignature migrationSignature) + { + using (var scope = _serviceProvider.CreateScope()) + { + var _repository = scope.ServiceProvider.GetRequiredService(); + await _repository.AddMigrationSignature(migrationSignature.ToEntity()); + } + } + } +}