In this post, we will show different ways to configure our application using the ASP.NET CORE 3.x and create a flexible Startup class.
Table of Contents
When we are configuring the DbContext in an ASP.NET Core web application, we typically use AddDbContext
extension method as follows:
services.AddDbContext<xxxDbContext>(dbContextOptionsBuilder =>
dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
("The name of the connection string in the configuration file.")
));
services.AddDbContext<xxxDbContext>((serviceProvider, dbContextOptionsBuilder) =>
{
var service = serviceProvider.GetService<xxx>();
dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
("The name of the connection string in the configuration file.");
});
If we take a closer look at the parameter of the AddDbContext
extension method, we’ll find that is an action, and through an Action, we can encapsulate a method, delegate, in-line delegate, lambda, etc.
In this case, the Action must construct the DbContext options.
What interests us most is configuring the DbContext according to our configuration ‘{environment}settings.json’.
How do we implement this scenario and why?
The answer to the question why would we do this:
We want to dramatically simplify and improve the experience for configuring DbContext and make them truly composable, where we can try to create a flexible scenario that automatically configures DbContext so that it can encapsulate a complete feature and provide an instant utility without having to go through various steps on how to manually configure it in different places in the Startup
configuration class.
From this point on, we will try to figure out how to implement this scenario and try to simplify all the concepts that we will encounter.
We will try to start with the main class through which we can start configuring the program that we are working on.
It is definitely a Startup
class, and I am aware in advance that everyone who writes ASP.NET Core code is familiar with it, and knows what they are doing in detail, but let’s get over it in a simple and quick way to talk.
Each ASP.NET Core application must have its own configuration code to configure the app’s services and to create the app’s request processing pipeline.
We can do this in two different ways:
By calling ConfigureServices and Configure
convenience methods on the host builder.
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder
(string[] args) => Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) => { })
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureServices(services =>
{
services.AddControllersWithViews();
}
)
.Configure(app =>
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
}
});
});
}
The Startup
class name is a convention by ASP.NET Core, we can give any name to the Startup
class.
Optionally, the Startup
class has two methods, the ConfigureServices method that tells ASP.NET Core which features are available and the Configure method that tells it how to use it.
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
}
}
When we start the ASP.NET Core application, the ASP.NET Core creates a new instance of the Startup
class and calls the ConfigureServices method to create its services. Then it calls the Configure method that sets up the request pipeline to handle incoming HTTP requests.
The Startup
class is typically specified by calling the WebHostBuilderExtensions.UseStartup<TStartup> method on the host builder:
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}
Thus, the Startup
class in our program will initially look like this:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<xxxDbContext>(dbContextOptionsBuilder =>
dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
("The name of the connection string in the configuration file.")
));
services.AddDbContext<xxxDbContext>((serviceProvider, dbContextOptionsBuilder) =>
{
var service = serviceProvider.GetService<xxx>();
dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
("The name of the connection string in the configuration file."));
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
}
}
But such a DbContext will be found in small programs and sometimes in medium programs, but in large programs, it will be difficult to manage the program with one DbContext.
We will see that we have to split a single DbContext into multiple DbContext serving the same specific context, and so we will have a group of DbContext that we always want to configure in all development environments, and we have to take that into account, sometimes we also change the entity framework provider.
At first, you will think that the matter is simple and what is to change the ConnectionString and everything will be fine, this is what we will also do, but in a different way, which we will see later, but at this moment, don’t forget what we said earlier that sometimes we change the data Entity Framework Provider.
Let us give an example to illustrate the problem in a test environment. Usually, we need to change the Entity Framework provider to an InMemory provider or a Sqlite provider and in the Sqlite provider, there are two possible scenarios either in InMemory or in System File.
However, these providers are not exclusive but are more commonly used in with SQL.
Now let’s get out of this context and talk about the most important pattern to designing such large DbContext, which is Bounded Context.
Bounded Context is a central pattern in Domain-Driven Design. It is the focus of DDD’s strategic design section which is all about dealing with large models and teams. DDD deals with large models by dividing them into different Bounded Contexts and being explicit about their interrelationships.
When you’re working with a large model and a large application, there are numerous benefits to designing smaller, more-compact models that are targeted to specific application tasks, rather than having a single model for the entire solution. In this column, I’ll introduce you to a concept from domain-driven design (DDD) — Bounded Context — and show you how to apply it to build a targeted model with EF, focusing on doing this with the greater flexibility of the EF Code First feature. If you’re new to DDD, this a great approach to learn even if you aren’t committing fully to DDD. And if you’re already using DDD, you’ll benefit by seeing how you can use EF while following DDD practices.
Accordingly to this pattern, we have to split a single DbContext into multiple DbContexts, our new Startup
class will become:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services
.AddDbContextPool<DbContextA>(dbContextOptionsBuilder =>
dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
("The name of the connection string in the configuration file.")
))
.AddDbContextPool<DbContextB>(dbContextOptionsBuilder =>
dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
("The name of the connection string in the configuration file.")
))
.AddDbContextPool<DbContextC>(dbContextOptionsBuilder =>
dbContextOptionsBuilder.UseSqlServer(Configuration.GetConnectionString
("The name of the connection string in the configuration file.")
));
}
}
We will notice from the above that any small change can lead to a major modification in our code.
So now, we’ll use some design patterns and OOP principles and create some classes that will help us facilitate this process.
To make our application settings more organized, and to provide strongly typed access to groups of related settings, we will use the Options pattern in ASP.NET Core.
We will create two new classes to configure the connection string settings and the entity framework provider settings:
public class ConnectionStringsOptions : IOptions<ConnectionStringsOptions>
{
public const string KEY_NAME = "ConnectionStringsOptions";
public ConnectionStringsOptions() : this(null, null, null, null) { }
public ConnectionStringsOptions(string serverName, string databaseName,
string userId, string password)
{
ServerName = serverName;
DatabaseName = databaseName;
UserId = userId;
Password = password;
}
public string ServerName { get; set; }
public string DatabaseName { get; set; }
public string UserId { get; set; }
public string Password { get; set; }
ConnectionStringsOptions IOptions<ConnectionStringsOptions>.Value => this;
}
public static class EntityFrameworkProviders
{
public static string SqlServer = "SQL-SERVER";
public static string SQLite = "SQLITE";
public static string InMemor = "IN-MEMOR";
}
public class EntityFrameworkOptions : IOptions<EntityFrameworkOptions>
{
public const string KEY_NAME = "EntityFrameworkOptions";
public EntityFrameworkOptions() : this(EntityFrameworkProviders.SqlServer, true) { }
public EntityFrameworkOptions(string provider, bool canMigrate)
{
Provider = provider;
CanMigrate = canMigrate;
}
public string Provider { get; set; }
public bool CanMigrate { get; set; }
EntityFrameworkOptions IOptions<EntityFrameworkOptions>.Value => this;
}
To use the current structure, we have to make a little change to our appsettings.json and appsettings.Development.json. This is shown below:
{
"EntityFrameworkOptions": {
"Provider": "SQL-SERVER",
"CanMigrate": true
},
"ConnectionStringsOptions": {
"ServerName": "xxx.database.windows.net",
"DatabaseName": "xxx",
"UserId": "xxx_Developers",
"Password": "xxxx-xxx-xxx-xxx"
}
}
{
"EntityFrameworkOptions": {
"Provider": "SQLITE",
"CanMigrate": true
},
"ConnectionStringsOptions": {
"ServerName": null,
"DatabaseName": "dev.db",
"UserId": null,
"Password": null
}
}
When we need to access the strongly typed settings, we just need to inject an instance of an IOptions<> class into the constructor of our consuming class, and let dependency injection handle the rest:
using System.Collections.Generic;
using ConfigureEFDbContext.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace ConfigureEFDbContext.Controllers
{
[ApiController]
[Route("[controller]")]
public class OptionsPatternController : ControllerBase
{
private readonly EntityFrameworkOptions _entityFrameworkOptions;
private readonly ConnectionStringsOptions _connectionStringsOptions;
public OptionsPatternController(IOptions<EntityFrameworkOptions>
entityFrameworkOptions, IOptions<ConnectionStringsOptions> connectionStringsOptions)
{
_entityFrameworkOptions = entityFrameworkOptions.Value;
_connectionStringsOptions = connectionStringsOptions.Value;
}
[HttpGet]
public IEnumerable<string> Get() => new[] { _entityFrameworkOptions.Provider,
_connectionStringsOptions.DatabaseName };
}
}
Because we need to provide a high level of flexibility in our code when we configure our DbContext and we need to separate the object’s construction from the objects themselves, we go to using Factory Pattern.
According to Wikipedia
In class-based programming, the factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created. This is done by creating objects by calling a factory method—either specified in an interface and implemented by child classes, or implemented in a base class and optionally overridden by derived classes—rather than by calling a constructor.
The IDbContextConfigurerFactory
is the Factory
interface and the DbContextConfigurerFactory
is the implementation for the Factory
.
using System;
using System.Collections.Generic;
using System.Reflection;
using ConfigureEFDbContext.EFProviderConnectionOptions;
using ConfigureEFDbContext.Options;
using Microsoft.Extensions.Options;
namespace ConfigureEFDbContext
{
public interface IDbContextConfigurerFactory
{
IDbContextConfigurer GetConfigurer(string migrationsAssembly = null);
}
public class DbContextConfigurerFactory : IDbContextConfigurerFactory
{
public DbContextConfigurerFactory(IOptions<EntityFrameworkOptions> options,
IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions)
{
EntityFrameworkOptions = options.Value;
Factories = new Dictionary<string, Func<string, IDbContextConfigurer>>() {
{EntityFrameworkProviders.SqlServer, (migrationsAssembly) =>
CreateSqlServerSetup(dbProviderConnectionOptions, migrationsAssembly)},
{EntityFrameworkProviders.SQLite, (migrationsAssembly) =>
CreateSqliteSetup(dbProviderConnectionOptions, migrationsAssembly)},
{EntityFrameworkProviders.InMemor, (migrationsAssembly) =>
CreateInMemorySetup(dbProviderConnectionOptions, migrationsAssembly)},
};
}
protected EntityFrameworkOptions EntityFrameworkOptions { get; }
protected Dictionary<string, Func<string, IDbContextConfigurer>> Factories { get; }
public virtual IDbContextConfigurer GetConfigurer(string migrationsAssembly = null)
=> Factories.ContainsKey(EntityFrameworkOptions.Provider)
? Factories[EntityFrameworkOptions.Provider]
(migrationsAssembly ?? Assembly.GetCallingAssembly().GetName().Name)
: default;
protected virtual IDbContextConfigurer CreateSqlServerConfigurer
(IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
string migrationsAssembly) => new SqlServerDbContextConfigurer
(dbProviderConnectionOptions, migrationsAssembly);
protected virtual IDbContextConfigurer CreateSqliteConfigurer
(IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
string migrationsAssembly) => new SqliteDbContextConfigurer
(dbProviderConnectionOptions, migrationsAssembly);
protected virtual IDbContextConfigurer CreateInMemoryConfigurer
(IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
string migrationsAssembly) => new InMemoryDbContextConfigurer
(dbProviderConnectionOptions, migrationsAssembly);
}
public class CacheableDbContextConfigurerFactory : DbContextConfigurerFactory
{
protected IDbContextConfigurer _sqlServerConfigurer;
protected IDbContextConfigurer _sqliteConfigurer;
protected IDbContextConfigurer _inMemoryConfigurer;
public CacheableDbContextConfigurerFactory(IOptions<EntityFrameworkOptions> options,
IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions) :
base(options, dbProviderConnectionOptions) { }
protected override IDbContextConfigurer CreateSqlServerConfigurer
(IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
string migrationsAssembly) => _sqlServerConfigurer ??= base.CreateSqlServerSetup
(dbProviderConnectionOptions, migrationsAssembly);
protected override IDbContextConfigurer CreateSqliteConfigurer
(IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
string migrationsAssembly) => _sqliteConfigurer ??= base.CreateSqliteSetup
(dbProviderConnectionOptions, migrationsAssembly);
protected override IDbContextConfigurer CreateInMemoryConfigurer
(IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
string migrationsAssembly) => _inMemoryConfigurer ??= base.CreateInMemorySetup
(dbProviderConnectionOptions, migrationsAssembly);
}
}
and here is the implementation for the Product
interface and concrete classes:
using System;
using ConfigureEFDbContext.EFProviderConnectionOptions;
using Microsoft.EntityFrameworkCore;
namespace ConfigureEFDbContext
{
public interface IDbContextConfigurer
{
void Configure(IServiceProvider serviceProvider, DbContextOptionsBuilder builder);
}
public abstract class DbContextConfigurer : IDbContextConfigurer
{
protected DbContextConfigurer
(IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
string migrationsAssembly)
{
DbProviderConnectionOptions = dbProviderConnectionOptions;
MigrationsAssembly = migrationsAssembly;
}
public IEntityFrameworkProviderConnectionOptions DbProviderConnectionOptions { get; }
public string MigrationsAssembly { get; }
public abstract void Configure(IServiceProvider serviceProvider,
DbContextOptionsBuilder builder);
}
public class SqlServerDbContextConfigurer : DbContextConfigurer
{
public SqlServerDbContextConfigurer
(IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
string migrationsAssembly) : base(dbProviderConnectionOptions, migrationsAssembly) { }
public override void Configure(IServiceProvider serviceProvider,
DbContextOptionsBuilder builder)
{
if (DbProviderConnectionOptions.UseConnectionString)
{
builder.UseSqlServer(
connectionString: DbProviderConnectionOptions.GetConnectionString(),
sqlServerDbContextOptionsBuilder =>
sqlServerDbContextOptionsBuilder.MigrationsAssembly(MigrationsAssembly)
);
}
else
{
builder.UseSqlServer(
connection: DbProviderConnectionOptions.GetConnection(),
sqlServerDbContextOptionsBuilder =>
sqlServerDbContextOptionsBuilder.MigrationsAssembly(MigrationsAssembly)
);
}
}
}
public class SqliteDbContextConfigurer : DbContextConfigurer
{
public SqliteDbContextConfigurer
(IEntityFrameworkProviderConnectionOptions dbProviderConnectionOptions,
string migrationsAssembly) : base(dbProviderConnectionOptions, migrationsAssembly) { }
public override void Configure(IServiceProvider serviceProvider,
DbContextOptionsBuilder builder)
{
if (DbProviderConnectionOptions.UseConnectionString)
{
builder.UseSqlite(
connectionString: DbProviderConnectionOptions.GetConnectionString(),
sqlServerDbContextOptionsBuilder =>
sqlServerDbContextOptionsBuilder.MigrationsAssembly(MigrationsAssembly)
);
}
else
{
builder.UseSqlite(
connection: DbProviderConnectionOptions.GetConnection(),
sqlServerDbContextOptionsBuilder =>
sqlServerDbContextOptionsBuilder.MigrationsAssembly(MigrationsAssembly)
);
}
}
}
public class InMemoryDbContextConfigurer : DbContextConfigurer
{
public InMemoryDbContextConfigurer(IEntityFrameworkProviderConnectionOptions
dbProviderConnectionOptions, string migrationsAssembly) :
base(dbProviderConnectionOptions, migrationsAssembly) { }
public override void Configure(IServiceProvider serviceProvider,
DbContextOptionsBuilder builder) => builder.UseInMemoryDatabase
(DbProviderConnectionOptions.GetConnectionString());
}
}
This factory is responsible for creating the classes that will configure DbContext by calling the GetConfigurer
method, and we will get an IDbContextConfigurer
instance that contains the Configure
method to initialize the DbContext.
To make Configure
method more flexible, we have followed the Single Responsibility Principle (SRP). So we’ve created some new classes.
The main task of this simple design is to read the configuration from appsetting.json or the current environment setting file, by Options Pattern that we talked about earlier and convert it into extensions that we can apply to the data provider, so if we want to add a new provider, we have to add another class for this provider, but don’t forget to add a new implementation to the IDbContextConfigurer
interface or inherit from the DbContextConfigurer
base class, for example: MySqlProviderConnectionOptions
.
using System.Data.Common;
using ConfigureEFDbContext.Common;
using ConfigureEFDbContext.Options;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Options;
namespace ConfigureEFDbContext
{
public interface IEntityFrameworkProviderConnectionOptions : IDisposableObject
{
bool UseConnectionString { get; }
string GetConnectionString();
DbConnection GetConnection();
}
public abstract class EntityFrameworkProviderConnectionOptions : DisposableObject,
IEntityFrameworkProviderConnectionOptions
{
public abstract bool UseConnectionString { get; }
public virtual DbConnection GetConnection() => null;
public virtual string GetConnectionString() => null;
}
public class SqlServerProviderConnectionOptions : EntityFrameworkProviderConnectionOptions
{
private readonly ConnectionStringsOptions _options;
public SqlServerProviderConnectionOptions
(IOptions<ConnectionStringsOptions> options) => _options = options.Value;
public override bool UseConnectionString => true;
public override string GetConnectionString() =>
$"Server={_options.ServerName};Database={_options.DatabaseName};
User Id={_options.UserId};Password={_options.Password};MultipleActiveResultSets=True";
}
public class SqliteProviderConnectionOptions : EntityFrameworkProviderConnectionOptions
{
private readonly ConnectionStringsOptions _options;
public SqliteProviderConnectionOptions
(IOptions<ConnectionStringsOptions> options) => _options = options.Value;
public override bool UseConnectionString => true;
public override string GetConnectionString() =>
$"Data Source={_options.DatabaseName};Cache=Shared;";
}
public class SqliteInMemoryProviderConnectionOptions :
EntityFrameworkProviderConnectionOptions
{
private readonly DbConnection _connection;
public SqliteInMemoryProviderConnectionOptions() =>
_connection = new SqliteConnection("Data Source=:memory:;Cache=Shared;");
public override bool UseConnectionString => false;
public override DbConnection GetConnection()
{
if (_connection.State != System.Data.ConnectionState.Open)
{
_connection.Open();
}
return _connection;
}
protected override void Dispose(bool disposing)
{
_connection.Dispose();
base.Dispose(disposing);
}
}
public class InMemoryProviderConnectionOptions : EntityFrameworkProviderConnectionOptions
{
private readonly ConnectionStringsOptions _options;
public InMemoryProviderConnectionOptions
(IOptions<ConnectionStringsOptions> options) => _options = options.Value;
public override bool UseConnectionString => true;
public override string GetConnectionString() => _options.DatabaseName;
}
}
After completing this scenario, we have to inject all the classes in Dependency Injection in the Startup
class. You will notice here that all the new methods that we have added to Startup
class are virtual
methods. This is to make this class overridable in the integration test app of MSTest unit testing, which we will add later.
using ConfigureEFDbContext.Options;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ConfigureEFDbContext
{
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment hostEnvironment)
{
Configuration = configuration;
HostEnvironment = hostEnvironment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment HostEnvironment { get; }
public virtual void ConfigureServices(IServiceCollection services)
{
this
.AddLogging(services)
.AddApplicationOptions(services)
.AddDbContextConfigurerFactory(services)
.AddEFProviderConnectionOptions(services)
.AddDbContextConfigurer(services)
.AddDbContext(services);
services.AddControllers();
}
public virtual void Configure(IApplicationBuilder app)
{
if (HostEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app
.UseHttpsRedirection()
.UseRouting()
.UseAuthorization()
.UseEndpoints(endpoints => endpoints.MapControllers());
}
protected virtual Startup AddLogging(IServiceCollection services)
{
services.AddLogging
(
builder =>
builder.AddConfiguration(Configuration.GetSection("Logging"))
.AddConsole()
.AddDebug()
);
return this;
}
protected virtual Startup AddApplicationOptions(IServiceCollection services)
{
services
.AddOptions()
.Configure<EntityFrameworkOptions>(Configuration.GetSection
(EntityFrameworkOptions.KEY_NAME))
.Configure<ConnectionStringsOptions>(Configuration.GetSection
(ConnectionStringsOptions.KEY_NAME))
;
return this;
}
protected virtual Startup AddDbContextConfigurerFactory(IServiceCollection services)
{
services.AddSingleton<IDbContextConfigurerFactory,
CacheableDbContextConfigurerFactory>();
return this;
}
protected virtual Startup AddEFProviderConnectionOptions(IServiceCollection services)
{
services.AddSingleton<IEntityFrameworkProviderConnectionOptions,
SqlServerProviderConnectionOptions>();
return this;
}
protected Startup AddDbContextConfigurer(IServiceCollection services)
{
services.AddSingleton(serviceProvider =>
serviceProvider.GetService<IDbContextConfigurerFactory>().GetConfigurer());
return this;
}
protected virtual Startup AddDbContext(IServiceCollection services)
{
AddDbContextPool<DbContext_1>(services);
AddDbContextPool<DbContext_2>(services);
AddDbContextPool<DbContext_3>(services);
services.AddScoped<IDbContext_1>(provider => provider.GetService<DbContext_1>());
services.AddScoped<IDbContext_2>(provider => provider.GetService<DbContext_2>());
services.AddScoped<IDbContext_3>(provider => provider.GetService<DbContext_3>());
return this;
}
private Startup AddDbContextPool<TContext>(IServiceCollection services)
where TContext : DbContext
{
services.AddDbContextPool<TContext>
(
(serviceProvider, dbContextOptionsBuilder) =>
serviceProvider.GetService<IDbContextConfigurer>().Configure
(serviceProvider, dbContextOptionsBuilder)
);
return this;
}
}
}
In the end, we will add the DbContexts that we used in our discussion, which are the simplest form of identification for this type of class.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace ConfigureEFDbContext
{
public interface IDbContext
{
DatabaseFacade Database { get; }
}
public interface IDbContext_1 : IDbContext { }
public interface IDbContext_2 : IDbContext { }
public interface IDbContext_3 : IDbContext { }
public class DbContext_1 : DbContext, IDbContext_1
{
public DbContext_1(DbContextOptions<DbContext_1> options) : base(options) { }
}
public class DbContext_2 : DbContext, IDbContext_2
{
public DbContext_2(DbContextOptions<DbContext_2> options) : base(options) { }
}
public class DbContext_3 : DbContext, IDbContext_3
{
public DbContext_3(DbContextOptions<DbContext_3> options) : base(options) { }
}
}
Here, we will add a controller so that we do a simple test of this scenario.
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace ConfigureEFDbContext.Controllers
{
[ApiController]
[Route("[controller]")]
public class DbContextsController : ControllerBase
{
private readonly IDbContext_1 _dbContext_1;
private readonly IDbContext_2 _dbContext_2;
private readonly IDbContext_3 _dbContext_3;
public DbContextsController(IDbContext_1 dbContext_1,
IDbContext_2 dbContext_2, IDbContext_3 dbContext_3)
{
_dbContext_1 = dbContext_1;
_dbContext_2 = dbContext_2;
_dbContext_3 = dbContext_3;
}
[HttpGet]
public IEnumerable<string> Get() => new[] {
_dbContext_1.Database.ProviderName,
_dbContext_2.Database.ProviderName,
_dbContext_3.Database.ProviderName
};
}
}
After testing the program with Postman, we will get this result:
In this post, we will not talk about the unit test or integration test because it is outside the scope of this topic, but we will show the necessary classes to run this test.
{
"EntityFrameworkOptions": {
"Provider": "IN-MEMOR",
"CanMigrate": false
},
"ConnectionStringsOptions": {
"ServerName": null,
"DatabaseName": "DEV-V1.db",
"UserId": null,
"Password": null
}
}
After inheriting from the Startup
class that we created earlier and override necessary methods, IntegrationStartup
will become:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace ConfigureEFDbContext.MSUnitTest
{
public class IntegrationStartup : Startup
{
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
services
.AddMvc()
.AddApplicationPart(typeof(Startup).Assembly);
}
public IntegrationStartup(IConfiguration configuration,
IWebHostEnvironment environment) : base(configuration, environment) { }
protected override
Startup AddEFProviderConnectionOptions(IServiceCollection services)
{
services.AddSingleton<IEntityFrameworkProviderConnectionOptions,
InMemoryProviderConnectionOptions>();
return this;
}
}
}
The necessary classes to run this test.
using System;
using System.IO;
using System.Reflection;
namespace ConfigureEFDbContext.MSUnitTest
{
public static class Helper
{
public static string GetParentProjectPath()
{
var parentProjectName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
var parentProjectFullName = $"{parentProjectName}.csproj";
var applicationBasePath = Directory.GetCurrentDirectory();
var directoryInfo = new DirectoryInfo(Directory.GetCurrentDirectory());
while (directoryInfo != null)
{
var projectDirectoryInfo = new DirectoryInfo(directoryInfo.FullName);
var parentProjectPath = Path.Combine(projectDirectoryInfo.FullName,
parentProjectName, parentProjectFullName);
if (projectDirectoryInfo.Exists && new FileInfo(parentProjectPath).Exists)
{
return Path.Combine(projectDirectoryInfo.FullName, parentProjectName);
}
directoryInfo = directoryInfo.Parent;
}
throw new Exception($"Th parent project {parentProjectName}
could not be located using the current application root {applicationBasePath}.");
}
}
}
Created an IntegrationWebApplicationFactory
independently of the test classes by inheriting from WebApplicationFactory.
using System.IO;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
namespace ConfigureEFDbContext.MSUnitTest.Fixtures
{
public class IntegrationWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override IWebHostBuilder CreateWebHostBuilder() =>
WebHost.CreateDefaultBuilder<IntegrationStartup>(null);
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
var contentRoot = Helper.GetParentProjectPath();
builder
.ConfigureAppConfiguration(config =>
{
var projectDir = Directory.GetCurrentDirectory();
var integrationSettingsPath =
Path.Combine(projectDir, "integration-settings.json");
var integrationConfig = new ConfigurationBuilder()
.SetBasePath(contentRoot)
.AddJsonFile(integrationSettingsPath, false)
.Build();
config.AddConfiguration(integrationConfig);
})
.UseContentRoot(contentRoot)
.UseEnvironment("Development")
.UseStartup<IntegrationStartup>();
builder.ConfigureTestServices(services =>
{
});
base.ConfigureWebHost(builder);
}
}
}
A class to test our controller class to make sure everything works well.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using ConfigureEFDbContext.MSUnitTest.Fixtures;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
namespace ConfigureEFDbContext.MSUnitTest
{
[TestClass]
public class DbContextsControllerTest
{
protected static IntegrationWebApplicationFactory _fixture;
protected static HttpClient _client;
protected readonly IConfiguration _configuration;
[ClassInitialize]
public static void TestFixtureSetup(TestContext context)
{
_fixture = new IntegrationWebApplicationFactory();
_client = _fixture.CreateClient();
_client.BaseAddress = new Uri("http://localhost:60128");
_client.DefaultRequestHeaders.Accept.Clear();
_client.DefaultRequestHeaders.Accept.Add
(new MediaTypeWithQualityHeaderValue("application/json"));
}
[TestInitialize]
public void Setup() { }
[ClassCleanup]
public static void TestFixtureTearDown()
{
_client.Dispose();
_fixture.Dispose();
}
[TestCleanup]
public void TearDown() { }
[TestMethod]
public async Task DbContexts__Should_Initialized()
{
var requestUri = new Uri("/DbContexts", UriKind.Relative);
var response = await _client.GetAsync(requestUri).ConfigureAwait(false);
var responseBody =
await response.Content.ReadAsStringAsync().ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = JsonConvert.DeserializeObject<List<string>>(responseBody);
Assert.IsNotNull(result);
}
}
}
In this post, I showed a new way to configure our application with the startup
class using a flexible way to implement this configuration.
I hope you liked the article. Please share your opinion in the comments section below.
You can find the source code for this demo on GitHub.
- 3rd October, 2020: Initial version