Přeskočit na hlavní obsah

DB repositories

Overview

The persistent layer provides database access to backend service implemented on .NET platform. The two basic patterns are supported: MongoDB and EFCore based databases. These db patterns can be combined together in multi-platform scenario using application context. See examples how to configure and register db repositories and db migrations for tenant, master and hybrid context. See also optional pattern for services supporting the application context isolation.

The core packages:

  • ASOL.Core.Persistence.*
  • ASOL.Core.Multitenancy.*

Usage

MongoDB platform

For MongoDB platform it is necessary to configure appsettings, dependency injection and http request pipeline. It is also necessary to provide registration of repositories and configure persisted entities and also provide tenant and master database migrations with or without application context.

Configuration of repositories (MongoDB)

  // MongoDB - tenant database (ASP)
"MongoDbConnectionOptions": {
"ConnectionString": "mongodb://#{Dns_ASP_MongoDB}#",
"DatabaseNamePrefix": "ASP",
"CollectionNamePrefix": "My_App"
},

// MongoDB - master database (ASP), optional
"MasterConnectionOptions": {
"ConnectionString": "mongodb://#{Dns_ASP_MongoDB}#",
"DatabaseName": "ASP-Master",
"CollectionNamePrefix": "My_App"
},

// MongoDB - tenant database (FS), optional
"GridFsConnectionOptions": {
"ConnectionString": "mongodb://#{Dns_ASP_MongoDB}#",
"DatabaseNamePrefix": "FS",
"CollectionNamePrefix": "My_App"
},

// MongoDB - master database (FS), optional
"MasterGridFsConnectionOptions": {
"ConnectionString": "mongodb://#{Dns_ASP_MongoDB}#",
"DatabaseName": "FS-Master",
"CollectionNamePrefix": "My_App"
},
// configuration of repositories for dependency injection and http request pipeline

public void ConfigureServices(IServiceCollection services)
{
// ...

//+ Complex Component (optional)
services.AddScoped<ComplexComponentApplicationContextProvider>();
services.AddScoped<IDbApplicationInfoResolver, ComplexComponentDbApplicationInfoResolver>();
//- Complex Component

//+ MongoDB
services.AddMongoDbMappingsAndRepositories(Configuration.GetSection(nameof(MongoDbConnectionOptions)));
services.AddHostedService<MasterDatabaseMigrationService>(); // <-- master migration
//- MongoDB

// ...
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IOptions<ReverseProxyOptions> reverseProxyOptions)
{
// ...

app.UseAuthentication();

app.UseMultitenancy();
app.UseIdentityManager();

app.UseApplicationContext(); // <-- application context (optional)
app.UseTenantDatabaseInitialization(); // <-- tenant migration

// ...
}
}

Registration of repositories in Persistence layer (MongoDB)

// extension method to register repositories in persistence layer

using System;
using System.Linq;
using ASOL.Core.Domain;
using ASOL.Core.Localization;
using ASOL.Core.Multitenancy.MongoDb.Extensions;
using ASOL.Core.Persistence.MongoDb.Serialization.IdGenerators;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;

namespace My.App.MongoDb
{
public static class MongoDbServiceCollectionExtensions
{
public static IServiceCollection AddMongoDbMappingsAndRepositories(this IServiceCollection services, IConfigurationSection configurationSection)
{
services.AddDefaultTenantDbContext(configurationSection); //use tenantDb by default
//alt: services.AddTenantDbContext<DbContext>() //use masterDb by default
services.AddRepoFactory();

//implicit and explicit tenant + master entities
services.AddHybridRepositoryAccessor<ITemplateRepository, ITemplateReadOnlyRepository, TemplateRepository>();

RegisterClassMaps();

//data, index, seed migrations
services.AddHybridMongoDbMigration()
// hybrid migrations
.AddHybridIndexMigration<CollectionIndexesMigration_1886>()
;

return services;
}

// ...
}
}

Tenant database migration middleware (MongoDB)

// extension method for tenant database migration middleware

/// <summary>
/// Initialize tenant database (i.e. cleanup, seed, initialize or migrate structure and data).
/// </summary>
/// <param name="builder">The application builder.</param>
/// <returns>The application builder. (fluent api)</returns>
public static IApplicationBuilder UseTenantDatabaseInitialization(this IApplicationBuilder builder)
{
builder.UseMiddleware<TenantDatabaseMigrationMiddleware>();

return builder;
}
// implementation of migration middleware for tenant database - without application context

using System.Threading;
using System.Threading.Tasks;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.MongoDb;
using ASOL.Core.Multitenancy.Persistence.Migrations;
using ASOL.Core.Persistence.EFCore;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;

namespace My.App.API.Middlewares
{
/// <summary>
/// Middleware for initializing the database model. Data migration, index migration and initial data seeding for the tenant database.
/// </summary>
public class TenantDatabaseMigrationMiddleware
{
/// <summary>
/// Initializes the instance
/// </summary>
/// <param name="next">Request delegate</param>
public TenantDatabaseMigrationMiddleware(RequestDelegate next)
{
Next = next;
}

private RequestDelegate Next { get; }

/// <summary>
/// Initializes the database model.
/// </summary>
/// <param name="context">Http context</param>
/// <param name="tenantContext">Tenant context</param>
/// <returns>Task</returns>
public async Task Invoke(HttpContext context, ITenantContext tenantContext, IMemoryCache cache)
{
if (!tenantContext.IsTenantValid)
{
await Next(context);
return;
}

//mongodb migration
var dbContext = context.RequestServices.GetRequiredService<ITenantDbContext>();
var migrationProcessor = context.RequestServices.GetRequiredService<ITenantDbMigrationProcessor>();
await migrationProcessor.ExecuteTenantMigrationAsync(context.RequestServices, dbContext, context.User, CancellationToken.None);

// one-time tenant initialization of additional services on startup (optional)
cache.GetOrCreate($"Services_{nameof(TenantDatabaseMigrationMiddleware)}_{tenantContext.TenantId}", cacheEntry =>
{
using (var scope = context.RequestServices.GetRequiredService<IRuntimeContextScope>())
{
var tenantHolder = scope.ScopeProvider.GetRequiredService<ITenantContextHolder>();
tenantHolder.TenantContext = new TenantContext { Tenant = new TenantInfo(tenantContext.TenantId, null) };

var clientPrincipal = ClaimsPrincipalBuilder.CreateClientPrincipal("My.App", tenantContext.TenantId)
.AddLocale(LocalizationContext.DefaultLanguageCode)
.AddAuthenticateType()
.Build();

scope.LocalImpersonate(clientPrincipal, Guid.NewGuid().ToString());

// ... initialization of additional providers/services, e.g.:
//_ = scope.ScopeProvider.GetRequiredService<MyServiceMetadataInitializer>().SynchronizeMetadata();

return true;
}
});

await Next(context);
}
}
}

