Asp.Net Core + Dapper + Repository 模式 + TDD 學習筆記


0x00 前言

之前一直使用的是 EF ,做了一個簡單的小項目后發現 EF 的表現並不是很好,就比如聯表查詢,因為現在的 EF Core 也沒有啥好用的分析工具,所以也不知道該怎么寫 Linq 生成出來的 Sql 效率比較高,於是這次的期末大作業決定使用性能強勁、輕便小巧的 ORM —— Dapper。

0x01 Repository 模式

Repository 模式幾乎出現在所有的 asp.net 樣例中,主要的作用是給業務層提供數據訪問的能力,與 DAL 的區別就在於:

Repository模式
Repository 是DDD中的概念,強調 Repository 是受 Domain 驅動的, Repository 中定義的功能要體現 Domain 的意圖和約束,而 Dal 更純粹的就是提供數據訪問的功能,並不嚴格受限於 Business 層。使用 Repository ,隱含着一種意圖傾向,就是 Domain 需要什么我才提供什么,不該提供的功能就不要提供,一切都是以 Domain 的需求為核心。
而使用Dal,其意圖傾向在於我 Dal 層能使用的數據庫訪問操作提供給 Business 層,你 Business 要用哪個自己選.換一個 Business 也可以用我這個 Dal,一切是以我 Dal 能提供什么操作為核心.

0x02 TDD(測試驅動開發)

TDD 的基本思路就是通過測試來推動整個開發的進行。而測試驅動開發技術並不只是單純的測試工作。
在我看來,TDD 的實施可以帶來以下的好處:

  • 在一個接口尚未完全確定的時候,通過編寫測試用例,可以幫助我們更好的描述接口的行為,幫助我們更好的了解抽象的需求。
  • 編寫測試用例的過程能夠促使我們將功能分解開,做出“高內聚,低耦合”的設計,因此,TDD 也是我們設計高可復用性的代碼的過程。
  • 編寫測試用例也是對接口調用方法最詳細的描述,Documation is cheap, show me the examples。測試用例代碼比詳盡的文檔不知道高到哪里去了。
  • 測試用例還能夠盡早的幫助我們發現代碼的錯誤,每當代碼發生了修改,可以方便的幫助我們驗證所做的修改對已經有效的功能是否有影響,從而使我們能夠更快的發現並定位 bug。

0x03 建模

在期末作業的系統中,需要實現一個站內通知的功能,首先,讓我們來簡單的建個模:

然后,依照這個模型,我創建好了對應的實體與接口:

 1 public interface IInsiteMsgService
 2 {
 3     /// <summary>
 4     /// 給一組用戶發送指定的站內消息
 5     /// </summary>
 6     /// <param name="msgs">站內消息數組</param>
 7     Task SentMsgsAsync(IEnumerable<InsiteMsg> msgs);
 8 
 9     /// <summary>
10     /// 發送一條消息給指定的用戶
11     /// </summary>
12     /// <param name="msg">站內消息</param>
13     void SentMsg(InsiteMsg msg);
14 
15     /// <summary>
16     /// 將指定的消息設置為已讀
17     /// </summary>
18     /// <param name="msgIdRecordIds">用戶消息記錄的 Id</param>
19     void ReadMsg(IEnumerable<int> msgIdRecordIds);
20 
21     /// <summary>
22     /// 獲取指定用戶的所有的站內消息,包括已讀與未讀
23     /// </summary>
24     /// <param name="userId">用戶 Id</param>
25     /// <returns></returns>
26     IEnumerable<InsiteMsg> GetInbox(int userId);
27 
28     /// <summary>
29     /// 刪除指定用戶的一些消息記錄
30     /// </summary>
31     /// <param name="userId">用戶 Id</param>
32     /// <param name="insiteMsgIds">用戶消息記錄 Id</param>
33     void DeleteMsgRecord(int userId, IEnumerable<int> insiteMsgIds);
34     }
View Code

