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 建模

在期末作业的系统中,需要实现一个站内通知的功能,首先,让我们来简单的建个模:

然后,依照这个模型,我创建好了对应的实体与接口:

 public interface IInsiteMsgService
{
/// <summary>
/// 给一组用户发送指定的站内消息
/// </summary>
/// <param name="msgs">站内消息数组</param>
Task SentMsgsAsync(IEnumerable<InsiteMsg> msgs); /// <summary>
/// 发送一条消息给指定的用户
/// </summary>
/// <param name="msg">站内消息</param>
void SentMsg(InsiteMsg msg); /// <summary>
/// 将指定的消息设置为已读
/// </summary>
/// <param name="msgIdRecordIds">用户消息记录的 Id</param>
void ReadMsg(IEnumerable<int> msgIdRecordIds); /// <summary>
/// 获取指定用户的所有的站内消息,包括已读与未读
/// </summary>
/// <param name="userId">用户 Id</param>
/// <returns></returns>
IEnumerable<InsiteMsg> GetInbox(int userId); /// <summary>
/// 删除指定用户的一些消息记录
/// </summary>
/// <param name="userId">用户 Id</param>
/// <param name="insiteMsgIds">用户消息记录 Id</param>
void DeleteMsgRecord(int userId, IEnumerable<int> insiteMsgIds);
}

InsiteMessage 实体:

 public class InsiteMsg
{
public int InsiteMsgId { get; set; }
/// <summary>
/// 消息发送时间
/// </summary>
public DateTime SentTime { get; set; } /// <summary>
/// 消息阅读时间,null 说明消息未读
/// </summary>
public DateTime? ReadTime { get; set; } public int UserId { get; set; } /// <summary>
/// 消息内容
/// </summary>
[MaxLength()]
public string Content { get; set; } public bool Status { get; set; }
}

建立测试

接下来,建立测试用例,来描述 Service 每个方法的行为,这里以 SentMsgsAsync 举例:

  1. 消息的状态如果是 false ,则引发 ArgumentException ,且不会被持久化
  2. 消息的内容如果是空的,则引发 ArgumentException ,且不会被持久化

根据上面的约束,测试用例代码也就出来了

 public class InsiteMsgServiceTests
{
/// <summary>
/// 消息发送成功,添加到数据库
/// </summary>
[Fact]
public void SentMsgTest()
{
//Mock repository
List<InsiteMsg> dataSet = new List<InsiteMsg>(); var msgRepoMock = new Mock<IInsiteMsgRepository>();
msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) =>
{
dataSet.AddRange(m);
}); //Arrange
IInsiteMsgService msgService = new InsiteMsgService(msgRepoMock.Object); var msgs = new List<InsiteMsg>
{
new InsiteMsg { Content="fuck", Status=true, UserId= },
new InsiteMsg { Content="fuck", Status=true, UserId= },
new InsiteMsg { Content="fuck", Status=true, UserId= },
new InsiteMsg { Content="fuck", Status=true, UserId= },
}; //action
msgService.SentMsgsAsync(msgs); dataSet.Should().BeEquivalentTo(msgs);
} /// <summary>
/// 消息的状态如果是 false ,则引发 <see cref="ArgumentException"/>,且不会被持久化
/// </summary>
[Fact]
public void SentMsgWithFalseStatusTest()
{
//Mock repository
List<InsiteMsg> dataSet = new List<InsiteMsg>();
var msgRepoMock = new Mock<IInsiteMsgRepository>();
msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) =>
{
dataSet.AddRange(m);
}); IInsiteMsgService msgService = new InsiteMsgService(msgRepoMock.Object); List<InsiteMsg> msgs = new List<InsiteMsg>
{
new InsiteMsg { Status = false, Content = "fuck" },
new InsiteMsg { Status = true, Content = "fuck" }
}; var exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs));
exception?.Result.Should().NotBeNull();
Assert.IsType<ArgumentException>(exception.Result);
dataSet.Count.Should().Equals();
} /// <summary>
/// 消息的内容如果是空的,则引发 <see cref="ArgumentException"/>,且不会被持久化
/// </summary>
[Fact]
public void SentMsgWithEmptyContentTest()
{
//Mock repository
List<InsiteMsg> dataSet = new List<InsiteMsg>();
var msgRepoMock = new Mock<IInsiteMsgRepository>();
msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) =>
{
dataSet.AddRange(m);
}); IInsiteMsgService msgService = new InsiteMsgService(msgRepoMock.Object); List<InsiteMsg> msgs = new List<InsiteMsg>
{
new InsiteMsg { Status = true, Content = "" }// empty
}; var exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs));
exception?.Result.Should().NotBeNull(because: "消息内容是空字符串");
Assert.IsType<ArgumentException>(exception.Result);
dataSet.Count.Should().Equals(); msgs = new List<InsiteMsg>
{
new InsiteMsg { Status = true, Content = " " }// space only
}; exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs));
exception?.Result.Should().NotBeNull(because: "消息内容只包含空格");
Assert.IsType<ArgumentException>(exception.Result);
dataSet.Count.Should().Equals(); msgs = new List<InsiteMsg>
{
new InsiteMsg { Status = true, Content = null }// null
}; exception = Record.ExceptionAsync(async () => await msgService.SentMsgsAsync(msgs));
exception?.Result.Should().NotBeNull(because: "消息内容是 null");
Assert.IsType<ArgumentException>(exception.Result);
dataSet.Count.Should().Equals();
}
}

