欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ASP.NET Core 实战:基于 Dapper 扩展你的数据访问方法

程序员文章站 2022-04-08 20:30:43
一、前言 在非静态页面的项目开发中,必定会涉及到对于数据库的访问,最开始呢,我们使用 Ado.Net,通过编写 SQL 帮助类帮我们实现对于数据库的快速访问,后来,ORM(Object Relational Mapping,对象关系映射)出现了,我们开始使用 EF、Dapper、NHibernate ......

 一、前言

  在非静态页面的项目开发中,必定会涉及到对于数据库的访问,最开始呢,我们使用 ado.net,通过编写 sql 帮助类帮我们实现对于数据库的快速访问,后来,orm(object relational mapping,对象关系映射)出现了,我们开始使用 ef、dapper、nhibernate,亦或是国人的 sqlsugar 代替我们原来的 sqlhelper.cs。通过这些 orm 工具,我们可以很快速的将数据库中的表与代码中的类进行映射,同时,通过编写 sql 或是 lambda 表达式的方式,更加便捷的实现对于数据层的访问。

  就像文章标题中所说的这样,在这个项目中我是使用的 dapper 来进行的数据访问,每个人都有自己的编程习惯,本篇文章只是介绍我在 grapefruit.vucore 这个项目中是如何基于 dapper 创建自己使用的帮助方法的,不会涉及各种 orm 工具的对比,请友善查看、讨论。

  系列目录地址:asp.net core 项目实战
  仓储地址:https://github.com/lanesra712/grapefruit.vucore

 二、step by step

  1、整体思路

  在 grapefruit.vucore 这个项目中,我选择将 sql 语句存储在 xml 文件中(xml 以嵌入的资源的方式嵌入到程序集中),通过编写中间件的方式,在程序运行时将存储有 sql 语句的 xml 程序集写入到 redis 缓存中。当使用到 sql 语句时,通过 redis 中的 key 值进行获取到 value,从而将 sql 语句与我们的代码进行拆分。

  涉及到的类文件主要是在以下的类库中,基于 dapper 的数据访问代码则位于基础构造层(02_infrastructure)中,而使用到这些数据访问代码的,有且仅在位于领域层(03_domain)中的代码。同时,领域层的文件分布结构和应用层(04_applicatin)保持相同。

ASP.NET Core 实战:基于 Dapper 扩展你的数据访问方法

  2、扩展数据访问方法

  在使用 dapper 之前,我们首先需要在 grapefruit.infrastructure 这个类库中添加对于 dapper 的引用。同时,因为需要将 sql 语句存储到 redis 缓存中,与之前使用 redis 存储 token 时相同,这里,也是使用的微软的分布式缓存接口,因此,同样需要添加对于此 dll 的引用。

install-package dapper
install-package microsoft.extensions.caching.abstractions

  在 grapefruit.infrastructure 类库中创建一个 dapper 文件夹,我们基于 dapper 的扩展代码全部置于此处,整个的代码结构如下图所示。

ASP.NET Core 实战:基于 Dapper 扩展你的数据访问方法

  在整个 dapper 文件夹下类/接口/枚举文件,主要可以按照功能分为三部分。

  2.1、辅助功能文件

  主要包含 databasetypeenum 这个枚举类以及 sqlcommand 这个用来将存储在 xml 中的 sql 进行映射的帮助类。

  databasetypeenum 这个数据库类型枚举类主要定义了可以使用的数据库类型。我们知道,dapper 这个 orm 主要是通过扩展 idbconnection 接口,从而给我们提供附加的数据操作功能,而我们在创建数据库连接对象时,不管是 sqlconnection 还是 mysqlconnection 最终对于数据库最基础的操作,都是继承于 idbconnection 这个接口。因此,我们可以在后面创建数据库连接对象时,通过不同的枚举值,创建针对不同数据库操作的数据库连接对象。

public enum databasetypeenum
{
    sqlserver = 1,
    mysql = 2,
    postgresql = 3,
    oracle = 4
}

  sqlcommand 这个类文件只是定义了一些属性,因为我是将 sql 语句写到 xml 文件中,同时会将 xml 文件存储到 redis 缓存中,因此,sqlcommand 这个类主要用来将我们获取到的 sql 语句与类文件做一个映射关系。

