¿Cómo manejo las conexiones de bases de datos con Dapper en .NET?

83

He estado jugando con Dapper, pero no estoy seguro de cuál es la mejor manera de manejar la conexión de la base de datos.

La mayoría de los ejemplos muestran el objeto de conexión que se crea en la clase de ejemplo, o incluso en cada método. Pero me parece incorrecto hacer referencia a una cadena de conexión en cada clss, incluso si se extrae de web.config.

Mi experiencia ha sido con el uso de DbDataContexto DbContextcon Linq para SQL o Entity Framework, por lo que esto es nuevo para mí.

¿Cómo estructuro mis aplicaciones web cuando utilizo Dapper como mi estrategia de acceso a datos?

Donald Hughes
fuente
Demasiado tarde pero; Lo implementé así: stackoverflow.com/a/45029588/5779732
Amit Joshi
using-dapper-asynchronously-in-asp-net-core-2 - exceptionnotfound.net/…
Himalaya Garg

Respuestas:

48

Microsoft.AspNetCore.All : v2.0.3 | Dapper : v1.50.2

No estoy seguro de si estoy usando las mejores prácticas correctamente o no, pero lo estoy haciendo de esta manera, para manejar múltiples cadenas de conexión.

Es fácil si solo tiene 1 cadena de conexión

Startup.cs

using System.Data;
using System.Data.SqlClient;

namespace DL.SO.Project.Web.UI
{
    public class Startup
    {
        public IConfiguration Configuration { get; private set; }

        // ......

        public void ConfigureServices(IServiceCollection services)
        {
            // Read the connection string from appsettings.
            string dbConnectionString = this.Configuration.GetConnectionString("dbConnection1");

            // Inject IDbConnection, with implementation from SqlConnection class.
            services.AddTransient<IDbConnection>((sp) => new SqlConnection(dbConnectionString));

            // Register your regular repositories
            services.AddScoped<IDiameterRepository, DiameterRepository>();

            // ......
        }
    }
}

DiameterRepository.cs

using Dapper;
using System.Data;

namespace DL.SO.Project.Persistence.Dapper.Repositories
{
    public class DiameterRepository : IDiameterRepository
    {
        private readonly IDbConnection _dbConnection;

        public DiameterRepository(IDbConnection dbConnection)
        {
            _dbConnection = dbConnection;
        }

        public IEnumerable<Diameter> GetAll()
        {
            const string sql = @"SELECT * FROM TABLE";

            // No need to use using statement. Dapper will automatically
            // open, close and dispose the connection for you.
            return _dbConnection.Query<Diameter>(sql);
        }

        // ......
    }
}

Problemas si tiene más de 1 cadena de conexión

Dado que se Dapperutiliza IDbConnection, debe pensar en una forma de diferenciar las diferentes conexiones de bases de datos.

Intenté crear múltiples interfaces, 'heredadas' de IDbConnection, correspondientes a diferentes conexiones de base de datos, e inyectar SqlConnectioncon diferentes cadenas de conexión de base de datos en Startup.

Eso falló porque SqlConnectionhereda DbConnectiony DbConnectioncomplementa no solo IDbConnectionsino también la Componentclase. Por lo tanto, sus interfaces personalizadas no podrán usar solo la SqlConnectionimplementación.

También intenté crear mi propia DbConnectionclase que toma una cadena de conexión diferente. Eso es demasiado complicado porque tienes que implementar todos los métodos de la DbConnectionclase. Perdiste la ayuda de SqlConnection.

Lo que termino haciendo

  1. Durante Startup, cargué todos los valores de cadena de conexión en un diccionario. También creé un enumpara todos los nombres de conexión de la base de datos para evitar cadenas mágicas.
  2. Inyecté el diccionario como Singleton.
  3. En lugar de inyectar IDbConnection, lo creé IDbConnectionFactorye inyecté como Transitorio para todos los repositorios. Ahora todos los repositorios toman en IDbConnectionFactorylugar de IDbConnection.
  4. ¿Cuándo elegir la conexión correcta? ¡En el constructor de todos los repositorios! Para hacer las cosas limpias, creé las clases base del repositorio y los repositorios heredaron de las clases base. La selección de la cadena de conexión correcta puede ocurrir en las clases base.

