.NET 云原生架构师训练营(模块二 基础巩固 MongoDB API重构)--学习笔记
2.5.8 MongoDB -- API重构
- Lighter.Domain
- Lighter.Application.Contract
- Lighter.Application
- LighterApi
- Lighter.Application.Tests
Lighter.Domain
将数据实体转移到 Lighter.Domain 层

Lighter.Application.Contract
将业务从controller 抽取到 Lighter.Application 层,并为业务建立抽象接口 Lighter.Application.Contract层

IQuestionService
namespace Lighter.Application.Contracts
{
public interface IQuestionService
{
Task<Question> GetAsync(string id, CancellationToken cancellationToken);
Task<QuestionAnswerReponse> GetWithAnswerAsync(string id, CancellationToken cancellationToken);
Task<List<Question>> GetListAsync(List<string> tags, CancellationToken cancellationToken, string sort = "createdAt", int skip = 0, int limit = 10);
Task<Question> CreateAsync(Question question, CancellationToken cancellationToken);
Task UpdateAsync(string id, QuestionUpdateRequest request, CancellationToken cancellationToken);
Task<Answer> AnswerAsync(string id, AnswerRequest request, CancellationToken cancellationToken);
Task CommentAsync(string id, CommentRequest request, CancellationToken cancellationToken);
Task UpAsync(string id, CancellationToken cancellationToken);
Task DownAsync(string id, CancellationToken cancellationToken);
}
}
Lighter.Application
实现业务接口

