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.
See Question&Answers more detail:
os