Merge pull request #1239 from cnurse/dev
Implement Database Migrations and add Multi-Database Support
This commit is contained in:
@ -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);
|
||||
|
27
Oqtane.Server/Repository/Context/DbConfig.cs
Normal file
27
Oqtane.Server/Repository/Context/DbConfig.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -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; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
20
Oqtane.Server/Repository/Interfaces/IDbConfig.cs
Normal file
20
Oqtane.Server/Repository/Interfaces/IDbConfig.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user