NDB.Infrastructure.DatabaseMigration
parent
a06a924f05
commit
5da47050d4
9
NDB.sln
9
NDB.sln
|
@ -56,7 +56,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDB.Extensions.Caching", "N
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CCEE458E-02A8-42FD-8F5F-A35481A23303}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CCEE458E-02A8-42FD-8F5F-A35481A23303}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NDB.Test.Api", "NDB.Test.Api\NDB.Test.Api.csproj", "{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDB.Test.Api", "NDB.Test.Api\NDB.Test.Api.csproj", "{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NDB.Infrastructure.DatabaseMigration", "infrastructure\NDB.Infrastructure.DatabaseMigration\NDB.Infrastructure.DatabaseMigration.csproj", "{74C7BE02-DD5C-49C2-8E88-E3AEA729E2AB}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
@ -104,6 +106,10 @@ Global
|
||||||
{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{74C7BE02-DD5C-49C2-8E88-E3AEA729E2AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{74C7BE02-DD5C-49C2-8E88-E3AEA729E2AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{74C7BE02-DD5C-49C2-8E88-E3AEA729E2AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{74C7BE02-DD5C-49C2-8E88-E3AEA729E2AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -131,6 +137,7 @@ Global
|
||||||
{3E045EE6-A290-467C-B503-3A6CB0065C97} = {A206A484-3ACF-4032-8B36-AC93A72B0B88}
|
{3E045EE6-A290-467C-B503-3A6CB0065C97} = {A206A484-3ACF-4032-8B36-AC93A72B0B88}
|
||||||
{CCEE458E-02A8-42FD-8F5F-A35481A23303} = {E0202271-4E92-4DB8-900D-B5FD745B9278}
|
{CCEE458E-02A8-42FD-8F5F-A35481A23303} = {E0202271-4E92-4DB8-900D-B5FD745B9278}
|
||||||
{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01} = {CCEE458E-02A8-42FD-8F5F-A35481A23303}
|
{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01} = {CCEE458E-02A8-42FD-8F5F-A35481A23303}
|
||||||
|
{74C7BE02-DD5C-49C2-8E88-E3AEA729E2AB} = {1C1D634E-06CC-4707-9564-E31A76F27D9E}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {87541BAB-3FAC-4ADB-A7FB-8228DA87843D}
|
SolutionGuid = {87541BAB-3FAC-4ADB-A7FB-8228DA87843D}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace NDB.Infrastructure.DatabaseMigration.Constants
|
||||||
|
{
|
||||||
|
public enum DatabaseType
|
||||||
|
{
|
||||||
|
SQLite,
|
||||||
|
SQLServer
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace NDB.Infrastructure.DatabaseMigration.DbContexts
|
||||||
|
{
|
||||||
|
internal class MigrationDbContext : DbContext
|
||||||
|
{
|
||||||
|
public MigrationDbContext(DbContextOptions<MigrationDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NDB.Infrastructure.DatabaseMigration.Constants;
|
||||||
|
using NDB.Infrastructure.DatabaseMigration.DbContexts;
|
||||||
|
using NDB.Infrastructure.DatabaseMigration.Models;
|
||||||
|
using NDB.Infrastructure.DatabaseMigration.Repositories;
|
||||||
|
using NDB.Infrastructure.DatabaseMigration.Services;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NDB.Infrastructure.DatabaseMigration
|
||||||
|
{
|
||||||
|
public static class DependencyInjectionExtensions
|
||||||
|
{
|
||||||
|
private const string _workspacePlaceholder = "{Workspace}";
|
||||||
|
|
||||||
|
public static void AddMigration(this IServiceCollection services,
|
||||||
|
DatabaseType databaseType = DatabaseType.SQLite,
|
||||||
|
string connectionName = "DatabaseConnection",
|
||||||
|
string workspacePath = "Workspace",
|
||||||
|
string scriptsDirectoryPath = "Scripts")
|
||||||
|
{
|
||||||
|
var serviceConfiguration = new ServiceConfiguration(databaseType, connectionName, workspacePath, scriptsDirectoryPath);
|
||||||
|
services.AddSingleton(serviceConfiguration);
|
||||||
|
services.AddDataAccess(serviceConfiguration);
|
||||||
|
services.AddSingleton<IMigrationService, MigrationService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddDataAccess(this IServiceCollection services, ServiceConfiguration serviceConfiguration)
|
||||||
|
{
|
||||||
|
services.AddScoped<IMigrationRepository, MigrationRepository>();
|
||||||
|
services.AddDbContextPool<MigrationDbContext>((serviceProvider, options) =>
|
||||||
|
{
|
||||||
|
var connectionString = GetDatabaseConnectionString(serviceProvider, serviceConfiguration.ConnectionName, serviceConfiguration.Workspace);
|
||||||
|
|
||||||
|
switch (serviceConfiguration.DatabaseType)
|
||||||
|
{
|
||||||
|
case DatabaseType.SQLite:
|
||||||
|
options.UseSqlite(connectionString);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DatabaseType.SQLServer:
|
||||||
|
options.UseSqlServer(connectionString);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException($"Database type {serviceConfiguration.DatabaseType} is not implemented.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDatabaseConnectionString(IServiceProvider serviceProvider, string connectionName, string workspace)
|
||||||
|
{
|
||||||
|
var configuration = serviceProvider.GetService<IConfiguration>();
|
||||||
|
var connectionString = configuration.GetConnectionString(connectionName);
|
||||||
|
|
||||||
|
if (connectionString.Contains(_workspacePlaceholder))
|
||||||
|
{
|
||||||
|
connectionString = connectionString.Replace(_workspacePlaceholder, workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UseMigration(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
var migrationService = app.ApplicationServices.GetService<IMigrationService>();
|
||||||
|
migrationService.Execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NDB.Infrastructure.DatabaseMigration.Models
|
||||||
|
{
|
||||||
|
internal class MigrationThumbprint
|
||||||
|
{
|
||||||
|
public MigrationSignature[] MigrationSignatures { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MigrationSignature
|
||||||
|
{
|
||||||
|
public DateTime MigrationDate { get; set; }
|
||||||
|
public string MachineName { get; set; }
|
||||||
|
public MigratedVersion[] MigratedVersions { get; set; }
|
||||||
|
public string LastVersion { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MigratedVersion
|
||||||
|
{
|
||||||
|
public string Version { get; set; }
|
||||||
|
public string[] Scripts { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ScriptPack
|
||||||
|
{
|
||||||
|
public string Path { get; set; }
|
||||||
|
public Version Version { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
using NDB.Infrastructure.DatabaseMigration.Constants;
|
||||||
|
|
||||||
|
namespace NDB.Infrastructure.DatabaseMigration.Models
|
||||||
|
{
|
||||||
|
internal class ServiceConfiguration
|
||||||
|
{
|
||||||
|
public DatabaseType DatabaseType { get; }
|
||||||
|
public string ConnectionName { get; }
|
||||||
|
public string Workspace { get; }
|
||||||
|
public string ScriptsDirectory { get; }
|
||||||
|
|
||||||
|
public ServiceConfiguration(DatabaseType databaseType, string connectionName, string workspace, string scriptsDirectory)
|
||||||
|
{
|
||||||
|
DatabaseType = databaseType;
|
||||||
|
ConnectionName = connectionName;
|
||||||
|
Workspace = workspace;
|
||||||
|
ScriptsDirectory = scriptsDirectory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.13" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.13" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NDB.Infrastructure.DatabaseMigration.Repositories
|
||||||
|
{
|
||||||
|
public interface IMigrationRepository
|
||||||
|
{
|
||||||
|
Task ExecuteSqlRaw(string commandText);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NDB.Infrastructure.DatabaseMigration.DbContexts;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NDB.Infrastructure.DatabaseMigration.Repositories
|
||||||
|
{
|
||||||
|
internal class MigrationRepository : IMigrationRepository
|
||||||
|
{
|
||||||
|
private readonly MigrationDbContext _dbContext;
|
||||||
|
|
||||||
|
public MigrationRepository(MigrationDbContext dbContext)
|
||||||
|
{
|
||||||
|
_dbContext = dbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteSqlRaw(string commandText)
|
||||||
|
{
|
||||||
|
using (var command = _dbContext.Database.GetDbConnection().CreateCommand())
|
||||||
|
{
|
||||||
|
command.CommandText = commandText;
|
||||||
|
await _dbContext.Database.OpenConnectionAsync();
|
||||||
|
await command.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace NDB.Infrastructure.DatabaseMigration.Services
|
||||||
|
{
|
||||||
|
public interface IMigrationService
|
||||||
|
{
|
||||||
|
public void Execute();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NDB.Infrastructure.DatabaseMigration.Models;
|
||||||
|
using NDB.Infrastructure.DatabaseMigration.Repositories;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
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<MigrationService> _logger;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ServiceConfiguration _configuration;
|
||||||
|
|
||||||
|
public MigrationService(ILogger<MigrationService> logger, IServiceProvider serviceProvider, ServiceConfiguration configuration)
|
||||||
|
{
|
||||||
|
_migrationSignaturesFilePath = Path.Combine(_configuration.Workspace, _migrationSignaturesFileName);
|
||||||
|
_logger = logger;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting migration...");
|
||||||
|
|
||||||
|
CheckWorkspace();
|
||||||
|
|
||||||
|
var localSignatures = GetMigrationSignatures();
|
||||||
|
var lastInstalledVersion = localSignatures?.OrderByDescending(z => z.MigrationDate).FirstOrDefault()?.LastVersion ?? "0.0.0";
|
||||||
|
var targetVersion = new Version(lastInstalledVersion);
|
||||||
|
var scriptPacks = GetScriptPacks();
|
||||||
|
var packsToInstall = scriptPacks.Where(p => p.Version > targetVersion);
|
||||||
|
|
||||||
|
if (!packsToInstall.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Nothing to migrate.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var signature = new MigrationSignature() { MachineName = System.Environment.MachineName, MigrationDate = DateTime.Now };
|
||||||
|
var migratedVersions = new List<MigratedVersion>();
|
||||||
|
|
||||||
|
foreach (var pack in packsToInstall.OrderBy(p => p.Version))
|
||||||
|
{
|
||||||
|
var scripts = Directory.GetFiles(pack.Path);
|
||||||
|
if (!scripts.Any())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_logger.LogInformation($"Running script pack: '{pack.Version}'");
|
||||||
|
|
||||||
|
Array.ForEach(scripts, s => RunScript(s));
|
||||||
|
var migratedVersion = new MigratedVersion() { Version = pack.Version.ToString(), Scripts = scripts.Select(z => Path.GetFileName(z)).ToArray() };
|
||||||
|
migratedVersions.Add(migratedVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
signature.MigratedVersions = migratedVersions.ToArray();
|
||||||
|
signature.LastVersion = packsToInstall.OrderByDescending(z => z.Version).First().Version.ToString();
|
||||||
|
|
||||||
|
var signatures = localSignatures != null ? new List<MigrationSignature>(localSignatures) : new List<MigrationSignature>();
|
||||||
|
signatures.Add(signature);
|
||||||
|
|
||||||
|
SaveMigrationSignatures(signatures.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptPack[] GetScriptPacks()
|
||||||
|
{
|
||||||
|
var scripts = Directory.GetDirectories(_configuration.ScriptsDirectory);
|
||||||
|
var packs = scripts.Select(z => new ScriptPack() { Path = z, Version = new Version(new DirectoryInfo(z).Name) });
|
||||||
|
|
||||||
|
return packs.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunScript(string path)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Running sql script: '{path}'");
|
||||||
|
var sqlContent = File.ReadAllText(path);
|
||||||
|
if (string.IsNullOrEmpty(sqlContent))
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
var _repository = scope.ServiceProvider.GetRequiredService<IMigrationRepository>();
|
||||||
|
_repository.ExecuteSqlRaw(sqlContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue