Implémentation d'API MongoDB dans une Architecture Cloud-Native .NET

2.5.7 MongoDB -- Implémentation des Points d'Accès API

Ce guide technique détaille l'implémentation des opérations CRUD et des fonctionnalités avancées avec MongoDB dans une application .NET. Nous couvrirons la récupération de documents, les requêtes complexes, les mises à jour, ainsi que les systèmes de votes et de commentaires.

  • Récupération d'une question unique
  • Liste des questions avec filtrage et pagination
  • Requête cross-collection pour les réponses
  • Création de questions
  • Modification de questions
  • Ajout de réponses
  • Gestion des commentaires
  • Système de votes
  • Contrôle des réponses

QuestionController

namespace CloudApi.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class QuestionController : ControllerBase
    {
        private readonly IMongoCollection<Question> _questions;

        private readonly IMongoCollection<Vote> _votes;

        private readonly IMongoCollection<Answer> _answers;

        public QuestionController(IMongoClient mongoClient)
        {
            var database = mongoClient.GetDatabase("qa_platform");

            _questions = database.GetCollection<Question>("questions");
            _votes = database.GetCollection<Vote>("votes");
            _answers = database.GetCollection<Answer>("answers");
        }
    }
}

Récupération d'une Question

Approche avec LINQ

[HttpGet]
[Route("{id}")]
public async Task<ActionResult<Question>> GetByIdAsync(string id, CancellationToken cancellationToken)
{
    var question = await _questions.AsQueryable()
        .FirstOrDefaultAsync(q => q.Id == id, cancellationToken: cancellationToken);
    if (question == null)
        return NotFound();

    return Ok(question);
}

Approche avec Filtres MongoDB

var filter = Builders<Question>.Filter.Eq(q => q.Id, id);
await _questions.Find(filter).FirstOrDefaultAsync(cancellationToken);

Construction Conditionnelle de Filtres

var filter = string.IsNullOrEmpty(id)
    ? Builders<Question>.Filter.Empty
    : Builders<Question>.Filter.Eq(q => q.Id, id);

Combinaison de Plusieurs Filtres

var baseFilter = Builders<Question>.Filter.Eq(q => q.Id, id);
var extendedFilter = Builders<Question>.Filter.And(
    baseFilter, 
    Builders<Question>.Filter.Eq(q => q.TenantId, "tenant_001")
);

Liste des Questions avec Pagination

  • Recherche par tableau avec AnyIn
  • Tri avec Sort et StringFieldDefinition
  • Pagination avec Skip et Limit
[HttpGet]
public async Task<ActionResult<List<Question>>> GetQuestionsAsync(
    [FromQuery] List<string> tags,
    CancellationToken cancellationToken, 
    [FromQuery] string sortField = "createdAt", 
    [FromQuery] int skip = 0,
    [FromQuery] int limit = 10)
{
    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>(sortField)
    );

    var result = await _questions
        .Find(filter)
        .Sort(sortDefinition)
        .Skip(skip)
        .Limit(limit)
        .ToListAsync(cancellationToken: cancellationToken);

    return Ok(result);
}

Requête Cross-Collection (Questions avec Réponses)

[HttpGet]
[Route("{id}/answers")]
public async Task<ActionResult> GetQuestionWithAnswersAsync(string id, CancellationToken cancellationToken)
{
    var query = from question in _questions.AsQueryable()
                where question.Id == id
                join answer in _answers.AsQueryable() on question.Id equals answer.QuestionId into answerList
                select new { question, answers = answerList };

    var result = await query.FirstOrDefaultAsync(cancellationToken);

    if (result == null)
        return NotFound();

    return Ok(result);
}

Création d'une Question

[HttpPost]
public async Task<ActionResult<Question>> CreateQuestionAsync(
    [FromBody] Question question,
    CancellationToken cancellationToken)
{
    question.Id = Guid.NewGuid().ToString();
    await _questions.InsertOneAsync(
        question, 
        new InsertOneOptions { BypassDocumentValidation = false },
        cancellationToken
    );
    return StatusCode((int) HttpStatusCode.Created, question);
}

Mise à Jour d'une Question

[HttpPatch]
[Route("{id}")]
public async Task<ActionResult> UpdateQuestionAsync(
    [FromRoute] string id, 
    [FromBody] QuestionUpdateRequest request,
    CancellationToken cancellationToken)
{
    if (string.IsNullOrEmpty(request.Summary))
        throw new ArgumentNullException(nameof(request.Summary));

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

    await _questions.UpdateOneAsync(filter, update, cancellationToken: cancellationToken);

    return Ok();
}

Construction Dynamique des Mises à Jour

var updateOperations = new List<UpdateDefinition<Question>>();

if (!string.IsNullOrWhiteSpace(request.Title))
    updateOperations.Add(Builders<Question>.Update.Set(q => q.Title, request.Title));

if (!string.IsNullOrWhiteSpace(request.Content))
    updateOperations.Add(Builders<Question>.Update.Set(q => q.Content, request.Content));

if (request.Tags != null && request.Tags.Any())
    updateOperations.Add(Builders<Question>.Update.Set(q => q.Tags, request.Tags));

updateOperations.Add(Builders<Question>.Update.Push(q => q.Comments,
    new Comment { Content = request.Summary, CreatedAt = DateTime.Now }));

var combinedUpdate = Builders<Question>.Update.Combine(updateOperations);

Ajout d'une Réponse

