Entity Framework zmienia połączenie w czasie wykonywania

80

Mam projekt internetowego interfejsu API, który odwołuje się do mojego modelu i zestawów DAL. Użytkownikowi pojawia się ekran logowania, na którym może wybierać różne bazy danych.

Buduję parametry połączenia w następujący sposób:

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

Przede wszystkim, jak właściwie zmienić połączenie kontekstu danych?

Po drugie, ponieważ jest to projekt internetowego interfejsu API, czy parametry połączenia (ustawione przy logowaniu według powyższego) są trwałe podczas interakcji użytkownika, czy też powinny być przekazywane za każdym razem do mojego kontekstu danych?

Ivan-Mark Debono
źródło
Dodałem małą alternatywę na wypadek, gdyby pasowała do twojego sposobu myślenia / wymagań zestawu narzędzi.
Jim Tollan
@ Ivan-Mark Jak rozwiązałeś tę część Po drugie, ponieważ jest to projekt interfejsu API sieci Web, czy ciąg połączenia (ustawiany przy logowaniu według powyższego) jest trwały podczas interakcji użytkownika, czy też powinien być przekazywany za każdym razem do mojego kontekstu danych
Narendra Singh Rathore
@NarendraSinghRathore Parametry połączenia są przechowywane w pliku konfiguracyjnym, którego kluczem jest nazwa bazy danych (lub coś innego). Użytkownik wybiera bazę danych podczas logowania i jest przechowywana w pamięci podręcznej, w której kluczem może być nazwa użytkownika. Użytkownik wysyła żądanie, przekazując swoją nazwę użytkownika jako nagłówek, a łańcuch połączenia jest pobierany i przekazywany do kontekstu danych.
Ivan-Mark Debono
@ Ivan-MarkDebono Czy możesz wyjaśnić tę pamięć podręczną ? Czy korzystasz z pamięci podręcznej lub sesji na zapleczu lub przechowujesz jako plik cookie na interfejsie użytkownika. Dzięki!
Narendra Singh Rathore
1
@NarendraSinghRathore MemoryCache in a singleton
Ivan-Mark Debono

Odpowiedzi:

110

Trochę za późno na tę odpowiedź, ale myślę, że istnieje potencjalny sposób na zrobienie tego za pomocą zgrabnej, małej metody rozszerzenia. Możemy skorzystać z konwencji EF zamiast konfiguracji oraz kilku małych wywołań struktury.

W każdym razie skomentowany kod i przykładowe użycie:

klasa metody rozszerzającej:

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

podstawowe zastosowanie:

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

Wiem, że masz już podstawową funkcjonalność, ale pomyślałem, że dodałoby to trochę różnorodności.

Jim Tollan
źródło
6
To świetnie, dziękuję! Jestem w stanie użyć tego w projekcie z wieloma dzierżawcami wraz z rozszerzonym Controller, który zawsze ustawi „db” kontrolera na bazę danych konkretnego klienta. To również uwalnia mnie (lub przyszłych administratorów / programistów) od konieczności tworzenia nowych parametrów połączenia dla każdego dodawanego klienta.
LukeP
3
tak, walczyłem przez dosłownie dni, próbując wymyślić realne, solidne rozwiązanie tego problemu, a ta prosta metoda rozszerzenia odpowiedziała na moje problemy. od czasu stworzenia go w listopadzie zeszłego roku nie musiałem dokonywać żadnych zmian, więc myślę, że jest dobrze przetestowany na drodze :). w każdym razie, cieszę się, że spełnia kilka kryteriów ... dobrze rozmawiać.
Jim Tollan
5
Otrzymuję ten błąd System.ArgumentException: Słowo kluczowe nie jest obsługiwane: „źródło danych” w EF 4
sheshadri
2
@ user1234 Otrzymałem również błąd: słowo kluczowe nie jest obsługiwane „źródło danych”. Aby rozwiązać ten problem, musiałem zmienić tę część jego kodu: // add a reference to System.Configuration var entityCnxStringBuilder = new EntityConnectionStringBuilder { ProviderConnectionString = new SqlConnectionStringBuilder(System.Configuration.ConfigurationManager .ConnectionStrings[configNameEf].ConnectionString).ConnectionString };
A.Ima
2
@jimtollan Za każdym razem, gdy tworzę nową instancję, jest ona tworzona ze starych parametrów połączenia zapisanych w pliku app.config !!
Abdulsalam Elsharif
62

DbContextma przeciążenie konstruktora, które akceptuje nazwę parametrów połączenia lub same parametry połączenia. Zaimplementuj własną wersję i przekaż ją do konstruktora podstawowego:

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

Następnie po prostu przekaż nazwę skonfigurowanych parametrów połączenia lub same parametry połączenia podczas tworzenia wystąpienia DbContext

var context = new MyDbContext( "..." );
Moho
źródło
Nie zdawałem sobie sprawy, że funkcja już istnieje w mojej klasie pochodnej DbContext, więc właśnie jej użyłem.
Brian Leeming
2
Myślę, że ta odpowiedź powinna oznaczać jako zatwierdzoną odpowiedź.
nic
2
Ta odpowiedź jest świetna, ale jak wyjaśnia @eMeL. Ta klasa jest generowana automatycznie, więc zamiast tego powinieneś utworzyć inną klasę w oparciu o tę, aby nie została nadpisana, jeśli zaktualizujesz model.
Juan Carlos Oropeza
4
@JuanCarlosOropeza: EF sprytnie oznacza wygenerowane klasy (bot hcontext i encje) jako częściowe, dzięki czemu możesz utworzyć własny plik, ponownie zadeklarować w nim swój DbContext (jako częściowe) i dodać tam swoje funkcje niestandardowe.
dotNET
14