DatabaseConnectionName.cs

namespace DL.SO.Project.Domain.Repositories
{
    public enum DatabaseConnectionName
    {
        Connection1,
        Connection2
    }
}

IDbConnectionFactory.cs

using System.Data;

namespace DL.SO.Project.Domain.Repositories
{
    public interface IDbConnectionFactory
    {
        IDbConnection CreateDbConnection(DatabaseConnectionName connectionName);
    }
}

DapperDbConenctionFactory - mi propia implementación de fábrica

namespace DL.SO.Project.Persistence.Dapper
{
    public class DapperDbConnectionFactory : IDbConnectionFactory
    {
        private readonly IDictionary<DatabaseConnectionName, string> _connectionDict;

        public DapperDbConnectionFactory(IDictionary<DatabaseConnectionName, string> connectionDict)
        {
            _connectionDict = connectionDict;
        }

        public IDbConnection CreateDbConnection(DatabaseConnectionName connectionName)
        {
            string connectionString = null;
            if (_connectDict.TryGetValue(connectionName, out connectionString))
            {
                return new SqlConnection(connectionString);
            }

            throw new ArgumentNullException();
        }
    }
}

Startup.cs

namespace DL.SO.Project.Web.UI
{
    public class Startup
    {
        // ......

        public void ConfigureServices(IServiceCollection services)
        {
            var connectionDict = new Dictionary<DatabaseConnectionName, string>
            {
                { DatabaseConnectionName.Connection1, this.Configuration.GetConnectionString("dbConnection1") },
                { DatabaseConnectionName.Connection2, this.Configuration.GetConnectionString("dbConnection2") }
            };

            // Inject this dict
            services.AddSingleton<IDictionary<DatabaseConnectionName, string>>(connectionDict);

            // Inject the factory
            services.AddTransient<IDbConnectionFactory, DapperDbConnectionFactory>();

            // Register your regular repositories
            services.AddScoped<IDiameterRepository, DiameterRepository>();

            // ......
        }
    }
}

DiameterRepository.cs

using Dapper;
using System.Data;

namespace DL.SO.Project.Persistence.Dapper.Repositories
{
    // Move the responsibility of picking the right connection string
    //   into an abstract base class so that I don't have to duplicate
    //   the right connection selection code in each repository.
    public class DiameterRepository : DbConnection1RepositoryBase, IDiameterRepository
    {
        public DiameterRepository(IDbConnectionFactory dbConnectionFactory)
            : base(dbConnectionFactory) { }

        public IEnumerable<Diameter> GetAll()
        {
            const string sql = @"SELECT * FROM TABLE";

            // No need to use using statement. Dapper will automatically
            // open, close and dispose the connection for you.
            return base.DbConnection.Query<Diameter>(sql);
        }

        // ......
    }
}

DbConnection1RepositoryBase.cs

using System.Data;
using DL.SO.Project.Domain.Repositories;

namespace DL.SO.Project.Persistence.Dapper
{
    public abstract class DbConnection1RepositoryBase
    {
        public IDbConnection DbConnection { get; private set; }

        public DbConnection1RepositoryBase(IDbConnectionFactory dbConnectionFactory)
        {
            // Now it's the time to pick the right connection string!
            // Enum is used. No magic string!
            this.DbConnection = dbConnectionFactory.CreateDbConnection(DatabaseConnectionName.Connection1);
        }
    }
}

Luego, para otros repositorios que necesitan comunicarse con las otras conexiones, puede crear una clase base de repositorio diferente para ellos.

using System.Data;
using DL.SO.Project.Domain.Repositories;

namespace DL.SO.Project.Persistence.Dapper
{
    public abstract class DbConnection2RepositoryBase
    {
        public IDbConnection DbConnection { get; private set; }

        public DbConnection2RepositoryBase(IDbConnectionFactory dbConnectionFactory)
        {
            this.DbConnection = dbConnectionFactory.CreateDbConnection(DatabaseConnectionName.Connection2);
        }
    }
}

using Dapper;
using System.Data;

namespace DL.SO.Project.Persistence.Dapper.Repositories
{
    public class ParameterRepository : DbConnection2RepositoryBase, IParameterRepository
    {
        public ParameterRepository (IDbConnectionFactory dbConnectionFactory)
            : base(dbConnectionFactory) { }

        public IEnumerable<Parameter> GetAll()
        {
            const string sql = @"SELECT * FROM TABLE";
            return base.DbConnection.Query<Parameter>(sql);
        }

        // ......
    }
}

Espero que todos estos ayuden.

David Liang
fuente
Exactamente lo que estoy buscando. Tuve el mismo problema y lo resolví de la misma manera, todavía no sé si esta es una buena práctica pero, en mi opinión, creo que lo es.
Ewerton
1
¿Sería mejor registrar IDbConnection para el alcance de IServiceProvider? Se puede crear un servicio y registrarse como fábrica de alcance singleton con diferentes conexiones y usando var scope = factory.CreateNonDefaultScope (); usando var connection = scope.ServiceProvider.GetRequiredService <IDbConnection> () obtendrá su conexión no predeterminada. Menos herencia también ayudará con la extensibilidad ...
también
27

Creé métodos de extensión con una propiedad que recupera la cadena de conexión de la configuración. Esto permite que las personas que llaman no tengan que saber nada sobre la conexión, ya sea abierta o cerrada, etc. Este método lo limita un poco ya que está ocultando algunas de las funciones de Dapper, pero en nuestra aplicación bastante simple, funcionó bien para nosotros. , y si necesitáramos más funcionalidad de Dapper siempre podríamos agregar un nuevo método de extensión que lo exponga.

internal static string ConnectionString = new Configuration().ConnectionString;

    internal static IEnumerable<T> Query<T>(string sql, object param = null)
    {
        using (SqlConnection conn = new SqlConnection(ConnectionString))
        {
            conn.Open();
            return conn.Query<T>(sql, param);
        }
    }

    internal static int Execute(string sql, object param = null)
    {
        using (SqlConnection conn = new SqlConnection(ConnectionString))
        {
            conn.Open();
            return conn.Execute(sql, param);
        }
    }
Shawn Hubbard
fuente
1
Una pregunta aquí. Dado que conn.Query devuelve IEnumerable <T>, ¿es seguro deshacerse inmediatamente del objeto de conexión? ¿No necesita IEnumerable la conexión para materializar los elementos a medida que se leen? ¿Deberíamos ejecutar una ToList ()?
Adrian Nasui
Tendría que regresar a Dapper para verificar, pero estoy bastante seguro de que tomé este patrón tal como está del código de producción en funcionamiento. Debería estar bien, pero por supuesto, debe probar cualquier código en Internet.
Shawn Hubbard
1
Si está utilizando un método de extensión de consulta elegante, no es necesario que abra la conexión explícitamente como se hace en el método en sí.
h-rai
4
El problema con el código anterior es que si pasa buffer: true al método Query, la conexión se eliminará antes de que se devuelvan los datos. Internamente, Dapper convertirá el enumerable en una lista antes de regresar.
Brian Vallelunga
@BrianVallelunga ¿no sería así buffered: false?
Jodrell
24

Se preguntó hace unos 4 años ... pero de todos modos, tal vez la respuesta sea útil para alguien aquí:

Lo hago así en todos los proyectos. Primero, creo una clase base que contiene algunos métodos auxiliares como este:

public class BaseRepository
{
    protected T QueryFirstOrDefault<T>(string sql, object parameters = null)
    {
        using (var connection = CreateConnection())
        {
            return connection.QueryFirstOrDefault<T>(sql, parameters);
        }
    }

    protected List<T> Query<T>(string sql, object parameters = null)
    {
        using (var connection = CreateConnection())
        {
            return connection.Query<T>(sql, parameters).ToList();
        }
    }

    protected int Execute(string sql, object parameters = null)
    {
        using (var connection = CreateConnection())
        {
            return connection.Execute(sql, parameters);
        }
    }

    // Other Helpers...

    private IDbConnection CreateConnection()
    {
        var connection = new SqlConnection(...);
        // Properly initialize your connection here.
        return connection;
    }
}

