程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> Asp.Net Core + Dapper + Repository 模式 + TDD 學習筆記,dappertdd

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

編輯:關於.NET

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


0x00 前言

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

0x01 Repository 模式

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

Repository模式:
0x02 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 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

    使用事務

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

    為了解決第一個問題,我們需要使用事務(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 的話,就沒辦法了。所以我就直接
    轉載:http://www.cnblogs.com/JacZhu/p/6112033.html

    1. 上一頁:
    2. 下一頁:
    Copyright © 程式師世界 All Rights Reserved