当前位置: 移动技术网 > IT编程>开发语言>.net > ASP.NET Core 实战:基于 Dapper 扩展你的数据访问方法

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

2019年02月26日  | 移动技术网IT编程  | 我要评论

厦门理工学院陈蕾,人体结构素描,kingsoft是什么

 一、前言

  在非静态页面的项目开发中,必定会涉及到对于数据库的访问,最开始呢,我们使用 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)保持相同。

  2、扩展数据访问方法

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

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

  在 grapefruit.infrastructure 类库中创建一个 dapper 文件夹,我们基于 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 授权,验证登录用户信息的功能。

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

  在 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>

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

 三、总结

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网