I honestly cannot believe how hard this is...first off the requirements that I am going for:
IDesignTimeDbContextFactory
which is IDbContextFactory renamed to be less confusing to developers as to what it doesappsettings.json
more than once. One reason is because my migrations are running in the domain of MyClassLibrary.Data
and there is no appsettings.js
file in that class library, I would have to to Copy to Output Directory
appsettings.js
. Another reason is that it just not very elegant.So here is what I have that currently works:
using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; using AppContext = Tsl.Example.Data.AppContext; namespace Tsl.Example { public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppContext> { public AppContext CreateDbContext(string[] args) { string basePath = AppDomain.CurrentDomain.BaseDirectory; string envName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); IConfigurationRoot configuration = new ConfigurationBuilder() .SetBasePath(basePath) .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{envName}.json", true) .Build(); var builder = new DbContextOptionsBuilder<AppContext>(); var connectionString = configuration.GetConnectionString("DefaultConnection"); builder.UseMySql(connectionString); return new AppContext(builder.Options); } } }
And here is my Program.cs:
using System.IO; using System.Reflection; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Tsl.Example { public class Program { public static void Main(string[] args) { BuildWebHost(args).Run(); } //public static IWebHost BuildWebHost(string[] args) => // WebHost.CreateDefaultBuilder(args) // .UseStartup<Startup>() // .Build(); /// <summary> /// This the magical WebHost.CreateDefaultBuilder method "unboxed", mostly, ConfigureServices uses an internal class so there is one piece of CreateDefaultBuilder that cannot be used here /// https://andrewlock.net/exploring-program-and-startup-in-asp-net-core-2-preview1-2/ /// </summary> /// <param name="args"></param> /// <returns></returns> public static IWebHost BuildWebHost(string[] args) { return new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureAppConfiguration((hostingContext, config) => { IHostingEnvironment env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); if (env.IsDevelopment()) { var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); if (appAssembly != null) { config.AddUserSecrets(appAssembly, optional: true); } } config.AddEnvironmentVariables(); if (args != null) { config.AddCommandLine(args); } }) .ConfigureLogging((hostingContext, logging) => { logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); logging.AddConsole(); logging.AddDebug(); }) //.UseIISIntegration() .UseDefaultServiceProvider((context, options) => { options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); }) .UseStartup<Startup>() .Build(); } } }
And here is my Startup.cs:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using ServiceStack; using Tsl.Example.Interfaces; using Tsl.Example.Provider; using AppContext = Tsl.Example.Data.AppContext; namespace Tsl.Example { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddTransient<IAppContext, AppContext>(); services.AddTransient<IExampleDataProvider, ExampleDataProvider>(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseServiceStack(new AppHost()); } } }
What I would like to do is use the IOptions pattern, so I created this class:
namespace Tsl.Example { /// <summary> /// Strongly typed settings to share in app using the .NET Core IOptions pattern /// https://andrewlock.net/how-to-use-the-ioptions-pattern-for-configuration-in-asp-net-core-rc2/ /// </summary> public class AppSettings { public string DefaultConnection { get; set; } } }
Added this line to Startup.ConfigureServices
:
services.Configure<AppSettings>(options => Configuration.GetSection("AppSettings").Bind(options));
And then tried and change my implementation of IDesignTimeDbContextFactory<AppContext>
to:
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppContext> { private readonly AppSettings _appSettings; public DesignTimeDbContextFactory(IOptions<AppSettings> appSettings) { this._appSettings = appSettings.Value; } public AppContext CreateDbContext(string[] args) { var builder = new DbContextOptionsBuilder<AppContext>(); builder.UseMySql(_appSettings.DefaultConnection); return new AppContext(builder.Options); } }
Unfortunately this did not work because the Ioptions<AppSettings>
argument of public DesignTimeDbContextFactory(IOptions<AppSettings> appSettings)
constructor is not injected. I assume this is because implementations of IDesignTimeDbContextFactory<AppContext>
are called at Design time and dependency injection is just not "ready" in .NET Core apps at design time?
I think it is kind of strange that it is so hard to inject an environment specific connection string using the Entity Framework Core 2.0 pattern of implementing IDesignTimeDbContextFactory
, and also not having to copy and load settings files like appsettings.json
more than once.
A DbContext instance represents a session with the database and can be used to query and save instances of your entities. DbContext is a combination of the Unit Of Work and Repository patterns. Entity Framework Core does not support multiple parallel operations being run on the same DbContext instance.
1 Answer. First, DbContext is a lightweight object; it is designed to be used once per business transaction. Making your DbContext a Singleton and reusing it throughout the application can cause other problems, like concurrency and memory leak issues. And the DbContext class is not thread safe.
You can use EF Core in APIs and applications that require the full . NET Framework, as well as those that target only the cross-platform .
The Entity Framework Core DbContext class represents a session with a database and provides an API for communicating with the database with the following capabilities: Database Connections. Data operations such as querying and persistance. Change Tracking.
If you are looking for solution to get database connection string from your custom settings class initialized from appsettings.json
file - that is how you can do this. Unfortunatelly you can't inject IOptions
via DI
to your IDesignTimeDbContextFactory
implementation constructor.
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppContext> { public AppContext CreateDbContext(string[] args) { // IDesignTimeDbContextFactory is used usually when you execute EF Core commands like Add-Migration, Update-Database, and so on // So it is usually your local development machine environment var envName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); // Prepare configuration builder var configuration = new ConfigurationBuilder() .SetBasePath(Path.Combine(Directory.GetCurrentDirectory())) .AddJsonFile("appsettings.json", optional: false) .AddJsonFile($"appsettings.{envName}.json", optional: false) .Build(); // Bind your custom settings class instance to values from appsettings.json var settingsSection = configuration.GetSection("Settings"); var appSettings = new AppSettings(); settingsSection.Bind(appSettings); // Create DB context with connection from your AppSettings var optionsBuilder = new DbContextOptionsBuilder<AppContext>() .UseMySql(appSettings.DefaultConnection); return new AppContext(optionsBuilder.Options); } }
Of course in your AppSettings
class and appsettings.json
you could have even more sophisticated logic of building the connection string. For instance, like this:
public class AppSettings { public bool UseInMemory { get; set; } public string Server { get; set; } public string Port { get; set; } public string Database { get; set; } public string User { get; set; } public string Password { get; set; } public string BuildConnectionString() { if(UseInMemory) return null; // You can set environment variable name which stores your real value, or use as value if not configured as environment variable var server = Environment.GetEnvironmentVariable(Host) ?? Host; var port = Environment.GetEnvironmentVariable(Port) ?? Port; var database = Environment.GetEnvironmentVariable(Database) ?? Database; var user = Environment.GetEnvironmentVariable(User) ?? User; var password = Environment.GetEnvironmentVariable(Password) ?? Password; var connectionString = $"Server={server};Port={port};Database={database};Uid={user};Pwd={password}"; return connectionString; } }
With just values stored in appsettings.json
:
{ "Settings": { "UseInMemory": false, "Server": "myserver", "Port": "1234", "Database": "mydatabase", "User": "dbuser", "Password": "dbpassw0rd" } }
With password and user stored in environment variables:
{ "Settings": { "UseInMemory": false, "Server": "myserver", "Port": "1234", "Database": "mydatabase", "User": "MY-DB-UID-ENV-VAR", "Password": "MY-DB-PWD-ENV-VAR" } }
In this case you should use it this way:
// Create DB context with connection from your AppSettings var optionsBuilder = new DbContextOptionsBuilder<AppContext>(); if(appSettings.UseInMemory) { optionsBuilder = appSettings.UseInMemory ? optionsBuilder.UseInMemoryDatabase("MyInMemoryDB") : optionsBuilder.UseMySql(appSettings.BuildConnectionString()); return new AppContext(optionsBuilder.Options);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With