Onestamente non posso credere quanto sia difficile ... prima di tutto i requisiti che sto cercando:
IDesignTimeDbContextFactory
che è IDbContextFactory rinominato per essere meno confuso per gli sviluppatori su ciò che fa appsettings.json
più di una volta. Una ragione è dovuta al fatto che le mie migrazioni sono in esecuzione nel dominio di MyClassLibrary.Data
e che non esiste un file appsettings.js
in quella libreria di classi, dovrei Copy to Output Directory
appsettings.js
. Un altro motivo è che non è molto elegante. Quindi ecco quello che ho che funziona attualmente:
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);
}
}
}
Ed ecco il mio 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();
}
}
}
Ed ecco il mio 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());
}
}
}
Quello che mi piacerebbe fare è usare il pattern IOptions , quindi ho creato questa classe:
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; }
}
}
Aggiunta questa riga a Startup.ConfigureServices
:
services.Configure<AppSettings>(options => Configuration.GetSection("AppSettings").Bind(options));
E quindi ho provato e modificato la mia implementazione di IDesignTimeDbContextFactory<AppContext>
in:
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);
}
}
Sfortunatamente questo non ha funzionato perché l' Ioptions<AppSettings>
del public DesignTimeDbContextFactory(IOptions<AppSettings> appSettings)
non viene iniettato. Presumo che ciò sia dovuto al fatto che le implementazioni di IDesignTimeDbContextFactory<AppContext>
vengono richiamate in fase di progettazione e l'iniezione delle dipendenze non è "pronta" nelle app di .NET Core in fase di progettazione?
Penso che sia piuttosto strano che sia così difficile iniettare una stringa di connessione specifica per l'ambiente utilizzando il modello Entity Framework Core 2.0 di implementazione di IDesignTimeDbContextFactory
e anche non dover copiare e caricare file di impostazioni come appsettings.json
più di una volta.
Se stai cercando una soluzione per ottenere una stringa di connessione al database dalla tua classe di impostazioni personalizzate inizializzata dal file appsettings.json
, ecco come puoi farlo. Sfortunatamente non è possibile iniettare IOptions
tramite DI
al IDesignTimeDbContextFactory
implementazione IDesignTimeDbContextFactory
.
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);
}
}
Ovviamente nella tua classe AppSettings
e in appsettings.json
potresti avere una logica ancora più sofisticata per costruire la stringa di connessione. Ad esempio, in questo modo:
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;
}
}
Con i soli valori memorizzati in appsettings.json
:
{
"Settings": {
"UseInMemory": false,
"Server": "myserver",
"Port": "1234",
"Database": "mydatabase",
"User": "dbuser",
"Password": "dbpassw0rd"
}
}
Con password e utente memorizzati nelle variabili di ambiente:
{
"Settings": {
"UseInMemory": false,
"Server": "myserver",
"Port": "1234",
"Database": "mydatabase",
"User": "MY-DB-UID-ENV-VAR",
"Password": "MY-DB-PWD-ENV-VAR"
}
}
In questo caso dovresti usarlo in questo modo:
// 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);
Sono un po 'confuso con la tua domanda. Stai usando l'iniezione di dipendenza per DbContext
o stai provando a inizializzare e costruire il contesto ad hoc?
Sto facendo quello che hai descritto in una delle mie soluzioni. Ecco la mia struttura di soluzione:
Startup.cs
public Startup(IHostingEnvironment env)
{
IConfigurationBuilder builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json")
.AddEnvironmentVariables();
// ...
}
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<MyDbContext>(
options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
sqlOptions => sqlOptions.EnableRetryOnFailure()));
// SQL configuration for non-injected dbcontext
DbContextOptionsBuilder<MyDbContext> builder = new DbContextOptionsBuilder<MyDbContext>();
builder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
services.AddSingleton(builder.Options);
// ...
}
MyDbContext.cs
public class MyDbContext : IdentityDbContext<ApplicationUser>
{
public MyDbContext(DbContextOptions options) : base(options) { }
}
Se non si utilizza l'iniezione delle dipendenze per passare DbContext, è possibile accedere alle proprietà SQL iniettando DbContextOptions<MyDbContext>
.
In questo esempio, il file appsettings viene letto solo una volta e tutto funziona correttamente.