Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Injecting Env Conn String into .NET Core 2.0 w/EF Core DbContext in different class lib than Startup prj & implementing IDesignTimeDbContextFactory

Tags:

I honestly cannot believe how hard this is...first off the requirements that I am going for:

  • Implementing Entity Framework Core 2.0' IDesignTimeDbContextFactory which is IDbContextFactory renamed to be less confusing to developers as to what it does
  • I do not want to have to do loading of appsettings.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.

like image 527
Brian Ogden Avatar asked Sep 06 '17 23:09

Brian Ogden


People also ask

How does DbContext work in .NET core?

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.

Should DbContext be a singleton?

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.

Can I use Efcore with .NET framework?

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 .

What is EF core DbContext?

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.


1 Answers

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); 
like image 199
Dmitry Pavlov Avatar answered Oct 21 '22 06:10

Dmitry Pavlov