Dynamic EfCore DbContext
Introduction
Developers choose to use entity-framework-core (EF Core) to demonstrate some features and techniques to make the persistence layer more flexible and more generic, also the fact that EF Core integrates well with aspnetcore, identity, and openIdDict to manage the different authorization stores.
First things first, as we explained earlier the application should be composed of different parts, each of which has a certain responsibility, and at the host level we configure the different parts, and at certain level we have a component that depends on another which will result in a tight coupling between them, to avoid this we simply use abstractions to have our differents layers depends on the abstraction rather than the concrete implementation, in our case we used an interface which will represent our DbContext, and we register it using AddDbContext<,> variation which associates a DbContext implementation with a service interface:
namespace NLayersApp.Persistence.Abstractions
{
public interface IContext: IDisposable
{
DbSet<T> Set<T>() where T: class;
Task SaveChangesAsync(CancellationToken cancellationToken);
IMutableModel ExternalModel { get; set; }
IModel Model { get; }
}
}
Our interface is simple and clean and contains just the minimum we need, a method to access DbSets generically, a Model and ExternalModel property which will represents the current model.
The implementation part is more tricky, and a seems at first look little bit complex, let's jump into the code:
public class TDbContext<TUser, TRole, TKey> : IdentityDbContext<TUser, TRole, TKey>, IContext
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>
{
private readonly Type[] _types;
private readonly ITypesResolver _innerTypesResolver;
public TDbContext(DbContextOptions options) : base(options)
{
}
public TDbContext(DbContextOptions options, ITypesResolver typesResolver) : this(options)
{
_innerTypesResolver = typesResolver;
}
/* omitted for brevity */
}
Our DbContext inherits IdentityDbContext, and at the same time implements our IContext interface, to keep the possibility to customize our users and roles entities we pass the generic argument parameter through the inheritance declaration, and we constraints our types to be childs of respectively IdentityUser<TKey>, IdentityRole<TKey>, IEquatable<TKey> , also we have two constructors the default one that reference call the base class constructor, and a second one which we use to load our ITypesResolver implementation, here is our ITypesResolver declaration:
public interface ITypesResolver
{
Type[] RegisteredTypes { get; }
Type Resolve(string typeName);
Func<string, Type> ResolveAction { get; }
}
Our context will loop through the exposed RegisteredTypes property to register each type as a DbSet:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.RegisterTypes(_innerTypesResolver.RegisteredTypes);
base.OnModelCreating(builder);
}
The RegisterTypes method is an extension method that will register types and apply shadow properties to them, you should ask about shadow properties, the answer is an awesome capability that makes it possible to add properties that are part of the Model but invisible to the application code, and are not present in our entities declarations, which is ideal for properties from aspects like auditing, below is the code of our the DbSetExtensions class:
DbSetExtension class
public static class ModelBuilderExtensions
{
public static void RegisterTypes(this ModelBuilder builder, params Type[] types)
{
foreach (var current in types)
{
builder.Entity(current);
builder.applyProperties(current);
}
}
The method will loop through the registered types and call applyProperties private extension method, which in turn get a reference to the _applyProperties<TType> method to call it using the type variable through the MakeGenericMethod reflection method.
private static void applyProperties(this ModelBuilder builder, Type type)
=> typeof(ModelBuilderExtensions)
.GetMethod(nameof(_applyProperties), BindingFlags.NonPublic | BindingFlags.Static)
.MakeGenericMethod(type)
.Invoke(builder, new[] { builder });
/* the applyProperties method above will call _applyProperties through reflection to pass the generic types arguments */
private static void _applyProperties<TType>(ref ModelBuilder builder)
where TType: class
{
builder
.AddAuditProperties<TType, Guid>()
.AddIsDeletedProperty<TType>();
}
The _applyProperties it self do nothing but calling the AddAuditProperties<TType, TKey> and AddIsDeletedProperty<TType> to add the shadow properties to our model definition:
public static ModelBuilder AddAuditProperties<TType, TKey>(this ModelBuilder builder)
where TType: class
{
builder.Entity<TType>().Property<TKey>("CreatedBy");
builder.Entity<TType>().Property<TKey>("ModifiedBy");
builder.Entity<TType>().Property<DateTime>("CreatedOn");
builder.Entity<TType>().Property<DateTime>("ModifiedOn");
return builder;
}
public static ModelBuilder AddIsDeletedProperty<TType>(this ModelBuilder builder)
where TType: class
{
builder.Entity<TType>().Property<bool>("IsDeleted");
return builder;
}
}
At the end of the process we have a model with identity entities and the registered types passed through the ITypesResolver.RegisteredTypes property.