將數據實體轉移到 Lighter.Domain 層git
將業務從controller 抽取到 Lighter.Application 層,併爲業務創建抽象接口 Lighter.Application.Contract層github
IQuestionServicemongodb
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); } }
實現業務接口數據庫
QuestionServiceapi
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); } } }
註冊服務app
Startupdom
services.AddScoped<IQuestionService, QuestionService>() .AddScoped<IAnswerService, AnswerService>();
調用服務async
QuestionControlleride
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(); } } }
創建單元測試項目,測試Lihgter.Application(須要使用到xunit、Mongo2go)單元測試
Mongo2go:內存級別引擎
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; } } }
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); } } }
https://github.com/MINGSON666/Personal-Learning-Library/tree/main/ArchitectTrainingCamp
本做品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、從新發布,但務必保留文章署名 鄭子銘 (包含連接: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的做品務必以相同的許可發佈。
若有任何疑問,請與我聯繫 (MingsonZheng@outlook.com) 。