QuestionService
namespace Lighter.Application
{
public class QuestionService : IQuestionService
{
private readonly IMongoCollection<Question> _questionCollection;
private readonly IMongoCollection<Vote> _voteCollection;
private readonly IMongoCollection<Answer> _answerCollection;
public QuestionService(IMongoClient mongoClient)
{
var database = mongoClient.GetDatabase("lighter");
_questionCollection = database.GetCollection<Question>("questions");
_voteCollection = database.GetCollection<Vote>("votes");
_answerCollection = database.GetCollection<Answer>("answers");
}
public async Task<Question> GetAsync(string id, CancellationToken cancellationToken)
{
// linq 查询
var question = await _questionCollection.AsQueryable()
.FirstOrDefaultAsync(q => q.Id == id, cancellationToken: cancellationToken);
//// mongo 查询表达式
////var filter = Builders<Question>.Filter.Eq(q => q.Id, id);
//// 构造空查询条件的表达式
//var filter = string.IsNullOrEmpty(id)
// ? Builders<Question>.Filter.Empty
// : Builders<Question>.Filter.Eq(q => q.Id, id);
//// 多段拼接 filter
//var filter2 = Builders<Question>.Filter.And(filter, Builders<Question>.Filter.Eq(q => q.TenantId, "001"));
//await _questionCollection.Find(filter).FirstOrDefaultAsync(cancellationToken);
return question;
}
public async Task<List<Question>> GetListAsync(List<string> tags, CancellationToken cancellationToken, string sort = "createdAt", int skip = 0, int limit = 10)
{
//// linq 查询
//await _questionCollection.AsQueryable().Where(q => q.ViewCount > 10)
// .ToListAsync(cancellationToken: cancellationToken);
var filter = Builders<Question>.Filter.Empty;
if (tags != null && tags.Any())
{
filter = Builders<Question>.Filter.AnyIn(q => q.Tags, tags);
}
var sortDefinition = Builders<Question>.Sort.Descending(new StringFieldDefinition<Question>(sort));
var result = await _questionCollection
.Find(filter)
.Sort(sortDefinition)
.Skip(skip)
.Limit(limit)
.ToListAsync(cancellationToken: cancellationToken);
return result;
}
public async Task<QuestionAnswerReponse> GetWithAnswerAsync(string id, CancellationToken cancellationToken)
{
// linq 查询
var query = from question in _questionCollection.AsQueryable()
where question.Id == id
join a in _answerCollection.AsQueryable() on question.Id equals a.QuestionId into answers
select new { question, answers };
var result = await query.FirstOrDefaultAsync(cancellationToken);
//// mongo 查询表达式
//var result = await _questionCollection.Aggregate()
// .Match(q => q.Id == id)
// .Lookup<Answer, QuestionAnswerReponse>(
// foreignCollectionName: "answers",
// localField: "answers",
// foreignField: "questionId",
// @as: "AnswerList")
// .FirstOrDefaultAsync(cancellationToken: cancellationToken);
return new QuestionAnswerReponse {AnswerList = result.answers};
}
public async Task<Answer> AnswerAsync(string id, AnswerRequest request, CancellationToken cancellationToken)
{
var answer = new Answer { QuestionId = id, Content = request.Content, Id = Guid.NewGuid().ToString() };
_answerCollection.InsertOneAsync(answer, cancellationToken);
var filter = Builders<Question>.Filter.Eq(q => q.Id, id);
var update = Builders<Question>.Update.Push(q => q.Answers, answer.Id);
await _questionCollection.UpdateOneAsync(filter, update, null, cancellationToken);
return answer;
}
public async Task CommentAsync(string id, CommentRequest request, CancellationToken cancellationToken)
{
var filter = Builders<Question>.Filter.Eq(q => q.Id, id);
var update = Builders<Question>.Update.Push(q => q.Comments,
new Comment { Content = request.Content, CreatedAt = DateTime.Now });
await _questionCollection.UpdateOneAsync(filter, update, null, cancellationToken);
}
public async Task<Question> CreateAsync(Question question, CancellationToken cancellationToken)
{
question.Id = Guid.NewGuid().ToString();
await _questionCollection.InsertOneAsync(question, new InsertOneOptions { BypassDocumentValidation = false },
cancellationToken);
return question;
}
public async Task DownAsync(string id, CancellationToken cancellationToken)
{
var vote = new Vote
{
Id = Guid.NewGuid().ToString(),
SourceType = ConstVoteSourceType.Question,
SourceId = id,
Direction = EnumVoteDirection.Down
};
await _voteCollection.InsertOneAsync(vote, cancellationToken);
var filter = Builders<Question>.Filter.Eq(q => q.Id, id);
var update = Builders<Question>.Update.Inc(q => q.VoteCount, -1).AddToSet(q => q.VoteDowns, vote.Id);
await _questionCollection.UpdateOneAsync(filter, update);
}
public async Task UpAsync(string id, CancellationToken cancellationToken)
{
var vote = new Vote
{
Id = Guid.NewGuid().ToString(),
SourceType = ConstVoteSourceType.Question,
SourceId = id,
Direction = EnumVoteDirection.Up
};
await _voteCollection.InsertOneAsync(vote, cancellationToken);
var filter = Builders<Question>.Filter.Eq(q => q.Id, id);
var update = Builders<Question>.Update.Inc(q => q.VoteCount, 1).AddToSet(q => q.VoteUps, vote.Id);
await _questionCollection.UpdateOneAsync(filter, update);
}
public async Task UpdateAsync(string id, QuestionUpdateRequest request, CancellationToken cancellationToken)
{
var filter = Builders<Question>.Filter.Eq(q => q.Id, id);
//var update = Builders<Question>.Update
// .Set(q => q.Title, request.Title)
// .Set(q => q.Content, request.Content)
// .Set(q => q.Tags, request.Tags)
// .Push(q => q.Comments, new Comment {Content = request.Summary, CreatedAt = DateTime.Now});
var updateFieldList = new List<UpdateDefinition<Question>>();
if (!string.IsNullOrWhiteSpace(request.Title))
updateFieldList.Add(Builders<Question>.Update.Set(q => q.Title, request.Title));
if (!string.IsNullOrWhiteSpace(request.Content))
updateFieldList.Add(Builders<Question>.Update.Set(q => q.Content, request.Content));
if (request.Tags != null && request.Tags.Any())
updateFieldList.Add(Builders<Question>.Update.Set(q => q.Tags, request.Tags));
updateFieldList.Add(Builders<Question>.Update.Push(q => q.Comments,
new Comment { Content = request.Summary, CreatedAt = DateTime.Now }));
var update = Builders<Question>.Update.Combine(updateFieldList);
await _questionCollection.UpdateOneAsync(filter, update, cancellationToken: cancellationToken);
}
}
}
LighterApi
注册服务
Startup
services.AddScoped<IQuestionService, QuestionService>()
.AddScoped<IAnswerService, AnswerService>();
调用服务
QuestionController
namespace LighterApi.Controller
{
[ApiController]
[Route("api/[controller]")]
public class QuestionController : ControllerBase
{
private readonly IQuestionService _questionService;
public QuestionController(IQuestionService questionService)
{
_questionService = questionService;
}
[HttpGet]
[Route("{id}")]
public async Task<ActionResult<Question>> GetAsync(string id, CancellationToken cancellationToken)
{
var question = await _questionService.GetAsync(id, cancellationToken);
if (question == null)
return NotFound();
return Ok(question);
}
[HttpGet]
[Route("{id}/answers")]
public async Task<ActionResult> GetWithAnswerAsync(string id, CancellationToken cancellationToken)
{
var result = await _questionService.GetWithAnswerAsync(id, cancellationToken);
if (result == null)
return NotFound();
return Ok(result);
}
[HttpGet]
public async Task<ActionResult<List<Question>>> GetListAsync([FromQuery] List<string> tags,
CancellationToken cancellationToken, [FromQuery] string sort = "createdAt", [FromQuery] int skip = 0,
[FromQuery] int limit = 10)
{
var result = await _questionService.GetListAsync(tags, cancellationToken, sort, skip, limit);
return Ok(result);
}
[HttpPost]
public async Task<ActionResult<Question>> CreateAsync([FromBody] Question question, CancellationToken cancellationToken)
{
question = await _questionService.CreateAsync(question, cancellationToken);
return StatusCode((int) HttpStatusCode.Created, question);
}
[HttpPatch]
[Route("{id}")]
public async Task<ActionResult> UpdateAsync([FromRoute] string id, [FromBody] QuestionUpdateRequest request, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(request.Summary))
throw new ArgumentNullException(nameof(request.Summary));
await _questionService.UpdateAsync(id, request, cancellationToken);
return Ok();
}
[HttpPost]
[Route("{id}/answer")]
public async Task<ActionResult<Answer>> AnswerAsync([FromRoute] string id, [FromBody] AnswerRequest request, CancellationToken cancellationToken)
{
var answer = await _questionService.AnswerAsync(id, request, cancellationToken);
return Ok(answer);
}
[HttpPost]
[Route("{id}/comment")]
public async Task<ActionResult> CommentAsync([FromRoute] string id, [FromBody] CommentRequest request, CancellationToken cancellationToken)
{
await _questionService.CommentAsync(id, request, cancellationToken);
return Ok();
}
[HttpPost]
[Route("{id}/up")]
public async Task<ActionResult> UpAsync([FromBody] string id, CancellationToken cancellationToken)
{
await _questionService.UpAsync(id, cancellationToken);
return Ok();
}
[HttpPost]
[Route("{id}/down")]
public async Task<ActionResult> DownAsync([FromBody] string id, CancellationToken cancellationToken)
{
await _questionService.DownAsync(id, cancellationToken);
return Ok();
}
}
}
Lighter.Application.Tests
建立单元测试项目,测试Lihgter.Application(需要使用到xunit、Mongo2go)

