运行时更改Entity Framework连接

88

我有一个 Web API 项目,引用了我的模型和数据访问层程序集。用户会看到一个登录界面,在该界面上可以选择不同的数据库。

我按如下方式构建连接字符串:

    public void Connect(Database database)
    {
        //Build an SQL connection string
        SqlConnectionStringBuilder sqlString = new SqlConnectionStringBuilder()
        {
            DataSource = database.Server,
            InitialCatalog = database.Catalog,
            UserID = database.Username,
            Password = database.Password,
        };

        //Build an entity framework connection string
        EntityConnectionStringBuilder entityString = new EntityConnectionStringBuilder()
        {
            Provider = database.Provider,
            Metadata = Settings.Default.Metadata,
            ProviderConnectionString = sqlString.ToString()
        };
    }

首先,我应该如何更改数据上下文的连接?

其次,由于这是一个Web API项目,连接字符串(在上面设置)是否在用户交互过程中持久存在,还是每次都需要传递给我的数据上下文?


我加了一点点替代方案,以防它符合你的思维方式/工具箱要求。 - jim tollan
@Ivan-Mark 你是怎么解决这个部分的?另外,由于这是一个 Web API 项目,连接字符串(在上面设置)是否在用户交互期间持久存在,还是每次都应该传递给我的数据上下文? - Narendra Singh Rathore
@NarendraSinghRathore 连接字符串存储在配置文件中,数据库名称(或其他内容)作为键。用户在登录时选择数据库,并将其存储在缓存中,其中键可能是用户名。用户通过标题传递用户名发出请求,然后检索连接字符串并将其传递给数据上下文。 - Ivan-Mark Debono
@Ivan-MarkDebono,你能解释一下这个缓存吗?你是在后端使用MemoryCache或Session还是在前端存储为Cookie?谢谢! - Narendra Singh Rathore
1
@NarendraSinghRathore 单例中的 MemoryCache - Ivan-Mark Debono
13个回答

122

回答有些晚了,但我认为有一个很好的扩展方法可以实现这个。我们可以利用EF的约定优于配置以及一些小框架调用。

无论如何,以下是注释代码和示例用法:

扩展方法类:

public static class ConnectionTools
{
    // all params are optional
    public static void ChangeDatabase(
        this DbContext source,
        string initialCatalog = "",
        string dataSource = "",
        string userId = "",
        string password = "",
        bool integratedSecuity = true,
        string configConnectionStringName = "") 
        /* this would be used if the
        *  connectionString name varied from 
        *  the base EF class name */
    {
        try
        {
            // use the const name if it's not null, otherwise
            // using the convention of connection string = EF contextname
            // grab the type name and we're done
            var configNameEf = string.IsNullOrEmpty(configConnectionStringName)
                ? source.GetType().Name 
                : configConnectionStringName;

            // add a reference to System.Configuration
            var entityCnxStringBuilder = new EntityConnectionStringBuilder
                (System.Configuration.ConfigurationManager
                    .ConnectionStrings[configNameEf].ConnectionString);

            // init the sqlbuilder with the full EF connectionstring cargo
            var sqlCnxStringBuilder = new SqlConnectionStringBuilder
                (entityCnxStringBuilder.ProviderConnectionString);

            // only populate parameters with values if added
            if (!string.IsNullOrEmpty(initialCatalog))
                sqlCnxStringBuilder.InitialCatalog = initialCatalog;
            if (!string.IsNullOrEmpty(dataSource))
                sqlCnxStringBuilder.DataSource = dataSource;
            if (!string.IsNullOrEmpty(userId))
                sqlCnxStringBuilder.UserID = userId;
            if (!string.IsNullOrEmpty(password))
                sqlCnxStringBuilder.Password = password;

            // set the integrated security status
            sqlCnxStringBuilder.IntegratedSecurity = integratedSecuity;

            // now flip the properties that were changed
            source.Database.Connection.ConnectionString 
                = sqlCnxStringBuilder.ConnectionString;
        }
        catch (Exception ex)
        {
            // set log item if required
        }
    }
}

基本用法:

// assumes a connectionString name in .config of MyDbEntities
var selectedDb = new MyDbEntities();
// so only reference the changed properties
// using the object parameters by name
selectedDb.ChangeDatabase
    (
        initialCatalog: "name-of-another-initialcatalog",
        userId: "jackthelady",
        password: "nomoresecrets",
        dataSource: @".\sqlexpress" // could be ip address 120.273.435.167 etc
    );

我知道你已经实现了基本功能,但是我认为这会增加一些多样性。


7
太好了,谢谢!我能在多租户项目中使用这个方法,并配合一个扩展的'Controller',使控制器的'db'始终设置为客户特定的数据库。这也使我(或任何未来的管理员/开发人员)不必为每个新添加的客户创建新的连接字符串。 - LukeP
5
是的,我为了解决这个问题苦苦挣扎了数天,而这个简单的扩展方法回答了我的问题。自从去年11月创建以来,我没有对它进行任何更改,所以我认为它已经经过了充分的测试 :). 不管怎样,很高兴它满足了一些需求...好聊天。 - jim tollan
5
我遇到了这个错误:System.ArgumentException: EF 4中不支持关键字“data source”。 - sheshadri
2
@user1234 我也遇到了错误:关键字不支持 'data source'。为了解决这个问题,我不得不更改他代码的这一部分: // 添加对 System.Configuration 的引用 var entityCnxStringBuilder = new EntityConnectionStringBuilder { ProviderConnectionString = new SqlConnectionStringBuilder(System.Configuration.ConfigurationManager .ConnectionStrings[configNameEf].ConnectionString).ConnectionString }; - A.Ima
2
@jimtollan 每次我创建新实例时,它都是从保存在 app.config 中的旧连接字符串创建的!! - Abdulsalam Elsharif
显示剩余10条评论

70

DbContext有一个构造函数重载,可以接受一个连接字符串的名称或连接字符串本身。实现自己的版本并将其传递给基础构造函数:

DbContext有一个构造函数重载,可以接受一个连接字符串的名称或连接字符串本身。实现您自己的版本,并将其传递给基类的构造函数:

public class MyDbContext : DbContext
{
    public MyDbContext( string nameOrConnectionString ) 
        : base( nameOrConnectionString )
    {
    }
}

当你实例化你的DbContext时,只需传递一个已配置的连接字符串的名称或一个连接字符串本身即可。

var context = new MyDbContext( "..." );

我没有意识到在我的DbContext派生类中已经存在该函数,所以我只是使用了它。 - Brian Leeming
2
我认为这个答案应该标记为批准的答案。 - NoughT
2
这个答案很好,但正如@eMeL所解释的那样。这个类是自动生成的,所以你应该基于这个类创建另一个类,这样如果你更新模型,它不会被覆盖。 - Juan Carlos Oropeza
4
EF 聪明地将生成的类(bot hcontext 和 entities)标记为 partial,这样您就可以创建自己的文件,在其中重新声明 DbContext(作为 partial),并在其中添加自定义函数。 - dotNET

16

Jim Tollan的答案非常好用,但我遇到了错误:“不支持的关键字 'data source'”。为了解决这个问题,我不得不更改他代码中的这一部分:

// add a reference to System.Configuration
var entityCnxStringBuilder = new EntityConnectionStringBuilder
    (System.Configuration.ConfigurationManager
            .ConnectionStrings[configNameEf].ConnectionString);

变成这样:

// add a reference to System.Configuration
var entityCnxStringBuilder = new EntityConnectionStringBuilder
{
    ProviderConnectionString = new  SqlConnectionStringBuilder(System.Configuration.ConfigurationManager
               .ConnectionStrings[configNameEf].ConnectionString).ConnectionString
};

非常抱歉。我知道我不应该用答案来回应其他答案,但我的回答对于评论来说太长了 :(


7
创建的类是“partial”!
public partial class Database1Entities1 : DbContext
{
    public Database1Entities1()
        : base("name=Database1Entities1")
    {
    }

...你可以这样调用它:

using (var ctx = new Database1Entities1())
      {
        #if DEBUG
        ctx.Database.Log = Console.Write;
        #endif

因此,您只需要为原始自动生成的类创建一个部分自己的类文件(与同一类名!),并添加一个新的构造函数,即连接字符串参数,就像Moho之前的答案一样。
之后,您可以针对原始对象使用带参数的构造函数。 :-)
例如:
using (var ctx = new Database1Entities1(myOwnConnectionString))
      {
        #if DEBUG
        ctx.Database.Log = Console.Write;
        #endif

以上解决方案对我有效。您可以从链接获取更多详细信息。 - Kartik Goyal

2
您可以通过使用IDbConnectionInterceptor来实现即时操作。这样做的好处是,您可以使用标准连接字符串而不是实体客户端版本,并且无需修改EDMX模型中的自动生成的上下文类或使用重载构造函数。 它只需要按预期工作!
例如,我们可以使用此功能将令牌化的连接字符串替换为来自密钥保管库的密码。
首先,实现该接口。 我仅显示需要实现的许多接口方法之一。 在这种情况下,我正在实现ConnectionStringGetting,并留空所有其他方法体:
public class SecretsDbConnectionInterceptor : IDbConnectionInterceptor
{
    public void ConnectionStringGetting(DbConnection connection, DbConnectionInterceptionContext<string> interceptionContext)
    {
        var originalConnectionString = connection.ConnectionString;
        try
        {
            connection.ConnectionString = /* Build your new connection string */;
        }
        catch (Exception e)
        {
            connection.ConnectionString = originalConnectionString;
            Trace.WriteLine(e.Message);
        }
    }
    
    // ... Many other methods here; leave them empty
}

您可以通过.config文件连接这个;只需在现有的<entityFramework />节点中添加一个<interceptor />,并使用您的新拦截器的完全限定类型名称:

  <entityFramework>
    <interceptors>
      <interceptor type="Foo.Bar.SecretsDbConnectionInterceptor, Foo.Bar" />
    </interceptors>
    ...
  </entityFramework>

或者,我的个人偏好是通过代码进行连接。这与配置版本等效。理想情况下,这应该在服务/UI项目的中进行,在控制台应用程序的
顶部进行,因为它必须在尝试建立任何新的之前运行:
DbInterception.Add(new Foo.Bar.SecretsDbConnectionInterceptor());

当您通过代码进行配置时,可以将参数传递给拦截器构造函数或使用 DI。

注意:拦截器代码每次在应用程序中创建任何 DbContext 的新实例时都会运行,因此要注意性能影响。您可以在拦截器内实现一些缓存策略,或者使其成为具有上下文名称/连接字符串映射或类似智能功能的单例实例。


0

如果你正在使用EFCore,那么你可以像下面这样创建一个新的连接字符串: 在你的上下文文件中(对于Sqlite)

public biorevContext(string connectionString) : base(GetOptions(connectionString))
    {
       this.Database.EnsureCreated();
    }
    private static DbContextOptions GetOptions(string connectionString)
    {
        return SqliteDbContextOptionsBuilderExtensions.UseSqlite(new DbContextOptionsBuilder(), connectionString).Options;
    }

对于MySql:

 public biorevContext(string connectionString) : base(GetOptions(connectionString))
    {
       this.Database.EnsureCreated();
    }
    private static DbContextOptions GetOptions(string connectionString)
    {
        return MySQLDbContextOptionsExtensions.UseMySQL(new DbContextOptionsBuilder(), connectionString).Options;
    }

对于 Sql:

    public biorevContext(string connectionString) : base(GetOptions(connectionString))
    {
       this.Database.EnsureCreated();
    } 
    private static DbContextOptions GetOptions(string connectionString)
    {
        return SqlServerDbContextOptionsExtensions.UseSqlServer(new DbContextOptionsBuilder(), connectionString).Options;
    }

然后你可以像这样使用它:

        var context = new biorevContext("connectionString");

0
在我的情况下,我使用的是ObjectContext而不是DbContext,因此我调整了被接受答案中的代码以适应这个目的。
public static class ConnectionTools
{
    public static void ChangeDatabase(
        this ObjectContext source,
        string initialCatalog = "",
        string dataSource = "",
        string userId = "",
        string password = "",
        bool integratedSecuity = true,
        string configConnectionStringName = "")
    {
        try
        {
            // use the const name if it's not null, otherwise
            // using the convention of connection string = EF contextname
            // grab the type name and we're done
            var configNameEf = string.IsNullOrEmpty(configConnectionStringName)
                ? Source.GetType().Name
                : configConnectionStringName;

            // add a reference to System.Configuration
            var entityCnxStringBuilder = new EntityConnectionStringBuilder
                (System.Configuration.ConfigurationManager
                    .ConnectionStrings[configNameEf].ConnectionString);

            // init the sqlbuilder with the full EF connectionstring cargo
            var sqlCnxStringBuilder = new SqlConnectionStringBuilder
                (entityCnxStringBuilder.ProviderConnectionString);

            // only populate parameters with values if added
            if (!string.IsNullOrEmpty(initialCatalog))
                sqlCnxStringBuilder.InitialCatalog = initialCatalog;
            if (!string.IsNullOrEmpty(dataSource))
                sqlCnxStringBuilder.DataSource = dataSource;
            if (!string.IsNullOrEmpty(userId))
                sqlCnxStringBuilder.UserID = userId;
            if (!string.IsNullOrEmpty(password))
                sqlCnxStringBuilder.Password = password;

            // set the integrated security status
            sqlCnxStringBuilder.IntegratedSecurity = integratedSecuity;

            // now flip the properties that were changed
            source.Connection.ConnectionString
                = sqlCnxStringBuilder.ConnectionString;
        }
        catch (Exception ex)
        {
            // set log item if required
        }
    }
}

我遇到了这个错误:“不支持的关键字:'数据源'”。我正在使用EF 4。 - sheshadri

0
在您的web.config或app.config中添加多个连接字符串。
然后,您可以将它们作为字符串获取,例如:
System.Configuration.ConfigurationManager.
    ConnectionStrings["entityFrameworkConnection"].ConnectionString;

然后使用字符串来设置:

Provider
Metadata
ProviderConnectionString

这里有更好的解释:

从 web.config 读取连接字符串


连接字符串存储在单独的 SQL Server 数据库中,并向用户呈现列表。 - Ivan-Mark Debono

0
string _connString = "metadata=res://*/Model.csdl|res://*/Model.ssdl|res://*/Model.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=localhost;initial catalog=DATABASE;persist security info=True;user id=sa;password=YourPassword;multipleactiveresultsets=True;App=EntityFramework&quot;";

EntityConnectionStringBuilder ecsb = new EntityConnectionStringBuilder(_connString);
ctx = new Entities(_connString);

您可以从web.config中获取连接字符串,然后将其设置在EntityConnectionStringBuilder构造函数中,并将EntityConnectionStringBuilder用作上下文构造函数的参数。

按用户名缓存连接字符串。使用一些通用方法来处理添加/检索缓存的简单示例。

private static readonly ObjectCache cache = MemoryCache.Default;

// add to cache
AddToCache<string>(username, value);

// get from cache

 string value = GetFromCache<string>(username);
 if (value != null)
 {
     // got item, do something with it.
 }
 else
 {
    // item does not exist in cache.
 }


public void AddToCache<T>(string token, T item)
    {
        cache.Add(token, item, DateTime.Now.AddMinutes(1));
    }

public T GetFromCache<T>(string cacheKey) where T : class
    {
        try
        {
            return (T)cache[cacheKey];
        }
        catch
        {
            return null;
        }
    }

是的,但是每次用户调用控制器的操作时,新的连接字符串需要传递给DbContext吗? - Ivan-Mark Debono
您可能会在每次调用后处理上下文,所以是的。上下文应该只存在于一个请求(工作单元)中。[解释](https://dev59.com/pGkv5IYBdhLWcg3wdQhT) - scheien
那么我应该在哪里存储用户的连接字符串以供其会话期间使用呢?(许多用户可以连接到 Web API 项目并具有不同的连接字符串) - Ivan-Mark Debono
缓存它,然后通过用户名或其他键检索它如何? - scheien

0

我有两个扩展方法可以将普通的连接字符串转换为Entity Framework格式。这个版本适用于类库项目,无需将连接字符串从app.config文件复制到主项目中。这是VB.Net编写的,但很容易转换为C#。

Public Module Extensions

    <Extension>
    Public Function ToEntityConnectionString(ByRef sqlClientConnStr As String, ByVal modelFileName As String, Optional ByVal multipleActiceResultSet As Boolean = True)
        Dim sqlb As New SqlConnectionStringBuilder(sqlClientConnStr)
        Return ToEntityConnectionString(sqlb, modelFileName, multipleActiceResultSet)
    End Function

    <Extension>
    Public Function ToEntityConnectionString(ByRef sqlClientConnStrBldr As SqlConnectionStringBuilder, ByVal modelFileName As String, Optional ByVal multipleActiceResultSet As Boolean = True)
        sqlClientConnStrBldr.MultipleActiveResultSets = multipleActiceResultSet
        sqlClientConnStrBldr.ApplicationName = "EntityFramework"

        Dim metaData As String = "metadata=res://*/{0}.csdl|res://*/{0}.ssdl|res://*/{0}.msl;provider=System.Data.SqlClient;provider connection string='{1}'"
        Return String.Format(metaData, modelFileName, sqlClientConnStrBldr.ConnectionString)
    End Function

End Module

接下来,我为DbContext创建了一个部分类:

Partial Public Class DlmsDataContext

    Public Shared Property ModelFileName As String = "AvrEntities" ' (AvrEntities.edmx)

    Public Sub New(ByVal avrConnectionString As String)
        MyBase.New(CStr(avrConnectionString.ToEntityConnectionString(ModelFileName, True)))
    End Sub

End Class

创建查询:
Dim newConnectionString As String = "Data Source=.\SQLEXPRESS;Initial Catalog=DB;Persist Security Info=True;User ID=sa;Password=pass"

Using ctx As New DlmsDataContext(newConnectionString)
    ' ...
    ctx.SaveChanges()
End Using

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接