InsiteMessage 實體:

 1 public class InsiteMsg
 2 {
 3     public int InsiteMsgId { get; set; }
 4     /// <summary>
 5     /// 消息發送時間
 6     /// </summary>
 7     public DateTime SentTime { get; set; }
 8 
 9     /// <summary>
10     /// 消息閱讀時間,null 說明消息未讀
11     /// </summary>
12     public DateTime? ReadTime { get; set; }
13     
14     public int UserId { get; set; }
15 
16     /// <summary>
17     /// 消息內容
18     /// </summary>
19     [MaxLength(200)]
20     public string Content { get; set; }
21 
22     public bool Status { get; set; }
23 }
View Code

建立測試

接下來,建立測試用例,來描述 Service 每個方法的行為,這里以 SentMsgsAsync 舉例:

  1. 消息的狀態如果是 false ,則引發 ArgumentException ,且不會被持久化
  2. 消息的內容如果是空的,則引發 ArgumentException ,且不會被持久化

根據上面的約束,測試用例代碼也就出來了

  1 public class InsiteMsgServiceTests
  2 {
  3     /// <summary>
  4     /// 消息發送成功,添加到數據庫
  5     /// </summary>
  6     [Fact]
  7     public void SentMsgTest()
  8     {
  9         //Mock repository
 10         List<InsiteMsg> dataSet = new List<InsiteMsg>();
 11 
 12         var msgRepoMock = new Mock<IInsiteMsgRepository>();
 13         msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) =>
 14         {
 15             dataSet.AddRange(m);
 16         });
 17 
 18         //Arrange
 19         IInsiteMsgService msgService = new InsiteMsgService(msgRepoMock.Object);
 20 
 21         var msgs = new List<InsiteMsg>
 22         {
 23             new InsiteMsg { Content="fuck", Status=true, UserId=123 },
 24             new InsiteMsg { Content="fuck", Status=true, UserId=123 },
 25             new InsiteMsg { Content="fuck", Status=true, UserId=123 },
 26             new InsiteMsg { Content="fuck", Status=true, UserId=123 },
 27         };
 28 
 29         //action
 30         msgService.SentMsgsAsync(msgs);
 31 
 32         dataSet.Should().BeEquivalentTo(msgs);
 33     }
 34 
 35     /// <summary>
 36     /// 消息的狀態如果是 false ,則引發 <see cref="ArgumentException"/>,且不會被持久化
 37     /// </summary>
 38     [Fact]
 39     public void SentMsgWithFalseStatusTest()
 40     {
 41         //Mock repository
 42         List<InsiteMsg> dataSet = new List<InsiteMsg>();
 43         var msgRepoMock = new Mock<IInsiteMsgRepository>();
 44         msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) =>
 45         {
 46             dataSet.AddRange(m);
 47         });
 48 
 49         IInsiteMsgService msgService = new InsiteMsgService(msgRepoMock.Object);
 50 
 51         List<InsiteMsg> msgs = new List<InsiteMsg>
 52         {
 53             new InsiteMsg { Status = false, Content = "fuck" },
 54             new InsiteMsg { Status = true, Content = "fuck" }
 55         };
 56 
 57         var exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs));
 58         exception?.Result.Should().NotBeNull();
 59         Assert.IsType<ArgumentException>(exception.Result);
 60         dataSet.Count.Should().Equals(0);
 61     }
 62 
 63     /// <summary>
 64     /// 消息的內容如果是空的,則引發 <see cref="ArgumentException"/>,且不會被持久化
 65     /// </summary>
 66     [Fact]
 67     public void SentMsgWithEmptyContentTest()
 68     {
 69         //Mock repository
 70         List<InsiteMsg> dataSet = new List<InsiteMsg>();
 71         var msgRepoMock = new Mock<IInsiteMsgRepository>();
 72         msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) =>
 73         {
 74             dataSet.AddRange(m);
 75         });
 76 
 77 
 78         IInsiteMsgService msgService = new InsiteMsgService(msgRepoMock.Object);
 79 
 80         List<InsiteMsg> msgs = new List<InsiteMsg>
 81         {
 82             new InsiteMsg { Status = true, Content = "" }// empty
 83         };
 84 
 85         var exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs));
 86         exception?.Result.Should().NotBeNull(because: "消息內容是空字符串");
 87         Assert.IsType<ArgumentException>(exception.Result);
 88         dataSet.Count.Should().Equals(0);
 89 
 90         msgs = new List<InsiteMsg>
 91         {
 92             new InsiteMsg { Status = true, Content = " " }// space only
 93         };
 94 
 95         exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs));
 96         exception?.Result.Should().NotBeNull(because: "消息內容只包含空格");
 97         Assert.IsType<ArgumentException>(exception.Result);
 98         dataSet.Count.Should().Equals(0);
 99 