Odpowiedź Jima Tollana działa świetnie, ale pojawił się błąd: słowo kluczowe nie jest obsługiwane jako „źródło danych”. Aby rozwiązać ten problem, musiałem zmienić tę część jego kodu:

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

do tego:

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

Bardzo przepraszam. Wiem, że nie powinienem odpowiadać na inne odpowiedzi, ale moja odpowiedź jest za długa na komentarz :(

A.Ima
źródło
6

Utworzona klasa jest „częściowa”!

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

... i nazywasz to tak:

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

więc musisz tylko utworzyć częściowy plik własnej klasy dla oryginalnej automatycznie wygenerowanej klasy (z tą samą nazwą klasy!) i dodać nowy konstruktor z parametrem połączenia, tak jak wcześniej odpowiedź Moho.

Po tym możesz użyć sparametryzowanego konstruktora przeciwko oryginalnemu. :-)

przykład:

using (var ctx = new Database1Entities1(myOwnConnectionString))
      {
        #if DEBUG
        ctx.Database.Log = Console.Write;
        #endif
eMeL
źródło
Powyższe rozwiązanie działa dla mnie. Więcej szczegółów można znaleźć pod linkiem
Kartik Goyal
0

Dodaj wiele parametrów połączenia w pliku web.config lub app.config.

Następnie możesz uzyskać je jako ciąg, taki jak:

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

Następnie użyj ciągu, aby ustawić:

Provider
Metadata
ProviderConnectionString

Lepiej jest to wyjaśnione tutaj:

Odczytaj parametry połączenia z web.config

Bryan Arbelo
źródło
parametry połączenia są przechowywane w oddzielnej bazie danych serwera sql, a lista jest prezentowana użytkownikowi.
Ivan-Mark Debono
0
string _connString = "metadata=res://*/Model.csdl|res://*/Model.ssdl|res://*/Model.msl;provider=System.Data.SqlClient;provider connection string="data source=localhost;initial catalog=DATABASE;persist security info=True;user id=sa;password=YourPassword;multipleactiveresultsets=True;App=EntityFramework"";

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

Możesz pobrać parametry połączenia z web.config i po prostu ustawić je w konstruktorze EntityConnectionStringBuilder i użyć EntityConnectionStringBuilder jako argumentu w konstruktorze dla kontekstu.

Buforuj parametry połączenia według nazwy użytkownika. Prosty przykład wykorzystujący kilka ogólnych metod do obsługi dodawania / pobierania z pamięci podręcznej.

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;
        }
    }
scheien
źródło
Tak, ale czy nowy ciąg połączenia musi być przekazywany do dbcontext za każdym razem, gdy użytkownik wywołuje akcję kontrolera?
Ivan-Mark Debono
Prawdopodobnie usuwałbyś kontekst po każdym wywołaniu, więc tak. Kontekst powinien istnieć tylko dla jednego żądania (jednostka pracy). Wyjaśnienie
scheien
Jak i gdzie przechowywać łańcuch połączenia użytkownika na czas jego sesji? (Wielu użytkowników może łączyć się z projektem interfejsu API sieci Web i mieć różne
ciągi
Co powiesz na buforowanie go i odzyskanie go według nazwy użytkownika lub innego klucza.
scheien
0

W moim przypadku używam ObjectContext w przeciwieństwie do DbContext, więc w tym celu poprawiłem kod w akceptowanej odpowiedzi.

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
        }
    }
}
David
źródło
Pojawił się ten błąd Słowo kluczowe nie jest obsługiwane: „źródło danych”. Używam EF 4
sheshadri
0

Chciałem mieć wiele źródeł danych w konfiguracji aplikacji. Dlatego po skonfigurowaniu sekcji w pliku app.config podmieniłem źródło danych, a następnie przekazałem je do kontekstu dbcontext jako ciąg połączenia.

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

Trochę mi to zajęło zrozumienie. Mam nadzieję, że to komuś pomoże. Zbyt skomplikowałem to. przed tym.

Jake Porter
źródło
0

Mam dwie metody rozszerzające, aby przekonwertować normalne parametry połączenia na format Entity Framework. Ta wersja działa dobrze z projektami bibliotek klas bez kopiowania parametrów połączenia z pliku app.config do projektu głównego. To jest VB.Net, ale można go łatwo przekonwertować na 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

Następnie tworzę częściową klasę dla 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

Tworzenie zapytania:

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
SZL
źródło
0

W przypadku baz danych SQL Server i SQLite użyj:

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

W przypadku oprogramowania SQLite upewnij się, że Microsoft.EntityFrameworkCore.Sqlitejest zainstalowany, a ciąg połączenia to po prostu „'DataSource =' + nazwa pliku”.

_sqliteDBsContext = new SqliteDBsContext(new DbContextOptionsBuilder<SqliteDBsContext>().UseSqlite("Connection String to SQLite DB").Options);
TiyebM
źródło
-6
Linq2SQLDataClassesDataContext db = new Linq2SQLDataClassesDataContext();

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

wypróbuj ten kod ...

saad mehmood khan
źródło