We resolved Audit, UnitOfWork and dynamic schema problems with an abstract DbContext that implemented these interfaces:
We’ll explain these interfaces below, but remember to call ConfigureServices( serviceProvider )
for your legacy DbContext in two ways:
// call inside constructor
public TimerDbContext(IServiceProvider serviceProvider, DbContextOptions<TimerDbContext> options)
: base(options)
{
ConfigureServices(serviceProvider);
}
public class TimerDbContextScopedFactory : IDbContextFactory<TimerDbContext>
{
private readonly IDbContextFactory<TimerDbContext> _pooledFactory;
private readonly IServiceProvider _serviceProvider;
public TimerDbContextScopedFactory(
IDbContextFactory<TimerDbContext> pooledFactory,
IServiceProvider serviceProvider)
{
_pooledFactory = pooledFactory;
_serviceProvider = serviceProvider;
}
public TimerDbContext CreateDbContext()
{
var context = _pooledFactory.CreateDbContext();
context.ConfigureServices(_serviceProvider);
return context;
}
}
Then register PooledDbContext
services.AddPooledDbContextFactory<TimerDbContext>((serviceProvider, options) =>
{
// configure DB options
});
services.AddScoped<TimerDbContextScopedFactory>();
services.AddScoped(sp => sp.GetRequiredService<TimerDbContextScopedFactory>().CreateDbContext());
The abstract class DbOptions contains:
The DbOptions<T> class derived from DbOptions will specify a DbContext type that it is used for.
Database context classes derived from DbContextBase will:
We provide IAuditableDbContext interface and its extensions to track changes, update audit columns, and finally, dispatch data change events.
public interface IAuditableDbContext
{
List<AuditEntry>? PendingAuditEntries { get; }
string? User { get; }
}
Database context classes derived from IAuditableDbContext will:
There are two ways to designate an entity type as auditable:
using Juice.Domain;
public class Content: IAuditable{
...
}
If your entity does not contain basic audit info properties (CreatedUser/ModifiedUser/CreatedDate/ModifiedDate) you can not access them but these columns are still available in your DB table.
using Juice.EF.Extensions;
public class ContentDbContext: DbContextBase{
...
protected override void ConfigureModel(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Content>(options =>
{
options.ToTable(nameof(Content), Schema);
options.IsAuditable();
});
}
}
You can see Audit service for more details.
This interface has only one purpose which is support to migrating the DBContext with an optional schema. After running Add-Migration command, you must modify the generated migration file to customize the schema.
...
using Juice.EF;
...
public partial class InitEventLog : Migration
{
private readonly ISchemaDbContext _schema;
public InitEventLog() { }
// add one more constructor to inject ISchemaDbContext service
public InitEventLog(ISchemaDbContext schema)
{
_schema = schema;
}
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: _schema.Schema);
migrationBuilder.CreateTable(
name: "IntegrationEventLog",
schema: _schema.Schema,
columns: table => new
...
You also need to replace the original IMigrationsAssembly with DbSchemaAwareMigrationAssembly in DbContextOptionsBuilder
...
using Juice.EF.Migrations;
...
services.AddDbContext<IntegrationEventLogContext>(options =>
{
...
options.ReplaceService<IMigrationsAssembly, DbSchemaAwareMigrationAssembly>();
});
You can read more about Unit of Work before continue. We define an IUnitOfWork interface to easy to work with DbContext transaction.
public interface IUnitOfWork
{
IDbContextTransaction? GetCurrentTransaction();
bool HasActiveTransaction { get; }
Task<IDbContextTransaction?> BeginTransactionAsync();
Task CommitTransactionAsync(IDbContextTransaction transaction);
void RollbackTransaction();
}
This abstract class has similar behaviors to DbContextBase above but added TenantInfo so we can pass tenant to DB or filter data by tenant. It is a bit different from the official MultiTenantDbContext from Finbuckle: it still works even though TenantInfo can not be resolved.
Sometimes we maybe want to have entities that can be shared from the Root tenant and accessible to other tenants (Ex: settings, users…). So we provide an extension to describe an entity as cross-tenant.
// IdentityDbContext.cs
using Juice.MultiTenant.EF.Extensions;
...
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// configure any type that has Multitenant attribute
builder.ConfigureMultiTenant();
// describe that User is cross tenant
builder.Entity<UserIdentity>().ToTable(TableConsts.IdentityUsers).IsCrossTenant();
...
}
What exactly does it do?
NOTE
The library can be accessed via Nuget: