Dapper in .Net Core

Posted 大道无术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Dapper in .Net Core相关的知识,希望对你有一定的参考价值。

一、前言

  关于什么是Dapper(详细入口),在此不做赘述;本文仅对Dapper在.Net Core中的使用作扼要说明,所陈代码以示例讲解为主,乃抛砖引玉,开发者可根据自身需要进行扩展和调整;其中如有疏漏之处,望不吝斧正。

二、Dapper环境搭建

当前以.Net Core WebAPI或MVC项目为例,框架版本为.NET 5.0,相关NuGet包引用如下:

Install-Package Dapper

Install-Package Dapper.Contrib

Install-Package Dapper.SqlBuilder

Install-Package System.Data.SqlClient

其中Dapper.Contrib和Dapper.SqlBuilder为Dapper的扩展,当然,Dapper的扩展还有如Dapper.Rainbow等其他包,根据自身需要引用,对相关引用作下说明:

  • Dapper:不言而喻;
  • Dapper.Contrib:可使用对象进行数据表的增删改查,免却SQL语句的编写;
  • Dapper.SqlBuilder:可以方便动态构建SQL语句,如Join、SELECT、Where、OrderBy等等;
  • System.Data.SqlClient:由于示例数据库为Sql Server,如MySql则引用MySql.Data;


对于Dapper.Contrib实体配置选项,以Product类为例,作扼要说明如下:

[Table("Product")]
public class Product

    [Key]  
    public int Id  get; set; 
    public string Name get; set; 
    public string Description  get; set; 
    public decimal Price  get; set; 
    public DateTime CreateTime  get; set; 

对于实体配置项,有如下几个主要项:

  • Table:指定数据库表名,可忽略;
  • Key:指定为自动增长主键;
  • ExplicitKey:指定非自动增长主键,如guid;
  • Computed:计算列属性,Insert、Update操作将忽略此列;
  • Write:是否可写入,true/false,如[Write(false)],false时Insert、Update操作将忽略此列,比如可扩展局部类作数据表额外查询字段使用;

对于数据表对象实体,可结合T4模板生成即可。

