111 votes

Comment gérer les connexions aux bases de données avec Dapper en .NET ?

J'ai joué avec Dapper, mais je ne suis pas sûr de la meilleure façon de gérer la connexion à la base de données.

La plupart des exemples montrent que l'objet de connexion est créé dans la classe d'exemple, voire dans chaque méthode. Mais il ne me semble pas correct de référencer une chaîne de connexion dans chaque classe, même si elle provient du web.config.

Mon expérience m'a permis d'utiliser un DbDataContext ou DbContext avec Linq to SQL ou Entity Framework, c'est donc nouveau pour moi.

Comment structurer mes applications web lorsque j'utilise Dapper comme stratégie d'accès aux données ?

104voto

David Liang Points 4863

Mise à jour : clarification à partir du commentaire de MarredCheese :

"Il n'est pas nécessaire d'utiliser une déclaration d'utilisation. Dapper ouvrira, fermera et éliminera automatiquement la connexion pour vous, fermera et éliminera la connexion pour vous". Ce n'est pas correct. Dapper ouvrira automatiquement les connexions fermées, et il fermera automatiquement les connexions fermées. automatiquement les connexions fermées qu'il s'est ouvert automatiquement mais il ne le fera pas automatiquement les connexions. Marc Gravell et Eric Lippert préconisent tous deux l'utilisation de Dapper aquí .

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

Je ne sais pas si j'utilise les meilleures pratiques correctement ou non, mais je procède de cette manière, afin de gérer multiples chaînes de connexion.

C'est facile si vous n'avez qu'une seule chaîne de connexion

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

        // ......
    }
}

Problèmes si vous avez plus d'une chaîne de connexion

Depuis Dapper utilise IDbConnection il faut trouver un moyen de différencier les différentes connexions à la base de données.

J'ai essayé de créer plusieurs interfaces, "héritées" de IDbConnection correspondant à différentes connexions à la base de données, et injecter SqlConnection avec des chaînes de connexion à la base de données différentes sur Startup .

Cet échec s'explique par le fait que SqlConnection hérite de DbConnection y DbConnection Les compléments ne se limitent pas seulement à l'aide à la recherche et au développement. IDbConnection mais aussi Component classe. Vos interfaces personnalisées ne pourront donc pas utiliser uniquement la classe SqlConnection l'implémentation.

J'ai également essayé de créer ma propre DbConnection qui prend une chaîne de connexion différente. C'est trop compliqué car il faut implémenter toutes les méthodes de DbConnection classe. Vous avez perdu l'aide de SqlConnection .

Ce que je fais en fin de compte

  1. Pendant Startup J'ai chargé toutes les valeurs des chaînes de connexion dans un dictionnaire. J'ai également créé un fichier enum pour tous les noms de connexion à la base de données afin d'éviter les chaînes magiques.
  2. J'ai injecté le dictionnaire en tant que Singleton.
  3. Au lieu d'injecter IDbConnection J'ai créé IDbConnectionFactory et l'injecter en tant que transitoire pour tous les dépôts. Maintenant, tous les dépôts prennent IDbConnectionFactory au lieu de IDbConnection .
  4. Quand choisir la bonne connexion ? Dans le constructeur de tous les référentiels ! Pour faire les choses proprement, j'ai créé des classes de base pour les référentiels et j'ai fait en sorte que les référentiels héritent des classes de base. La sélection de la bonne chaîne de connexion peut se faire dans les classes de 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 - ma propre implémentation de la fabrique

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

Ensuite, pour les autres référentiels qui ont besoin de communiquer avec les autres connexions, vous pouvez créer une classe de base de référentiel différente pour eux.

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

        // ......
    }
}

J'espère que tout cela vous aidera.

35voto

Pavel Melnikov Points 488

La question a été posée il y a environ 4 ans... mais quoi qu'il en soit, la réponse sera peut-être utile à quelqu'un ici :

Je procède ainsi pour tous les projets. Tout d'abord, je crée une classe de base qui contient quelques méthodes d'aide comme celle-ci :

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

Et avec une telle classe de base, je peux facilement créer de vrais dépôts sans aucun code de base :

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

Ainsi, tout le code lié à Dapper, aux SqlConnection-s et aux autres éléments d'accès à la base de données se trouve à un seul endroit (BaseRepository). Tous les dépôts réels sont des méthodes simples et propres d'une ligne.

J'espère que cela aidera quelqu'un.

30voto

Shawn Hubbard Points 377

J'ai créé des méthodes d'extension avec une propriété qui récupère la chaîne de connexion de la configuration. Cela permet aux appelants de ne pas avoir à connaître la connexion, si elle est ouverte ou fermée, etc. Cette méthode vous limite un peu puisque vous cachez une partie de la fonctionnalité de Dapper, mais dans notre application assez simple, cela a bien fonctionné pour nous, et si nous avions besoin de plus de fonctionnalités de Dapper, nous pourrions toujours ajouter une nouvelle méthode d'extension qui les exposerait.

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

8voto

Romi Petrelis Points 516

Je procède de la manière suivante :

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

Ensuite, partout où j'installe mes dépendances (ex : Global.asax.cs ou Startup.cs), je fais quelque chose comme :

var connectionFactory = new Func<IDbConnection>(() => {
    var conn = new SqlConnection(
        ConfigurationManager.ConnectionStrings["connectionString-name"];
    conn.Open();
    return conn;
});

7voto

Sam Saffron Points 56236

L'expression "meilleures pratiques" est un terme très chargé. J'aime les DbDataContext comme un conteneur de style Dapper.Rainbow promouvoir. Il vous permet de coupler les CommandTimeout Les membres de l'équipe de l'UE ont été formés à l'utilisation de l'outil, à la transaction et à d'autres aides.

Par exemple :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X