[HttpPost]
[Route("{id}/answer")]
public async Task<ActionResult<Answer>> SubmitAnswerAsync(
    [FromRoute] string id, 
    [FromBody] AnswerRequest request,
    CancellationToken cancellationToken)
{
    var answer = new Answer 
    {
        QuestionId = id, 
        Content = request.Content, 
        Id = Guid.NewGuid().ToString()
    };
    
    await _answers.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 _questions.UpdateOneAsync(filter, update, null, cancellationToken);

    return Ok();
}

Ajout d'un Commentaire

[HttpPost]
[Route("{id}/comment")]
public async Task<ActionResult> AddCommentAsync(
    [FromRoute] string id, 
    [FromBody] 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 _questions.UpdateOneAsync(filter, update, null, cancellationToken);

    return Ok();
}

Système de Votes

[HttpPost]
[Route("{id}/up")]
public async Task<ActionResult> VoteUpAsync([FromBody] string id, CancellationToken cancellationToken)
{
    var vote = new Vote
    {
        Id = Guid.NewGuid().ToString(),
        SourceType = VoteSourceType.Question,
        SourceId = id,
        Direction = VoteDirection.Up
    };

    await _votes.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 _questions.UpdateOneAsync(filter, update);

    return Ok();
}

[HttpPost]
[Route("{id}/down")]
public async Task<ActionResult> VoteDownAsync([FromBody] string id, CancellationToken cancellationToken)
{
    var vote = new Vote
    {
        Id = Guid.NewGuid().ToString(),
        SourceType = VoteSourceType.Question,
        SourceId = id,
        Direction = VoteDirection.Down
    };

    await _votes.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 _questions.UpdateOneAsync(filter, update);

    return Ok();
}

AnswerController - Gestion des Réponses

namespace CloudApi.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class AnswerController : ControllerBase
    {
        private readonly IMongoCollection<Vote> _votes;
        private readonly IMongoCollection<Answer> _answers;

        public AnswerController(IMongoClient mongoClient)
        {
            var database = mongoClient.GetDatabase("qa_platform");
            _votes = database.GetCollection<Vote>("votes");
            _answers = database.GetCollection<Answer>("answers");
        }

        [HttpGet]
        public async Task<ActionResult<Answer>> GetAnswersByQuestionAsync(
            [FromQuery] string questionId, 
            CancellationToken cancellationToken)
        {
            var answers = await _answers.AsQueryable()
                .Where(a => a.QuestionId == questionId)
                .ToListAsync(cancellationToken);
            return Ok(answers);
        }

        [HttpPatch]
        [Route("{id}")]
        public async Task<ActionResult> UpdateAnswerAsync(
            string id, 
            string content, 
            string summary,
            CancellationToken cancellationToken)
        {
            var filter = Builders<Answer>.Filter.Eq(q => q.Id, id);

            var update = Builders<Answer>.Update
                .Set(q => q.Content, content)
                .Push(q => q.Comments, new Comment { 
                    Content = summary, 
                    CreatedAt = DateTime.Now 
                });

            await _answers.UpdateOneAsync(filter, update, cancellationToken: cancellationToken);

            return Ok();
        }

        [HttpPost]
        [Route("{id}/comment")]
        public async Task<ActionResult> AddCommentToAnswerAsync(
            [FromRoute] string id, 
            [FromBody] CommentRequest request,
            CancellationToken cancellationToken)
        {
            var filter = Builders<Answer>.Filter.Eq(q => q.Id, id);
            var update = Builders<Answer>.Update.Push(q => q.Comments,
                new Comment { Content = request.Content, CreatedAt = DateTime.Now });

            await _answers.UpdateOneAsync(filter, update, null, cancellationToken);

            return Ok();
        }

        [HttpPost]
        [Route("{id}/up")]
        public async Task<ActionResult> VoteUpAnswerAsync(
            [FromBody] string id, 
            CancellationToken cancellationToken)
        {
            var vote = new Vote
            {
                Id = Guid.NewGuid().ToString(),
                SourceType = VoteSourceType.Answer,
                SourceId = id,
                Direction = VoteDirection.Up
            };

            await _votes.InsertOneAsync(vote, cancellationToken);

            var filter = Builders<Answer>.Filter.Eq(q => q.Id, id);
            var update = Builders<Answer>.Update
                .Inc(q => q.VoteCount, 1)
                .AddToSet(q => q.VoteUps, vote.Id);
            
            await _answers.UpdateOneAsync(filter, update);

            return Ok();
        }

        [HttpPost]
        [Route("{id}/down")]
        public async Task<ActionResult> VoteDownAnswerAsync(
            [FromBody] string id, 
            CancellationToken cancellationToken)
        {
            var vote = new Vote
            {
                Id = Guid.NewGuid().ToString(),
                SourceType = VoteSourceType.Answer,
                SourceId = id,
                Direction = VoteDirection.Down
            };

            await _votes.InsertOneAsync(vote, cancellationToken);

            var filter = Builders<Answer>.Filter.Eq(q => q.Id, id);
            var update = Builders<Answer>.Update
                .Inc(q => q.VoteCount, -1)
                .AddToSet(q => q.VoteDowns, vote.Id);
            
            await _answers.UpdateOneAsync(filter, update);

            return Ok();
        }
    }
}

Étiquettes: MongoDB .NET cloud-native API aspnetcore

Publié le 17 octobre à 07h42