Mongo2go:内存级别引擎
访问 Mongo 内存数据库
SharedFixture
namespace Lighter.Application.Tests
{
public class SharedFixture:IAsyncLifetime
{
private MongoDbRunner _runner;
public MongoClient Client { get; private set; }
public IMongoDatabase Database { get; private set; }
public async Task InitializeAsync()
{
_runner = MongoDbRunner.Start();
Client = new MongoClient(_runner.ConnectionString);
Database = Client.GetDatabase("db");
//var hostBuilder = Program.CreateWebHostBuilder(new string[0]);
//var host = hostBuilder.Build();
//ServiceProvider = host.Services;
}
public Task DisposeAsync()
{
_runner?.Dispose();
_runner = null;
return Task.CompletedTask;
}
}
}
QuestionServiceTests
namespace Lighter.Application.Tests
{
[Collection(nameof(SharedFixture))]
public class QuestionServiceTests
{
private readonly SharedFixture _fixture;
private readonly QuestionService _questionService;
public QuestionServiceTests(SharedFixture fixture)
{
_fixture = fixture;
_questionService = new QuestionService(_fixture.Client);
}
private async Task<Question> CreateOrGetOneQuestionWithNoAnswerAsync()
{
var collection = _fixture.Database.GetCollection<Question>("question");
var filter = Builders<Question>.Filter.Size(q => q.Answers, 0);
var question = await collection.Find(filter).FirstOrDefaultAsync();
if (question != null)
return question;
question = new Question { Title = "问题一" };
return await _questionService.CreateAsync(question, CancellationToken.None);
}
private async Task<QuestionAnswerReponse> CreateOrGetOneQuestionWithAnswerAsync()
{
var collection = _fixture.Database.GetCollection<Question>("question");
var filter = Builders<Question>.Filter.SizeGt(q => q.Answers, 0);
var question = await collection.Find(filter).FirstOrDefaultAsync();
if (question != null)
return await _questionService.GetWithAnswerAsync(question.Id, CancellationToken.None);
// 不存在则创建一个没有回答的问题,再添加一个答案
question = await CreateOrGetOneQuestionWithNoAnswerAsync();
var answer = new AnswerRequest { Content = "问题一的回答一" };
await _questionService.AnswerAsync(question.Id, answer, CancellationToken.None);
return await _questionService.GetWithAnswerAsync(question.Id, CancellationToken.None);
}
[Fact]
public async Task GetAsync_WrongId_ShoudReturnNull()
{
var result = await _questionService.GetAsync("empty", CancellationToken.None);
result.Should().BeNull();
}
[Fact]
public async Task CreateAsync_Right_ShouldBeOk()
{
var question = await CreateOrGetOneQuestionWithNoAnswerAsync();
question.Should().NotBeNull();
var result = await _questionService.GetAsync(question.Id, CancellationToken.None);
question.Title.Should().Be(result.Title);
}
[Fact]
public async Task AnswerAsync_Right_ShouldBeOk()
{
var question = await CreateOrGetOneQuestionWithNoAnswerAsync();
question.Should().NotBeNull();
var answer = new AnswerRequest { Content = "问题一的回答一" };
await _questionService.AnswerAsync(question.Id, answer, CancellationToken.None);
var questionWithAnswer = await _questionService.GetWithAnswerAsync(question.Id, CancellationToken.None);
questionWithAnswer.Should().NotBeNull();
questionWithAnswer.AnswerList.Should().NotBeEmpty();
questionWithAnswer.AnswerList.First().Content.Should().Be(answer.Content);
}
[Fact]
public async Task UpAsync_Right_ShouldBeOk()
{
var before = await CreateOrGetOneQuestionWithNoAnswerAsync();
await _questionService.UpAsync(before.Id, CancellationToken.None);
var after = await _questionService.GetAsync(before.Id, CancellationToken.None);
after.Should().NotBeNull();
after.VoteCount.Should().Be(before.VoteCount+1);
after.VoteUps.Count.Should().Be(1);
}
[Fact]
public async Task DownAsync_Right_ShouldBeOk()
{
var before = await CreateOrGetOneQuestionWithNoAnswerAsync();
await _questionService.DownAsync(before.Id, CancellationToken.None);
var after = await _questionService.GetAsync(before.Id, CancellationToken.None);
after.Should().NotBeNull();
after.VoteCount.Should().Be(before.VoteCount-1);
after.VoteDowns.Count.Should().Be(1);
}
public async Task UpdateAsync_WithNoSummary_ShoudThrowException()
{
var before = await CreateOrGetOneQuestionWithNoAnswerAsync();
var updateRequest = new QuestionUpdateRequest { Title = before.Title + "-updated" };
await _questionService.UpdateAsync(before.Id, updateRequest, CancellationToken.None);
var after = await _questionService.GetAsync(before.Id, CancellationToken.None);
after.Should().NotBeNull();
after.Title.Should().Be(updateRequest.Title);
}
[Fact]
public async Task UpdateAsync_Right_ShoudBeOk()
{
var before = await CreateOrGetOneQuestionWithNoAnswerAsync();
var updateRequest = new QuestionUpdateRequest { Title = before.Title + "-updated", Summary ="summary" };
await _questionService.UpdateAsync(before.Id, updateRequest , CancellationToken.None);
var after = await _questionService.GetAsync(before.Id, CancellationToken.None);
after.Should().NotBeNull();
after.Title.Should().Be(updateRequest.Title);
}
[Fact]
public async Task UpdateAsync_Right_CommentsShouldAppend()
{
var before = await CreateOrGetOneQuestionWithNoAnswerAsync();
var updateRequest = new QuestionUpdateRequest { Title = before.Title + "-updated", Summary = "summary" };
await _questionService.UpdateAsync(before.Id, updateRequest, CancellationToken.None);
var after = await _questionService.GetAsync(before.Id, CancellationToken.None);
after.Comments.Should().NotBeEmpty();
after.Comments.Count.Should().Be(before.Comments.Count+1);
}
}
}
运行单元测试