Y al tener una clase base de este tipo, puedo crear fácilmente repositorios reales sin ningún código repetitivo:

public class AccountsRepository : BaseRepository
{
    public Account GetById(int id)
    {
        return QueryFirstOrDefault<Account>("SELECT * FROM Accounts WHERE Id = @Id", new { id });
    }

    public List<Account> GetAll()
    {
        return Query<Account>("SELECT * FROM Accounts ORDER BY Name");
    }

    // Other methods...
}

Entonces, todo el código relacionado con Dapper, SqlConnection-s y otras cosas de acceso a la base de datos se encuentra en un solo lugar (BaseRepository). Todos los repositorios reales son métodos limpios y simples de 1 línea.

Espero que ayude a alguien.

Pavel Melnikov
fuente
1
BaseRepositoryes una herencia innecesaria ya que no proporciona ningún método o propiedad pública o abstracta. En cambio, esto podría ser una DBHelperclase.
Josh Noe
¿Puede ser mejor pasar CreateConnectiona la propia clase?
hellboy
Puede ser ... Pero personalmente me gusta mantener todo simple. Si tiene mucha lógica en CreateConnection (...) puede ser una buena idea. En mis proyectos, este método es tan simple como "devolver nueva conexión (connectionString)", por lo que se puede usar en línea sin el método CreateConnection (...) separado.
Pavel Melnikov
1
Además, como señaló Nick-s, en las últimas versiones de Dapper no es necesario abrir la conexión a la base de datos manualmente. Dapper lo abrirá automáticamente. actualizó la publicación.
Pavel Melnikov
inyectarlo imo. services.AddScoped<IDbConnection>(p => new SqlConnection(connString)entonces solo pídelo donde sea necesario
Sinaesthetic
8

Lo hago así:

internal class Repository : IRepository {

    private readonly Func<IDbConnection> _connectionFactory;

    public Repository(Func<IDbConnection> connectionFactory) 
    {
        _connectionFactory = connectionFactory;
    }

    public IWidget Get(string key) {
        using(var conn = _connectionFactory()) 
        {
            return conn.Query<Widget>(
               "select * from widgets with(nolock) where widgetkey=@WidgetKey", new { WidgetKey=key });
        }
    }
}

Luego, dondequiera que conecte mis dependencias (por ejemplo: Global.asax.cs o Startup.cs), hago algo como:

var connectionFactory = new Func<IDbConnection>(() => {
    var conn = new SqlConnection(
        ConfigurationManager.ConnectionStrings["connectionString-name"];
    conn.Open();
    return conn;
});
Romi Petrelis
fuente
Una pregunta aquí. Dado que conn.Query devuelve Ienumerable <T>, ¿es seguro deshacerse de inmediato de la conexión? ¿No necesita IEnumerable la conexión para materializar los elementos a medida que se leen?
Adrian Nasui
1
@AdrianNasui: Actualmente, el comportamiento predeterminado de Dapper es ejecutar su SQL y almacenar en búfer todo el lector al regresar, por lo IEnumerable<T>que ya está materializado. Si pasa buffered: false, sí, deberá consumir la salida antes de salir del usingbloque.
Jacob Krall
7

La mejor práctica es un término muy cargado. Me gusta un DbDataContextcontenedor de estilo como Dapper.Rainbow promueve. Le permite acoplar la CommandTimeouttransacción y otros ayudantes.

Por ejemplo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;

using Dapper;

// to have a play, install Dapper.Rainbow from nuget

namespace TestDapper
{
    class Program
    {
        // no decorations, base class, attributes, etc 
        class Product 
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Description { get; set; }
            public DateTime? LastPurchase { get; set; }
        }

        // container with all the tables 
        class MyDatabase : Database<MyDatabase>
        {
            public Table<Product> Products { get; set; }
        }

        static void Main(string[] args)
        {
            var cnn = new SqlConnection("Data Source=.;Initial Catalog=tempdb;Integrated Security=True");
            cnn.Open();

            var db = MyDatabase.Init(cnn, commandTimeout: 2);

            try
            {
                db.Execute("waitfor delay '00:00:03'");
            }
            catch (Exception)
            {
                Console.WriteLine("yeah ... it timed out");
            }


            db.Execute("if object_id('Products') is not null drop table Products");
            db.Execute(@"create table Products (
                    Id int identity(1,1) primary key, 
                    Name varchar(20), 
                    Description varchar(max), 
                    LastPurchase datetime)");

            int? productId = db.Products.Insert(new {Name="Hello", Description="Nothing" });
            var product = db.Products.Get((int)productId);

            product.Description = "untracked change";

            // snapshotter tracks which fields change on the object 
            var s = Snapshotter.Start(product);
            product.LastPurchase = DateTime.UtcNow;
            product.Name += " World";

            // run: update Products set LastPurchase = @utcNow, Name = @name where Id = @id
            // note, this does not touch untracked columns 
            db.Products.Update(product.Id, s.Diff());

            // reload
            product = db.Products.Get(product.Id);


            Console.WriteLine("id: {0} name: {1} desc: {2} last {3}", product.Id, product.Name, product.Description, product.LastPurchase);
            // id: 1 name: Hello World desc: Nothing last 12/01/2012 5:49:34 AM

            Console.WriteLine("deleted: {0}", db.Products.Delete(product.Id));
            // deleted: True 


            Console.ReadKey();
        }
    }
}
Sam Saffron
fuente
15
¿No está el OP preguntando más sobre la parte SqlConnection ([[CONN STRING HERE]])? Él dice: "Pero me parece incorrecto hacer referencia a una cadena de conexión en cada clase (incluso en cada método)". Creo que se pregunta si los usuarios de Dapper hemos generado un patrón (de algún tipo) para envolver el lado de la creación de conexiones para SECO / esconde esa lógica. (Aparte del OP, si puede usar Dapper.Rainbow, hágalo ... ¡es realmente bueno!)
ckittel
4

Prueba esto:

public class ConnectionProvider
    {
        DbConnection conn;
        string connectionString;
        DbProviderFactory factory;

        // Constructor that retrieves the connectionString from the config file
        public ConnectionProvider()
        {
            this.connectionString = ConfigurationManager.ConnectionStrings[0].ConnectionString.ToString();
            factory = DbProviderFactories.GetFactory(ConfigurationManager.ConnectionStrings[0].ProviderName.ToString());
        }

        // Constructor that accepts the connectionString and Database ProviderName i.e SQL or Oracle
        public ConnectionProvider(string connectionString, string connectionProviderName)
        {
            this.connectionString = connectionString;
            factory = DbProviderFactories.GetFactory(connectionProviderName);
        }

        // Only inherited classes can call this.
        public DbConnection GetOpenConnection()
        {
            conn = factory.CreateConnection();
            conn.ConnectionString = this.connectionString;
            conn.Open();

            return conn;
        }

    }
Shuaib
fuente
6
¿Cómo maneja el cierre / eliminación de la conexión en su solución?
jpshook
@JPShook: creo que está usando using. (ref stackoverflow.com/a/4717859/2133703 )
MacGyver
4

¿Todo el mundo parece estar abriendo sus conexiones demasiado pronto? Tenía esta misma pregunta, y después de buscar en la fuente aquí: https://github.com/StackExchange/dapper-dot-net/blob/master/Dapper/SqlMapper.cs

Verá que cada interacción con la base de datos verifica la conexión para ver si está cerrada y la abre según sea necesario. Debido a esto, simplemente utilizamos declaraciones de uso como las anteriores sin conn.open (). De esta manera, la conexión se abre lo más cerca posible de la interacción. Si lo nota, también cierra inmediatamente la conexión. Esto también será más rápido que si se cierra automáticamente durante la eliminación.

Uno de los muchos ejemplos de esto del repositorio anterior:

    private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition command, Action<IDbCommand, object> paramReader)
    {
        IDbCommand cmd = null;
        bool wasClosed = cnn.State == ConnectionState.Closed;
        try
        {
            cmd = command.SetupCommand(cnn, paramReader);
            if (wasClosed) cnn.Open();
            int result = cmd.ExecuteNonQuery();
            command.OnCompleted();
            return result;
        }
        finally
        {
            if (wasClosed) cnn.Close();
            cmd?.Dispose();
        }
    }

