Как мне обрабатывать подключения к базе данных с помощью Dapper в .NET?

86

Я играл с Dapper, но не уверен, как лучше всего обрабатывать соединение с базой данных.

В большинстве примеров показано, что объект подключения создается в классе примера или даже в каждом методе. Но мне кажется неправильным ссылаться на строку подключения в каждом clss, даже если она берется из web.config.

Мой опыт был с использованием DbDataContextили DbContextс Linq для SQL или Entity Framework, так что это для меня ново.

Как структурировать свои веб-приложения при использовании Dapper в качестве стратегии доступа к данным?

Дональд Хьюз
источник
Слишком поздно, но; Я реализовал это так: stackoverflow.com/a/45029588/5779732
Амит Джоши
using-dapper-asynchronously-in-asp-net-core-2 - exceptionnotfound.net/…
Гималаи Гарг

Ответы:

54

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

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

Это просто, если у вас всего 1 строка подключения

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

            // ......
        }
    }
}

ДиаметрRepository.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);
        }

        // ......
    }
}

Проблемы, если у вас более 1 строки подключения

Поскольку DapperиспользуетIDbConnection , вам нужно подумать о способе различать разные соединения с базой данных.

Я попытался создать несколько интерфейсов, унаследованных от IDbConnectionразличных подключений к базе данных, и ввести SqlConnectionразные строки подключения к базе данных наStartup .

Это не удалось, потому что SqlConnectionнаследуется DbConnectionи DbConnectionдополняется не только классом, IDbConnectionно и Componentклассом. Таким образом, ваши пользовательские интерфейсы не смогут использовать толькоSqlConnection реализацию.

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

Что я в итоге делаю

  1. Во время Startupэтого я загрузил все значения строки подключения в словарь. Я также создалenum для всех имен подключения к базе данных, чтобы избежать волшебных строк.
  2. Я ввел словарь как Синглтон.
  3. Вместо того, чтобы вводить IDbConnection, я создал IDbConnectionFactoryи внедрил это как Transient для всех репозиториев. Теперь все репозитории берут IDbConnectionFactoryвместоIDbConnection .
  4. Когда выбрать правильное соединение? В конструкторе всех репозиториев! Чтобы все было в порядке, я создал базовые классы репозиториев, и репозитории унаследованы от базовых классов. Правильный выбор строки подключения может происходить в базовых классах.

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 - моя собственная реализация фабрики

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

            // ......
        }
    }
}

ДиаметрRepository.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);
        }
    }
}

Затем для других репозиториев, которым необходимо взаимодействовать с другими соединениями, вы можете создать для них другой базовый класс репозитория.

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

        // ......
    }
}

Надеюсь, что все это поможет.

Дэвид Лян
источник
Именно то, что я ищу. У меня была такая же проблема, и я решил ее таким же образом, я до сих пор не знаю, хорошая ли это практика, но, на мой взгляд, я думаю, что это так.
Эвертон
1
Было бы лучше зарегистрировать IDbConnection для области IServiceProvider? Можно создать службу и зарегистрироваться как одноэлементную фабрику областей видимости с различными подключениями и с помощью var scope = factory.CreateNonDefaultScope (); используя var connection = scope.ServiceProvider.GetRequiredService <IDbConnection> (), вы получите соединение не по умолчанию. Меньше наследования также поможет с расширяемостью ...
тоже
Это то, что я ищу. Замечательная работа @David. Спасибо
Шашват Пракаш
27

Я создал методы расширения со свойством, которое извлекает строку подключения из конфигурации. Это позволяет вызывающим абонентам ничего не знать о соединении, открыто оно или закрыто и т. Д. Этот метод немного ограничивает вас, так как вы скрываете некоторые функции Dapper, но в нашем довольно простом приложении он отлично работал у нас. , и если нам понадобится больше функциональности от Dapper, мы всегда можем добавить новый метод расширения, который предоставляет его.

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);
        }
    }
Шон Хаббард
источник
1
Один вопрос. Поскольку conn.Query возвращает IEnumerable <T>, безопасно ли немедленно удалить объект подключения? Разве IEnumerable не нуждается в соединении, чтобы материализовать элементы по мере их чтения? Стоит ли запускать ToList ()?
Адриан
Мне пришлось бы вернуться в Dapper, чтобы проверить, но я почти уверен, что взял этот шаблон как из рабочего производственного кода. Это должно быть нормально, но, конечно, вы должны протестировать любой код в Интернете.
Шон Хаббард
2
Если вы используете метод расширения dapper Query, вам не нужно явно открывать соединение, как это делается в самом методе.
h-
4
Проблема с приведенным выше кодом заключается в том, что если вы передадите buffered: true методу Query, соединение будет удалено до того, как данные будут возвращены. Внутри Dapper перед возвратом преобразует перечисляемое в список.
Брайан Валлелунга
@BrianVallelunga, не так ли buffered: false?
Джодрелл
26

Его спрашивали года 4 назад ... но все равно, может быть, ответ здесь кому-нибудь пригодится:

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

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

И имея такой базовый класс, я могу легко создавать настоящие репозитории без какого-либо шаблонного кода:

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

Таким образом, весь код, связанный с Dapper, SqlConnection-s и другими средствами доступа к базе данных, находится в одном месте (BaseRepository). Все настоящие репозитории - это чистые и простые однострочные методы.

Надеюсь, это кому-то поможет.

Павел Мельников
источник
2
BaseRepositoryявляется ненужным наследованием, поскольку не предоставляет никаких общедоступных или абстрактных методов или свойств. Вместо этого это может быть DBHelperкласс.
Джош Ной
Может лучше перейти CreateConnectionв собственный класс?
hellboy
Может быть ... Но лично я люблю, чтобы все было просто. Если у вас много логики в CreateConnection (...), это может быть хорошей идеей. В моих проектах этот метод так же прост, как «вернуть новое соединение (connectionString)», поэтому его можно использовать в строке без отдельного метода CreateConnection (...).
Павел Мельников
1
Кроме того, как указал nick-s, в последних версиях Dapper вам не нужно открывать соединение с базой данных вручную. Dapper откроет его автоматически. обновил пост.
Павел Мельников
ввести это имо. services.AddScoped<IDbConnection>(p => new SqlConnection(connString)тогда просто попросите его там, где это необходимо
Sinaesthetic
8

Я так делаю:

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

Затем, когда я подключаю свои зависимости (например, Global.asax.cs или Startup.cs), я делаю что-то вроде:

var connectionFactory = new Func<IDbConnection>(() => {
    var conn = new SqlConnection(
        ConfigurationManager.ConnectionStrings["connectionString-name"];
    conn.Open();
    return conn;
});
Роми Петрелис
источник
Один вопрос. Поскольку conn.Query возвращает Ienumerable <T>, безопасно ли немедленно удалить соединение? Разве IEnumerable не нуждается в соединении, чтобы материализовать элементы по мере их чтения?
Адриан
1
@AdrianNasui: В настоящее время по умолчанию Dapper выполняет ваш SQL и буферизует весь читатель при возврате, поэтому IEnumerable<T>он уже материализован. Если вы пройдете buffered: false, да, вам нужно будет использовать вывод перед выходом из usingблока.
Джейкоб Кролл
7

Лучшая практика - это действительно загруженный термин. Мне нравится DbDataContextконтейнер стиля, который продвигает Dapper.Rainbow . Он позволяет CommandTimeoutобъединять помощников, транзакций и других.

Например:

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();
        }
    }
}
Сэм Шафран
источник
15
Разве OP не спрашивает больше о SqlConnection ([[CONN STRING HERE]])? Он говорит: «Но мне кажется неправильным ссылаться на строку подключения в каждом классе (даже в каждом методе)». Я думаю, он задается вопросом, создали ли мы, пользователи Dapper, шаблон (своего рода), оборачивающий сторону создания подключения для СУХОЙ / скрыть эту логику. (Помимо OP, если вы можете использовать Dapper.Rainbow, сделайте это ... это действительно здорово!)
ckittel
4

Попробуй это:

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

    }
Шуайб
источник
6
Как вы справляетесь с закрытием / удалением соединения в своем решении?
jpshook
@JPShook - я думаю, он использует using. (ref stackoverflow.com/a/4717859/2133703 )
MacGyver
4

Кажется, все слишком рано открывают свои связи? У меня был тот же вопрос, и после того, как я покопался здесь в источнике - https://github.com/StackExchange/dapper-dot-net/blob/master/Dapper/SqlMapper.cs

Вы обнаружите, что каждое взаимодействие с базой данных проверяет соединение, чтобы увидеть, закрыто ли оно, и открывает его при необходимости. По этой причине мы просто используем операторы using, подобные приведенным выше, без conn.open (). Таким образом, соединение открывается как можно ближе к взаимодействию. Если вы заметили, он также сразу закрывает соединение. Это также будет быстрее, чем закрытие автоматически во время утилизации.

Один из многих примеров этого из репо выше:

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

Ниже приведен небольшой пример того, как мы используем оболочку для Dapper под названием DapperWrapper. Это позволяет нам обернуть все методы Dapper и Simple Crud для управления подключениями, обеспечения безопасности, ведения журнала и т. Д.

  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;
      }
    }
  }
Заббу
источник
1
Это действительно полезно, зная, что Dapper оставит соединение открытым, если оно уже открыто, когда получит его. Теперь я предварительно открываю соединение с базой данных, прежде чем я передам / использую его с Dapper, и я получил прирост производительности в 6 раз - спасибо!
Крис Смит
2

Оборачиваю соединение вспомогательным классом:

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
}

Примеры использования:

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

Так что мне не нужно каждый раз явно открывать соединение. Кроме того, вы можете использовать его таким образом для удобства будущего рефакторинга:

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

Что TableName<T>()можно найти в этом ответе .

Сергей
источник
0

Привет, @donaldhughes, я тоже новичок в этом, и я использую для этого: 1 - Создать класс для получения моей строки подключения 2 - Вызвать класс строки подключения в Использование

Смотреть:

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

И работает нормально.

Габриэль Скавасса
источник