web-dev-qa-db-fra.com

Injecter Env Conn String dans .NET Core 2.0 avec EF Core DbContext dans une librairie de classe différente de celle de Startup prj & implémenter IDesignTimeDbContextFactory

Honnêtement, je ne peux pas croire à quel point c'est difficile… Tout d'abord, les exigences que je vais respecter:

  • IDesignTimeDbContextFactory d'implémentation de Entity Framework Core 2.0, qui est IDbContextFactory renommé de manière à rendre les développeurs moins confus quant à son travail
  • Je ne veux pas avoir à charger appsettings.json plus d'une fois. L'une des raisons est que mes migrations sont exécutées dans le domaine MyClassLibrary.Data et qu'il n'y a pas de fichier appsettings.js dans cette bibliothèque de classes; il me faudrait donc Copy to Output Directoryappsettings.js. Une autre raison est que ce n'est pas très élégant.

Alors voici ce que j'ai qui fonctionne actuellement:

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

Et voici mon 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();
        }
    }
}

Et voici mon 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());
        }
    }
}

Ce que je voudrais faire est d'utiliser le modèle IOptions , alors j'ai créé cette 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; }
    }
}

Ajout de cette ligne à Startup.ConfigureServices:

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

Et puis essayé et changer mon implémentation de IDesignTimeDbContextFactory<AppContext> en:

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

Malheureusement, cela n'a pas fonctionné car l'argument Ioptions<AppSettings> du constructeur public DesignTimeDbContextFactory(IOptions<AppSettings> appSettings) n'est pas injecté. Je suppose que cela est dû au fait que les implémentations de IDesignTimeDbContextFactory<AppContext> sont appelées au moment du design et que l’injection de dépendance n’est tout simplement pas "prête" dans les applications .NET Core au moment du design?

Je trouve étrange qu’il soit si difficile d’injecter une chaîne de connexion spécifique à l’environnement à l’aide du modèle Entity Framework Core 2.0 consistant à implémenter IDesignTimeDbContextFactory, sans avoir à copier et à charger plusieurs fois des fichiers de paramètres comme appsettings.json.

15
Brian Ogden

Si vous cherchez une solution pour obtenir une chaîne de connexion à la base de données à partir de votre classe de paramètres personnalisés initialisée à partir du fichier appsettings.json, vous pouvez le faire. Malheureusement, vous ne pouvez pas injecter IOptions via DI à votre constructeur 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);
   }
}

Bien sûr, dans votre classe AppSettings et appsettings.json, vous pourriez avoir une logique encore plus sophistiquée pour construire la chaîne de connexion. Par exemple, comme ceci: 

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

Avec seulement les valeurs stockées dans appsettings.json:

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

Avec mot de passe et utilisateur stockés dans des variables d'environnement:

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

Dans ce cas, vous devriez l'utiliser de cette façon: 

// 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

Je suis un peu confus avec votre question. Utilisez-vous l'injection de dépendance pour la variable DbContext ou essayez-vous d'initialiser et de construire le contexte ad hoc?

Je fais ce que vous avez décrit dans l'une de mes solutions. Voici la structure de ma solution:

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

Si vous n'utilisez pas l'injection de dépendance pour transmettre le DbContext, vous pouvez accéder aux propriétés SQL en injectant DbContextOptions<MyDbContext> à la place.

Dans cet exemple, le fichier appsettings est lu une seule fois et tout fonctionne.

0
Mitchell Skurnik