Master database migration service (MongoDB)

// implementation of migration service for MongoDB Master database - without application context

using System;
using System.Threading;
using System.Threading.Tasks;
using ASOL.Core.Identity;
using ASOL.Core.Identity.Contexts.Factories;
using ASOL.Core.Identity.Options;
using ASOL.Core.Persistence.MongoDb;
using ASOL.Core.Persistence.MongoDb.Migrations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace My.App.API.Services
{
/// <summary>
/// Master database migration service as background service. Execute when API service started.
/// </summary>
public class MasterDatabaseMigrationService : BackgroundService
{
/// <summary>
/// Initialize service
/// </summary>
public MasterDatabaseMigrationService(IServiceProvider provider, ILogger<MasterDatabaseMigrationService> logger)
{
Provider = provider;
Logger = logger;
}

/// <summary>
/// Global service provider
/// </summary>
protected IServiceProvider Provider { get; }

/// <summary>
/// The logger instance
/// </summary>
protected readonly ILogger Logger { get; }

/// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
using var scope = Provider.CreateScope();

var masterRuntimeContextHolder = scope.ServiceProvider.GetRequiredService<IScopeContextHolder<IMasterRuntimeContext>>();
var masterRuntimeContextFactory = scope.ServiceProvider.GetRequiredService<IMasterRuntimeContextFactory>();
masterRuntimeContextHolder.Context = masterRuntimeContextFactory.Create();

var processor = scope.ServiceProvider.GetRequiredService<IDbMigrationProcessor>();
var dbContextFactory = scope.ServiceProvider.GetRequiredService<IMasterRepoFactory>();
var dbContext = dbContextFactory.GetOrCreateMasterDbContext();
var ssoAuth = scope.ServiceProvider.GetRequiredService<IOptions<SsoAuthOptions>>();
var user = ClaimsPrincipalBuilder.CreateClientPrincipal(ssoAuth.Value.ClientId).AddAuthenticateType().Build();
await processor.ExecuteMigrationAsync(scope.ServiceProvider, dbContext, user, stoppingToken);
}
catch (Exception e)
{
logger.LogError(e, "Error while executing master database migration");
}
}
}
}

EFCore platform

For EFCore platform it is necessary to configure your development appsettings or development secrets, dependency injection and http request pipeline. It is also necessary to provide registration of repositories and configure persisted entities and also provide tenant and master database migrations with or without application context.

Configuration of repositories (EFCore)

Configuration of repositories - Tenant database context
{
"ConnectionStrings": {
// EFCore - tenant database (ASP)
"DbConnectionString": "<YOUR DEVELOPMENT CONNECTION STRING>", // "Server=localhost;Port=5432;Database={{DatabaseName}};User Id=apiuser;Password=mysecretpassword;"

// EFCore - tenant database (FS), optional
"FsDbConnectionString": "<YOUR DEVELOPMENT CONNECTION STRING>",
}
}
// configuration of repositories for dependency injection and http request pipeline

public void ConfigureServices(IServiceCollection services)
{
// ...

//+ Complex component (optional)
services.AddScoped<ComplexComponentApplicationContextProvider>();
services.AddScoped<IDbApplicationInfoResolver, ComplexComponentDbApplicationInfoResolver>();
//- Complex component

//+ EFCore
services.AddEfCorePersistence<DatabaseContextConfigurationProvider>();
//- EFCore

// ...
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IOptions<ReverseProxyOptions> reverseProxyOptions)
{
// ...

app.UseAuthentication();

app.UseMultitenancy();
app.UseIdentityManager();

app.UseApplicationContext(); // <-- application context (optional)
app.UseTenantDatabaseInitialization(); // <-- tenant migration - necessary for application context (optional)

// ...
}
Configuration of repositories — Master database context
{
"ConnectionStrings": {
// EFCore - master database
"MasterDbConnectionString": "<YOUR DEVELOPMENT CONNECTION STRING>" // example: Server=localhost;Port=5432;Database=ASP-Master;User Id=apiuser;Password=mysecretpassword;"
}
}
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddEfCorePersistence<DatabaseContextConfigurationProvider>();
// ...
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IOptions<ReverseProxyOptions> reverseProxyOptions)
{
// ...

app.UseAuthentication();

app.UseMultitenancy();
app.UseIdentityManager();

app.UseApplicationContext(); // <-- application context (optional)
app.UseMasterDatabaseInitialization(); // <-- master database migration

// ...
}
Configuration of repositories — Hybrid database context
{
"ConnectionStrings": {
// EFCore - tenant database (ASP)
"DbConnectionString": "<YOUR DEVELOPMENT CONNECTION STRING>", // "Server=localhost;Port=5432;Database={{DatabaseName}};User Id=apiuser;Password=mysecretpassword;"
// EFCore - master database
"MasterDbConnectionString": "<YOUR DEVELOPMENT CONNECTION STRING>" // example: Server=localhost;Port=5432;Database=ASP-Master;User Id=apiuser;Password=mysecretpassword;"
}
}

public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddEfCorePersistence<TenantDatabaseContextConfigurationProvider, MasterDatabaseContextConfigurationProvider>();
// ...
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IOptions<ReverseProxyOptions> reverseProxyOptions)
{
// ...

app.UseAuthentication();

app.UseMultitenancy();
app.UseIdentityManager();

app.UseApplicationContext(); // <-- application context (optional)
app.UseHybridDatabaseInitialization(); // <-- hybrid database migration

// ...
}