public class sqlcommand
{
    /// <summary>
    /// sql语句名称
    /// </summary>
    public string name { get; set; }

    /// <summary>
    /// sql语句或存储过程内容
    /// </summary>
    public string sql { get; set; }
}

  2.2、sql 存储读取

  对于 sql 语句的存储、读取,我定义了一个 idatarepository 接口,datarepository 继承于 idatarepository 实现对于 sql 语句的操作。

public interface idatarepository
{
    /// <summary>
    /// 获取 sql 语句
    /// </summary>
    /// <param name="commandname"></param>
    /// <returns></returns>
    string getcommandsql(string commandname);

    /// <summary>
    /// 批量写入 sql 语句
    /// </summary>
    void loaddataxmlstore();
}

  存储 sql 的 xml 我是以附加的资源存储到 dll 中,因此,这里我是通过加载 dll 的方式获取到所有的 sql 语句,之后,根据 name 属性判断 redis 中是否存在,当不存在时就写入 redis 缓存中。核心的代码如下所示,如果你需要查看完整的代码,可以去 github 上查看。

/// <summary>
/// 载入dll中包含的sql语句
/// </summary>
/// <param name="fullpath">命令名称</param>
private void loadcommandxml(string fullpath)
{
    sqlcommand command = null;
    assembly dll = assembly.loadfile(fullpath);
    string[] xmlfiles = dll.getmanifestresourcenames();
    for (int i = 0; i < xmlfiles.length; i++)
    {
        stream stream = dll.getmanifestresourcestream(xmlfiles[i]);
        xelement rootnode = xelement.load(stream);
        var targetnodes = from n in rootnode.descendants("command")
                          select n;
        foreach (var item in targetnodes)
        {
            command = new sqlcommand
            {
                name = item.attribute("name").value.tostring(),
                sql = item.value.tostring().replace("<![cdata[", "").replace("]]>", "")
            };
            command.sql = command.sql.replace("\r\n", "").replace("\n", "").trim();
            loadsql(command.name, command.sql);
        }
    }
}

/// <summary>
/// 载入sql语句
/// </summary>
/// <param name="commandname">sql语句名称</param>
/// <param name="commandsql">sql语句内容</param>
private void loadsql(string commandname, string commandsql)
{
    if (string.isnullorempty(commandname))
    {
        throw new argumentnullexception("commandname is null or empty!");
    }

    string result = getcommandsql(commandname);

    if (string.isnullorempty(result))
    {
        storetocache(commandname, commandsql);
    }
}

  2.3、数据操作

  对于数据的操作,这里我定义了 idataaccess 这个接口,提供了同步、异步的方式,实现对于数据的访问。在项目开发中,对于数据的操作,更多的还是根据字段值获取对象、获取对象集合、执行 sql 获取受影响的行数,获取字段值,所以,这里主要就定义了这几类的方法。

public interface idataaccess
{
    /// 关闭数据库连接
    bool closeconnection(idbconnection connection);

    /// 数据库连接
    idbconnection dbconnection();

    /// 执行sql语句或存储过程返回对象
    t execute<t>(string sql, object param, bool hastransaction = false, commandtype commandtype = commandtype.text);

    /// 执行sql语句返回对象
    t execute<t>(string sql, object param, idbtransaction transaction, idbconnection connection, commandtype commandtype = commandtype.text);

    /// 执行sql语句或存储过程返回对象
    task<t> executeasync<t>(string sql, object param, bool hastransaction = false, commandtype commandtype = commandtype.text);

    /// 执行sql语句返回对象
    task<t> executeasync<t>(string sql, object param, idbtransaction transaction, idbconnection connection, commandtype commandtype = commandtype.text);

    /// 执行sql语句或存储过程,返回ilist<t>对象
    ilist<t> executeilist<t>(string sql, object param, bool hastransaction = false, commandtype commandtype = commandtype.text);

    /// 执行sql语句或存储过程,返回ilist<t>对象
    ilist<t> executeilist<t>(string sql, object param, idbtransaction transaction, idbconnection connection, commandtype commandtype = commandtype.text);

    /// 执行sql语句或存储过程,返回ilist<t>对象
    task<ilist<t>> executeilistasync<t>(string sql, object param, bool hastransaction = false, commandtype commandtype = commandtype.text);