GitHub源码链接:
https://github.com/MINGSON666/Personal-Learning-Library/tree/main/ArchitectTrainingCamp

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。
.NET 云原生架构师训练营(模块二 基础巩固 MongoDB API重构)--学习笔记的更多相关文章
- .NET 云原生架构师训练营(权限系统 RGCA 开发任务)--学习笔记
目录 目标 模块拆分 OPM 开发任务 目标 基于上一讲的模块划分做一个任务拆解,根据任务拆解实现功能 模块拆分 模块划分已经完成了边界的划分,边界内外职责清晰 OPM 根据模块拆分画出 OPM(Ob ...
- .NET 云原生架构师训练营(权限系统 代码实现 ActionAccess)--学习笔记
目录 开发任务 代码实现 开发任务 DotNetNB.Security.Core:定义 core,models,Istore:实现 default memory store DotNetNB.Secu ...
- .NET 云原生架构师训练营(权限系统 代码实现 WebApplication)--学习笔记
目录 开发任务 代码实现 开发任务 DotNetNB.Security.Core:定义 core,models,Istore:实现 default memory store DotNetNB.WebA ...
- .NET 云原生架构师训练营(权限系统 系统演示 ActionAccess)--学习笔记
目录 模块拆分 环境配置 默认用户 ActionAccess 模块拆分 环境配置 mysql migration mysql docker pull mysql docker run -p 3306: ...
- .NET 云原生架构师训练营(权限系统 系统演示 EntityAccess)--学习笔记
目录 模块拆分 EntityAccess 模块拆分 EntityAccess 实体权限 属性权限 实体权限 创建 student https://localhost:7018/Student/dotn ...
- .NET 云原生架构师训练营(权限系统 代码实现 EntityAccess)--学习笔记
目录 开发任务 代码实现 开发任务 DotNetNB.Security.Core:定义 core,models,Istore:实现 default memory store DotNetNB.Secu ...
- .NET 云原生架构师训练营(权限系统 代码实现 Identity)--学习笔记
目录 开发任务 代码实现 开发任务 DotNetNB.Security.Core:定义 core,models,Istore:实现 default memory store DotNetNB.Secu ...
- .NET 云原生架构师训练营(模块二 基础巩固 MongoDB 问答系统)--学习笔记
2.5.6 MongoDB -- 问答系统 MongoDB 数据库设计 API 实现概述 MongoDB 数据库设计 设计优化 内嵌(mongo)还是引用(mysql) 数据一致性 范式:将数据分散到 ...
- .NET 云原生架构师训练营(模块二 基础巩固 MongoDB 聚合)--学习笔记
2.5.5 MongoDB -- 聚合 排序 索引类型 创建索引 排序 // 升序 db.getCollection('author').find({}).sort({"age": ...
随机推荐
- 「Elasticsearch」SpringBoot快速集成ES
Elastic Search 的底层是开源库 Lucene.但是Lucene的使用门槛比较高,必须自己写代码去调用它的接口.而Elastic Search的出现正是为了解决了这个问题,它是 Lucen ...
- java课堂作业--异常处理
一. 运行结果: 二. 结果: ArrayIndexOutOfBoundsException/内层try-catch 发生ArithmeticException 三. 结果: ArrayIndexOu ...
- web安全漏洞
1.什么是Web漏洞 WEB漏洞通常是指网站程序上的漏洞,可能是由于代码编写者在编写代码时考虑不周全等原因而造成的漏洞.如果网站存在WEB漏洞并被黑客攻击者利用,攻击者可以轻易控制整个网站,并可进一步 ...
- Scrum冲刺_Day07
一.团队展示: 1.项目:light_note备忘录 2.队名:删库跑路队 3.团队成员 队员(不分先后) 项目角色 黄敦鸿 后端工程师.测试 黄华 后端工程师.测试 黄骏鹏 后端工程师.测试 黄源钦 ...
- 题解-CF1065E Side Transmutations
CF1065E Side Transmutations \(n\) 和 \(m\) 和 \(k\) 和序列 \(b_i(1\le i\le m,1\le b_i\le b_{i+1}\le \frac ...
- 查看java程序中对象占用空间大小
需要引入的jar包: <dependency> <groupId>com.carrotsearch</groupId> <artifactId>java ...
- win32 C++制作美观按钮,告别win32 API编程中默认的灰色按钮
使用win32 API制作美观按钮,当鼠标移入/移出按钮时改变按钮背景颜色,类似HTML网页中的效果,告别win32 API编程中默认的灰色按钮,效果图见下面动图和视频. 下载地址: 按钮效果(win ...
- vue2中$emit $on $off实现组件之间的联动,绝对有你想了解的
在vue2开发中,你肯定会遇到组件之间联动的问题,现在我们就来说说哪个神奇的指令可以满足我们的需求. 一.先上实例: 需求:点击A组件或者B组件可以使C组件的名称相应发生改变,同样,点击A组件也会使对 ...
- 网络 IO 模型简单介绍
一.同步阻塞 IO(BIO) 当用户线程调用了 read 系统调用,内核(kernel)就开始了 IO 的第一个阶段:准备数据.很多时候,数据在一开始还没有到达(比如,还没有收到一个完整的Socket ...
- 第一次软件工程与UML的编程作业
博客班级 https://edu.cnblogs.com/campus/fzzcxy/2018SE1/ 作业要求 https://edu.cnblogs.com/campus/fzzcxy/2018S ...