动态更改 Asp.Net Core 中的连接字符串

Dynamically change connection string in Asp.Net Core

我想在控制器中更改 sql 连接字符串,而不是在 ApplicationDbContext 中。我正在使用 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?
    } }

我该怎么做?

我们有一个与您相似的案例。我们所做的是在 ConfigureServices 方法中使用 IServiceCollectionimplementationfactory 重载 启动class,像这样:

//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 Yuriy N. 询问 AddTransient 和 AddDbContext 之间的区别是什么,这是一个有效的问题......但事实并非如此。让我解释一下。

这与原问题无关。

但是...话虽如此,在这种情况下,使用 entity framework 实现自己的 'implementation factory'(这是我的回答中最重要的注意事项)可能比我们需要什么。

然而,对于这些问题,我们现在可以幸运地查看 GitHub 中的源代码,所以我查了一下 AddDbContext 到底做了什么。好吧……这并不难。这些 'add'(和 'use')扩展方法只不过是方便的方法,请记住这一点。因此,您需要添加 AddDbContext 提供的所有服务以及选项。也许您甚至可以重用 AddDbContext 扩展方法,只需添加您自己的带有实现工厂的重载。

所以,回到你的问题。 AddDbContext 做一些 EF 特定的事情。如您所见,它们将允许您在以后的版本中度过一生(瞬态,单例)。 AddTransient 是 Asp.Net 核心,可让您添加所需的任何服务。你需要一个实施工厂。

这样是不是更清楚了?

通过将连接字符串逻辑移动到 DbContext 的 OnConfiguring 方法中,我能够更改每个请求的连接字符串。

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

对我有用:

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

如果您想根据活动的 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);
        });

更新

大约一年后,我的解决方案看起来与此处其他答案的点点滴滴相似,所以请允许我为您总结一下。

您可以在启动文件中添加 HttpContextAccessor 的单例:

services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddDbContext<ERPContext>();

这将解决上下文构造函数中的注入问题:

public class ERPContext : DbContext
{
    private readonly HttpContext _httpContext;

    public ERPContext(DbContextOptions<ERPContext> options, IHttpContextAccessor httpContextAccessor = null)
        : base(options)
    {
        _httpContext = httpContextAccessor?.HttpContext;
    }

    //..

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            var clientClaim = _httpContext?.User.Claims.Where(c => c.Type == ClaimTypes.GroupSid).Select(c => c.Value).SingleOrDefault();
            if (clientClaim == null) clientClaim = "DEBUG"; // Let's say there is no http context, like when you update-database from PMC
            optionsBuilder.UseSqlServer(RetrieveYourBeautifulClientConnection(clientClaim));
        }
    }

    //..
}

这将为您提供一种简洁的方式来访问和提取声明并决定您的连接。

正如评论中 @JamesWilkins 所述,将为创建的每个上下文实例调用 OnConfiguring()。

注意可选访问器和 !optionsBuilder.IsConfigured。 您将需要它们来简化您将覆盖上下文配置的测试。

@ginalx 和@jcmordan 的回答非常适合我的用例。我喜欢这些答案的一点是,我可以在 Startup.cs 中完成所有操作,并保持所有其他 类 的构造代码。我想为 Web Api 请求提供一个可选的查询字符串参数,并将其替换为创建 DbContext 的基本连接字符串。我将基本字符串保留在 appsettings.json 中,并根据传入的参数或默认值(如果提供了 none)对其进行格式化,即:

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

对我来说最终的 ConfigureServices 方法看起来像(我想连接到 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"
            });
        });
    }

所有其他答案都不适合我。所以我想与那些在运行时更改数据库连接字符串的人分享我的方法。

我的应用程序是用 asp.net 核心 2.2Entity FrameworkMySql.

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 Class

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 Class

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 Class

public static class DbManager
{
    public static string DbName;

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

然后,您需要一些控制器来设置 dbName

控制器Class

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

您可能需要在这里或那里做一些小把戏。但你会明白的。在应用程序的开头,它没有连接详细信息。所以你必须使用 Controller 明确地设置它。希望这会对某人有所帮助。

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

虽然晚了,但 EF Core 中最简单的技巧是使用 nuget Microsoft.EntityFrameworkCore.Relational:

_dbContext.Database.GetDbConnection().ConnectionString = "NEW_CONN_STRING";

当连接字符串因任何原因不存在于您的应用程序中时 config/settings 或者您想使用 DbContext 的一个实例处理具有相同结构的多个数据库(再次,出于任何原因)时,这很有用。

永久暂时取决于您为 DbContext 选择的注入类型 life-cycle。如果您将其作为 Singleton 服务注入,它将是 永久 推荐。

我选择了这个解决方案:

而不是

services.AddScoped<IMyDbContext, MyDbContext>();

我去了

services.AddTransient<IMyDbContext, MyDbContext>(resolver =>
{
    var context= resolver.GetService<MyDbContext>();
    var config = resolver.GetService<IConfiguration>();
    var connectionString = config.GetConnectionString("MyDb");
    context.GetDbConnection().ConnectionString = connectionString;
    return context;
});

在运行时覆盖设置:

Configuration["ConnectionStrings:MyDb"] = newConnectionString;

我创建了一个 .net6 控制台 应用程序并循环 1 到 10 以插入到 test1 数据库和 test2 数据库: Program.cs :

Console.WriteLine("Hello, World!");

for (int i = 1; i <= 10; i++)
{

    if (i % 2 == 0)
    {
        var _context = new AppDbContext("Data Source=.\SQLEXPRESS;Initial Catalog=test2;Integrated Security=True"); // test2
        _context.Tbls.Add(new Tbl { Title = i.ToString() });
        _context.SaveChanges();
    }
    else
    {
        var _context = new AppDbContext("Data Source=.\SQLEXPRESS;Initial Catalog=test1;Integrated Security=True"); // test1
        _context.Tbls.Add(new Tbl { Title = i.ToString() });
        _context.SaveChanges();
    }
}

AppDbContext.cs :

public partial class AppDbContext : DbContext
    {
        public AppDbContext(string connectionString) : base(GetOptions(connectionString))
        {
        }

        public virtual DbSet<Tbl> Tbls { get; set; }

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