三、Dapper封装

  关于Dapper数据访问,这里参考Github上的某示例(入口:https://github.com/EloreTec/UnitOfWorkWithDapper),作修改调整封装如下:

定义DapperDBContext类

 

public abstract class DapperDBContext : IContext
    
        private IDbConnection _connection;
        private IDbTransaction _transaction;
        private int? _commandTimeout = null;
        private readonly DapperDBContextOptions _options;

        public bool IsTransactionStarted  get; private set; 

        protected abstract IDbConnection CreateConnection(string connectionString);

        protected DapperDBContext(IOptions<DapperDBContextOptions> optionsAccessor)
        
            _options = optionsAccessor.Value;

            _connection = CreateConnection(_options.Configuration);
            _connection.Open();

            DebugPrint("Connection started.");
        

        #region Transaction

        public void BeginTransaction()
        
            if (IsTransactionStarted)
                throw new InvalidOperationException("Transaction is already started.");

            _transaction = _connection.BeginTransaction();
            IsTransactionStarted = true;

            DebugPrint("Transaction started.");
        

        public void Commit()
        
            if (!IsTransactionStarted)
                throw new InvalidOperationException("No transaction started.");

            _transaction.Commit();
            _transaction = null;

            IsTransactionStarted = false;

            DebugPrint("Transaction committed.");
        

        public void Rollback()
        
            if (!IsTransactionStarted)
                throw new InvalidOperationException("No transaction started.");

            _transaction.Rollback();
            _transaction.Dispose();
            _transaction = null;

            IsTransactionStarted = false;

            DebugPrint("Transaction rollbacked and disposed.");
        

        #endregion Transaction

        #region Dapper.Contrib.Extensions

        public async Task<T> GetAsync<T>(int id) where T : class, new()
        
            return await _connection.GetAsync<T>(id, _transaction, _commandTimeout);
        

        public async Task<T> GetAsync<T>(string id) where T : class, new()
        
            return await _connection.GetAsync<T>(id, _transaction, _commandTimeout);
        

        public async Task<IEnumerable<T>> GetAllAsync<T>() where T : class, new()
        
            return await _connection.GetAllAsync<T>();
        

        public long Insert<T>(T model) where T : class, new()
        
            return _connection.Insert<T>(model, _transaction, _commandTimeout);
        

        public async Task<int> InsertAsync<T>(T model) where T : class, new()
        
            return await _connection.InsertAsync<T>(model, _transaction, _commandTimeout);
        
        public bool Update<T>(T model) where T : class, new()
        
            return _connection.Update<T>(model, _transaction, _commandTimeout);
        

        public async Task<bool> UpdateAsync<T>(T model) where T : class, new()
        
            return await _connection.UpdateAsync<T>(model, _transaction, _commandTimeout);          
        

        public async Task<Page<T>> PageAsync<T>(long pageIndex, long pageSize, string sql, object param = null)
        
            DapperPage.BuildPageQueries((pageIndex - 1) * pageSize, pageSize, sql, out string sqlCount, out string sqlPage);

            var result = new Page<T>
            
                CurrentPage = pageIndex,
                ItemsPerPage = pageSize,
                TotalItems = await _connection.ExecuteScalarAsync<long>(sqlCount, param)
            ;
            result.TotalPages = result.TotalItems / pageSize;

            if ((result.TotalItems % pageSize) != 0)
                result.TotalPages++;

            result.Items = await _connection.QueryAsync<T>(sqlPage, param);
            return result;
        
      

        #endregion


        #region Dapper Execute & Query
      

        public int ExecuteScalar(string sql, object param = null, CommandType commandType = CommandType.Text)
        
            return _connection.ExecuteScalar<int>(sql, param, _transaction, _commandTimeout, commandType);
        

        public async Task<int> ExecuteScalarAsync(string sql, object param = null, CommandType commandType = CommandType.Text)
        
            return await _connection.ExecuteScalarAsync<int>(sql, param, _transaction, _commandTimeout, commandType);
        
        public int Execute(string sql, object param = null, CommandType commandType = CommandType.Text)
        
            return _connection.Execute(sql, param, _transaction, _commandTimeout, commandType);
        

        public async Task<int> ExecuteAsync(string sql, object param = null, CommandType commandType = CommandType.Text)
        
            return await _connection.ExecuteAsync(sql, param, _transaction, _commandTimeout, commandType);
        

        public IEnumerable<T> Query<T>(string sql, object param = null, CommandType commandType = CommandType.Text)
        
            return _connection.Query<T>(sql, param, _transaction, true, _commandTimeout, commandType);
        

        public async Task<IEnumerable<T>> QueryAsync<T>(string sql, object param = null, CommandType commandType = CommandType.Text)
        
            return await _connection.QueryAsync<T>(sql, param, _transaction, _commandTimeout, commandType);
        

        public T QueryFirstOrDefault<T>(string sql, object param = null, CommandType commandType = CommandType.Text)
        
            return _connection.QueryFirstOrDefault<T>(sql, param, _transaction, _commandTimeout, commandType);
        

        public async Task<T> QueryFirstOrDefaultAsync<T>(string sql, object param = null, CommandType commandType = CommandType.Text)
        
            return await _connection.QueryFirstOrDefaultAsync<T>(sql, param, _transaction, _commandTimeout, commandType);
        
        public IEnumerable<TReturn> Query<TFirst, TSecond, TReturn>(string sql, Func<TFirst, TSecond, TReturn> map, object param = null, string splitOn = "Id", CommandType commandType = CommandType.Text)
        
            return _connection.Query(sql, map, param, _transaction, true, splitOn, _commandTimeout, commandType);
        

        public async Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TReturn>(string sql, Func<TFirst, TSecond, TReturn> map, object param = null, string splitOn = "Id", CommandType commandType = CommandType.Text)
        
            return await _connection.QueryAsync(sql, map, param, _transaction, true, splitOn, _commandTimeout, commandType);
        

        public async Task<SqlMapper.GridReader> QueryMultipleAsync(string sql, object param = null, CommandType commandType = CommandType.Text)
        
            return await _connection.QueryMultipleAsync(sql, param, _transaction, _commandTimeout, commandType);
        

        #endregion Dapper Execute & Query

        public void Dispose()
        
            if (IsTransactionStarted)
                Rollback();

            _connection.Close();
            _connection.Dispose();
            _connection = null;

            DebugPrint("Connection closed and disposed.");
        

        private void DebugPrint(string message)
        
#if DEBUG
            Debug.Print(">>> UnitOfWorkWithDapper - Thread 0: 1", Thread.CurrentThread.ManagedThreadId, message);
#endif
        
    

 

public class DapperDBContextOptions : IOptions<DapperDBContextOptions>
    
        public string Configuration  get; set; 

        DapperDBContextOptions IOptions<DapperDBContextOptions>.Value
        
            get  return this; 
        
    

 public interface IContext : IDisposable
    
        bool IsTransactionStarted  get; 

        void BeginTransaction();

        void Commit();

        void Rollback();
    

 

以上代码涵盖了Dapper访问数据库的基本操作,分同步和异步,其中大部分不作赘述,着重说下分页部分;

异步分页构建(PageAsync)

分页这里为方便调用,只需传入要查询的Sql语句(如:SELECT * FROM Table,必须带Order BY)、页索引、页大小即可;

至于具体如何构建的,这里参照某小型ORM工具PetaPoco,抽取相关代码如下,有兴趣的同学也可以自行改造:

public class Page<T>
    
        /// <summary>
        /// The current page number contained in this page of result set 
        /// </summary>
        public long CurrentPage  get; set; 

        /// <summary>
        /// The total number of pages in the full result set
        /// </summary>
        public long TotalPages  get; set; 

        /// <summary>
        /// The total number of records in the full result set
        /// </summary>
        public long TotalItems  get; set; 

        /// <summary>
        /// The number of items per page
        /// </summary>
        public long ItemsPerPage  get; set; 

        /// <summary>
        /// The actual records on this page
        /// </summary>
        public IEnumerable<T> Items  get; set; 
        //public List<T> Items  get; set; 
    
    public class DapperPage
    
        public static void BuildPageQueries(long skip, long take, string sql, out string sqlCount, out string sqlPage)
        
            // Split the SQL
            if (!PagingHelper.SplitSQL(sql, out PagingHelper.SQLParts parts))
                throw new Exception("Unable to parse SQL statement for paged query");

            sqlPage = BuildPageSql.BuildPageQuery(skip, take, parts);
            sqlCount = parts.sqlCount;
        
    

    static class BuildPageSql
    
        public static string BuildPageQuery(long skip, long take, PagingHelper.SQLParts parts)
        
            parts.sqlSelectRemoved = PagingHelper.rxOrderBy.Replace(parts.sqlSelectRemoved, "", 1);
            if (PagingHelper.rxDistinct.IsMatch(parts.sqlSelectRemoved))
            
                parts.sqlSelectRemoved = "peta_inner.* FROM (SELECT " + parts.sqlSelectRemoved + ") peta_inner";
            
            var sqlPage = string.Format("SELECT * FROM (SELECT ROW_NUMBER() OVER (0) peta_rn, 1) peta_paged WHERE peta_rn>2 AND peta_rn<=3",
                                    parts.sqlOrderBy ?? "ORDER BY (SELECT NULL)", parts.sqlSelectRemoved, skip, skip + take);
            //args = args.Concat(new object[]  skip, skip + take ).ToArray();

            return sqlPage;
        

        //SqlServer 2012及以上
        public static string BuildPageQuery2(long skip, long take, PagingHelper.SQLParts parts)
        
            parts.sqlSelectRemoved = PagingHelper.rxOrderBy.Replace(parts.sqlSelectRemoved, "", 1);
            if (PagingHelper.rxDistinct.IsMatch(parts.sqlSelectRemoved))
            
                parts.sqlSelectRemoved = "peta_inner.* FROM (SELECT " + parts.sqlSelectRemoved + ") peta_inner";
                

            var sqlOrderBy = parts.sqlOrderBy ?? "ORDER BY (SELECT NULL)";
            var sqlPage = $"SELECT parts.sqlSelectRemoved sqlOrderBy OFFSET skip ROWS FETCH NEXT take ROWS ONLY";
            return sqlPage;
        
    

    static class PagingHelper
    
        public struct SQLParts
        
            public string sql;
            public string sqlCount;
            public string sqlSelectRemoved;
            public string sqlOrderBy;
        

        public static bool SplitSQL(string sql, out SQLParts parts)
        
            parts.sql = sql;
            parts.sqlSelectRemoved = null;
            parts.sqlCount = null;
            parts.sqlOrderBy = null;

            // Extract the columns from "SELECT <whatever> FROM"
            var m = rxColumns.Match(sql);
            if (!m.Success)
                return false;

            // Save column list and replace with COUNT(*)
            Group g = m.Groups[1];
            parts.sqlSelectRemoved = sql.Substring(g.Index);

            if (rxDistinct.IsMatch(parts.sqlSelectRemoved))
                parts.sqlCount = sql.Substring(0, g.Index) + "COUNT(" + m.Groups[1].ToString().Trim() + ") " + sql.Substring(g.Index + g.Length);
            else
                parts.sqlCount = sql.Substring(0, g.Index) + "COUNT(*) " + sql.Substring(g.Index + g.Length);


            // Look for the last "ORDER BY <whatever>" clause not part of a ROW_NUMBER expression
            m = rxOrderBy.Match(parts.sqlCount);
            if (!m.Success)
            
                parts.sqlOrderBy = null;
            
            else
            
                g = m.Groups[0];
                parts.sqlOrderBy = g.ToString();
                parts.sqlCount = parts.sqlCount.Substring(0, g.Index) + parts.sqlCount.Substring(g.Index + g.Length);
            

            return true;
        

        public static Regex rxColumns = new Regex(@"\\A\\s*SELECT\\s+((?:\\((?>\\((?<depth>)|\\)(?<-depth>)|.?)*(?(depth)(?!))\\)|.)*?)(?<!,\\s+)\\bFROM\\b", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled);
        public static Regex rxOrderBy = new Regex(@"\\bORDER\\s+BY\\s+(?!.*?(?:\\)|\\s+)AS\\s)(?:\\((?>\\((?<depth>)|\\)(?<-depth>)|.?)*(?(depth)(?!))\\)|[\\w\\(\\)\\.])+(?:\\s+(?:ASC|DESC))?(?:\\s*,\\s*(?:\\((?>\\((?<depth>)|\\)(?<-depth>)|.?)*(?(depth)(?!))\\)|[\\w\\(\\)\\.])+(?:\\s+(?:ASC|DESC))?)*", RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled);
        public static Regex rxDistinct = new Regex(@"\\ADISTINCT\\s", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled);
    

 

对于构建分页语句,分别示例BuildPageQuery和BuildPageQuery2,前者为通过ROW_NUMBER进行分页(针对SqlServer2005、2008),后者通过OFFSET、FETCH分页(针对SqlServer2012及以上版本),相关辅助操作类一览便知,如果使用MySql数据库,可酌情自行封装; 

至于Where查询的进一步封装,有兴趣的也可兑Dapper lamada查询进行扩展。

 

定义工作单元与事务

    public interface IUnitOfWork : IDisposable
    
        void SaveChanges();
    

    public interface IUnitOfWorkFactory
    
        IUnitOfWork Create();
    

public class UnitOfWork : IUnitOfWork
    
        private readonly IContext _context;

        public UnitOfWork(IContext context)
        
            _context = context;
            _context.BeginTransaction();
        

        public void SaveChanges()
        
            if (!_context.IsTransactionStarted)
                throw new InvalidOperationException("Transaction have already been commited or disposed.");

            _context.Commit();
        

        public void Dispose()
        
            if (_context.IsTransactionStarted)
                _context.Rollback();
        
    

public class DapperUnitOfWorkFactory : IUnitOfWorkFactory
    
        private readonly DapperDBContext _context;

        public DapperUnitOfWorkFactory(DapperDBContext context)
        
            _context = context;
        

        public IUnitOfWork Create()
        
            return new UnitOfWork(_context);
        
    

 

定义数据仓储

    #region Product
    public partial interface IProductRepository
    
        Task<Product> GetAsync(int id);

        Task<IEnumerable<Product>> GetAllAsync();

        long Insert(Product model);

        Task<int> InsertAsync(Product model);

        bool Update(Product model);

        Task<bool> UpdateAsync(Product model);       

        int Count(string where, object param = null);

        Task<int> CountAsync(string where, object param = null);

        bool Exists(string where, object param = null);

        Task<bool> ExistsAsync(string where, object param = null);        

        Product FirstOrDefault(string where, object param = null);

        Task<Product> FirstOrDefaultAsync(string where, object param = null);

        T FirstOrDefault<T>(string sql, object param = null);

        Task<T> FirstOrDefaultAsync<T>(string sql, object param = null);

        IEnumerable<Product> Fetch(SqlBuilder where);

        Task<IEnumerable<Product>> FetchAsync(SqlBuilder where);

        IEnumerable<T> Fetch<T>(string sql, SqlBuilder where, bool orderBy = true);

        Task<IEnumerable<T>> FetchAsync<T>(string sql, SqlBuilder where, bool orderBy = true);

        Task<Page<Product>> PageAsync(long pageIndex, long pageSize, SqlBuilder builder);

        Task<Page<T>> PageAsync<T>(string sql, long pageIndex, long pageSize, SqlBuilder builder);

        Task<SqlMapper.GridReader> QueryMultipleAsync(string sql, object param = null);
    

    public partial class ProductRepository : IProductRepository
    
        private readonly DapperDBContext _context;
        public ProductRepository(DapperDBContext context)
        
            _context = context;
        

        public async Task<Product> GetAsync(int id)
         
            return await _context.GetAsync<Product>(id);
        

        public async Task<IEnumerable<Product>> GetAllAsync()
        
            return await _context.GetAllAsync<Product>();
        

        public long Insert(Product model)
        
            return _context.Insert<Product>(model);
        

        public async Task<int> InsertAsync(Product model)
        
            return await _context.InsertAsync<Product>(model);
            

        public bool Update(Product model)
         
            return _context.Update<Product>(model);
            

        public async Task<bool> UpdateAsync(Product model)
         
            return await _context.UpdateAsync<Product>(model);
        

        public int Count(string where, object param = null)
        
            string strSql = $"SELECT COUNT(1) FROM Product where";
            return _context.ExecuteScalar(strSql, param);
        

        public async Task<int> CountAsync(string where, object param = null)
        
            string strSql = $"SELECT COUNT(1) FROM Product where";
            return await _context.ExecuteScalarAsync(strSql, param);
        

        public bool Exists(string where, object param = null)
        
            string strSql = $"SELECT TOP 1 1 FROM Product where";
            var count = _context.ExecuteScalar(strSql, param);
            return count > 0;
        

        public async Task<bool> ExistsAsync(string where, object param = null)
        
            string strSql = $"SELECT TOP 1 1 FROM Product where";
            var count = await _context.ExecuteScalarAsync(strSql, param);
            return count > 0;
        

        public Product FirstOrDefault(string where, object param = null)
        
            string strSql = $"SELECT TOP 1 * FROM Product where"asp.net core 使用Mysql和Dapper

.Net Core中Dapper的使用详解

.net core 3.1 加入ORM框架(Dapper)

.net core系列之《.net core中使用MySql以及Dapper》

.NET Core开发日志——Dapper与MySQL

asp.net core 系列之webapi集成Dapper的简单操作教程