DeutschLernen/GermanApp/Application/Services/QuizService.cs
Lasse Rune Hansen 242d5ea60b feat(backend): Complete Quiz System implementation
- Add Quiz entity with full domain logic (Create, Update, Activate, Deactivate)
- Add QuizQuestion entity updated to use QuizId instead of LessonId
- Add QuizRepository with all CRUD and query methods
- Add QuizQuestionRepository with QuizId-based and LessonId-based (legacy) methods
- Add QuizDto, CreateQuizDto, UpdateQuizDto, QuizWithQuestionsDto, QuizListItemDto
- Add QuizQuestionDto updated with QuizId, LessonId, QuizTitle, Points fields
- Add CreateQuizCommand, UpdateQuizCommand, DeleteQuizCommand, GetQuizWithQuestionsCommand
- Add QuizService with full quiz management (CRUD, activate, deactivate, counts, queries)
- Add QuizQuestionService updated with QuizId-based methods
- Add QuizzesController with comprehensive endpoints (GET, POST, PUT, DELETE)
- Add QuizzesController endpoints for quiz questions (by-quiz, random, active)
- Update QuizQuestionsController with new QuizId-based endpoints (backward compatible)
- Register QuizRepository and QuizService in DI container
- Update IRepository interfaces with QuizId-based methods
- Add SubmitQuizDto for quiz answer submission

Clean Architecture layers maintained:
- Domain: Quiz, QuizQuestion entities with business logic
- Application: DTOs, Commands, Services
- Infrastructure: Repositories, DbContext
- Presentation: Controllers

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-13 07:41:46 +02:00

273 lines
9.7 KiB
C#

