145 lines
5.5 KiB
C#
145 lines
5.5 KiB
C#
using MongoDB.Driver;
|
|
using QuestionsApi.Models;
|
|
|
|
namespace QuestionsApi.Services;
|
|
|
|
public class QuestionStore
|
|
{
|
|
private readonly IMongoCollection<Question> _col;
|
|
|
|
public QuestionStore(IConfiguration cfg)
|
|
{
|
|
var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
|
var dbName = cfg["MongoDB:DatabaseName"] ?? "GameDb";
|
|
var client = new MongoClient(cs);
|
|
var db = client.GetDatabase(dbName);
|
|
_col = db.GetCollection<Question>("Questions");
|
|
|
|
var externalIdIndex = Builders<Question>.IndexKeys.Ascending(q => q.ExternalId);
|
|
_col.Indexes.CreateOne(new CreateIndexModel<Question>(externalIdIndex, new CreateIndexOptions { Unique = true }));
|
|
var systemIndex = Builders<Question>.IndexKeys.Ascending(q => q.System).Ascending(q => q.IsPublished);
|
|
_col.Indexes.CreateOne(new CreateIndexModel<Question>(systemIndex));
|
|
var tagsIndex = Builders<Question>.IndexKeys.Ascending(q => q.Tags);
|
|
_col.Indexes.CreateOne(new CreateIndexModel<Question>(tagsIndex));
|
|
}
|
|
|
|
public async Task<List<Question>> SearchAsync(QuestionSearchRequest req, bool includeUnpublished)
|
|
{
|
|
var builder = Builders<Question>.Filter;
|
|
var filters = new List<FilterDefinition<Question>>();
|
|
|
|
if (!includeUnpublished)
|
|
filters.Add(builder.Eq(q => q.IsPublished, true));
|
|
if (!string.IsNullOrWhiteSpace(req.System) && req.System != "All Systems")
|
|
filters.Add(builder.Eq(q => q.System, req.System.Trim()));
|
|
if (!string.IsNullOrWhiteSpace(req.Discipline))
|
|
filters.Add(builder.Eq(q => q.Discipline, req.Discipline.Trim()));
|
|
if (req.Tags.Count > 0)
|
|
filters.Add(builder.All(q => q.Tags, req.Tags.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t.Trim())));
|
|
if (req.DifficultyMin is not null)
|
|
filters.Add(builder.Gte(q => q.Difficulty, req.DifficultyMin.Value));
|
|
if (req.DifficultyMax is not null)
|
|
filters.Add(builder.Lte(q => q.Difficulty, req.DifficultyMax.Value));
|
|
|
|
var filter = filters.Count == 0 ? builder.Empty : builder.And(filters);
|
|
var limit = Math.Clamp(req.Limit, 1, 80);
|
|
var questions = await _col.Find(filter).Limit(limit).ToListAsync();
|
|
return questions.OrderBy(_ => Guid.NewGuid()).ToList();
|
|
}
|
|
|
|
public async Task<Question?> GetByIdAsync(string id) =>
|
|
await _col.Find(q => q.Id == id).FirstOrDefaultAsync();
|
|
|
|
public async Task<Question?> CreateAsync(UpsertQuestionRequest req)
|
|
{
|
|
var question = FromRequest(req);
|
|
question.CreatedUtc = DateTime.UtcNow;
|
|
question.UpdatedUtc = DateTime.UtcNow;
|
|
|
|
try
|
|
{
|
|
await _col.InsertOneAsync(question);
|
|
return question;
|
|
}
|
|
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<Question?> UpdateAsync(string id, UpsertQuestionRequest req)
|
|
{
|
|
var question = FromRequest(req);
|
|
question.Id = id;
|
|
question.UpdatedUtc = DateTime.UtcNow;
|
|
|
|
var existing = await GetByIdAsync(id);
|
|
if (existing is null)
|
|
return null;
|
|
|
|
question.CreatedUtc = existing.CreatedUtc;
|
|
var result = await _col.ReplaceOneAsync(q => q.Id == id, question);
|
|
return result.MatchedCount == 0 ? null : question;
|
|
}
|
|
|
|
public async Task<int> UpsertManyByExternalIdAsync(IEnumerable<UpsertQuestionRequest> requests)
|
|
{
|
|
var count = 0;
|
|
foreach (var req in requests)
|
|
{
|
|
var question = FromRequest(req);
|
|
question.UpdatedUtc = DateTime.UtcNow;
|
|
|
|
var filter = Builders<Question>.Filter.Eq(q => q.ExternalId, question.ExternalId);
|
|
var existing = await _col.Find(filter).FirstOrDefaultAsync();
|
|
if (existing is not null)
|
|
{
|
|
question.Id = existing.Id;
|
|
question.CreatedUtc = existing.CreatedUtc;
|
|
}
|
|
else
|
|
{
|
|
question.CreatedUtc = DateTime.UtcNow;
|
|
}
|
|
|
|
await _col.ReplaceOneAsync(filter, question, new ReplaceOptions { IsUpsert = true });
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync(string id)
|
|
{
|
|
var result = await _col.DeleteOneAsync(q => q.Id == id);
|
|
return result.DeletedCount > 0;
|
|
}
|
|
|
|
public async Task<List<string>> GetSystemsAsync(bool includeUnpublished)
|
|
{
|
|
var filter = includeUnpublished
|
|
? Builders<Question>.Filter.Empty
|
|
: Builders<Question>.Filter.Eq(q => q.IsPublished, true);
|
|
var systems = await _col.Distinct(q => q.System, filter).ToListAsync();
|
|
return systems.Where(s => !string.IsNullOrWhiteSpace(s)).OrderBy(s => s).ToList();
|
|
}
|
|
|
|
private static Question FromRequest(UpsertQuestionRequest req) => new()
|
|
{
|
|
ExternalId = req.ExternalId.Trim(),
|
|
Stem = req.Stem.Trim(),
|
|
LeadIn = req.LeadIn.Trim(),
|
|
Choices = req.Choices,
|
|
CorrectChoiceId = req.CorrectChoiceId.Trim(),
|
|
Explanation = req.Explanation.Trim(),
|
|
ChoiceExplanations = req.ChoiceExplanations,
|
|
Discipline = req.Discipline.Trim(),
|
|
System = req.System.Trim(),
|
|
Difficulty = req.Difficulty,
|
|
Labs = req.Labs,
|
|
Media = req.Media,
|
|
Tags = req.Tags.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToList(),
|
|
IsPublished = req.IsPublished
|
|
};
|
|
}
|