实现接口以通过测试

 namespace Hive.Domain.Services.Concretes
{
public class InsiteMsgService : IInsiteMsgService
{
private readonly IInsiteMsgRepository _msgRepo; public InsiteMsgService(IInsiteMsgRepository msgRepo)
{
_msgRepo = msgRepo;
} public async Task SentMsgsAsync(IEnumerable<InsiteMsg> msgs)
{
foreach (InsiteMsg msg in msgs)
{
if (!msg.Status || string.IsNullOrWhiteSpace(msg.Content))
{
throw new ArgumentException("不能将无效的消息插入", nameof(msgs));
}
msg.SentTime = DateTime.Now;
msg.ReadTime = null;
}
await _msgRepo.InsertAsync(msgs);
} public void SentMsg(InsiteMsg msg)
{
if (!msg.Status || string.IsNullOrWhiteSpace(msg.Content))
{
throw new ArgumentException("不能将无效的消息插入", nameof(msg));
}
msg.SentTime = DateTime.Now;
msg.ReadTime = null;
_msgRepo.Insert(msg);
} public void ReadMsg(IEnumerable<int> msgs, int userId)
{
var ids = msgs.Distinct();
_msgRepo.UpdateReadTime(ids, userId);
} public async Task<IEnumerable<InsiteMsg>> GetInboxAsync(int userId)
{
return await _msgRepo.GetByUserIdAsync(userId);
} public void DeleteMsgRecord(int userId, IEnumerable<int> insiteMsgIds)
{
_msgRepo.DeleteMsgRecoreds(userId, insiteMsgIds.Distinct());
}
}
}

上面的一些代码很明了,就懒得逐块注释了,函数注释足矣~

验证测试

测试当然全部通过啦,这里就不放图了

为了将数据访问与逻辑代码分离,这里我使用了 Repository
模式—— IInsiteMsgRepository ,下面给出这个接口的定义:

 namespace Hive.Domain.Repositories.Abstracts
{
public interface IInsiteMsgRepository
{
/// <summary>
/// 插入一条消息
/// </summary>
/// <param name="msg">消息实体</param>
void Insert(InsiteMsg msg); Task InsertAsync(IEnumerable<InsiteMsg> msgs); /// <summary>
/// 根据消息 id 获取消息内容,不包含阅读状态
/// </summary>
/// <param name="id">消息 Id</param>
/// <returns></returns>
InsiteMsg GetById(int id); /// <summary>
/// 更新消息的阅读时间为当前时间
/// </summary>
/// <param name="msgIds">消息的 Id</param>
/// <param name="userId">用户 Id</param>
void UpdateReadTime(IEnumerable<int> msgIds,int userId); /// <summary>
/// 获取跟指定用户相关的所有消息
/// </summary>
/// <param name="id">用户 id</param>
/// <returns></returns>
Task<IEnumerable<InsiteMsg>> GetByUserIdAsync(int id); /// <summary>
/// 删除指定的用户的消息记录
/// </summary>
/// <param name="userId">用户 Id</param>
/// <param name="msgRIds">消息 Id</param>
void DeleteMsgRecoreds(int userId, IEnumerable<int> msgRIds);
}
}