using GermanApp.Application.DTOs;
using GermanApp.Domain.Entities;
using GermanApp.Domain.Exceptions;
using GermanApp.Domain.Interfaces;
namespace GermanApp.Application.Services;
/// <summary>
/// Application service for managing quizzes.
/// This is part of the Application layer.
/// </summary>
public class QuizService
{
private readonly IQuizRepository _quizRepository;
private readonly IQuizQuestionRepository _quizQuestionRepository;
private readonly IQuizOptionRepository _quizOptionRepository;
private readonly ILessonRepository _lessonRepository;
public QuizService(
IQuizRepository quizRepository,
IQuizQuestionRepository quizQuestionRepository,
IQuizOptionRepository quizOptionRepository,
ILessonRepository lessonRepository)
{
_quizRepository = quizRepository;
_quizQuestionRepository = quizQuestionRepository;
_quizOptionRepository = quizOptionRepository;
_lessonRepository = lessonRepository;
}
/// <summary>
/// Gets all quizzes.
/// </summary>
public virtual async Task<IReadOnlyList<QuizDto>> GetAllQuizzesAsync(CancellationToken cancellationToken = default)
{
var quizzes = await _quizRepository.GetAllOrderedAsync(cancellationToken);
return quizzes.Select(q => q.ToDto()).ToList();
}
/// <summary>
/// Gets all quizzes as list items (summary).
/// </summary>
public virtual async Task<IReadOnlyList<QuizListItemDto>> GetAllQuizListItemsAsync(CancellationToken cancellationToken = default)
{
var quizzes = await _quizRepository.GetAllOrderedAsync(cancellationToken);
return quizzes.Select(q => q.ToListItemDto()).ToList();
}
/// <summary>
/// Gets a quiz by its ID.
/// </summary>
public virtual async Task<QuizDto?> GetQuizByIdAsync(int id, CancellationToken cancellationToken = default)
{
var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
return quiz?.ToDto();
}
/// <summary>
/// Gets a quiz with its questions by ID.
/// </summary>
public virtual async Task<QuizWithQuestionsDto?> GetQuizWithQuestionsAsync(int id, CancellationToken cancellationToken = default)
{
var quiz = await _quizRepository.GetWithQuestionsAsync(id, cancellationToken);
return quiz?.ToQuizWithQuestionsDto();
}
/// <summary>
/// Gets quizzes by lesson ID.
/// </summary>
public virtual async Task<IReadOnlyList<QuizDto>> GetQuizzesByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
var quizzes = await _quizRepository.GetByLessonAsync(lessonId, cancellationToken);
return quizzes.Select(q => q.ToDto()).ToList();
}
/// <summary>
/// Gets active quizzes by lesson ID.
/// </summary>
public virtual async Task<IReadOnlyList<QuizDto>> GetActiveQuizzesByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
var quizzes = await _quizRepository.GetActiveByLessonAsync(lessonId, cancellationToken);
return quizzes.Select(q => q.ToDto()).ToList();
}
/// <summary>
/// Gets the first quiz for a lesson.
/// </summary>
public virtual async Task<QuizDto?> GetFirstQuizByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
var quiz = await _quizRepository.GetFirstByLessonAsync(lessonId, cancellationToken);
return quiz?.ToDto();
}
/// <summary>
/// Creates a new quiz.
/// </summary>
public virtual async Task<QuizDto> CreateQuizAsync(CreateQuizDto dto, CancellationToken cancellationToken = default)
{
// Validate that the lesson exists
var lesson = await _lessonRepository.GetByIdAsync(dto.LessonId, cancellationToken);
if (lesson == null)
throw new NotFoundException("Lesson does not exist");
// Validate business rules
if (dto.PassingScore < 0 || dto.PassingScore > 100)
{
throw new ValidationException("Passing score must be between 0 and 100");
}
if (dto.TimeLimitMinutes < 0)
{
throw new ValidationException("Time limit cannot be negative");
}
// Convert DTO to domain entity
var quiz = dto.ToEntity();
// Add to repository
var createdQuiz = await _quizRepository.AddAsync(quiz, cancellationToken);
// Return DTO representation
return createdQuiz.ToDto();
}
/// <summary>
/// Updates an existing quiz.
/// </summary>
public virtual async Task<QuizDto?> UpdateQuizAsync(int id, UpdateQuizDto dto, CancellationToken cancellationToken = default)
{
var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
if (quiz == null)
return null;
// Validate that the lesson exists
var lesson = await _lessonRepository.GetByIdAsync(dto.LessonId, cancellationToken);
if (lesson == null)
throw new NotFoundException("Lesson does not exist");
// Validate business rules
if (dto.PassingScore < 0 || dto.PassingScore > 100)
{
throw new ValidationException("Passing score must be between 0 and 100");
}
if (dto.TimeLimitMinutes < 0)
{
throw new ValidationException("Time limit cannot be negative");
}
// Update the quiz
quiz.UpdateFromDto(dto);
await _quizRepository.UpdateAsync(quiz, cancellationToken);
// Reload and return
var updatedQuiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
return updatedQuiz?.ToDto();
}
/// <summary>
/// Deletes a quiz by its ID.
/// </summary>
public virtual async Task<bool> DeleteQuizAsync(int id, CancellationToken cancellationToken = default)
{
var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
if (quiz == null)
return false;
// First, delete all options for all questions in this quiz
var questions = await _quizQuestionRepository.GetByQuizAsync(id, cancellationToken);
foreach (var question in questions)
{
await _quizOptionRepository.DeleteByQuestionAsync(question.Id, cancellationToken);
}
// Then delete all questions for this quiz
foreach (var question in questions)
{
await _quizQuestionRepository.DeleteAsync(question, cancellationToken);
}
// Finally, delete the quiz itself
await _quizRepository.DeleteAsync(quiz, cancellationToken);
return true;
}
/// <summary>
/// Checks if a quiz exists for a lesson.
/// </summary>
public virtual async Task<bool> ExistsByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
return await _quizRepository.ExistsByLessonAsync(lessonId, cancellationToken);
}
/// <summary>
/// Checks if a quiz exists by ID.
/// </summary>
public virtual async Task<bool> ExistsAsync(int id, CancellationToken cancellationToken = default)
{
return await _quizRepository.ExistsAsync(id, cancellationToken);
}
/// <summary>
/// Gets the total number of quizzes.
/// </summary>
public virtual async Task<int> GetTotalQuizCountAsync(CancellationToken cancellationToken = default)
{
var quizzes = await _quizRepository.GetAllAsync(cancellationToken);
return quizzes.Count;
}
/// <summary>
/// Gets the total number of quizzes for a lesson.
/// </summary>
public virtual async Task<int> GetQuizCountByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
var quizzes = await _quizRepository.GetByLessonAsync(lessonId, cancellationToken);
return quizzes.Count;
}
/// <summary>
/// Gets quizzes by passing score range.
/// </summary>
public virtual async Task<IReadOnlyList<QuizDto>> GetQuizzesByPassingScoreRangeAsync(int minScore, int maxScore, CancellationToken cancellationToken = default)
{
var quizzes = await _quizRepository.GetAllAsync(cancellationToken);
return quizzes.Where(q => q.PassingScore >= minScore && q.PassingScore <= maxScore)
.Select(q => q.ToDto())
.ToList();
}
/// <summary>
/// Gets active quizzes.
/// </summary>
public virtual async Task<IReadOnlyList<QuizDto>> GetActiveQuizzesAsync(CancellationToken cancellationToken = default)
{
var quizzes = await _quizRepository.GetAllAsync(cancellationToken);
return quizzes.Where(q => q.IsActive)
.Select(q => q.ToDto())
.ToList();
}
/// <summary>
/// Activates a quiz.
/// </summary>
public virtual async Task<QuizDto?> ActivateQuizAsync(int id, CancellationToken cancellationToken = default)
{
var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
if (quiz == null)
return null;
quiz.Activate();
await _quizRepository.UpdateAsync(quiz, cancellationToken);
var updatedQuiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
return updatedQuiz?.ToDto();
}
/// <summary>
/// Deactivates a quiz.
/// </summary>
public virtual async Task<QuizDto?> DeactivateQuizAsync(int id, CancellationToken cancellationToken = default)
{
var quiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
if (quiz == null)
return null;
quiz.Deactivate();
await _quizRepository.UpdateAsync(quiz, cancellationToken);
var updatedQuiz = await _quizRepository.GetByIdAsync(id, cancellationToken);
return updatedQuiz?.ToDto();
}
}