Entity Framework меняет соединение во время выполнения

У меня есть проект веб-API, который ссылается на мою модель и сборки DAL. Пользователю предоставляется экран входа в систему, где он может выбирать разные базы данных.

Я строю строку подключения следующим образом:

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

Прежде всего, как мне на самом деле изменить соединение контекста данных?

И во-вторых, поскольку это проект веб-API, является ли строка подключения (настроенная для входа в систему выше) постоянной в течение всего взаимодействия с пользователем, или она должна передаваться каждый раз в мой контекст данных?

13 ответов

Решение

Немного опоздал с этим ответом, но я думаю, что есть потенциальный способ сделать это с помощью аккуратного метода расширения. Мы можем воспользоваться соглашением 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
    );

Я знаю, что у вас уже есть базовая функциональность, но я подумал, что это добавит немного разнообразия.

DbContext имеет перегрузку конструктора, которая принимает имя строки подключения или самой строки подключения. Реализуйте свою собственную версию и передайте ее базовому конструктору:

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

Затем просто передайте имя настроенной строки подключения или самой строки подключения, когда вы создаете DbContext

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

Ответ Джима Толлана отлично работает, но я получил сообщение об ошибке: ключевое слово не поддерживает "источник данных". Чтобы решить эту проблему, мне пришлось изменить эту часть своего кода:

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

Мне очень жаль. Я знаю, что не должен использовать ответы, чтобы отвечать на другие ответы, но мой ответ слишком длинный для комментария:(

Созданный класс является "частичным"!

public partial class Database1Entities1 : DbContext
{
    public Database1Entities1()
        : base("name=Database1Entities1")
    {
    }

... и вы называете это так:

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

Таким образом, вам нужно только создать частичный собственный файл класса для исходного автоматически сгенерированного класса (с тем же именем класса!) и добавить новый конструктор с параметром строки подключения, как ответ Мохо ранее.

После этого вы можете использовать параметризованный конструктор против оригинала.:-)

пример:

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

Вы можете сделать это на лету сIDbConnectionInterceptor. Преимущество этого заключается в том, что вы можете работать со стандартной строкой подключения, а не с версией Entity Client, а также не нужно изменять автоматически сгенерированные классы контекста в модели 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; просто добавьте <interceptor />к существующему <entityFramework />node с полным именем типа вашего нового перехватчика:

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

Или, мое личное предпочтение, вы можете подключить его с помощью кода. Это эквивалентно версии конфигурации. В идеале это должно происходить в Application_Startupв сервисе/проекте пользовательского интерфейса или ближе к началу Mainв консольном приложении, потому что оно должно запускаться до того, как вы попытаетесь установить какие-либо новые s:

      DbInterception.Add(new Foo.Bar.SecretsDbConnectionInterceptor());

При настройке с помощью кода вы можете передать параметры конструктору перехватчика или использовать DI.

Примечание: код перехватчика запускается каждый раз, когда вы создаете новый экземпляр любого DbContextв вашем приложении, поэтому остерегайтесь влияния на производительность. Вы можете реализовать некоторую стратегию кэширования в своем перехватчике или сделать его одноэлементным экземпляром с отображением имени контекста/строки подключения или что-то в этом роде.

Добавьте несколько строк подключения в ваш web.config или app.config.

Затем вы можете получить их в виде строки:

System.Configuration.ConfigurationManager.
    ConnectionStrings["entityFrameworkConnection"].ConnectionString;

Затем используйте строку для установки:

Provider
Metadata
ProviderConnectionString

Это лучше объяснить здесь:

Прочитать строку подключения из web.config

Для баз данных SQL Server и SQLite используйте:

_sqlServerDBsContext = new SqlServerDBsContext(new DbContextOptionsBuilder<SqlServerDBsContext>().UseSqlServer("Connection String to SQL DB").Options);

Для SQLite убедитесь, что Microsoft.EntityFrameworkCore.Sqlite установлен, то строка подключения будет просто "'DataSource='+ имя файла".

_sqliteDBsContext = new SqliteDBsContext(new DbContextOptionsBuilder<SqliteDBsContext>().UseSqlite("Connection String to SQLite DB").Options);

В моем случае я использую 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
        }
    }
}

хорошо, если вы работаете с 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");

У меня есть два метода расширения для преобразования обычной строки подключения в формат 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

Я хотел, чтобы в конфигурации приложения было несколько источников данных. Поэтому после настройки раздела в app.config я выгрузил источник данных и затем передал его в dbcontext в качестве строки подключения.

//Get the key/value connection string from app config  
var sect = (NameValueCollection)ConfigurationManager.GetSection("section");  
var val = sect["New DataSource"].ToString();

//Get the original connection string with the full payload  
var entityCnxStringBuilder = new EntityConnectionStringBuilder(ConfigurationManager.ConnectionStrings["OriginalStringBuiltByADO.Net"].ConnectionString);     

//Swap out the provider specific connection string  
entityCnxStringBuilder.ProviderConnectionString = val;

//Return the payload with the change in connection string.   
return entityCnxStringBuilder.ConnectionString;

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

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;
        }
    }
Linq2SQLDataClassesDataContext db = new Linq2SQLDataClassesDataContext();

var query = from p in db.SyncAudits orderby p.SyncTime descending select p;
Console.WriteLine(query.ToString());

попробуйте этот код...

Другие вопросы по тегам