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
- Lamar isn't maintaining the DbContextOptions parameter that I'm passing into the Constructor Selection
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));