Entity Framework cambiar la conexión en tiempo de ejecución

80

Tengo un proyecto de API web que hace referencia a mi modelo y ensamblajes DAL. Al usuario se le presenta una pantalla de inicio de sesión, donde puede seleccionar diferentes bases de datos.

Construyo la cadena de conexión de la siguiente manera:

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

En primer lugar, ¿cómo cambio realmente la conexión del contexto de datos?

Y en segundo lugar, como se trata de un proyecto de API web, ¿la cadena de conexión (configurada en el inicio de sesión anterior) es persistente a lo largo de la interacción del usuario o debería pasarse cada vez a mi contexto de datos?

Ivan-Mark Debono
fuente
Agregué una pequeña alternativa en caso de que encajara en sus requisitos de mentalidad / caja de herramientas.
Jim Tollan
@ Ivan-Mark ¿Cómo resolvió esta parte? Y en segundo lugar, como se trata de un proyecto de API web, la cadena de conexión (configurada en el inicio de sesión anterior) es persistente a lo largo de la interacción del usuario o debe pasarse cada vez a mi contexto de datos
Narendra Singh Rathore
@NarendraSinghRathore Las cadenas de conexión se almacenan en un archivo de configuración con el nombre de la base de datos (o algo más) como clave. El usuario selecciona una base de datos al iniciar sesión y se almacena en un caché donde la clave puede ser el nombre de usuario. El usuario realiza una solicitud pasando su nombre de usuario como encabezado y la cadena de conexión se recupera y se pasa al contexto de datos.
Ivan-Mark Debono
@ Ivan-MarkDebono ¿Puedes explicar este caché ? ¿Está utilizando memorycache o sesión en el backend o almacenando como cookie en el frontend? ¡Gracias!
Narendra Singh Rathore
1
@NarendraSinghRathore MemoryCache en un singleton
Ivan-Mark Debono

Respuestas:

110

Un poco tarde en esta respuesta, pero creo que hay una forma potencial de hacer esto con un pequeño método de extensión. Podemos aprovechar la convención EF sobre la configuración más algunas pequeñas llamadas al marco.

De todos modos, el código comentado y el uso de ejemplo:

clase de método de extensión:

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

uso básico:

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

Sé que ya tiene la funcionalidad básica en su lugar, pero pensé que esto agregaría un poco de diversidad.

Jim Tollan
fuente
6
Esto es genial, gracias! Puedo usar esto en un proyecto de múltiples inquilinos junto con un extendido Controllerque siempre establecerá el 'db' del controlador en su db específico del cliente. Esto también me libera a mí (oa cualquier administrador / desarrollador futuro) de tener que crear una nueva cadena de conexión para cada cliente que se agrega.
LukeP
3
sí, luché literalmente durante días tratando de encontrar una solución sólida viable para este problema y este método de extensión simple respondió a mis problemas. Desde que lo creé en noviembre del año pasado, no he tenido que hacerle ningún cambio, así que creo que está bien probado en carretera :). de todos modos, me alegro de que cumpla algunas casillas ... es bueno hablar.
jim tollan
5
Recibo este error System.ArgumentException: palabra clave no admitida: 'fuente de datos' en EF 4
sheshadri
2
@ user1234 También recibí el Error: Palabra clave no admitida 'fuente de datos'. Para resolver este problema tuve que cambiar esta parte de su código: // add a reference to System.Configuration var entityCnxStringBuilder = new EntityConnectionStringBuilder { ProviderConnectionString = new SqlConnectionStringBuilder(System.Configuration.ConfigurationManager .ConnectionStrings[configNameEf].ConnectionString).ConnectionString };
A.Ima
2
@jimtollan ¡¡Cada vez que creo una nueva instancia, se creó a partir de la cadena de conexión anterior que se guardó en app.config !!
Abdulsalam Elsharif
62

DbContexttiene una sobrecarga de constructor que acepta el nombre de una cadena de conexión o una cadena de conexión en sí. Implementa tu propia versión y pásala al constructor base:

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

Luego, simplemente pase el nombre de una cadena de conexión configurada o una cadena de conexión cuando cree una instancia de su DbContext

var context = new MyDbContext( "..." );
Moho
fuente
No me di cuenta de que la función ya existía en mi clase derivada de DbContext, así que solo la usé.
Brian Leeming
2
Creo que esta respuesta debería marcar como respuesta aprobada.
nada
2
Esta respuesta es genial, pero como explica @eMeL. Esta clase se genera automáticamente, por lo que debe crear otra clase basada en esta para que no se sobrescriba si actualiza el modelo.
Juan Carlos Oropeza
4
@JuanCarlosOropeza: EF marca inteligentemente las clases generadas (bot hcontext y entidades) como parciales, por lo que puede crear su propio archivo, volver a declarar su DbContext en él (como parcial) y agregar sus funciones personalizadas allí.
dotNET
14

La respuesta de Jim Tollan funciona muy bien, pero recibí el Error: palabra clave no admitida 'fuente de datos'. Para resolver este problema tuve que cambiar esta parte de su código:

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

a esto:

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

Lo siento mucho. Sé que no debería usar respuestas para responder a otras respuestas, pero mi respuesta es demasiado larga para un comentario :(

A.Ima
fuente
6

¡La clase creada es 'parcial'!

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

... y lo llamas así:

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

por lo tanto, solo necesita crear un archivo de clase propio parcial para la clase original generada automáticamente (¡con el mismo nombre de clase!) Y agregar un nuevo constructor con parámetro de cadena de conexión, como la respuesta de Moho antes.

Después de eso, puede usar el constructor parametrizado contra el original. :-)

ejemplo:

using (var ctx = new Database1Entities1(myOwnConnectionString))
      {
        #if DEBUG
        ctx.Database.Log = Console.Write;
        #endif
eMeL
fuente
La solución anterior funciona para mí. Puede obtener más detalles en el enlace
Kartik Goyal
0

Agregue varias cadenas de conexión en su web.config o app.config.

Entonces puedes obtenerlos como una cadena como:

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

Luego usa la cadena para establecer:

Provider
Metadata
ProviderConnectionString

Se explica mejor aquí:

Leer la cadena de conexión de web.config

Bryan Arbelo
fuente
las cadenas de conexión se almacenan en una base de datos separada del servidor SQL y se presenta una lista al usuario.
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);

Puede obtener la cadena de conexión de web.config, y simplemente establecerla en el constructor EntityConnectionStringBuilder, y usar EntityConnectionStringBuilder como argumento en el constructor para el contexto.

Almacene en caché la cadena de conexión por nombre de usuario. Ejemplo simple usando un par de métodos genéricos para manejar la adición / recuperación de caché.

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
fuente
Sí, pero ¿es necesario pasar la nueva cadena de conexión al dbcontext cada vez que el usuario llama a la acción de un controlador?
Ivan-Mark Debono
Probablemente eliminaría el contexto después de cada llamada, así que sí. El contexto solo debe vivir para una solicitud (Unidad de trabajo). Explicación
Scheien
Entonces, ¿cómo y dónde almacenaría la cadena de conexión del usuario durante su sesión? (Muchos usuarios pueden conectarse al proyecto de API web y pueden tener cadenas de conexión diferentes)
Ivan-Mark Debono
¿Qué tal almacenarlo en caché y recuperarlo por nombre de usuario o alguna otra clave?
Scheien
0

En mi caso, estoy usando ObjectContext en lugar de DbContext, así que modifiqué el código en la respuesta aceptada para ese propósito.

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
fuente
Recibí este error Palabra clave no admitida: 'fuente de datos'. Estoy usando EF 4
sheshadri
0

Quería tener múltiples fuentes de datos en la configuración de la aplicación. Entonces, después de configurar una sección en app.config, cambié la fuente de datos y luego la paso al dbcontext como la cadena de conexión.

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

Esto me tomó un poco entender. Espero que ayude a alguien. Lo estaba haciendo demasiado complicado. antes de esto.

Jake Porter
fuente
0

Tengo dos métodos de extensión para convertir la cadena de conexión normal al formato Entity Framework. Esta versión funciona bien con proyectos de biblioteca de clases sin copiar las cadenas de conexión del archivo app.config al proyecto principal. Esto es VB.Net pero fácil de convertir a 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

Después de eso, creo una clase parcial para 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

Creando una consulta:

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
fuente
0

Tanto para SQL Server como para bases de datos SQLite, use:

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

Para SQLite, asegúrese de que Microsoft.EntityFrameworkCore.Sqliteesté instalado, luego la cadena de conexión es simplemente "'DataSource =' + el nombre del archivo".

_sqliteDBsContext = new SqliteDBsContext(new DbContextOptionsBuilder<SqliteDBsContext>().UseSqlite("Connection String to SQLite DB").Options);
TiyebM
fuente
-6
Linq2SQLDataClassesDataContext db = new Linq2SQLDataClassesDataContext();

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

prueba este código ...

saad mehmood khan
fuente