0x00 前言 之前一直使用的是 EF ,做了一個簡單的小項目後發現 EF 的表現並不是很好,就比如聯表查詢,因為現在的 EF Core 也沒有啥好用的分析工具,所以也不知道該怎麼寫 Linq 生成出來的 Sql 效率比較高,於是這次的期末大作業決定使用性能強勁、輕便小巧的 ORM —— Dappe ...
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
舉例:
- 消息的狀態如果是 false ,則引發
ArgumentException
,且不會被持久化 - 消息的內容如果是空的,則引發
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
使用事務
在創建併發送新的站內消息到用戶的時候,需要先插入消息本體,然後再把消息跟目標用戶之間在關聯表中建立聯繫,所以我們需要考慮到下麵兩個問題:
- 數據的一致性