Database context configuration provider (EFCore)

Database context configuration provider - Tenant database context

using System;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Persistence.EFCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace My.App.API;

public class DatabaseContextConfigurationProvider : BaseTenantDbContextConfigurationProvider, ITenantDbContextConfigurationProvider
{
public DatabaseContextConfigurationProvider(IConfiguration configuration, ITenantContext tenantContext, IServiceProvider serviceProvider)
: base(configuration, tenantContext)
{
ServiceProvider = serviceProvider;
}

protected IServiceProvider ServiceProvider { get; }

public override string GetConnectionName(ITenantContext tenantContext, DbContext dbContext)
{
return "DbConnectionString"; // from appsettings.json connection name
}

public override void Configure(DbContext dbContext, DbContextOptionsBuilder optionsBuilder)
{
if (Program.IsRunningFromMigrationTools)
{
SetMigrationTenant(Configuration["MigrationTenant"] ?? "Migration"); // migration running from EFTool commands: add-migration, remove-migration or update-database
SuppressHistoryTableMapping = true;
}
base.Configure(dbContext, optionsBuilder);
}

public override void OnConfiguring(string connectionName, string connectionString, ITenantContext tenantContext, DbContext dbContext, DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(connectionString, h => h.MigrationsHistoryTable(DbConstants.MigrationsHistoryTableName, GetSchemaName(DbConstants.SchemaName)))
.ReplaceService<IModelCacheKeyFactory, DynamicSchemaModelCacheKeyFactory>()
.ReplaceService<IMigrationsAssembly, DbSchemaAwareMigrationAssembly>();
}

protected virtual string GetSchemaName(string hostSchemaName)
{
var applicationInfo = ServiceProvider.GetService<IEFCoreApplicationInfo>();
return DbSchemaHelper.GetSchemaName(applicationInfo, hostSchemaName);
}
}

using System;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Persistence.EFCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace My.App.API;

public class DatabaseContextConfigurationProviderFs : BaseTenantDbContextConfigurationProvider, IFsDatabaseContextConfigurationProvider
{
public DatabaseContextConfigurationProviderFs(IConfiguration configuration, ITenantContext tenantContext, IServiceProvider serviceProvider)
: base(configuration, tenantContext)
{
ServiceProvider = serviceProvider;
}

protected IServiceProvider ServiceProvider { get; }

public override string GetConnectionName(ITenantContext tenantContext, DbContext dbContext)
{
return "FsDbConnectionString"; // from appsettings.json connection name
}

public override void Configure(DbContext dbContext, DbContextOptionsBuilder optionsBuilder)
{
if (Program.IsRunningFromMigrationTools)
{
SetMigrationTenant(Configuration["MigrationTenant"] ?? "Migration"); // migration running from EFTool commands: add-migration, remove-migration or update-database
SuppressHistoryTableMapping = true;
}
base.Configure(dbContext, optionsBuilder);
}

public override void OnConfiguring(string connectionName, string connectionString, ITenantContext tenantContext, DbContext dbContext, DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(connectionString, h => h.MigrationsHistoryTable(DbConstants.MigrationsHistoryTableName, GetSchemaName(DbConstants.FsSchemaName)))
.ReplaceService<IModelCacheKeyFactory, DynamicSchemaModelCacheKeyFactory>()
.ReplaceService<IMigrationsAssembly, DbSchemaAwareMigrationAssembly>();
}

protected virtual string GetSchemaName(string hostSchemaName)
{
var applicationInfo = ServiceProvider.GetService<IEFCoreApplicationInfo>();
return DbSchemaHelper.GetSchemaName(applicationInfo, hostSchemaName);
}
}
Database context configuration provider - Master database context
using ASOL.Core.Identity;
using ASOL.Core.Identity.Contexts.Factories;
using ASOL.Core.Persistence.EFCore;
using ASxOL.MyOwnProject.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace My.App.API;

internal class MasterDatabaseContextConfigurationProvider(
IConfiguration configuration,
IMasterRuntimeContext masterRuntimeContext,
IMasterRuntimeContextFactory masterRuntimeContextFactory)
: BaseMasterDbContextConfigurationProvider(
configuration,
!Program.IsRunningFromMigrationTools ? masterRuntimeContext : masterRuntimeContextFactory.Create())
{
public override string GetConnectionName(DbContext dbContext)
{
return "MasterDbConnectionString" ;
}

public override void Configure(DbContext dbContext, DbContextOptionsBuilder optionsBuilder)
{
if (Program.IsRunningFromMigrationTools)
{
// migration running from EFTool commands: add-migration, remove-migration or update-database
SuppressHistoryTableMapping = true;
}
base.Configure(dbContext, optionsBuilder);
}

public override void OnConfiguring(string connectionName, string connectionString, DbContext dbContext, DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(connectionString, h => h.MigrationsHistoryTable(Constants.MigrationsHistoryTableName, Constants.SchemaName));
}
}

Database context configuration provider - Hybrid database context
using ASOL.Core.Identity;
using ASOL.Core.Identity.Contexts.Factories;
using ASOL.Core.Persistence.EFCore;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.EFCore;
using ASxOL.MyOwnProject.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace ASxOL.MyOwnProject.API;

internal class MasterDatabaseContextConfigurationProvider(
IConfiguration configuration,
IMasterRuntimeContext masterRuntimeContext,
IMasterRuntimeContextFactory masterRuntimeContextFactory)
: BaseMasterDbContextConfigurationProvider(
configuration,
!Program.IsRunningFromMigrationTools ? masterRuntimeContext : masterRuntimeContextFactory.Create())
{
public override string GetConnectionName(DbContext dbContext)
{
return "MasterDbConnectionString" ;
}

public override void Configure(DbContext dbContext, DbContextOptionsBuilder optionsBuilder)
{
if (Program.IsRunningFromMigrationTools)
{
// migration running from EFTool commands: add-migration, remove-migration or update-database
SuppressHistoryTableMapping = true;
}
base.Configure(dbContext, optionsBuilder);
}

public override void OnConfiguring(string connectionName, string connectionString, DbContext dbContext, DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(connectionString, h => h.MigrationsHistoryTable(Constants.MigrationsHistoryTableName, Constants.SchemaName));
}
}

