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

posted @ 2017-01-04 15:43  landonzeng  阅读(2965)  评论(0编辑  收藏  举报