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
SortetStringFieldDefinition - Pagination avec
SkipetLimit
[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();
}
}
}