internal class TenantDatabaseContextConfigurationProvider(IConfiguration configuration, ITenantContext tenantContext)
: BaseTenantDbContextConfigurationProvider(configuration, tenantContext)
{
public override string GetConnectionName(ITenantContext tenantContext, DbContext dbContext)
{
return "AppDbConnectionString";
}

public override void Configure(DbContext dbContext, DbContextOptionsBuilder optionsBuilder)
{
if (Program.IsRunningFromMigrationTools)
{
// migration running from EFTool commands: add-migration, remove-migration or update-database
SetMigrationTenant(Configuration["MigrationTenant"] ?? "Migration");
SuppressHistoryTableMapping = true;
}
base.Configure(dbContext, optionsBuilder);
}

public override void OnConfiguring(string connectionName, string connectionString, ITenantContext tenantContext, DbContext dbContext, DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(connectionString, h => h.MigrationsHistoryTable(Constants.MigrationsHistoryTableName, Constants.SchemaName));
}
}

Registration of repositories in Persistence layer (EFCore)

Registration of repositories in Persistence layer — Tenant database context
using ASOL.Core.Persistence;
using ASOL.Core.Persistence.EFCore;
using ASOL.Core.Persistence.EFCore.Extensions;
using ASOL.IdentityManager.Authorization.Repositories;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace My.App.EFCore;

public static IServiceCollection AddEfCorePersistenc<TTenantDbContextConfigurationProvider>(this IServiceCollection services)
where TTenantDbContextConfigurationProvider : class, ITenantDbContextConfigurationProvider
{
services.AddTenantDbContext<DatabaseContext, TTenantDbContextConfigurationProvider>();
services.AddTenantRepositoryAccessor<ITestItemRepository, ITestItemReadOnlyRepository, TestItemRepository, DatabaseContext>();
return services;
}
using System;
using System.Linq;
using System.Reflection;
using ASxOL.MyOwnProject.Domain;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Persistence.EFCore;
using Microsoft.EntityFrameworkCore;

namespace ASxOL.MyOwnProject.Persistence;

public class DatabaseContext : TenantDbContext, IDbContextSchema
{
public DatabaseContext(ITenantContext tenantContext,
ITenantDbContextConfigurationProvider configurationProvider,
DbContextOptions<DatabaseContext> options,
IServiceProvider serviceProvider)
: base(tenantContext, configurationProvider, options, serviceProvider)
{
Schema = DbSchemaHelper.GetSchemaName(ApplicationInfo, DbConstants.SchemaName);
}

public string Schema { get; }

/// <inheritdoc cref="Folder"/>
public DbSet<Folder> Folders { get; set; }

public DbSet<DocumentPermission<Folder>> FolderPermissions { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Set default schema name
modelBuilder.HasDefaultSchema(DbConstants.SchemaName);

// Apply entity configurations
ApplyConfigurations(modelBuilder);

// Disable cascade delete for all relationships
foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(x => x.GetForeignKeys()))
{
relationship.DeleteBehavior = DeleteBehavior.Restrict;
}

// Apply entity configurations
ApplyConfigurations(modelBuilder);

base.OnModelCreating(modelBuilder);
base.MappingHistoryTables(modelBuilder);
}

private void ApplyConfigurations(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new FolderConfiguration(Schema));
modelBuilder.ApplyConfiguration(new FolderDocumentPermissionConfiguration(Schema));
}

public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
return base.SaveChanges(acceptAllChangesOnSuccess);
}

public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
}

// implementation of database context for tenant database (FS) - hybrid usage with application context support

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Persistence.EFCore;
using ASOL.IdentityManager.Authorization;
using Microsoft.EntityFrameworkCore;

namespace My.App.EFCore;

public class DatabaseContextFs : TenantDbContext, IDbContextSchema
{
public DatabaseContextFs(ITenantContext tenantContext,
IFsDatabaseContextConfigurationProvider configurationProvider,
DbContextOptions<DatabaseContextFs> options,
IServiceProvider serviceProvider)
: base(tenantContext, configurationProvider, options, serviceProvider)
{
Schema = DbSchemaHelper.GetSchemaName(ApplicationInfo, DbConstants.FsSchemaName);
}

public string Schema { get; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Set default schema name
modelBuilder.HasDefaultSchema(DbConstants.FsSchemaName);

// Apply entity configurations
ApplyConfigurations(modelBuilder);

// Disable cascade delete for all relationships
foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(x => x.GetForeignKeys()))
{
relationship.DeleteBehavior = DeleteBehavior.Restrict;
}

// Apply entity configurations
ApplyConfigurations(modelBuilder);

base.OnModelCreating(modelBuilder);
base.MappingHistoryTables(modelBuilder);
}

private void ApplyConfigurations(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new ContentConfiguration(Schema));
}
}
Registration of repositories in Persistence layer — Master database context
using ASxOL.MyOwnProject.Domain.Repositories;
using ASxOL.MyOwnProject.Persistence.Repositories;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Multitenancy.EFCore.Extensions;
using Microsoft.Extensions.DependencyInjection;

namespace ASxOL.MyOwnProject.Persistence;

public static IServiceCollection AddEfCorePersistenc<TMasterDbContextConfigurationProvider>(this IServiceCollection services)
where TMasterDbContextConfigurationProvider : class, IMasterDbContextConfigurationProvider
{
services.AddMasterDbContext<DatabaseContext, TMasterDbContextConfigurationProvider>();
services.AddMasterRepositoryAccessor<ITestItemRepository, ITestItemReadOnlyRepository, TestItemRepository, DatabaseContext>();
return services;
}
using System;
using System.Linq;
using System.Reflection;
using ASxOL.MyOwnProject.Domain;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Persistence.EFCore;
using Microsoft.EntityFrameworkCore;

namespace ASxOL.MyOwnProject.Persistence;

public class DatabaseContext(
DbContextOptions<DatabaseContext> options,
IMasterDbContextConfigurationProvider configurationProvider,
IServiceProvider serviceProvider)
: MasterDbContext(
options,
configurationProvider,
serviceProvider)
{
public DbSet<TestItem> TestItems { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply entity configurations
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());

// Set default schema name
modelBuilder.HasDefaultSchema(Constants.SchemaName);

// Disable cascade delete for all relationships, support soft-delete
foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(x => x.GetForeignKeys()))
{
relationship.DeleteBehavior = DeleteBehavior.Restrict;
}

base.OnModelCreating(modelBuilder);
}
}
Registration of repositories in Persistence layer — Hybrid database context
using ASxOL.MyOwnProject.Domain.Repositories;
using ASxOL.MyOwnProject.Persistence.Repositories;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Multitenancy.EFCore.Extensions;
using ASOL.Core.Persistence.EFCore;
using Microsoft.Extensions.DependencyInjection;

namespace ASxOL.MyOwnProject.Persistence;

public static IServiceCollection AddEfCorePersistence<TTenantDbContextConfigurationProvider, TMasterDbContextConfigurationProvider>(this IServiceCollection services)
where TTenantDbContextConfigurationProvider : class, ITenantDbContextConfigurationProvider
where TMasterDbContextConfigurationProvider : class, IMasterDbContextConfigurationProvider
{
services.AddHybridDbContext<DatabaseContext, TTenantDbContextConfigurationProvider, TMasterDbContextConfigurationProvider>();
services.AddHybridRepositoryAccessor<ITestItemRepository, ITestItemReadOnlyRepository, TestItemRepository, DatabaseContext>();
return services;
}
using System;
using System.Linq;
using System.Reflection;
using ASxOL.MyOwnProject.Domain;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Persistence.EFCore;
using Microsoft.EntityFrameworkCore;

namespace ASxOL.MyOwnProject.Persistence;

public class DatabaseContext(
DbContextOptions<DatabaseContext> options,
IDynamicDbContextConfigurationProvider configurationProvider,
IServiceProvider serviceProvider,
ITenantContext tenantContext)
: HybridDbContext(
options,
configurationProvider,
serviceProvider,
tenantContext)
{
public DbSet<TestItem> TestItems { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply entity configurations
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());

// Set default schema name
modelBuilder.HasDefaultSchema(Constants.SchemaName);

// Disable cascade delete for all relationships, support soft-delete
foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(x => x.GetForeignKeys()))
{
relationship.DeleteBehavior = DeleteBehavior.Restrict;
}

base.OnModelCreating(modelBuilder);
}
}

Database migrations (EFCore)

Database migrations — Tenant database context
public static IApplicationBuilder UseTenantDatabaseInitialization(this IApplicationBuilder app)
{
app.UseMiddleware<DatabaseMigrateOnRuntimeMiddleware>();
return app;
}
using System.Threading.Tasks;
using ASxOL.MyOwnProject.Persistence;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Persistence.EFCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace ASxOL.MyOwnProject.API.Middlewares;

internal class DatabaseMigrateOnRuntimeMiddleware(RequestDelegate next, ILogger<DatabaseMigrateOnRuntimeMiddleware> logger)
{
private RequestDelegate Next { get; } = next;
public ILogger<DatabaseMigrateOnRuntimeMiddleware> Log { get; } = logger;

public async Task Invoke(HttpContext context, ITenantContextHolder contextHolder, IMemoryCache cache)
{
var dbContext = context.RequestServices.GetRequiredService<DatabaseContext>();

// tenant migrations
if (dbContext.GetType().IsAssignableTo(typeof(TenantDbContext)) || dbContext.GetType().IsAssignableTo(typeof(HybridDbContext)))
{
var cacheKey = $"{nameof(DatabaseMigrateOnRuntimeMiddleware)}_{contextHolder.TenantContext?.TenantId}";

if (!contextHolder.TenantContext?.IsTenantValid == false && !cache.TryGetValue(cacheKey, out _))
{
var dynamicDbContextFactory = context.RequestServices.GetRequiredService<IDynamicDbContextFactory>();
var tenantDbContext = dynamicDbContextFactory.CreateDbContext<DatabaseContext, ITenantDbContextConfigurationProvider>();
await tenantDbContext.Database.MigrateAsync();
cache.Set(cacheKey, true);
}
}

await Next(context);
}
}
Database migrations — Master database context
public static IApplicationBuilder UseMasterDatabaseInitialization(this IApplicationBuilder app)
{
app.UseMiddleware<DatabaseMigrateOnRuntimeMiddleware>();
return app;
}
using System.Threading.Tasks;
using ASxOL.MyOwnProject.Persistence;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Persistence.EFCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace ASxOL.MyOwnProject.API.Middlewares;

internal class DatabaseMigrateOnRuntimeMiddleware(RequestDelegate next, ILogger<DatabaseMigrateOnRuntimeMiddleware> logger)
{
private RequestDelegate Next { get; } = next;
public ILogger<DatabaseMigrateOnRuntimeMiddleware> Log { get; } = logger;

public async Task Invoke(HttpContext context, ITenantContextHolder contextHolder, IMemoryCache cache)
{
var dbContext = context.RequestServices.GetRequiredService<DatabaseContext>();

// master migrations
if (dbContext.GetType().IsAssignableTo(typeof(MasterDbContext)) || dbContext.GetType().IsAssignableTo(typeof(HybridDbContext)))
{
const string cacheMasterKey = $"{nameof(DatabaseMigrateOnRuntimeMiddleware)}_Master";
if (!cache.TryGetValue(cacheMasterKey, out _))
{
var dynamicDbContextFactory = context.RequestServices.GetRequiredService<IDynamicDbContextFactory>();
var masterDbContext = dynamicDbContextFactory.CreateDbContext<DatabaseContext, IMasterDbContextConfigurationProvider>();
await masterDbContext.Database.MigrateAsync();
cache.Set(cacheMasterKey, true);
}
}

await Next(context);
}
}
Database migrations — Hybrid database context
public static IApplicationBuilder UseHybridDatabaseInitialization(this IApplicationBuilder app)
{
app.UseMiddleware<DatabaseMigrateOnRuntimeMiddleware>();
return app;
}
using System.Threading.Tasks;
using ASxOL.MyOwnProject.Persistence;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.EFCore;
using ASOL.Core.Persistence.EFCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace ASxOL.MyOwnProject.API.Middlewares;

internal class DatabaseMigrateOnRuntimeMiddleware(RequestDelegate next, ILogger<DatabaseMigrateOnRuntimeMiddleware> logger)
{
private RequestDelegate Next { get; } = next;
public ILogger<DatabaseMigrateOnRuntimeMiddleware> Log { get; } = logger;

public async Task Invoke(HttpContext context, ITenantContextHolder contextHolder, IMemoryCache cache)
{
var dbContext = context.RequestServices.GetRequiredService<DatabaseContext>();

// tenant migrations
if (contextHolder.TenantContext?.IsTenantValid == true && dbContext.GetType().IsAssignableTo(typeof(TenantDbContext)) || dbContext.GetType().IsAssignableTo(typeof(HybridDbContext)))
{
var cacheKey = $"{nameof(DatabaseMigrateOnRuntimeMiddleware)}_{contextHolder.TenantContext?.TenantId}";

if (!contextHolder.TenantContext?.IsTenantValid == false && !cache.TryGetValue(cacheKey, out _))
{
var dynamicDbContextFactory = context.RequestServices.GetRequiredService<IDynamicDbContextFactory>();
var tenantDbContext = dynamicDbContextFactory.CreateDbContext<DatabaseContext, ITenantDbContextConfigurationProvider>();
await tenantDbContext.Database.MigrateAsync();
cache.Set(cacheKey, true);
}
}

// master migrations
if (dbContext.GetType().IsAssignableTo(typeof(MasterDbContext)) || dbContext.GetType().IsAssignableTo(typeof(HybridDbContext)))
{
const string cacheMasterKey = $"{nameof(DatabaseMigrateOnRuntimeMiddleware)}_Master";
if (!cache.TryGetValue(cacheMasterKey, out _))
{
var dynamicDbContextFactory = context.RequestServices.GetRequiredService<IDynamicDbContextFactory>();
var masterDbContext = dynamicDbContextFactory.CreateDbContext<DatabaseContext, IMasterDbContextConfigurationProvider>();
await masterDbContext.Database.MigrateAsync();
cache.Set(cacheMasterKey, true);
}
}

await Next(context);
}
}

Usage of application context (optional)

ApplicationContext middleware

// middleware to resolve application context

/// <summary>
/// Initialize application context.
/// </summary>
/// <param name="builder">The application builder.</param>
/// <returns>The application builder. (fluent api)</returns>
public static IApplicationBuilder UseApplicationContext(this IApplicationBuilder builder)
{
builder.UseMiddleware<ApplicationContextMiddleware>();

return builder;
}
// implementation of application context middleware

using System;
using System.Threading;
using System.Threading.Tasks;
using ASOL.Core.Multitenancy;
using ASOL.Core.Persistence;
using ASOL.Core.Persistence.EFCore;
using ASOL.Core.Persistence.MongoDb;
using My.App.API.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace My.App.API.Middlewares
{
/// <summary>
/// Middleware for initialization of application context.
/// </summary>
public class ApplicationContextMiddleware
{
/// <summary>
/// Initializes the instance
/// </summary>
/// <param name="next">Request delegate</param>
public ApplicationContextMiddleware(RequestDelegate next)
{
Next = next;
}

private RequestDelegate Next { get; }

/// <summary>
/// Initializes the application context.
/// </summary>
/// <param name="context">Http context</param>
/// <param name="tenantContext">Tenant context</param>
/// <returns>Task</returns>
public async Task Invoke(HttpContext context, ITenantContext tenantContext)
{
//note: application context (i.e. app isolation) is activated only for tenant context for now
if (!tenantContext.IsTenantValid)
{
await Next(context);
return;
}

//note: two variants are supported - applicationCode provided via routing segment or query parameter (preferred)
//string applicationCode = context.Request.RouteValues["applicationCode"]?.ToString();
string applicationCode = context.Request.Query["applicationCode"].ToString();
if (!string.IsNullOrEmpty(applicationCode))
{
var appContextProvider = context.RequestServices.GetRequiredService<ComplexComponentApplicationContextProvider>();
await appContextProvider.CheckIfApplicationExistsAsync(applicationCode);
//HACK (not supported in POC): await appContextProvider.CheckApplicationAuthorizationAsync(applicationCode, controllerContext);
ResolveApplicationContext(context.RequestServices, appContextProvider.ApplicationResolver, appContextProvider.Logger, applicationCode);
}

await Next(context);
}

public static void ResolveApplicationContext(IServiceProvider serviceProvider, IDbApplicationInfoResolver applicationResolver, ILogger logger, string applicationCode)
{
applicationResolver.TryGetInfoByApplicationCode(applicationCode, out IDbApplicationInfo applicationInfo);
switch (applicationInfo)
{
case IMongoDbApplicationInfo mongoDbAppInfo:
{
var appInfoHolder = serviceProvider.GetRequiredService<MongoDbApplicationInfoHolder>();
appInfoHolder.SetInfo(mongoDbAppInfo);
break;
}
case IEFCoreApplicationInfo efcoreDbAppInfo:
{
var appInfoHolder = serviceProvider.GetRequiredService<EFCoreApplicationInfoHolder>();
appInfoHolder.SetInfo(efcoreDbAppInfo);
break;
}
default:
{
logger.LogWarning($"Application '{applicationCode}' doesn't support db isolation.");
break;
}
}
}
}
}

Combined db scope selector to switch MongoDB and EFCore platforms based on application context

// implementation of combined db scope selector - hybrid usage with application context support

using System;
using ASOL.Core.Multitenancy.Contracts;
using ASOL.Core.Multitenancy.Persistence;
using ASOL.Core.Persistence;
using ASOL.Core.Persistence.EFCore;
using Microsoft.Extensions.DependencyInjection;

namespace My.App.EFCore
{
/// <summary>
/// Represents the master (MongoDB) / tenant (MongoDB or EFCore) db scope selector for repositories.
/// </summary>
/// <typeparam name="T">The repository interface type</typeparam>
public class CombinedDbScopeSelector<T> : IDbScopeSelector<T> where T : class, IRepository
{
private readonly IServiceProvider _serviceProvider;

private T _tenantRepository; //MongoDB or EFCore
private T _masterRepository; //MongoDB

/// .ctor
public CombinedDbScopeSelector(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

/// <inheritdoc cref="IDbScopeSelector{T}.GetRepository(bool)"/>
public virtual T GetRepository(bool useMasterDbScope)
{
return useMasterDbScope
? GetOrInitRepository(ref _masterRepository, useMasterDbScope)
: GetOrInitRepository(ref _tenantRepository, useMasterDbScope);
}

/// <inheritdoc cref="IDbScopeSelector{T}.GetRepository(DataAccessLevel, bool)"/>
public virtual T GetRepository(DataAccessLevel accessLevel, bool usePublicDbContextForAuto = false)
{
return accessLevel switch
{
DataAccessLevel.Auto => GetRepository(usePublicDbContextForAuto),
DataAccessLevel.Private => GetRepository(useMasterDbScope: false),
DataAccessLevel.Public => GetRepository(useMasterDbScope: true),
_ => throw new NotSupportedException($"AccessLevel '{accessLevel}' isn't supported."),
};
}

private T GetOrInitRepository(ref T repositoryField, bool useMasterDbScope)
{
//try get repository
if (repositoryField != null)
{
return repositoryField;
}

//initialize repository
if (useMasterDbScope)
{
//MongoDB always
var accessor = _serviceProvider.GetRequiredService<IMasterDbScope<T>>();
repositoryField = accessor.Instance;
}
else
{
//HACK - ignore application isolation of specific persisted entity, e.g. for FileUploadInfo
var ignoreAppIsolation = false; //e.g.: typeof(IReadOnlyRepository<FileUploadInfo>).IsAssignableFrom(typeof(T));

//note: EFCore when current application context is EFCore otherwise MongoDB,
// i.e. MongoDB when current application context is MongoDB or not defined
var efCoreInfo = _serviceProvider.GetService<IEFCoreApplicationInfo>(); //optional
if (ignoreAppIsolation || efCoreInfo == null)
{
var accessor = _serviceProvider.GetRequiredService<ITenantDbScope<T>>();
repositoryField = accessor.Instance;
}
else
{
var accessor = _serviceProvider.GetRequiredService<EFCoreRepoAccessor<T>>();
repositoryField = accessor.Instance;
}
}

return repositoryField;
}
}
}

MongoDB platform with application context

Tenant database migration middleware (MongoDB) - hybrid usage with application context

// implementation of migration middleware for tenant database - hybrid usage with application context support

using System.Threading;
using System.Threading.Tasks;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.MongoDb;
using ASOL.Core.Multitenancy.Persistence.Migrations;
using ASOL.Core.Persistence.EFCore;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;

namespace My.App.API.Middlewares
{
/// <summary>
/// Middleware for initializing the database model. Data migration, index migration and initial data seeding for the tenant database.
/// </summary>
public class TenantDatabaseMigrationMiddleware
{
/// <summary>
/// Initializes the instance
/// </summary>
/// <param name="next">Request delegate</param>
public TenantDatabaseMigrationMiddleware(RequestDelegate next)
{
Next = next;
}

private RequestDelegate Next { get; }

/// <summary>
/// Initializes the database model.
/// </summary>
/// <param name="context">Http context</param>
/// <param name="tenantContext">Tenant context</param>
/// <returns>Task</returns>
public async Task Invoke(HttpContext context, ITenantContext tenantContext, IMemoryCache cache)
{
if (!tenantContext.IsTenantValid)
{
await Next(context);
return;
}

//mongodb migration
var dbContext = context.RequestServices.GetRequiredService<ITenantDbContext>();
var migrationProcessor = context.RequestServices.GetRequiredService<ITenantDbMigrationProcessor>();
await migrationProcessor.ExecuteTenantMigrationsAsync(context.RequestServices, dbContext, context.User, CancellationToken.None);

//efcore migration - hybrid usage with application context
var efCoreAppInfo = context.RequestServices.GetService<IEFCoreApplicationInfo>();
if (efCoreAppInfo != null && efCoreAppInfo.IsValid)
{
_ = cache.GetOrCreate($"{nameof(TenantDatabaseMigrationMiddleware)}_{tenantContext.TenantId}_{efCoreAppInfo.ApplicationCode}", cacheEntry =>
{
//ASP database
var dbContext = context.RequestServices.GetRequiredService<DatabaseContext>();
dbContext.Database.Migrate();

//FS database (optional)
var dbContextFs = context.RequestServices.GetRequiredService<DatabaseContextFs>();
dbContextFs.Database.Migrate();

return true;
});
}

// one-time tenant initialization of additional services on startup (optional)
// ...

await Next(context);
}
}
}

Master database migration service (MongoDB) - hybrid usage with application context

// implementation of migration service for MongoDB Master database - hybrid usage with application context support

using System;
using System.Threading;
using System.Threading.Tasks;
using ASOL.Core.Identity;
using ASOL.Core.Identity.Contexts.Factories;
using ASOL.Core.Identity.Options;
using ASOL.Core.Multitenancy.MongoDb;
using ASOL.Core.Persistence.MongoDb.Migrations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace My.App.API.Services
{
/// <summary>
/// Master database migration service as background service. Execute when API service started.
/// </summary>
public class MasterDatabaseMigrationService : BackgroundService
{
/// <summary>
/// Initialize service
/// </summary>
public MasterDatabaseMigrationService(IServiceProvider provider, ILogger<MasterDatabaseMigrationService> logger)
{
Provider = provider;
Logger = logger;
}

/// <summary>
/// Global service provider
/// </summary>
protected IServiceProvider Provider { get; }

/// <summary>
/// The logger instance
/// </summary>
protected readonly ILogger Logger { get; }

/// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
using var scope = Provider.CreateScope();

var masterRuntimeContextHolder = scope.ServiceProvider.GetRequiredService<IScopeContextHolder<IMasterRuntimeContext>>();
var masterRuntimeContextFactory = scope.ServiceProvider.GetRequiredService<IMasterRuntimeContextFactory>();
masterRuntimeContextHolder.Context = masterRuntimeContextFactory.Create();

var processor = scope.ServiceProvider.GetRequiredService<IDbMigrationProcessor>();
var dbContextFactory = scope.ServiceProvider.GetRequiredService<IMasterRepoFactory>();
var dbContext = dbContextFactory.GetOrCreateMasterDbContext();
var options = RepoFactoryHelper.GetOptions(dbContext);
var ssoAuth = scope.ServiceProvider.GetRequiredService<IOptions<SsoAuthOptions>>();
var user = ClaimsPrincipalBuilder.CreateClientPrincipal(ssoAuth.Value.ClientId).AddAuthenticateType().Build();
await processor.ExecuteMigrationsAsync(scope.ServiceProvider, options, user, stoppingToken);
}
catch (Exception e)
{
logger.LogError(e, "Error while executing master database migration");
}
}
}
}
//RepoFactoryHelper with GetOptions method