A continuación se muestra un pequeño ejemplo de cómo usamos un Wrapper para Dapper llamado DapperWrapper. Esto nos permite empaquetar todos los métodos Dapper y Simple Crud para administrar conexiones, proporcionar seguridad, registro, etc.

  public class DapperWrapper : IDapperWrapper
  {
    public IEnumerable<T> Query<T>(string query, object param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null)
    {
      using (var conn = Db.NewConnection())
      {
          var results = conn.Query<T>(query, param, transaction, buffered, commandTimeout, commandType);
          // Do whatever you want with the results here
          // Such as Security, Logging, Etc.
          return results;
      }
    }
  }
Zabbu
fuente
1
Esto es realmente útil sabiendo que Dapper dejará la conexión abierta si ya está abierta cuando la obtenga. Ahora estoy pre-abriendo la conexión db antes de pasarla / usarla con Dapper y obtuve una ganancia de rendimiento 6x, ¡gracias!
Chris Smith
2

Envuelvo la conexión con la clase de ayuda:

public class ConnectionFactory
{
    private readonly string _connectionName;

    public ConnectionFactory(string connectionName)
    {
        _connectionName = connectionName;
    }

    public IDbConnection NewConnection() => new SqlConnection(_connectionName);

    #region Connection Scopes

    public TResult Scope<TResult>(Func<IDbConnection, TResult> func)
    {
        using (var connection = NewConnection())
        {
            connection.Open();
            return func(connection);
        }
    }

    public async Task<TResult> ScopeAsync<TResult>(Func<IDbConnection, Task<TResult>> funcAsync)
    {
        using (var connection = NewConnection())
        {
            connection.Open();
            return await funcAsync(connection);
        }
    }

    public void Scope(Action<IDbConnection> func)
    {
        using (var connection = NewConnection())
        {
            connection.Open();
            func(connection);
        }
    }

    public async Task ScopeAsync<TResult>(Func<IDbConnection, Task> funcAsync)
    {
        using (var connection = NewConnection())
        {
            connection.Open();
            await funcAsync(connection);
        }
    }

    #endregion Connection Scopes
}

Ejemplos de uso:

public class PostsService
{
    protected IConnectionFactory Connection;

    // Initialization here ..

    public async Task TestPosts_Async()
    {
        // Normal way..
        var posts = Connection.Scope(cnn =>
        {
            var state = PostState.Active;
            return cnn.Query<Post>("SELECT * FROM [Posts] WHERE [State] = @state;", new { state });
        });

        // Async way..
        posts = await Connection.ScopeAsync(cnn =>
        {
            var state = PostState.Active;
            return cnn.QueryAsync<Post>("SELECT * FROM [Posts] WHERE [State] = @state;", new { state });
        });
    }
}

Entonces no tengo que abrir explícitamente la conexión cada vez. Además, puede usarlo de esta manera por conveniencia de la futura refactorización:

var posts = Connection.Scope(cnn =>
{
    var state = PostState.Active;
    return cnn.Query<Post>($"SELECT * FROM [{TableName<Post>()}] WHERE [{nameof(Post.State)}] = @{nameof(state)};", new { state });
});

Lo que se TableName<T>()puede encontrar en esta respuesta .

Sergey
fuente
0

Hola @donaldhughes, yo también soy nuevo en esto, y uso para hacer esto: 1 - Crear una clase para obtener mi Cadena de conexión 2 - Llamar a la clase de cadena de conexión en un uso

Mira:

DapperConnection.cs

public class DapperConnection
{

    public IDbConnection DapperCon {
        get
        {
            return new SqlConnection(ConfigurationManager.ConnectionStrings["Default"].ToString());

        }
    }
}

DapperRepository.cs

  public class DapperRepository : DapperConnection
  {
       public IEnumerable<TBMobileDetails> ListAllMobile()
        {
            using (IDbConnection con = DapperCon )
            {
                con.Open();
                string query = "select * from Table";
                return con.Query<TableEntity>(query);
            }
        }
     }

Y funciona bien.

Gabriel Scavassa
fuente