Dapper在.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 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 GetAsync(int id) where T : class, new()
{
return await _connection.GetAsync(id, _transaction, _commandTimeout);
}

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

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

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

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

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

public async Task> PageAsync(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
{
CurrentPage = pageIndex,
ItemsPerPage = pageSize,
TotalItems = await _connection.ExecuteScalarAsync(sqlCount, param)
};
result.TotalPages = result.TotalItems / pageSize;

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

result.Items = await _connection.QueryAsync(sqlPage, param);
return result;
}

#endregion


#region Dapper Execute & Query

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

public async Task ExecuteScalarAsync(string sql, object param = null, CommandType commandType = CommandType.Text)
{
return await _connection.ExecuteScalarAsync(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 ExecuteAsync(string sql, object param = null, CommandType commandType = CommandType.Text)
{
return await _connection.ExecuteAsync(sql, param, _transaction, _commandTimeout, commandType);
}

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

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

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

public async Task QueryFirstOrDefaultAsync(string sql, object param = null, CommandType commandType = CommandType.Text)
{
return await _connection.QueryFirstOrDefaultAsync(sql, param, _transaction, _commandTimeout, commandType);
}
public IEnumerable Query(string sql, Func 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> QueryAsync(string sql, Func 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 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
{
public string Configuration { get; set; }

DapperDBContextOptions IOptions.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
    {
        /// 
        /// The current page number contained in this page of result set 
        /// 
        public long CurrentPage { get; set; }

        /// 
        /// The total number of pages in the full result set
        /// 
        public long TotalPages { get; set; }

        /// 
        /// The total number of records in the full result set
        /// 
        public long TotalItems { get; set; }

        /// 
        /// The number of items per page
        /// 
        public long ItemsPerPage { get; set; }

        /// 
        /// The actual records on this page
        /// 
        public IEnumerable Items { get; set; }
        //public List 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  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 " 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>)|.?)*(?(depth)(?!))\)|[\w\(\)\.])+(?:\s+(?:ASC|DESC))?(?:\s*,\s*(?:\((?>\((?)|\)(?<-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 GetAsync(int id);

Task> GetAllAsync();

long Insert(Product model);

Task InsertAsync(Product model);

bool Update(Product model);

Task UpdateAsync(Product model);

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

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

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

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

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

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

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

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

IEnumerable Fetch(SqlBuilder where);

Task> FetchAsync(SqlBuilder where);

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

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

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

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

Task QueryMultipleAsync(string sql, object param = null);
}

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

public async Task GetAsync(int id)
{
return await _context.GetAsync(id);
}

public async Task> GetAllAsync()
{
return await _context.GetAllAsync();
}

public long Insert(Product model)
{
return _context.Insert(model);
}

public async Task InsertAsync(Product model)
{
return await _context.InsertAsync(model);
}

public bool Update(Product model)
{
return _context.Update(model);
}

public async Task UpdateAsync(Product model)
{
return await _context.UpdateAsync(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 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 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}";
return _context.QueryFirstOrDefault(strSql, param);
}

public async Task FirstOrDefaultAsync(string where, object param = null)
{
string strSql = $"SELECT TOP 1 * FROM Product {where}";
return await _context.QueryFirstOrDefaultAsync(strSql, param);
}

public T FirstOrDefault(string sql, object param = null)
{
return _context.QueryFirstOrDefault(sql, param);
}

public async Task FirstOrDefaultAsync(string sql, object param = null)
{
return await _context.QueryFirstOrDefaultAsync(sql, param);
}

public IEnumerable Fetch(SqlBuilder where)
{
var strSql = where.AddTemplate(@"SELECT * FROM Product /**where**/ /**orderby**/");
return _context.Query(strSql.RawSql, strSql.Parameters);
}

public async Task> FetchAsync(SqlBuilder where)
{
var strSql = where.AddTemplate(@"SELECT * FROM Product /**where**/ /**orderby**/");
return await _context.QueryAsync(strSql.RawSql, strSql.Parameters);
}

public IEnumerable Fetch(string sql, SqlBuilder where, bool orderBy = true)
{
var _sql = orderBy ? $"{sql} /**where**/ /**orderby**/" : $"{sql} /**where**/";
var strSql = where.AddTemplate(_sql);
return _context.Query(strSql.RawSql, strSql.Parameters);
}

public async Task> FetchAsync(string sql, SqlBuilder where, bool orderBy = true)
{
var _sql = orderBy ? $"{sql} /**where**/ /**orderby**/" : $"{sql} /**where**/";
var strSql = where.AddTemplate(_sql);
return await _context.QueryAsync(strSql.RawSql, strSql.Parameters);
}

public async Task> PageAsync(long pageIndex, long pageSize, SqlBuilder builder)
{
var strSql = "SELECT * FROM Product";
return await PageAsync(strSql, pageIndex, pageSize, builder);
}

public async Task> PageAsync(string sql, long pageIndex, long pageSize, SqlBuilder builder)
{
var strSql = builder.AddTemplate($"{sql} /**where**/ /**orderby**/");
return await _context.PageAsync(pageIndex, pageSize, strSql.RawSql, strSql.Parameters);
}

public async Task QueryMultipleAsync(string sql, object param = null)
{
return await _context.QueryMultipleAsync(sql, param);
}
}
#endregion


根据自身需要进行调整或扩展,一般借助T4模板生成

数据库连接

通过Ioptions模式读取配置文件appsettings中连接字符串

public class MyDBContext : DapperDBContext
    {
        public MyDBContext(IOptions optionsAccessor) : base(optionsAccessor)
        {
        }

        protected override IDbConnection CreateConnection(string connectionString)
        {
            IDbConnection conn = new SqlConnection(connectionString);
            return conn;
        }
    }

四、Dapper使用

Startup.cs注入并读取数据库连接字符串

{
  "SQLConnString": "Data Source=(local);Initial Catalog=database;Persist Security Info=True;User ID=sa;Password=123456;MultipleActiveResultSets=True;",  
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
services.AddDapperDBContext(options =>
            {
                options.Configuration = Configuration["SQLConnString"];
            });

简单示例WebAPI或Net Core MVC下的调用示例:

public class ProductController : BaseController
{
private readonly IProductRepository _productRepository;

public ProductController(
IProductRepository productRepository

)
{

_productRepository = productRepository;

}

//商品列表
[HttpGet]
public async Task ProductList(DateTime? startDate, DateTime? endDate, int id = 1, int productStatus = 0, string keyword = "")
{
var model = new ProductModels();
var builder = new Dapper.SqlBuilder();
builder.Where("ProductStatus!=@ProductStatus", new { ProductStatus = productStatus });

if (startDate.HasValue)
{
builder.Where("CreateTime>=@startDate", new { startDate = startDate.Value});
}
if (endDate.HasValue)
{
builder.Where("CreateTime<@endDate", new { endDate = endDate.Value.AddDays(1)});
}

if (!string.IsNullOrWhiteSpace(keyword))
{
builder.Where("Name LIKE @keyword", new { keyword = $"%{StringHelper.ReplaceSql(keyword)}%" });
}

builder.OrderBy("SortNum DESC,CreateTime DESC");

var list = await _productRepository.PageAsync(id, PageSize, builder);

model.ProductList = new PagedList(list.Items, id, PageSize, list.TotalItems);

if (Request.IsAjaxRequest())
return PartialView("_ProductList", model.ProductList);

return View(model);
}

//添加商品
[HttpPost]
public async Task AddProduct(ProductModels model)
{
return await _productRepository.InsertAsync(model);
}


}

public partial interface IProductService
{
Task AddProduct(Product productInfo, List skuList);

}
public class ProductService: IProductService
{
private readonly DapperDBContext _context;
private readonly IUnitOfWorkFactory _uowFactory;

public ProductService(DapperDBContext context, IUnitOfWorkFactory uowFactory)
{
_context = context;
_uowFactory = uowFactory;
}

///


/// 添加商品
///

///
///
///
public async Task AddProduct(Product productInfo, List skuList)
{
var result = false;
using (var uow = _uowFactory.Create())
{
//添加产品
await _context.InsertAsync(productInfo);

//添加Sku库存售价

//await _context.InsertAsync(skuList);

uow.SaveChanges();
result = true;
}
return result;
}

 

相关