zoukankan      html  css  js  c++  java
  • .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) 。

  • 相关阅读:
    print函数的全面认识
    基本数据类型的变量
    文件操作认识二
    微信小程序制作随笔1
    .NET三层架构的改进以及通用数据库访问组件的实现
    实现基于通用数据访问组件的三层架构之补充篇
    Windows服务程序的编写、调试、部署
    winform下使用缓存
    重温Petshop 谈谈对三层架构的理解兼发布一个通用的数据访问控件(oracle免装客户端)
    实现基于通用数据访问组件的三层架构之实战篇
  • 原文地址:https://www.cnblogs.com/MingsonZheng/p/14249227.html
Copyright © 2011-2022 走看看