100         msgs = new List<InsiteMsg>
101         {
102             new InsiteMsg { Status = true, Content = null }// null
103         };
104 
105         exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs));
106         exception?.Result.Should().NotBeNull(because: "消息內容是 null");
107         Assert.IsType<ArgumentException>(exception.Result);
108         dataSet.Count.Should().Equals(0);
109     }
110 }
View Code

實現接口以通過測試

 1 namespace Hive.Domain.Services.Concretes
 2 {
 3     public class InsiteMsgService : IInsiteMsgService
 4     {
 5         private readonly IInsiteMsgRepository _msgRepo;
 6 
 7         public InsiteMsgService(IInsiteMsgRepository msgRepo)
 8         {
 9             _msgRepo = msgRepo;
10         }
11 
12 
13         public async Task SentMsgsAsync(IEnumerable<InsiteMsg> msgs)
14         {
15             foreach (InsiteMsg msg in msgs)
16             {
17                 if (!msg.Status || string.IsNullOrWhiteSpace(msg.Content))
18                 {
19                     throw new ArgumentException("不能將無效的消息插入", nameof(msgs));
20                 }
21                 msg.SentTime = DateTime.Now;
22                 msg.ReadTime = null;
23             }
24             await _msgRepo.InsertAsync(msgs);
25         }
26 
27         public void SentMsg(InsiteMsg msg)
28         {
29             if (!msg.Status || string.IsNullOrWhiteSpace(msg.Content))
30             {
31                 throw new ArgumentException("不能將無效的消息插入", nameof(msg));
32             }
33             msg.SentTime = DateTime.Now;
34             msg.ReadTime = null;
35             _msgRepo.Insert(msg);
36         }
37 
38         public void ReadMsg(IEnumerable<int> msgs, int userId)
39         {
40             var ids = msgs.Distinct();
41             _msgRepo.UpdateReadTime(ids, userId);
42         }
43 
44         public async Task<IEnumerable<InsiteMsg>> GetInboxAsync(int userId)
45         {
46             return await _msgRepo.GetByUserIdAsync(userId);
47         }
48 
49         public void DeleteMsgRecord(int userId, IEnumerable<int> insiteMsgIds)
50         {
51             _msgRepo.DeleteMsgRecoreds(userId, insiteMsgIds.Distinct());
52         }
53     }
54 }
View Code

上面的一些代碼很明了,就懶得逐塊注釋了,函數注釋足矣~

驗證測試

測試當然全部通過啦,這里就不放圖了

為了將數據訪問與邏輯代碼分離,這里我使用了 Repository
模式—— IInsiteMsgRepository ,下面給出這個接口的定義:

 1 namespace Hive.Domain.Repositories.Abstracts
 2 {
 3     public interface IInsiteMsgRepository
 4     {
 5         /// <summary>
 6         /// 插入一條消息
 7         /// </summary>
 8         /// <param name="msg">消息實體</param>
 9         void Insert(InsiteMsg msg);
10 
11         Task InsertAsync(IEnumerable<InsiteMsg> msgs);
12 
13         /// <summary>
14         /// 根據消息 id 獲取消息內容,不包含閱讀狀態
15         /// </summary>
16         /// <param name="id">消息 Id</param>
17         /// <returns></returns>
18         InsiteMsg GetById(int id);
19 
20         /// <summary>
21         /// 更新消息的閱讀時間為當前時間
22         /// </summary>
23         /// <param name="msgIds">消息的 Id</param>
24         /// <param name="userId">用戶 Id</param>
25         void UpdateReadTime(IEnumerable<int> msgIds,int userId);
26 
27         /// <summary>
28         /// 獲取跟指定用戶相關的所有消息
29         /// </summary>
30         /// <param name="id">用戶 id</param>
31         /// <returns></returns>
32         Task<IEnumerable<InsiteMsg>> GetByUserIdAsync(int id);
33 
34         /// <summary>
35         /// 刪除指定的用戶的消息記錄
36         /// </summary>
37         /// <param name="userId">用戶 Id</param>
38         /// <param name="msgRIds">消息 Id</param>
39         void DeleteMsgRecoreds(int userId, IEnumerable<int> msgRIds);
40     }
41 }
View Code