但是在测试阶段,我并不想把仓库实现掉,所以这里就用上了 Moq.Mock

 List<InsiteMsg> dataSet = new List<InsiteMsg>();
var msgRepoMock = new Mock<IInsiteMsgRepository>();
msgRepoMock.Setup(r => r.InsertAsync(It.IsAny<IEnumerable<InsiteMsg>>())).Callback<IEnumerable<InsiteMsg>>((m) =>
{
dataSet.AddRange(m);
});

上面的代码模拟了一个 IInsiteMsgRepository 对象,在我们调用这个对象的 InsertAsync 方法的时候,这个对象就把传入的参数添加到一个集合中去。
模拟出来的对象可以通过 msgMock.Object 访问。

0x04 实现 Repository

使用事务

在创建并发送新的站内消息到用户的时候,需要先插入消息本体,然后再把消息跟目标用户之间在关联表中建立联系,所以我们需要考虑到下面两个问题:

  1. 数据的一致性
  2. 在建立联系前必须获取到消息的 Id

为了解决第一个问题,我们需要使用事务(Transaction),就跟在 ADO.NET 中使用事务一样,可以使用一个简单的套路:

 _conn.Open();
try
{
using (var transaction = _conn.BeginTransaction())
{
// execute some sql
transaction.Commit();
}
}
finally
{
_conn.Close();
}

在事务中,一旦部分操作失败了,我们就可以回滚(Rollback)到初始状态,这样要么所有的操作全部成功执行,要么一条操作都不会执行,数据完整性、一致性得到了保证。

在上面的代码中,using 块内,Commit()之前的语句一旦执行出错(抛出异常),程序就会自动 Rollback。

在数据库中,Id 是一个自增字段,为了获取刚刚插入的实体的 Id 可以使用 last_insert_id() 这个函数(For MySql),这个函数返回当前连接过程中,最后插入的行的自增的主键的值。

最终实现

 using Hive.Domain.Repositories.Abstracts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hive.Domain.Entities;
using System.Data.Common;
using Dapper; namespace Hive.Domain.Repositories.Concretes
{
public class InsiteMsgRepository : IInsiteMsgRepository
{
private readonly DbConnection _conn; public InsiteMsgRepository(DbConnection conn)
{
_conn = conn;
} public void DeleteMsgRecoreds(int userId, IEnumerable<int> msgIds)
{
var param = new
{
UserId = userId,
MsgIds = msgIds
};
string sql = $@"
UPDATE insite_msg_record
SET Status = 0
WHERE UserId = @{nameof(param.UserId)}
AND Status = 1
AND InsiteMsgId IN @{nameof(param.MsgIds)}";
try
{
_conn.Open();
using (var transaction = _conn.BeginTransaction())
{
_conn.Execute(sql, param, transaction);
transaction.Commit();
}
}
finally
{
_conn.Close();
} } public InsiteMsg GetById(int id)
{
throw new NotImplementedException();
} public async Task<IEnumerable<InsiteMsg>> GetByUserIdAsync(int id)
{
string sql = $@"
SELECT
ReadTime,
SentTime,
insite_msg.InsiteMsgId,
Content,
UserId
FROM insite_msg_record, insite_msg
WHERE UserId = @{nameof(id)}
AND insite_msg.InsiteMsgId = insite_msg_record.InsiteMsgId
AND insite_msg.Status = TRUE
AND insite_msg_record.Status = 1";
var inboxMsgs = await _conn.QueryAsync<InsiteMsg>(sql, new { id });
inboxMsgs = inboxMsgs.OrderBy(m => m.ReadTime);
return inboxMsgs;
} public async Task InsertAsync(IEnumerable<InsiteMsg> msgs)
{
var msgContents = msgs.Select(m => new { m.Content, m.SentTime });
string insertSql = $@"
INSERT INTO insite_msg (SentTime, Content)
VALUES (@SentTime, @Content)";
_conn.Open();
// 开启一个事务,保证数据插入的完整性
try
{
using (var transaction = _conn.BeginTransaction())
{
// 首先插入消息实体
var insertMsgTask = _conn.ExecuteAsync(insertSql, msgContents, transaction);
// 等待消息实体插入完成
await insertMsgTask;
var msgRecords = msgs.Select(m => new { m.UserId, m.InsiteMsgId });
// 获取消息的 Id
int firstId = (int)(_conn.QuerySingle("SELECT last_insert_id() AS FirstId").FirstId);
firstId = firstId - msgs.Count() + ;
foreach (var m in msgs)
{
m.InsiteMsgId = firstId;
firstId++;
}
// 插入消息记录
insertSql = $@"
INSERT INTO insite_msg_record (UserId, InsiteMsgId)
VALUES (@UserId, @InsiteMsgId)";
await _conn.ExecuteAsync(insertSql, msgRecords);
transaction.Commit();
}
}
catch (Exception)
{
_conn.Close();
throw;
} } public void Insert(InsiteMsg msg)
{
string sql = $@"
INSERT INTO insite_msg (SentTime, Content)
VALUE (@{nameof(msg.SentTime)}, @{nameof(msg.Content)})";
_conn.Execute(sql, new { msg.SentTime, msg.Content });
string recordSql = $@"
INSERT INTO insite_msg_record (UserId, InsiteMsgId)
VALUE (@{nameof(msg.UserId)}, @{nameof(msg.InsiteMsgId)})";
_conn.Execute(recordSql, new { msg.UserId, msg.InsiteMsgId });
} public void UpdateReadTime(IEnumerable<int> msgsIds, int userId)
{
var param = new
{
UserId = userId,
Msgs = msgsIds
};
// 只更新发送给指定用户的指定消息
string sql = $@"
UPDATE insite_msg_record
SET ReadTime = now()
WHERE UserId = @{nameof(param.UserId)}
AND Status = 1
AND InsiteMsgId IN @{nameof(param.Msgs)}";
try
{
_conn.Open();
using (var transaction = _conn.BeginTransaction())
{
_conn.Execute(sql, param, transaction);
transaction.Commit();
}
}
finally
{
_conn.Close();
}
}
}
}

0x05 测试 Repository

测试 Repository 这部分还是挺难的,没办法编写单元测试,EF 的话还可以用 内存数据库,但是 Dapper 的话,就没办法了。所以我就直接
写了测试用的 API,通过 API 直接调用 Repository 的方法,然后往测试数据库里面读写数据。

转载:http://www.cnblogs.com/JacZhu/p/6112033.html