    /// 执行sql语句或存储过程,返回ilist<t>对象
    task<ilist<t>> executeilistasync<t>(string sql, object param, idbtransaction transaction, idbconnection connection, commandtype commandtype = commandtype.text);

    /// 执行sql语句或存储过程返回受影响行数
    int executenonquery(string sql, object param, bool hastransaction = false, commandtype commandtype = commandtype.text);

    /// 执行sql语句或存储过程返回受影响行数
    int executenonquery(string sql, object param, idbtransaction transaction, idbconnection connection, commandtype commandtype = commandtype.text);

    /// 执行sql语句或存储过程返回受影响行数
    task<int> executenonqueryasync(string sql, object param, bool hastransaction = false, commandtype commandtype = commandtype.text);

    /// 执行sql语句或存储过程返回受影响行数
    task<int> executenonqueryasync(string sql, object param, idbtransaction transaction, idbconnection connection, commandtype commandtype = commandtype.text);

    /// 执行语句返回t对象
    t executescalar<t>(string sql, object param, bool hastransaction = false, commandtype commandtype = commandtype.text);

    /// 执行语句返回t对象
    task<t> executescalarasync<t>(string sql, object param, bool hastransaction = false, commandtype commandtype = commandtype.text);
}

  在 idataaccess 接口的功能实现与调用上,我采用了代理模式的方式,会涉及到 dataaccess、dataaccessproxy、dataaccessproxyfactory、dbmanager 这四个类文件,之间的调用过程如下。

  dataaccess 是接口的实现类,通过下面的几个类进行隐藏,不直接暴露给外界方法。一些接口的实现如下所示。

/// <summary>
/// 创建数据库连接
/// </summary>
/// <returns></returns>
public idbconnection dbconnection()
{
    idbconnection connection = null;
    switch (_databasetype)
    {
        case databasetypeenum.sqlserver:
            connection = new sqlconnection(_connectionstring);
            break;
        case databasetypeenum.mysql:
            connection = new mysqlconnection(_connectionstring);
            break;
    };
    return connection;
}

/// <summary>
/// 执行sql语句或存储过程,返回ilist<t>对象
/// </summary>
/// <typeparam name="t">类型</typeparam>
/// <param name="sql">sql语句 or 存储过程名</param>
/// <param name="param">参数</param>
/// <param name="transaction">外部事务</param>
/// <param name="connection">数据库连接</param>
/// <param name="commandtype">命令类型</param>
/// <returns></returns>
public ilist<t> executeilist<t>(string sql, object param, idbtransaction transaction, idbconnection connection, commandtype commandtype = commandtype.text)
{
    ilist<t> list = null;
    if (connection.state == connectionstate.closed)
    {
        connection.open();
    }
    try
    {
        if (commandtype == commandtype.text)
        {
            list = connection.query<t>(sql, param, transaction, true, null, commandtype.text).tolist();
        }
        else
        {
            list = connection.query<t>(sql, param, transaction, true, null, commandtype.storedprocedure).tolist();
        }
    }
    catch (exception ex)
    {
        _logger.logerror($"sql语句:{sql},使用外部事务执行 executeilist<t> 方法出错,错误信息:{ex.message}");
        throw ex;
    }
    return list;
}

  dbmanager 是外界方法访问的类,通过 createdataaccess 方法会创建一个 idataaccess 对象,从而达到访问接口中方法的目的。

[threadstatic]
private static idataaccess _smssqlfactory;

/// <summary>
/// 
/// </summary>
/// <param name="cp"></param>
/// <returns></returns>
private static idataaccess createdataaccess(connectionparameter cp)
{
    return new dataaccessproxy(dataaccessproxyfactory.create(cp));
}

/// <summary>
/// mssql 数据库连接字符串
/// </summary>
public static idataaccess mssql
{
    get
    {
        connectionparameter cp;
        if (_smssqlfactory == null)
        {
            cp = new connectionparameter
            {
                connectionstring = configurationmanager.getconfig("connectionstrings:mssqlconnection"),
                databasetype = databasetypeenum.sqlserver
            };
            _smssqlfactory = createdataaccess(cp);
        }
        return _smssqlfactory;
    }
}

  dataaccessproxy 就是实际接口功能实现类的代理,通过有参构造函数的方式进行调用,同时,类中继承于 idataaccess 的方法都是不实现的,都是通过 _dataaccess 调用接口中的方法。