但是在測試階段,我並不想把倉庫實現掉,所以這里就用上了 Moq.Mock

1 List<InsiteMsg> dataSet = new List<InsiteMsg>();
2         var msgRepoMock = new Mock<IInsiteMsgRepository>();
3         msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) =>
4         {
5             dataSet.AddRange(m);
6         });
View Code

上面的代碼模擬了一個 IInsiteMsgRepository 對象,在我們調用這個對象的 InsertAsync 方法的時候,這個對象就把傳入的參數添加到一個集合中去。
模擬出來的對象可以通過 msgMock.Object 訪問。

0x04 實現 Repository

使用事務

在創建並發送新的站內消息到用戶的時候,需要先插入消息本體,然后再把消息跟目標用戶之間在關聯表中建立聯系,所以我們需要考慮到下面兩個問題:

  1. 數據的一致性
  2. 在建立聯系前必須獲取到消息的 Id

為了解決第一個問題,我們需要使用事務(Transaction),就跟在 ADO.NET 中使用事務一樣,可以使用一個簡單的套路:

 1 _conn.Open();
 2 try
 3 {
 4     using (var transaction = _conn.BeginTransaction())
 5     {
 6         // execute some sql
 7         transaction.Commit();
 8     }
 9 }
10 finally
11 {
12     _conn.Close();
13 }
View Code

在事務中,一旦部分操作失敗了,我們就可以回滾(Rollback)到初始狀態,這樣要么所有的操作全部成功執行,要么一條操作都不會執行,數據完整性、一致性得到了保證。

在上面的代碼中,using 塊內,Commit()之前的語句一旦執行出錯(拋出異常),程序就會自動 Rollback。

在數據庫中,Id 是一個自增字段,為了獲取剛剛插入的實體的 Id 可以使用 last_insert_id() 這個函數(For MySql),這個函數返回當前連接過程中,最后插入的行的自增的主鍵的值。