using System;
using System.Reflection;
using ASOL.Core.Multitenancy.MongoDb;
using ASOL.Core.Persistence.MongoDb;
using ASOL.Core.Persistence.MongoDb.Options;

namespace My.App.MongoDb
{
/// <summary>
/// Represents the helper for <see cref="RepoFactory"/>.
/// </summary>
public class RepoFactoryHelper
{
public static MongoDbConnectionOptions GetOptions(IDbContext dbContext)
{
if (!(dbContext is DbContext))
{
throw new ArgumentException($"DbContext isn't derived from '{nameof(DbContext)}' class.");
}

var optionsProp = typeof(DbContext).GetProperty("Options", BindingFlags.NonPublic | BindingFlags.Instance);
return (MongoDbConnectionOptions)optionsProp.GetValue(dbContext, null);
}
}
}

EFCore platform with application context

See also EFCore platform examples for common usage without application context. See below EFCore platform examples for hybrid usage with application context.

Registration of repositories in Persistence layer (EFCore) - hybrid usage with application context

// extension method to register repositories in persistence layer - hybrid usage with application context support

using ASOL.Core.Persistence;
using ASOL.Core.Persistence.EFCore;
using ASOL.Core.Persistence.EFCore.Extensions;
using ASOL.IdentityManager.Authorization.Repositories;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace My.App.EFCore
{
public static class EfCoreDbServiceCollectionExtensions
{
public static IServiceCollection AddEfCorePersistence<TPersistenceConfigurationProvider>(this IServiceCollection services)
where TPersistenceConfigurationProvider : class, ITenantDbContextConfigurationProvider
{
//use tenantDb by default, see Startup (DatabaseContext, DatabaseContextConfigurationProvider)
services.AddTenantDbContext<DatabaseContext, TPersistenceConfigurationProvider>();

//hybrid usage with application context
services.AddEFCoreApplicationInfo();
services.AddScoped(typeof(IDbScopeSelector<>), typeof(CombinedDbScopeSelector<>));

// register all repositories
services.AddEFCoreTenantRepositoryAccessor<IMyExampleRepository, IMyExampleReadOnlyRepository, MyExampleRepository>();
services.AddEFCoreTenantRepositoryAccessor<IDocumentPermissionTypedRepository<MyExample>, IDocumentPermissionTypedReadOnlyRepository<MyExample>, MyExampleDocumentPermissionTypedRepository>();
// ...

return services;
}
}
}

Tenant database migration middleware (EFCore) - hybrid usage with application context

// extension method for tenant database migration middleware
public static IApplicationBuilder UseTenantDatabaseInitialization(this IApplicationBuilder builder)
{
builder.UseMiddleware<TenantDatabaseMigrationMiddleware>();
return builder;
}
// implementation of migration middleware for tenant database - hybrid usage with application context support

using System.Threading;
using System.Threading.Tasks;
using ASOL.Core.Multitenancy;
using ASOL.Core.Multitenancy.MongoDb;
using ASOL.Core.Multitenancy.Persistence.Migrations;
using ASOL.Core.Persistence.EFCore;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;

namespace My.App.API.Middlewares
{
/// <summary>
/// Middleware for initializing the database model. Data migration, index migration and initial data seeding for the tenant database.
/// </summary>
public class TenantDatabaseMigrationMiddleware
{
/// <summary>
/// Initializes the instance
/// </summary>
/// <param name="next">Request delegate</param>
public TenantDatabaseMigrationMiddleware(RequestDelegate next)
{
Next = next;
}

private RequestDelegate Next { get; }

/// <summary>
/// Initializes the database model.
/// </summary>
/// <param name="context">Http context</param>
/// <param name="tenantContext">Tenant context</param>
/// <returns>Task</returns>
public async Task Invoke(HttpContext context, ITenantContext tenantContext, IMemoryCache cache)
{
if (!tenantContext.IsTenantValid)
{
await Next(context);
return;
}

//mongodb migrations based on application context
var dbContext = context.RequestServices.GetRequiredService<ITenantDbContext>();
var migrationProcessor = context.RequestServices.GetRequiredService<ITenantDbMigrationProcessor>();
await migrationProcessor.ExecuteTenantMigrationsAsync(context.RequestServices, dbContext, context.User, CancellationToken.None);

//efcore migrations based on application context
var efCoreAppInfo = context.RequestServices.GetService<IEFCoreApplicationInfo>();
if (efCoreAppInfo != null && efCoreAppInfo.IsValid)
{
_ = cache.GetOrCreate($"{nameof(TenantDatabaseMigrationMiddleware)}_{tenantContext.TenantId}_{efCoreAppInfo.ApplicationCode}", cacheEntry =>
{
//ASP database
var dbContext = context.RequestServices.GetRequiredService<DatabaseContext>();
dbContext.Database.Migrate();

//FS database
var dbContextFs = context.RequestServices.GetRequiredService<DatabaseContextFs>();
dbContextFs.Database.Migrate();

return true;
});
}

// one-time tenant initialization of additional services on startup (optional)
// ...

await Next(context);
}
}
}