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