最終實現

  1 using Hive.Domain.Repositories.Abstracts;
  2 using System;
  3 using System.Collections.Generic;
  4 using System.Linq;
  5 using System.Threading.Tasks;
  6 using Hive.Domain.Entities;
  7 using System.Data.Common;
  8 using Dapper;
  9 
 10 namespace Hive.Domain.Repositories.Concretes
 11 {
 12     public class InsiteMsgRepository : IInsiteMsgRepository
 13     {
 14         private readonly DbConnection _conn;
 15 
 16         public InsiteMsgRepository(DbConnection conn)
 17         {
 18             _conn = conn;
 19         }
 20 
 21         public void DeleteMsgRecoreds(int userId, IEnumerable<int> msgIds)
 22         {
 23             var param = new
 24             {
 25                 UserId = userId,
 26                 MsgIds = msgIds
 27             };
 28             string sql = $@"
 29                 UPDATE insite_msg_record
 30                 SET Status = 0
 31                 WHERE UserId = @{nameof(param.UserId)}
 32                     AND Status = 1
 33                     AND InsiteMsgId IN @{nameof(param.MsgIds)}";
 34             try
 35             {
 36                 _conn.Open();
 37                 using (var transaction = _conn.BeginTransaction())
 38                 {
 39                     _conn.Execute(sql, param, transaction);
 40                     transaction.Commit();
 41                 }
 42             }
 43             finally
 44             {
 45                 _conn.Close();
 46             }
 47 
 48         }
 49 
 50         public InsiteMsg GetById(int id)
 51         {
 52             throw new NotImplementedException();
 53         }
 54 
 55         public async Task<IEnumerable<InsiteMsg>> GetByUserIdAsync(int id)
 56         {
 57             string sql = $@"
 58                 SELECT
 59                     ReadTime,
 60                     SentTime,
 61                     insite_msg.InsiteMsgId,
 62                     Content,
 63                     UserId
 64                 FROM insite_msg_record, insite_msg
 65                 WHERE UserId = @{nameof(id)}
 66                     AND insite_msg.InsiteMsgId = insite_msg_record.InsiteMsgId
 67                     AND insite_msg.Status = TRUE
 68                     AND insite_msg_record.Status = 1";
 69             var inboxMsgs = await _conn.QueryAsync<InsiteMsg>(sql, new { id });
 70             inboxMsgs = inboxMsgs.OrderBy(m => m.ReadTime);
 71             return inboxMsgs;
 72         }
 73 
 74         public async Task InsertAsync(IEnumerable<InsiteMsg> msgs)
 75         {
 76             var msgContents = msgs.Select(m => new { m.Content, m.SentTime });
 77             string insertSql = $@"
 78                 INSERT INTO insite_msg (SentTime, Content)
 79                 VALUES (@SentTime, @Content)";
 80             _conn.Open();
 81             // 開啟一個事務,保證數據插入的完整性
 82             try
 83             {
 84                 using (var transaction = _conn.BeginTransaction())
 85                 {
 86                     // 首先插入消息實體
 87                     var insertMsgTask = _conn.ExecuteAsync(insertSql, msgContents, transaction);
 88                     // 等待消息實體插入完成
 89                     await insertMsgTask;
 90                     var msgRecords = msgs.Select(m => new { m.UserId, m.InsiteMsgId });
 91                     // 獲取消息的 Id
 92                     int firstId = (int)(_conn.QuerySingle("SELECT last_insert_id() AS FirstId").FirstId);
 93                     firstId = firstId - msgs.Count() + 1;
 94                     foreach (var m in msgs)
 95                     {
 96                         m.InsiteMsgId = firstId;
 97                         firstId++;
 98                     }
 99                     // 插入消息記錄
100                     insertSql = $@"
101                         INSERT INTO insite_msg_record (UserId, InsiteMsgId) 
102                         VALUES (@UserId, @InsiteMsgId)";
103                     await _conn.ExecuteAsync(insertSql, msgRecords);
104                     transaction.Commit();
105                 }
106             }
107             catch (Exception)
108             {
109                 _conn.Close();
110                 throw;
111             }
112 
113         }
114 
115         public void Insert(InsiteMsg msg)
116         {
117             string sql = $@"
118                 INSERT INTO insite_msg (SentTime, Content) 
119                 VALUE (@{nameof(msg.SentTime)}, @{nameof(msg.Content)})";
120             _conn.Execute(sql, new { msg.SentTime, msg.Content });
121             string recordSql = $@"
122                 INSERT INTO insite_msg_record (UserId, InsiteMsgId)
123                 VALUE (@{nameof(msg.UserId)}, @{nameof(msg.InsiteMsgId)})";
124             _conn.Execute(recordSql, new { msg.UserId, msg.InsiteMsgId });
125         }
126 
127         public void UpdateReadTime(IEnumerable<int> msgsIds, int userId)
128         {
129             var param = new
130             {
131                 UserId = userId,
132                 Msgs = msgsIds
133             };
134             // 只更新發送給指定用戶的指定消息
135             string sql = $@"
136                 UPDATE insite_msg_record
137                 SET ReadTime = now()
138                 WHERE UserId = @{nameof(param.UserId)}
139                     AND Status = 1
140                     AND InsiteMsgId IN @{nameof(param.Msgs)}";
141             try
142             {
143                 _conn.Open();
144                 using (var transaction = _conn.BeginTransaction())
145                 {
146                     _conn.Execute(sql, param, transaction);
147                     transaction.Commit();
148                 }
149             }
150             finally
151             {
152                 _conn.Close();
153             }
154         }
155     }
156 }
View Code

0x05 測試 Repository

測試 Repository 這部分還是挺難的,沒辦法編寫單元測試,EF 的話還可以用 內存數據庫,但是 Dapper 的話,就沒辦法了。所以我就直接
寫了測試用的 API,通過 API 直接調用 Repository 的方法,然后往測試數據庫里面讀寫數據。

轉載:http://www.cnblogs.com/JacZhu/p/6112033.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM