Merge pull request #1239 from cnurse/dev

Implement Database Migrations and add Multi-Database Support
This commit is contained in:
Shaun Walker
2021-04-19 21:11:11 -04:00
committed by GitHub
108 changed files with 4006 additions and 453 deletions

View File

@ -1,37 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Configuration;
using Oqtane.Extensions;
using Oqtane.Models;
using Oqtane.Interfaces;
using Oqtane.Migrations.Framework;
using Oqtane.Repository.Databases.Interfaces;
using Oqtane.Shared;
// ReSharper disable BuiltInTypeReferenceStyleForMemberAccess
namespace Oqtane.Repository
{
public class DBContextBase : IdentityUserContext<IdentityUser>
{
private ITenantResolver _tenantResolver;
private IHttpContextAccessor _accessor;
private readonly ITenantResolver _tenantResolver;
private readonly IHttpContextAccessor _accessor;
private readonly IConfiguration _configuration;
private string _connectionString;
private string _databaseType;
public DBContextBase(ITenantResolver tenantResolver, IHttpContextAccessor accessor)
public DBContextBase(ITenantResolver tenantResolver, IHttpContextAccessor httpContextAccessor)
{
_connectionString = String.Empty;
_tenantResolver = tenantResolver;
_accessor = accessor;
_accessor = httpContextAccessor;
}
public DBContextBase(IDbConfig dbConfig, ITenantResolver tenantResolver)
{
_accessor = dbConfig.Accessor;
_configuration = dbConfig.Configuration;
_connectionString = dbConfig.ConnectionString;
_databaseType = dbConfig.DatabaseType;
Databases = dbConfig.Databases;
_tenantResolver = tenantResolver;
}
public IEnumerable<IOqtaneDatabase> Databases { get; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var tenant = _tenantResolver.GetTenant();
if (tenant != null)
optionsBuilder.ReplaceService<IMigrationsAssembly, MultiDatabaseMigrationsAssembly>();
if (string.IsNullOrEmpty(_connectionString) && _tenantResolver != null)
{
var connectionString = tenant.DBConnectionString
.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString());
optionsBuilder.UseOqtaneDatabase(connectionString);
var tenant = _tenantResolver.GetTenant();
if (tenant != null)
{
_connectionString = tenant.DBConnectionString
.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString());
_databaseType = tenant.DBType;
}
else
{
if (!String.IsNullOrEmpty(_configuration.GetConnectionString("DefaultConnection")))
{
_connectionString = _configuration.GetConnectionString("DefaultConnection")
.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString());
}
_databaseType = _configuration.GetSection(SettingKeys.DatabaseSection)[SettingKeys.DatabaseTypeKey];
}
}
if (!string.IsNullOrEmpty(_connectionString) && !string.IsNullOrEmpty(_databaseType))
{
if (Databases != null)
{
optionsBuilder.UseOqtaneDatabase(Databases.Single(d => d.Name == _databaseType), _connectionString);
}
else
{
optionsBuilder.UseOqtaneDatabase(_databaseType, _connectionString);
}
}
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
if (Databases != null)
{
var database = Databases.Single(d => d.Name == _databaseType);
database.UpdateIdentityStoreTableNames(builder);
}
}
public override int SaveChanges()
{
DbContextUtils.SaveChanges(this, _accessor);

View File

@ -0,0 +1,27 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Oqtane.Interfaces;
namespace Oqtane.Repository
{
public class DbConfig : IDbConfig
{
public DbConfig(IHttpContextAccessor accessor, IConfiguration configuration, IEnumerable<IOqtaneDatabase> databases)
{
Accessor = accessor;
Configuration = configuration;
Databases = databases;
}
public IHttpContextAccessor Accessor { get; }
public IConfiguration Configuration { get; }
public IEnumerable<IOqtaneDatabase> Databases { get; set; }
public string ConnectionString { get; set; }
public string DatabaseType { get; set; }
}
}

View File

@ -6,7 +6,7 @@ using Oqtane.Models;
namespace Oqtane.Repository
{
public class DbContextUtils
public static class DbContextUtils
{
public static void SaveChanges(DbContext context, IHttpContextAccessor accessor)
{

View File

@ -1,26 +1,36 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Oqtane.Extensions;
using Oqtane.Interfaces;
using Oqtane.Models;
// ReSharper disable CheckNamespace
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Oqtane.Repository
{
public class InstallationContext : DbContext
{
private readonly string _connectionString;
private readonly IOqtaneDatabase _database;
public InstallationContext(string connectionString)
public InstallationContext(IOqtaneDatabase database, string connectionString)
{
_connectionString = connectionString;
_database = database;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseOqtaneDatabase(_connectionString);
=> optionsBuilder.UseOqtaneDatabase(_database, _connectionString);
public virtual DbSet<Alias> Alias { get; set; }
public virtual DbSet<Tenant> Tenant { get; set; }
public virtual DbSet<ModuleDefinition> ModuleDefinition { get; set; }
public virtual DbSet<Job> Job { get; set; }
public virtual DbSet<JobLog> JobLog { get; set; }
}
}

View File

@ -1,33 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Models;
using Microsoft.Extensions.Configuration;
using Oqtane.Extensions;
using Oqtane.Interfaces;
using Oqtane.Migrations.Framework;
using Oqtane.Repository.Databases.Interfaces;
using Oqtane.Shared;
// ReSharper disable BuiltInTypeReferenceStyleForMemberAccess
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable CheckNamespace
namespace Oqtane.Repository
{
public class MasterDBContext : DbContext
public class MasterDBContext : DbContext, IMultiDatabase
{
private readonly IHttpContextAccessor _accessor;
private readonly IConfiguration _configuration;
private readonly IDbConfig _dbConfig;
public MasterDBContext(DbContextOptions<MasterDBContext> options, IHttpContextAccessor accessor, IConfiguration configuration) : base(options)
public MasterDBContext(DbContextOptions<MasterDBContext> options, IDbConfig dbConfig) : base(options)
{
_accessor = accessor;
_configuration = configuration;
_dbConfig = dbConfig;
Databases = dbConfig.Databases;
}
public IEnumerable<IOqtaneDatabase> Databases { get; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!String.IsNullOrEmpty(_configuration.GetConnectionString("DefaultConnection")))
{
var connectionString = _configuration.GetConnectionString("DefaultConnection")
.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString());
optionsBuilder.ReplaceService<IMigrationsAssembly, MultiDatabaseMigrationsAssembly>();
optionsBuilder.UseOqtaneDatabase(connectionString);
var connectionString = _dbConfig.ConnectionString;
var configuration = _dbConfig.Configuration;
var databaseType = _dbConfig.DatabaseType;
if(string.IsNullOrEmpty(connectionString) && configuration != null)
{
if (!String.IsNullOrEmpty(configuration.GetConnectionString("DefaultConnection")))
{
connectionString = configuration.GetConnectionString("DefaultConnection")
.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString());
}
databaseType = configuration.GetSection(SettingKeys.DatabaseSection)[SettingKeys.DatabaseTypeKey];
}
if (!string.IsNullOrEmpty(connectionString) && !string.IsNullOrEmpty(databaseType))
{
if (Databases != null)
{
optionsBuilder.UseOqtaneDatabase(Databases.Single(d => d.Name == databaseType), connectionString);
}
else
{
optionsBuilder.UseOqtaneDatabase(databaseType, connectionString);
}
}
base.OnConfiguring(optionsBuilder);
}
@ -39,7 +71,7 @@ namespace Oqtane.Repository
public override int SaveChanges()
{
DbContextUtils.SaveChanges(this, _accessor);
DbContextUtils.SaveChanges(this, _dbConfig.Accessor);
return base.SaveChanges();
}

View File

@ -1,11 +1,19 @@
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Oqtane.Interfaces;
using Oqtane.Models;
using Oqtane.Repository.Databases.Interfaces;
// ReSharper disable CheckNamespace
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Oqtane.Repository
{
public class TenantDBContext : DBContextBase
public class TenantDBContext : DBContextBase, IMultiDatabase
{
public TenantDBContext(IDbConfig dbConfig, ITenantResolver tenantResolver) : base(dbConfig, tenantResolver) { }
public virtual DbSet<Site> Site { get; set; }
public virtual DbSet<Page> Page { get; set; }
public virtual DbSet<PageModule> PageModule { get; set; }
@ -20,13 +28,6 @@ namespace Oqtane.Repository
public virtual DbSet<Notification> Notification { get; set; }
public virtual DbSet<Folder> Folder { get; set; }
public virtual DbSet<File> File { get; set; }
public virtual DbSet<Language> Language { get; set; }
public TenantDBContext(ITenantResolver tenantResolver, IHttpContextAccessor accessor) : base(tenantResolver, accessor)
{
// DBContextBase handles multi-tenant database connections
}
}
}

View File

@ -0,0 +1,20 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Oqtane.Interfaces;
namespace Oqtane.Repository
{
public interface IDbConfig
{
public IHttpContextAccessor Accessor { get; }
public IConfiguration Configuration { get; }
public IEnumerable<IOqtaneDatabase> Databases { get; set; }
public string ConnectionString { get; set; }
public string DatabaseType { get; set; }
}
}

View File

@ -1,5 +1,5 @@
using System.Data.SqlClient;
using System.Reflection;
using System.Reflection;
using Microsoft.Data.SqlClient;
using Oqtane.Models;
namespace Oqtane.Repository
@ -7,8 +7,13 @@ namespace Oqtane.Repository
public interface ISqlRepository
{
void ExecuteScript(Tenant tenant, string script);
bool ExecuteScript(string connectionString, Assembly assembly, string filename);
bool ExecuteScript(Tenant tenant, Assembly assembly, string filename);
int ExecuteNonQuery(Tenant tenant, string query);
SqlDataReader ExecuteReader(Tenant tenant, string query);
}
}

View File

@ -1,10 +1,13 @@
using System;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Data.SqlClient;
using Oqtane.Models;
// ReSharper disable ConvertToUsingDeclaration
// ReSharper disable InvertIf
// ReSharper disable BuiltInTypeReferenceStyleForMemberAccess
namespace Oqtane.Repository
{
@ -13,35 +16,41 @@ namespace Oqtane.Repository
public void ExecuteScript(Tenant tenant, string script)
{
// execute script in curent tenant
foreach (string query in script.Split("GO", StringSplitOptions.RemoveEmptyEntries))
// execute script in current tenant
foreach (var query in script.Split("GO", StringSplitOptions.RemoveEmptyEntries))
{
ExecuteNonQuery(tenant, query);
}
}
public bool ExecuteScript(Tenant tenant, Assembly assembly, string filename)
public bool ExecuteScript(string connectionString, Assembly assembly, string fileName)
{
// script must be included as an Embedded Resource within an assembly
bool success = true;
string script = "";
var success = true;
var script = GetScriptFromAssembly(assembly, fileName);
if (assembly != null)
if (!string.IsNullOrEmpty(script))
{
string name = assembly.GetManifestResourceNames().FirstOrDefault(item => item.EndsWith("." + filename));
if (name != null)
try
{
Stream resourceStream = assembly.GetManifestResourceStream(name);
if (resourceStream != null)
foreach (var query in script.Split("GO", StringSplitOptions.RemoveEmptyEntries))
{
using (var reader = new StreamReader(resourceStream))
{
script = reader.ReadToEnd();
}
ExecuteNonQuery(connectionString, query);
}
}
catch
{
success = false;
}
}
return success;
}
public bool ExecuteScript(Tenant tenant, Assembly assembly, string fileName)
{
var success = true;
var script = GetScriptFromAssembly(assembly, fileName);
if (!string.IsNullOrEmpty(script))
{
try
@ -58,13 +67,27 @@ namespace Oqtane.Repository
}
public int ExecuteNonQuery(Tenant tenant, string query)
{
return ExecuteNonQuery(tenant.DBConnectionString, query);
}
public SqlDataReader ExecuteReader(Tenant tenant, string query)
{
SqlConnection conn = new SqlConnection(FormatConnectionString(tenant.DBConnectionString));
SqlCommand cmd = conn.CreateCommand();
PrepareCommand(conn, cmd, query);
var dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
return dr;
}
private int ExecuteNonQuery(string connectionString, string query)
{
var conn = new SqlConnection(FormatConnectionString(connectionString));
var cmd = conn.CreateCommand();
using (conn)
{
PrepareCommand(conn, cmd, query);
int val = -1;
var val = -1;
try
{
val = cmd.ExecuteNonQuery();
@ -77,13 +100,28 @@ namespace Oqtane.Repository
}
}
public SqlDataReader ExecuteReader(Tenant tenant, string query)
private string GetScriptFromAssembly(Assembly assembly, string fileName)
{
SqlConnection conn = new SqlConnection(FormatConnectionString(tenant.DBConnectionString));
SqlCommand cmd = conn.CreateCommand();
PrepareCommand(conn, cmd, query);
var dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
return dr;
// script must be included as an Embedded Resource within an assembly
var script = "";
if (assembly != null)
{
var name = assembly.GetManifestResourceNames().FirstOrDefault(item => item.EndsWith("." + fileName));
if (name != null)
{
var resourceStream = assembly.GetManifestResourceStream(name);
if (resourceStream != null)
{
using (var reader = new StreamReader(resourceStream))
{
script = reader.ReadToEnd();
}
}
}
}
return script;
}
private void PrepareCommand(SqlConnection conn, SqlCommand cmd, string query)