/// <summary>
/// 
/// </summary>
private readonly idataaccess _dataaccess;

/// <summary>
/// ctor
/// </summary>
/// <param name="dataaccess"></param>
public dataaccessproxy(idataaccess dataaccess)
{
    _dataaccess = dataaccess ?? throw new argumentnullexception("dataaccess is null");
}

/// <summary>
/// 执行sql语句或存储过程,返回ilist<t>对象
/// </summary>
/// <typeparam name="t">类型</typeparam>
/// <param name="sql">sql语句 or 存储过程名</param>
/// <param name="param">参数</param>
/// <param name="transaction">外部事务</param>
/// <param name="connection">数据库连接</param>
/// <param name="commandtype">命令类型</param>
/// <returns></returns>
public ilist<t> executeilist<t>(string sql, object param, idbtransaction transaction, idbconnection connection, commandtype commandtype = commandtype.text)
{
    return _dataaccess.executeilist<t>(sql, param, transaction, connection, commandtype);
}

  dataaccessproxyfactory 这个类有一个 create 静态方法,通过实例化 dataaccess 类的方式返回 idataaccess 接口,从而达到真正调用到接口实现类。

/// <summary>
/// 创建数据库连接字符串
/// </summary>
/// <param name="cp"></param>
/// <returns></returns>
public static idataaccess create(connectionparameter cp)
{
    if (string.isnullorempty(cp.connectionstring))
    {
        throw new argumentnullexception("connectionstring is null or empty!");
    }
    return new dataaccess(cp.connectionstring, cp.databasetype);
}

  3、使用方法

  因为我们对于 sql 语句的获取全部是从缓存中获取的,因此,我们需要在程序执行前将所有的 sql 语句写入 redis 中。在 asp.net mvc 中,我们可以在 application_start 方法中进行调用,但是在 asp.net core 中,我一直没找到如何实现仅在程序开始运行时执行代码,所以,这里,我采用了中间件的形式将 sql 语句存储到 redis 中,当然,你的每一次请求,都会调用到这个中间件。如果大家有好的方法,欢迎在评论区里指出。

public class dappermiddleware
{
    private readonly ilogger _logger;

    private readonly idatarepository _repository;

    private readonly requestdelegate _request;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="repository"></param>
    /// <param name="logger"></param>
    /// <param name="request"></param>
    public dappermiddleware(idatarepository repository, ilogger<dappermiddleware> logger, requestdelegate request)
    {
        _repository = repository;
        _logger = logger;
        _request = request;
    }

    /// <summary>
    /// 注入中间件到httpcontext中
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async task invokeasync(httpcontext context)
    {
        stopwatch sw = new stopwatch();
        sw.start();

        //加载存储xml的dll
        _repository.loaddataxmlstore();

        sw.stop();
        timespan ts = sw.elapsed;

        _logger.loginformation($"加载存储 xml 文件dll,总共用时:{ts.totalminutes} 秒");

        await _request(context);
    }
}

  中间件的实现,只是调用了之前定义的 idatarepository 接口中的 loaddataxmlstore 方法,同时记录下了加载的时间。在 dappermiddlewareextensions 这个静态类中,定义了中间件的使用方法,之后我们在 startup 的 configure 方法里调用即可。

public static class dappermiddlewareextensions
{
    /// <summary>
    /// 调用中间件
    /// </summary>
    /// <param name="builder"></param>
    /// <returns></returns>
    public static iapplicationbuilder usedapper(this iapplicationbuilder builder)
    {
        return builder.usemiddleware<dappermiddleware>();
    }
}

  中间件的调用代码如下,同时,因为我们在中间件中通过依赖注入的方式使用到了 idatarepository 接口,所以,我们也需要在 configureservices 中注入该接口,这里,采用单例的方式即可。

public class startup
{
    // this method gets called by the runtime. use this method to add services to the container.
    public void configureservices(iservicecollection services)
    {
        //di sql data
        services.addtransient<idatarepository, datarepository>();
    }

