Динамическое изменение строки подключения в Asp.Net Core

Я хочу изменить строку соединения sql в контроллере, а не в ApplicationDbContext. Я использую Core Asp.Net Core и Entity Framework Core.

Например:

public class MyController : Controller {
    private readonly ApplicationDbContext _dbContext
    public MyController(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    private void ChangeConnectionString()
    {
    // So, what should be here?
    } }

Как я могу это сделать?

Ответ 1

У нас есть случай, похожий на вас. То, что мы сделали, это использовать implementationfactory перегрузку IServiceCollection в методе ConfigureServices класса запуска, например, так:

//First register a custom made db context provider
services.AddTransient<ApplicationDbContextFactory>();
//Then use implementation factory to get the one you need
services.AddTransient(provider => provider.GetService<ApplicationDbContextFactory>().CreateApplicationDbContext());

Мне сейчас очень трудно реализовать CreateApplicationDbContext для вас, потому что это полностью зависит от того, что именно вы хотите. Но после того, как вы выяснили, как именно вы хотите это сделать, основы метода в любом случае должны выглядеть следующим образом:

public ApplicationDbContext CreateApplicationDbContext(){
  //TODO Something clever to create correct ApplicationDbContext with ConnectionString you need.
} 

Как только это будет реализовано, вы можете добавить правильный ApplicationDbContext в ваш контроллер, как вы это делали в конструкторе:

public MyController(ApplicationDbContext dbContext)
{
    _dbContext = dbContext;
}

Или метод действия в контроллере:

public IActionResult([FromServices] ApplicationDbContext dbContext){
}

Как бы вы ни реализовывали детали, дело в том, что фабрика реализации будет создавать ваш ApplicationDbContext каждый раз, когда вы внедряете его.

Скажите, если вам нужна дополнительная помощь в реализации этого решения.

Обновление № 1 Юрий Н. спросил, в чем разница между AddTransient и AddDbContext, что является правильным вопросом... И это не так. Позволь мне объяснить.

Это не относится к первоначальному вопросу.

НО... Сказав это, реализация вашей собственной "фабрики реализации" (которая является наиболее важной вещью, на которую следует обратить внимание в моем ответе) может в этом случае с структурой сущностей быть немного сложнее, чем нам нужно.

Тем не менее, с такими вопросами мы можем, к счастью, взглянуть на исходный код в GitHub, поэтому я посмотрел, что именно делает AddDbContext. И хорошо... Это не очень сложно. Эти методы расширения "добавить" (и "использовать") - не более чем удобные методы, запомните это. Таким образом, вам нужно добавить все сервисы, которые делает AddDbContext, плюс опции. Может быть, вы даже можете повторно использовать метод расширения AddDbContext, просто добавьте свою собственную перегрузку с фабрикой реализации.

Итак, вернемся к вашему вопросу. AddDbContext делает некоторые специфичные для EF вещи. Как вы можете видеть, они позволят вам передать жизнь в более позднем выпуске (переходный процесс, синглтон). AddTransient - это Asp.Net Core, который позволяет вам добавлять любые нужные вам сервисы. И вам нужна фабрика реализации.

Это делает это более ясным?

Ответ 2

Этого достаточно, если вы хотите выбрать строку подключения для HTTP-запроса на основе активных параметров HTTP-запроса.

    using Microsoft.AspNetCore.Http;

    //..

    services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

    services.AddDbContext<ERPContext>((serviceProvider, options) =>
        {
            var httpContext = serviceProvider.GetService<IHttpContextAccessor>().HttpContext;
            var httpRequest = httpContext.Request;
            var connection = GetConnection(httpRequest);
            options.UseSqlServer(connection);
        });

У вас нет легкого доступа ни к пользователю, ни к его претензиям. Если вы не сделали это вручную.

Как извлечь и получить претензию из токена?

Ответ 3

Мне удалось изменить строку соединения для каждого запроса, переместив логику строки подключения в метод OnConfiguring DbContext.

В Startup.cs#ConfigureServices метод: services.AddDbContext<MyDbContext>();

В MyDbContext.cs я добавил сервисы, которые мне нужно вставить в конструктор.

    private IConfigurationRoot _config;
    private HttpContext _httpContext;

    public MyDbContext(DbContextOptions options, IConfigurationRoot config, IHttpContextAccessor httpContextAccessor) 
          : base(options)
    {
        _config = config;
        _httpContext = httpContextAccessor.HttpContext;
    }

Затем переопределите OnConfiguring:

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var connString = BuildConnectionString(); // Your connection string logic here

        optionsBuilder.UseSqlServer(connString);
    }

Ответ 4

Ответы @ginalx и @jcmordan идеально подходят для моего случая использования. Что мне нравится в этих ответах, так это то, что я могу делать все это в Startup.cs и содержать все остальные классы в чистоте от кода конструкции. Я хочу предоставить необязательный параметр querystring для запроса Web Api и подставить его в базовую строку соединения, которая создает DbContext. Я сохраняю базовую строку в appsettings.json и форматирую ее на основе переданного параметра или значения по умолчанию, если оно не указано, т.е.

"IbmDb2Formatted": "DATABASE={0};SERVER=servername;UID=userId;PWD=password"

