I have a custom component with its own Migrations, DbContext & UnitOfWork. I'm trying to upgrade it to use EF Core & Lamar.
THE GOAL:
1. When people don't pass-in DbContextOptions...configure using the defaults in OnConfiguring
2. When people pass-in DbContextOptions use them...and ignore the defaults in OnConfiguring
PLEASE SEE UPDATES AT THE BOTTOM
The reason I want this...
People should be able to configure a different connection string (using their own name).
People should be able to configure the in-memory databases for their own Unit Tests.
THE ISSUE:
I have tried 3 different approaches (see below)...but all of them result in (at least) 1 of the following:
1. Lamar isn't respecting the parameter in my Constructor Selection
2. Lamar passes in its own set of empty DbContextOptions (and ignores mine)
Please take a look at each approach before answering.
Please note...getting rid of the default constructor still results in an empty DbContextOptions
APPROACH NUMBER 1: The SelectConstructor Option (see images below)
Here, the user utilizes the defaults & has "DefaultDb" in their app settings file.
When calling from the CONSOLE APPLICATION...the default constructor gets called & all is well.
When calling from the UNIT TEST...BOTH constructors get called...AND the "UseInMemoryDatabase" options are ignored.
// ------------------------------------------
// CONSOLE APPLICATION PROJECT CONFIGURATION
Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.LookForRegistries();
scan.SingleImplementationsOfInterface();
});
// Notice no constructor options are defined here (at all)
For<DbContext>().Use<WorkflowComponentDbContext>();
ForConcreteType<WorkflowComponentUnitOfWork>().Configure.Setter<DbContext>().Is<WorkflowComponentDbContext>();
// -------------------------------
// UNIT TEST PROJECT CONFIGURATION
var settings = BuildAppConfiguration();
var optionsBuilder = new DbContextOptionsBuilder<WorkflowComponentDbContext>();
optionsBuilder.UseInMemoryDatabase("WorkflowComponentDb");
// Notice the "SelectConstructor" option is used here
ForConcreteType<WorkflowComponentDbContext>().Configure.SelectConstructor(() => new WorkflowComponentDbContext(optionsBuilder.Options));
// ------------------
// Concrete DbContext
// NOTE: Some code is excluded for brevity
public class WorkflowComponentDbContext : DbContext
{
private IConfigurationRoot _settings;
[DefaultConstructor] //<-- Added due to "Greediest Constructor" requirement in Lamar
public WorkflowComponentDbContext()
{
// If used...optionsBuilder.IsConfigured value in "OnConfiguring" should always be FALSE
BuildAppConfiguration();
}
public WorkflowComponentDbContext(DbContextOptions<WorkflowComponentDbContext> options) : base(options)
{
// If used...optionsBuilder.IsConfigured value should always be TRUE
}
private void BuildAppConfiguration()
{
var builder = new ConfigurationBuilder();
builder.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
_settings = builder.Build();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if(!optionsBuilder.IsConfigured)
optionsBuilder.UseSqlServer(_settings.GetConnectionString("DefaultDb"));
}
}
APPROACH NUMBER 2: The Configure.Ctor option (see images below)
Here, the user utilizes the defaults & has "DefaultDb" in their appsettings file.
When calling from the CONSOLE APPLICATION...the default constructor gets called & all is well.
When calling from the UNIT TEST...only the DEFAULT constructor gets called...and the "OnConfiguring" options are incorrectly applied.
// ------------------------------------------
// CONSOLE APPLICATION PROJECT CONFIGURATION
Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.LookForRegistries();
scan.SingleImplementationsOfInterface();
});
// Notice no constructor-options are defined here (at all)
For<DbContext>().Use<WorkflowComponentDbContext>();
ForConcreteType<WorkflowComponentUnitOfWork>().Configure.Setter<DbContext>().Is<WorkflowComponentDbContext>();
// -------------------------------
// UNIT TEST PROJECT CONFIGURATION
var settings = BuildAppConfiguration();
var optionsBuilder = new DbContextOptionsBuilder<WorkflowComponentDbContext>();
optionsBuilder.UseInMemoryDatabase("WorkflowComponentDb");
// Notice the "Configure.Ctor" option is used here
ForConcreteType<WorkflowComponentDbContext>().Configure.Ctor<DbContextOptions<WorkflowComponentDbContext>>().Is(optionsBuilder.Options);
// ------------------
// Concrete DbContext
// NOTE: Some code excluded for brevity
public class WorkflowComponentDbContext : DbContext
{
private IConfigurationRoot _settings;
[DefaultConstructor] //<-- Added due to "Greediest Constructor" requirement in Lamar
public WorkflowComponentDbContext()
{
// If used...optionsBuilder.IsConfigured value in "OnConfiguring" should always be FALSE
BuildAppConfiguration();
}
public WorkflowComponentDbContext(DbContextOptions<WorkflowComponentDbContext> options) : base(options)
{
// If used...optionsBuilder.IsConfigured value should always be TRUE
}
private void BuildAppConfiguration()
{
var builder = new ConfigurationBuilder();
builder.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
_settings = builder.Build();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if(!optionsBuilder.IsConfigured)
optionsBuilder.UseSqlServer(_settings.GetConnectionString("DefaultDb"));
}
}
APPROACH NUMBER 3: Remove the Default Constructor (see images below)
Here, I remove the default-constructor entirely & rely-on DbContextOptions getting set.
When calling from the CONSOLE APPLICATION...the DbContextOptions constructor gets called...but the options are empty.
When calling from the UNIT TEST...the DbContextOptions constructor gets called...but the options are empty.
// ------------------------------------------
// CONSOLE APPLICATION PROJECT CONFIGURATION
Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.LookForRegistries();
scan.SingleImplementationsOfInterface();
});
// DbContextOptions (trying to create a way to add ConnectionStrings Dynamically)
var settings = BuildAppConfiguration();
var optionsBuilder = new DbContextOptionsBuilder<WorkflowComponentDbContext>();
optionsBuilder.UseSqlServer(settings.GetConnectionString("WorkflowComponentDb"));
// Neithier of these CTOR options work
//ForConcreteType<WorkflowComponentDbContext>().Configure.Ctor<DbContextOptions<WorkflowComponentDbContext>>().Is(optionsBuilder.Options);
ForConcreteType<WorkflowComponentDbContext>().Configure.SelectConstructor(() => new WorkflowComponentDbContext(optionsBuilder.Options));
// -------------------------------
// UNIT TEST PROJECT CONFIGURATION
var settings = BuildAppConfiguration();
var optionsBuilder = new DbContextOptionsBuilder<WorkflowComponentDbContext>();
optionsBuilder.UseInMemoryDatabase("WorkflowComponentDb");
// Neithier of these CTOR options work
//ForConcreteType<WorkflowComponentDbContext>().Configure.Ctor<DbContextOptions<WorkflowComponentDbContext>>().Is(optionsBuilder.Options);
ForConcreteType<WorkflowComponentDbContext>().Configure.SelectConstructor(() => new WorkflowComponentDbContext(optionsBuilder.Options));
// ------------------
// Concrete DbContext
// NOTE: Some code excluded for brevity
public class WorkflowComponentDbContext : DbContext
{
private IConfigurationRoot _settings;
public WorkflowComponentDbContext(DbContextOptions<WorkflowComponentDbContext> options) : base(options)
{
// Lamar is not passing-in the concrete options that I created
}
private void BuildAppConfiguration()
{
var builder = new ConfigurationBuilder();
builder.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
_settings = builder.Build();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if(!optionsBuilder.IsConfigured)
optionsBuilder.UseSqlServer(_settings.GetConnectionString("DefaultDb")); //<--- This should not get called...but does
}
}
UPDATES:
The original author of Lamar gave me a couple clues "to think about". Although I am still having the same problems...below are the current changes resulting from our discussion.
CURRENT ISSUE
Discussed changes are as follows:
"How I am registering"
I am using the Bootstrapping a Container approach by passing-in a ServiceRegistry...and then using the resulting Container to create an instance.
"There’s no magic crossover between the 1st & 2nd registrations"
In "ATTEMPT 1" both constructors were getting called. This was because I had 2 configfurations for WorkflowComponentDbContext
. I removed the first one & rewrote the configuration as follows:
//For<DbContext>().Use<WorkflowComponentDbContext>();
For<DbContext>().Use<WorkflowComponentDbContext>().SelectConstructor(() => new WorkflowComponentDbContext(optionsBuilder.Options));
“Lamar isn't respecting the parameter in my Constructor Selection”
I'm still having this problem. So, I've decided to remove the Default Constructor
until I can fix it.
// --------------------------
// The new Concrete DbContext
// NOTE: Some code is excluded for brevity
public class WorkflowComponentDbContext : DbContext
{
private IConfigurationRoot _settings;
public WorkflowComponentDbContext(DbContextOptions<WorkflowComponentDbContext> options) : base(options)
{
// ISSUE: The options I'm receiving are still empty
}
private void BuildAppConfiguration()
{
var builder = new ConfigurationBuilder();
builder.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
_settings = builder.Build();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if(!optionsBuilder.IsConfigured)
optionsBuilder.UseSqlServer(_settings.GetConnectionString("DefaultDb"));
}
}
// ------------------------------------------
// CONSOLE APPLICATION PROJECT CONFIGURATION
Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.LookForRegistries();
scan.SingleImplementationsOfInterface();
});
// DbContextOptions
var settings = BuildAppConfiguration();
var optionsBuilder = new DbContextOptionsBuilder<WorkflowComponentDbContext>();
optionsBuilder.UseSqlServer(settings.GetConnectionString("WorkflowComponentDb"));
For<DbContext>().Use<WorkflowComponentDbContext>().Singleton().SelectConstructor(() => new WorkflowComponentDbContext(optionsBuilder.Options));
As it turns out...
Here is the layout of projects
LIBRARIES Sample.WorkflowComponent (library)
SERVICE PROJECT Sample.Console (application layer)
THE CODE:
// BUSINESS LIBRARY - Concrete DbContext
public class WorkflowComponentDbContext : DbContext
{
#region <Fields & Constants>
private IConfigurationRoot _settings;
#endregion
#region <Constructors>
public WorkflowComponentDbContext()
{
// NOTE: You must have this constructor for Migrations to work
BuildAppConfiguration();
}
public WorkflowComponentDbContext(DbContextOptions<WorkflowComponentDbContext> options) : base(options)
{
// This will always be the DbContextOptions passed-in by the HostBuilder's AppConfiguration
BuildAppConfiguration();
}
#endregion
#region <Properties>
public virtual DbSet<ContextType> Context { get; set; }
public virtual DbSet<ObjectState> ObjectState { get; set; }
public virtual DbSet<ObjectStateEvent> ObjectStateEvent { get; set; }
public virtual DbSet<Workflow> Workflow { get; set; }
public virtual DbSet<WorkflowEvent> WorkflowEvent { get; set; }
public virtual DbSet<WorkflowTransition> WorkflowTransition { get; set; }
#endregion
#region <Functions>
// TABLE-VALUE Function
public virtual IQueryable<ObjectStateDetail> GetCurrentObjectState(long contextId, string contextTypeFullName) => FromExpression(() => GetCurrentObjectState(contextId, contextTypeFullName));
public virtual IQueryable<ObjectStateDetail> ListObjectStates(long contextId, string contextTypeFullName) => FromExpression(() => ListObjectStates(contextId, contextTypeFullName));
public virtual IQueryable<ObjectStateEventDetail> ListObjectStateEvents(long contextId, string contextTypeFullName) => FromExpression(() => ListObjectStateEvents(contextId, contextTypeFullName));
#endregion
#region <Methods>
protected void BuildAppConfiguration()
{
var builder = new ConfigurationBuilder();
builder.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
_settings = builder.Build();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Default Configuration
if(!optionsBuilder.IsConfigured)
optionsBuilder.UseSqlServer(_settings.GetConnectionString(JsonSettings.ConnectionStrings.DefaultDb));
}
/// <summary>Pre-convention model configuration</summary>
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
// None shown here
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Data Transfer Objects (keyless types)
builder.ApplyConfiguration(new ObjectStateDetailConfiguration());
builder.ApplyConfiguration(new ObjectStateEventDetailConfiguration());
// GLOBAL Configurations
builder.ApplyGlobalEntityConfiguration(new AuditableStandardUserNameGlobalConfiguration());
// ENTITY Configurations
builder.ApplyConfiguration(new ContextTypeConfiguration());
builder.ApplyConfiguration(new ObjectStateConfiguration());
builder.ApplyConfiguration(new ObjectStateEventConfiguration());
builder.ApplyConfiguration(new WorkflowConfiguration());
builder.ApplyConfiguration(new WorkflowEventConfiguration());
builder.ApplyConfiguration(new WorkflowTransitionConfiguration());
// ADD Functions
var methodGetCurrentObjectState = typeof(WorkflowComponentDbContext).GetMethod(nameof(GetCurrentObjectState), new[] { typeof(long), typeof(string) });
builder.HasDbFunction(methodGetCurrentObjectState)
.HasSchema("component")
.HasName("tvfn_GetCurrentObjectState");
var methodListObjectStates = typeof(WorkflowComponentDbContext).GetMethod(nameof(ListObjectStates), new[] { typeof(long), typeof(string) });
builder.HasDbFunction(methodListObjectStates)
.HasSchema("component")
.HasName("tvfn_ListObjectStates");
var methodListObjectStateEvents = typeof(WorkflowComponentDbContext).GetMethod(nameof(ListObjectStateEvents), new[] { typeof(long), typeof(string) });
builder.HasDbFunction(methodListObjectStateEvents)
.HasSchema("component")
.HasName("tvfn_ListObjectStateEvents");
}
#endregion
}
// BUSINESS LIBRARY - Lamar Container Registry
public class ContainerRegistry : ServiceRegistry
{
public ContainerRegistry()
{
Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.LookForRegistries();
scan.SingleImplementationsOfInterface();
});
// --------
// DATABASE
For(typeof(IAuditResolverOf<>)).Use(typeof(StandardUserNameAuditResolver<>));
For(typeof(IAuditableRepository<>)).Use(typeof(GenericAuditableRepository<>));
For(typeof(IWindowsIdentityHelper)).Use(typeof(WindowsIdentityHelper));
// Policies
Policies.Add<GenericAuditableRepositoryConfiguredInstancePolicy>();
Policies.Add<UnitOfWorkConfiguredInstancePolicy>();
}
}
// BUSINESS LIBRARY - IoC (Single Point of Entry)
public static class IoC
{
#region <Methods>
public static ServiceRegistry Build()
{
var registry = new ServiceRegistry();
// Registration
registry.IncludeRegistry<ContainerRegistry>();
return new ServiceRegistry(registry);
}
#endregion
}
// CONSOLE APPLICATION - IoC (consumes all desired Registries)
public static class IoC
{
#region <Methods>
public static ServiceRegistry Build()
{
var registry = new ServiceRegistry();
// Registration
registry.IncludeRegistry<Bushido.WorkflowComponent.DependencyResolution.ContainerRegistry>();
registry.IncludeRegistry<Bushido.Project.Business.DependencyResolution.ContainerRegistry>();
return new ServiceRegistry(registry);
}
#endregion
}
// CONSOLE APPLICATION - Program
using Bushido.Framework.Configuration;
using Bushido.Project.Business.Data;
using Bushido.WorkflowComponent.Data;
using Bushido.WorkflowComponent.Integration.DependencyResolution;
using Lamar;
using Lamar.Microsoft.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Reflection;
// -----------
// STARTS HERE
Console.WriteLine("Working...");
var container = Build(args); //<-- Args are inferred
var project_UnitOfWork = container.GetRequiredService<ProjectUnitOfWork>();
var component_UnitOfWork = container.GetRequiredService<WorkflowComponentUnitOfWork>();
Console.WriteLine("Finshed...");
#region <Methods>
static IContainer Build(string[] args)
{
IHostBuilder hostBuilder = CreateHostBuilder(args);
IHost host = hostBuilder.Build();
IServiceScope serviceScope = host.Services.CreateScope();
IServiceProvider serviceProvider = serviceScope.ServiceProvider;
return serviceProvider.GetRequiredService<IContainer>();
}
static IHostBuilder CreateHostBuilder(string[] args)
{
// TRANSIENT objects are always different; a new instance is provided to every controller and every service.
// SCOPED objects are the same within a request, but different across different requests.
// SINGLETON objects are the same for every object and every request.
var builder = new HostBuilder()
.ConfigureAppConfiguration((hostingContext, config) =>
{
var assemblyConfigurationAttribute = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
var buildConfiguration = assemblyConfigurationAttribute?.Configuration;
switch (buildConfiguration)
{
case "Development":
case "ModelOffice":
case "Production":
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{buildConfiguration}.json", optional: true, reloadOnChange: true);
break;
case "Release":
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.Production.json", optional: true, reloadOnChange: true);
break;
default:
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
break;
}
})
.UseServiceProviderFactory<ServiceRegistry>(new LamarServiceProviderFactory())
.ConfigureServices((hostContext, services) =>
{
var connectionString = hostContext.Configuration.GetConnectionString(JsonSettings.ConnectionStrings.WorkflowComponentDb);
services.AddLamar(IoC.Build());
services.AddScoped<ProjectUnitOfWork>();
services.AddScoped<WorkflowComponentUnitOfWork>();
services.AddScoped<ProjectDbContext>();
services.AddScoped<WorkflowComponentDbContext>();
services.AddDbContext<ProjectDbContext>((provider, options) => { options.UseSqlServer(connectionString); }); //<-- Singleton?
services.AddDbContext<WorkflowComponentDbContext>((provider, options) => { options.UseSqlServer(connectionString); }); //<-- Singleton?
})
.UseConsoleLifetime();
return builder;
}
#endregion