Asp.Net Core + Dapper + Repository 模式 + TDD 学习笔记的更多相关文章

  1. Asp.net core Identity + identity server + angular 学习笔记 (第三篇)

    register -> login 讲了 我们来讲讲 forgot password -> reset password  和 change password 吧 先来 forgot pa ...

  2. Asp.net core Identity + identity server + angular 学习笔记 (第二篇)

    先纠正一下第一篇的的错误. 在 Login.cshtml 和 Login.cshtml.cs 里, 本来应该是 Register 我却写成 Login . cshtml 修改部分 <form a ...

  3. Asp.net core Identity + identity server + angular 学习笔记 (第一篇)

    用了很长一段时间了, 但是一直没有做过任何笔记,感觉 identity 太多东西要写了, 提不起劲. 但是时间一久很多东西都记不清了. 还是写一轮吧. 加深记忆. 这是 0-1 的笔记, 会写好多篇. ...

  4. ASP.NET Core微服务 on K8S学习笔记(第一章:详解基本对象及服务发现)

    课程链接:http://video.jessetalk.cn/course/explore 良心课程,大家一起来学习哈! 任务1:课程介绍 任务2:Labels and Selectors 所有资源对 ...

  5. Asp.net core Identity + identity server + angular 学习笔记 (第五篇)

    ABAC (Attribute Based Access Control) 基于属性得权限管理. 属性就是 key and value 表达力非常得强. 我们可以用 key = role value ...

  6. Asp.net core Identity + identity server + angular 学习笔记 (第四篇)

    来说说 RBAC (role based access control) 这是目前全世界最通用的权限管理机制, 当然使用率高并不是说它最好. 它也有很多局限的. 我们来讲讲最简单的 role base ...

  7. Asp.net Webform 使用Repository模式实现CRUD操作代码生成工具

    Asp.net Webform 使用Repository模式实现CRUD操作代码生成工具 介绍 该工具是通过一个github上的开源项目修改的原始作者https://github.com/Supere ...

  8. 《ASP.NET4从入门到精通》学习笔记2

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/dongdongdongJL/article/details/37610807   <ASP.N ...

  9. UML和模式应用学习笔记-1(面向对象分析和设计)

    UML和模式应用学习笔记-1(面向对象分析和设计) 而只是对情节的记录:此处的用例场景为:游戏者请求掷骰子.系统展示结果:如果骰子的总点数是7,则游戏者赢得游戏,否则为输 (2)定义领域模型:在领域模 ...

随机推荐

  1. MIP开发常见问题解答

    校验相关 1. MIP 页面的 <a>链接校验报错,MIP 是强制 target="_blank" 吗? 如果想直接跳转MIP页,可以用mip-link 组件:MIP ...

  2. 一步步开发自己的博客 .NET版(10、前端对话框和消息框的实现)

    关于前端对话框.消息框的优秀插件多不胜数.造轮子是为了更好的使用轮子,并不是说自己造的轮子肯定好.所以,这个博客系统基本上都是自己实现的,包括日志记录.响应式布局.评论功能等等一些本可以使用插件的.好 ...

  3. javascript的api设计原则

    前言 本篇博文来自一次公司内部的前端分享,从多个方面讨论了在设计接口时遵循的原则,总共包含了七个大块.系卤煮自己总结的一些经验和教训.本篇博文同时也参考了其他一些文章,相关地址会在后面贴出来.很难做到 ...

  4. iOS---iOS10适配iOS当前所有系统的远程推送

    一.iOS推送通知简介 众所周知苹果的推送通知从iOS3开始出现, 每一年都会更新一些新的用法. 譬如iOS7出现的Silent remote notifications(远程静默推送), iOS8出 ...

  5. Visual Studio 2012远程调试中遇到的问题

    有的时候开发环境没问题的代码在生产环境中会某些开发环境无法重现的问题,或者需要对生产环境代码进行远程调试该怎么办? Vs已经提供给开发者远程调试的工具 下面简单讲讲该怎么用,前期准备:1.本地登录账户 ...

  6. 玩转spring boot——MVC应用

    如何快速搭建一个MCV程序? 参照spring官方例子:https://spring.io/guides/gs/serving-web-content/ 一.spring mvc结合thymeleaf ...

  7. Axure 8.0.0.3312可用注册码

    用户名:aaa 注册码:2GQrt5XHYY7SBK/4b22Gm4Dh8alaR0/0k3gEN5h7FkVPIn8oG3uphlOeytIajxGU 用户名:axureuser 序列号:8wFfI ...

  8. ntp

    一: 在一台可以连接外网的服务器A上配置ntp: 配置  /etc/ntp.conf  文件: server 202.120.2.101            # local clock (LCL) ...

  9. html中返回上一页的各种写法【转】

    超链接返回上一页代码: <a href="#" onClick="javascript :history.back(-1);">返回上一页</ ...

  10. iOS10之Expected App Behaviors

    昨天上架到appStore的时候碰到个问题,构建好后上传到itunesconnect的的包都用不了, 显示错误为:此构建版本无效. 或者英文显示为:ITC.apps.preReleaseBuild.e ...