    // this method gets called by the runtime. use this method to configure the http request pipeline.
    public void configure(iapplicationbuilder app, ihostingenvironment env, iapiversiondescriptionprovider provider)
    {
        //load sql data
        app.usedapper();
    }
}

   当所有的 sql 语句写入到缓存中后,我们就可以使用了,这里的示例代码实现的是上一篇(asp.net core 实战:基于 jwt token 的权限控制全揭露)中,进行 jwt token 授权,验证登录用户信息的功能。

ASP.NET Core 实战:基于 Dapper 扩展你的数据访问方法

  整个的调用过程如下图所示。

ASP.NET Core 实战:基于 Dapper 扩展你的数据访问方法

  在 secretdomain 中,我定义了一个 getuserforloginasync 方法,通过帐户名和密码获取用户的信息,调用了之前定义的数据访问方法。 

public class secretdomain : isecretdomain
{
    #region initialize

    /// <summary>
    /// 仓储接口
    /// </summary>
    private readonly idatarepository _repository;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="repository"></param>
    public secretdomain(idatarepository repository)
    {
        _repository = repository;
    }

    #endregion

    #region api implements

    /// <summary>
    /// 根据帐户名、密码获取用户实体信息
    /// </summary>
    /// <param name="account">账户名</param>
    /// <param name="password">密码</param>
    /// <returns></returns>
    public async task<identityuser> getuserforloginasync(string account, string password)
    {
        stringbuilder strsql = new stringbuilder();
        strsql.append(_repository.getcommandsql("secret_getuserbyloginasync"));
        string sql = strsql.tostring();

        return await dbmanager.mssql.executeasync<identityuser>(sql, new
        {
            account,
            password
        });
    }

    #endregion
}

  xml 的结构如下所示,注意,这里需要修改 xml 的属性,生成操作改为附加的资源。

<?xml version="1.0" encoding="utf-8" ?>
<commands>
  <command name="secret_getuserbyloginasync">
    <![cdata[
        select id ,name ,account ,password ,salt
          from identityuser
          where account=@account and password=@password;
      ]]>
  </command>
  <command name="secret_newid">
    <![cdata[
        select newid();
      ]]>
  </command>
</commands>

ASP.NET Core 实战:基于 Dapper 扩展你的数据访问方法

  因为篇幅原因,这里就不把所有的代码都列出来,整个调用的过程演示如下,如果有不明白的,或是有什么好的建议的,欢迎在评论区中提出。因为,数据库表并没有设计好,这里只是建了一个实验用的表,,这里我使用的是 sql server 2012,创建表的 sql 语句如下。

use [grapefruitvucore]
go

alter table [dbo].[identityuser] drop constraint [df_user_id]
go

/****** object:  table [dbo].[identityuser]    script date: 2019/2/24 9:41:15 ******/
drop table [dbo].[identityuser]
go

/****** object:  table [dbo].[identityuser]    script date: 2019/2/24 9:41:15 ******/
set ansi_nulls on
go

set quoted_identifier on
go

create table [dbo].[identityuser](
    [id] [uniqueidentifier] not null,
    [name] [nvarchar](50) not null,
    [account] [nvarchar](50) not null,
    [password] [nvarchar](100) not null,
    [salt] [uniqueidentifier] not null,
 constraint [pk__user__3214ec07d257c709] primary key clustered 
(
    [id] asc
)with (pad_index = off, statistics_norecompute = off, ignore_dup_key = off, allow_row_locks = on, allow_page_locks = on) on [primary]
) on [primary]

go

alter table [dbo].[identityuser] add  constraint [df_user_id]  default (newid()) for [id]
go

ASP.NET Core 实战:基于 Dapper 扩展你的数据访问方法

 三、总结

    这一章主要是介绍下我是如何使用 dapper 构建我的数据访问帮助方法的,每个人都会有自己的编程习惯,这里只是给大家提供一个思路,适不适合你就不一定啦。因为年后工作开始变得多起来了,现在主要都是周末才能写博客了,所以更新的速度会变慢些,同时,这一系列的文章,按我的设想,其实还有一两篇文章差不多就结束了(vue 前后端交互、docker 部署),嗯,因为 vue 那块我还在学习中(其实就是很长时间没看了。。。),所以接下来的一段时间可能会侧重于 vue 系列(vue.js 牛刀小试),asp.net core 系列可能会不定期更新,希望大家同样可以多多关注啊。最后,感谢之前赞赏的小伙伴。