Окончательный метод ConfigureServices для меня выглядит так (obvs. Я подключаюсь к DB2 не SQL, но это случайно):

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();

        services.AddDbContext<Db2Context>(((serviceProvider, options) =>
        {
            var httpContext = serviceProvider.GetService<IHttpContextAccessor>().HttpContext;
            var httpRequest = httpContext.Request;

            // Get the 'database' querystring parameter from the request (if supplied - default is empty).
           // TODO: Swap this out for an enum.
            var databaseQuerystringParameter = httpRequest.Query["database"].ToString();

            // Get the base, formatted connection string with the 'DATABASE' paramter missing.
            var db2ConnectionString = Configuration.GetConnectionString("IbmDb2Formatted");

            if (!databaseQuerystringParameter.IsNullOrEmpty())
            {
                // We have a 'database' param, stick it in.
                db2ConnectionString = string.Format(db2ConnectionString, databaseQuerystringParameter);
            }
            else
            {
                // We havent been given a 'database' param, use the default.
                var db2DefaultDatabaseValue = Configuration.GetConnectionString("IbmDb2DefaultDatabaseValue");
                db2ConnectionString = string.Format(db2ConnectionString, db2DefaultDatabaseValue);
            }

            // Build the EF DbContext using the built conn string.
            options.UseDb2(db2ConnectionString, p => p.SetServerInfo(IBMDBServerType.OS390));
        }));

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new Info
            {
                Title = "DB2 API",
                Version = "v1"
            });
        });
    }

Ответ 5

Это работает для меня:

public void ConfigureServices(IServiceCollection services)
{
    // .....
    services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddTransient<School360DbContext>(provider =>
    {
        return ResolveDbContext(provider, hostingEnv);
    });
    // ..
}

private MyDbContext ResolveDbContext(IServiceProvider provider, IHostingEnvironment hostingEnv)
{
    string connectionString = Configuration.GetConnectionString("DefaultConnection");

    string SOME_DB_IDENTIFYER = httpContextAccessor.HttpContext.User.Claims
        .Where(c => c.Type == "[SOME_DB_IDENTIFYER]").Select(c => c.Value).FirstOrDefault();
    if (!string.IsNullOrWhiteSpace(SOME_DB_IDENTIFYER))
    {
        connectionString = connectionString.Replace("[DB_NAME]", $"{SOME_DB_IDENTIFYER}Db");
    }

    var dbContext = new DefaultDbContextFactory().CreateDbContext(connectionString);

    // ....
    return dbContext;
}

Ответ 6

Я знаю, что другие люди уже ответили. Но я хотел бы поделиться своим подходом для людей, которые хотят изменить строку подключения к БД во время выполнения.

Мое приложение было создано с использованием asp.net core 2.2 с Entity Framework и MySql.

StartUp.cs

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddDbContext<MyDbContext>();

    ...

MyDbContext Class

public partial class MyDbContext : DbContext
{
    public MyDbContext()
    {
    }

    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (DbManager.DbName != null && !optionsBuilder.IsConfigured)
        {
            var dbName = DbManager.DbName;
            var dbConnectionString = DbManager.GetDbConnectionString(dbName);
            optionsBuilder.UseMySql(dbConnectionString);
        }
    }

    ...

Json - файл с информацией о соединении

[
  {
    "name": "DB1",
    "dbconnection": "server=localhost;port=3306;user=username;password=password;database=dbname1"
  },
  {
    "name": "DB2",
    "dbconnection": "server=localhost;port=3306;user=username;password=password;database=dbname2"
  }
]

Класс DbConnection

using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;


public class DbConnection
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("dbconnection")]
    public string Dbconnection { get; set; }

    public static List<DbConnection> FromJson(string json) => JsonConvert.DeserializeObject<List<DbConnection>>(json, Converter.Settings);
}

    internal static class Converter
    {
        public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
        {
            MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
            DateParseHandling = DateParseHandling.None,
            Converters =
            {
                new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
            },
        };
    }
}

Класс DbConnectionManager

public static class DbConnectionManager
{
    public static List<DbConnection> GetAllConnections()
    {
        List<DbConnection> result;
        using (StreamReader r = new StreamReader("myjsonfile.json"))
        {
            string json = r.ReadToEnd();
            result = DbConnection.FromJson(json);
        }
        return result;
    }

    public static string GetConnectionString(string dbName)
    {
        return GetAllConnections().FirstOrDefault(c => c.Name == dbName)?.Dbconnection;
    }
}

Класс DbManager

public static class DbManager
{
    public static string DbName;

    public static string GetDbConnectionString(string dbName)
    {
        return DbConnectionManager.GetConnectionString(dbName);
    }
}

Затем вам понадобится контроллер, который настроит dbName.

Класс контроллера

[Route("dbselect/{dbName}")]
public IActionResult DbSelect(string dbName)
{
    // Set DbName for DbManager.
    DbManager.DbName = dbName;

    dynamic myDynamic = new System.Dynamic.ExpandoObject();
    myDynamic.DbName = dbName;
    var json = JsonConvert.SerializeObject(myDynamic);
    return Content(json, "application/json");
}

Тебе, возможно, придется кое-что проделать здесь и там. но вы получите идею. В начале приложения, оно не имеет детализации соединения. поэтому вы должны установить его явно с помощью контроллера. Надеюсь, это кому-нибудь поможет.

Ответ 8

Startup.cs для статического соединения

services.AddScoped<MyContext>(_ => new MyContext(Configuration.GetConnectionString("myDB")));

Repository.cs для динамического соединения

using (var _context = new MyContext(@"server=....){
context.Table1....
}

Table1MyContext.cs

public MyContext(string connectionString) : base(GetOptions(connectionString))
{
}

private static DbContextOptions GetOptions(string connectionString)
{
    return SqlServerDbContextOptionsExtensions.UseSqlServer(new DbContextOptionsBuilder(), connectionString).Options;
}