8

.NET 云原生架构师训练营(模块二 基础巩固 MongoDB API重构)--学习笔记

 3 years ago
source link: http://www.cnblogs.com/MingsonZheng/p/14249227.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

2.5.8 MongoDB -- API重构

  • Lighter.Domain
  • Lighter.Application.Contract
  • Lighter.Application
  • LighterApi
  • Lighter.Application.Tests

Lighter.Domain

将数据实体转移到 Lighter.Domain 层

Q7Vbi2q.jpg!mobile

Lighter.Application.Contract

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

JzQVB3a.jpg!mobile

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

实现业务接口

JrqINvy.jpg!mobile

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)

yYrYn2b.jpg!mobile

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);
        }
    }
}

运行单元测试

IvURniJ.jpg!mobile

GitHub源码链接:

https://github.com/MINGSON666/Personal-Learning-Library/tree/main/ArchitectTrainingCamp

VrIr6j.png!mobile

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 ([email protected]) 。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK