wake-up-neo.com

Einfügen von Env Conn String in .NET Core 2.0 mit EF Core DbContext in einer anderen Klassenlib als Startup prj und Implementierung von IDesignTimeDbContextFactory

Ich kann ehrlich gesagt nicht glauben, wie schwer das ist ... zuerst von den Anforderungen, die ich anstrebe:

  • Implementierung von Entity Framework Core 2.0 'IDesignTimeDbContextFactory', das IDbContextFactory umbenannt wurde, um die Entwickler weniger verwirrend zu machen
  • Ich möchte appsettings.json nicht mehr als einmal laden. Ein Grund ist, dass meine Migrationen in der Domäne MyClassLibrary.Data ausgeführt werden und es keine appsettings.js-Datei in dieser Klassenbibliothek gibt. Ich müsste Copy to Output Directoryappsettings.js. Ein weiterer Grund ist, dass es einfach nicht sehr elegant ist.

Also hier ist was ich habe, das derzeit funktioniert:

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);
        }
    }
}

Und hier ist mein 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();
        }
    }
}

Und hier ist meine 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());
        }
    }
}

Was ich gerne machen würde, ist das IOptions-Muster , also habe ich diese Klasse erstellt:

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; }
    }
}

Diese Zeile wurde zu Startup.ConfigureServices hinzugefügt:

  services.Configure<AppSettings>(options => Configuration.GetSection("AppSettings").Bind(options));

Und dann versuchte und änderte meine Implementierung von 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);
    }
}

Leider hat dies nicht funktioniert, da das Ioptions<AppSettings>-Argument des public DesignTimeDbContextFactory(IOptions<AppSettings> appSettings)-Konstruktors nicht eingefügt wird. Ich gehe davon aus, dass Implementierungen von IDesignTimeDbContextFactory<AppContext> zur Entwurfszeit aufgerufen werden und die Abhängigkeitsinjektion in .NET Core-Anwendungen zur Entwurfszeit nicht "fertig" ist.

Ich finde es irgendwie seltsam, dass es so schwierig ist, eine umgebungsspezifische Verbindungszeichenfolge mit dem Entity Framework Core 2.0-Muster für die Implementierung von IDesignTimeDbContextFactory einzufügen und Einstellungsdateien wie appsettings.json nicht mehr als einmal zu kopieren und zu laden.

15
Brian Ogden

Wenn Sie nach einer Lösung zum Abrufen der Datenbankverbindungszeichenfolge aus der benutzerdefinierten Einstellungsklasse aus der appsettings.json-Datei suchen, können Sie dies tun. Leider können Sie nicht IOptions über DI in Ihren IDesignTimeDbContextFactory-Implementierungskonstruktor injizieren.

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);
   }
}

Natürlich können Sie in Ihrer AppSettings-Klasse und appsettings.json eine noch ausgefeiltere Logik zum Erstellen der Verbindungszeichenfolge haben. Zum Beispiel so: 

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;
   }
}

Mit nur Werten, die in appsettings.json gespeichert sind:

{
  "Settings": {
    "UseInMemory": false,
    "Server": "myserver",
    "Port": "1234",
    "Database": "mydatabase",
    "User": "dbuser",
    "Password": "dbpassw0rd"
  }
}

Mit Passwort und Benutzer in Umgebungsvariablen gespeichert:

{
  "Settings": {
    "UseInMemory": false,
    "Server": "myserver",
    "Port": "1234",
    "Database": "mydatabase",
    "User": "MY-DB-UID-ENV-VAR",
    "Password": "MY-DB-PWD-ENV-VAR"
  }
}

In diesem Fall sollten Sie es folgendermaßen verwenden: 

// 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);
1
Dmitry Pavlov

Ich bin etwas verwirrt mit deiner Frage. Verwenden Sie Abhängigkeitsinjektion für die DbContext oder versuchen Sie, den Kontext ad hoc zu initialisieren und zu konstruieren?

Ich mache, was Sie in einer meiner Lösungen beschrieben haben. Hier ist meine Lösungsstruktur:

  • Corp.ApplicationName.Data
  • Corp.ApplicationName.Web

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) { }
}

Wenn Sie keine Abhängigkeitsinjektion zum Übergeben von DbContext verwenden, können Sie auf die SQL-Eigenschaften zugreifen, indem Sie stattdessen DbContextOptions<MyDbContext> eingeben.

In diesem Beispiel wird die appsettings-Datei nur einmal gelesen und alles funktioniert einfach.

0