天天看點

ASP.NET Core 實戰:基于 Dapper 擴充你的資料通路方法

ASP.NET Core 實戰:基于 Dapper 擴充你的資料通路方法

一、前言

在非靜态頁面的項目開發中,必定會涉及到對于資料庫的通路,最開始呢,我們使用 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 系列可能會不定期更新,希望大家同樣可以多多關注啊。最後,感謝之前贊賞的小夥伴。

作者:

墨墨墨墨小宇

出處:

https://www.cnblogs.com/danvic712/p/10425501.html