Initial commit: GermanApp with Clean Architecture
- Domain layer: Lesson entity, GermanWord value object, repository interfaces - Application layer: CQRS commands, DTOs with mapping - Infrastructure layer: EF Core with SQLite, LessonRepository - Presentation layer: Minimal API endpoints for lessons CRUD Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
commit
d90e4792d6
15 changed files with 788 additions and 0 deletions
60
GermanApp/Application/DTOs/LessonDto.cs
Normal file
60
GermanApp/Application/DTOs/LessonDto.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using GermanApp.Domain.Entities;
|
||||
|
||||
namespace GermanApp.Application.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for Lesson - used for API responses.
|
||||
/// This is a read-only representation of a Lesson entity.
|
||||
/// </summary>
|
||||
public record LessonDto(
|
||||
int Id,
|
||||
string Title,
|
||||
string Description,
|
||||
int Level,
|
||||
string LevelDescription,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for creating a new Lesson.
|
||||
/// </summary>
|
||||
public record CreateLessonDto(
|
||||
string Title,
|
||||
string Description,
|
||||
int Level);
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for updating an existing Lesson.
|
||||
/// </summary>
|
||||
public record UpdateLessonDto(
|
||||
string Title,
|
||||
string Description,
|
||||
int Level);
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for mapping between Lesson entity and DTOs.
|
||||
/// </summary>
|
||||
public static class LessonDtoExtensions
|
||||
{
|
||||
public static LessonDto ToDto(this Lesson lesson) => new(
|
||||
lesson.Id,
|
||||
lesson.Title,
|
||||
lesson.Description,
|
||||
lesson.Level,
|
||||
GetLevelDescription(lesson.Level),
|
||||
lesson.CreatedAt,
|
||||
lesson.UpdatedAt);
|
||||
|
||||
private static string GetLevelDescription(int level) => level switch
|
||||
{
|
||||
1 => "A1 (Beginner)",
|
||||
2 => "A2 (Elementary)",
|
||||
3 => "B1 (Intermediate)",
|
||||
4 => "B2 (Upper Intermediate)",
|
||||
5 => "C1 (Advanced)",
|
||||
_ => "Unknown"
|
||||
};
|
||||
|
||||
public static Lesson ToEntity(this CreateLessonDto dto) =>
|
||||
Lesson.Create(dto.Title, dto.Description, dto.Level);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
using GermanApp.Application.DTOs;
|
||||
using GermanApp.Domain.Entities;
|
||||
using GermanApp.Domain.Exceptions;
|
||||
using GermanApp.Domain.Interfaces;
|
||||
|
||||
namespace GermanApp.Application.UseCases.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Command for creating a new lesson.
|
||||
/// This follows the CQRS pattern - commands represent write operations.
|
||||
/// </summary>
|
||||
public record CreateLessonCommand(CreateLessonDto LessonData) : ICommand<LessonDto>;
|
||||
|
||||
/// <summary>
|
||||
/// Handler for the CreateLessonCommand.
|
||||
/// </summary>
|
||||
public class CreateLessonCommandHandler : ICommandHandler<CreateLessonCommand, LessonDto>
|
||||
{
|
||||
private readonly ILessonRepository _lessonRepository;
|
||||
|
||||
public CreateLessonCommandHandler(ILessonRepository lessonRepository)
|
||||
{
|
||||
_lessonRepository = lessonRepository;
|
||||
}
|
||||
|
||||
public async Task<LessonDto> Handle(CreateLessonCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert DTO to domain entity
|
||||
var lesson = command.LessonData.ToEntity();
|
||||
|
||||
// Validate business rules (could be extracted to a validator)
|
||||
if (lesson.Level < 1 || lesson.Level > 5)
|
||||
{
|
||||
throw new ValidationException("Level must be between 1 and 5");
|
||||
}
|
||||
|
||||
// Add to repository
|
||||
var createdLesson = await _lessonRepository.AddAsync(lesson, cancellationToken);
|
||||
|
||||
// Return DTO representation
|
||||
return createdLesson.ToDto();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic command interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The result type</typeparam>
|
||||
public interface ICommand<out TResult>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic command handler interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TCommand">The command type</typeparam>
|
||||
/// <typeparam name="TResult">The result type</typeparam>
|
||||
public interface ICommandHandler<in TCommand, TResult>
|
||||
where TCommand : ICommand<TResult>
|
||||
{
|
||||
Task<TResult> Handle(TCommand command, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
// Note: For MediatR integration, you would implement IRequestHandler<TCommand, TResult>
|
||||
// instead of the custom interfaces above.
|
||||
58
GermanApp/Domain/Entities/Lesson.cs
Normal file
58
GermanApp/Domain/Entities/Lesson.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
namespace GermanApp.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a German language lesson in the system.
|
||||
/// </summary>
|
||||
public class Lesson
|
||||
{
|
||||
// Private setter for domain behavior, but internal for EF Core
|
||||
public int Id { get; private set; }
|
||||
public string Title { get; private set; } = string.Empty;
|
||||
public string Description { get; private set; } = string.Empty;
|
||||
public int Level { get; private set; }
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
public DateTime? UpdatedAt { get; private set; }
|
||||
|
||||
// Navigation properties would go here in EF Core
|
||||
// public ICollection<VocabularyWord> Words { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for EF Core deserialization.
|
||||
/// </summary>
|
||||
private Lesson() { }
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to create a new lesson.
|
||||
/// </summary>
|
||||
public static Lesson Create(string title, string description, int level)
|
||||
{
|
||||
return new Lesson
|
||||
{
|
||||
Title = title,
|
||||
Description = description,
|
||||
Level = level,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the lesson details.
|
||||
/// </summary>
|
||||
public void Update(string title, string description, int level)
|
||||
{
|
||||
Title = title;
|
||||
Description = description;
|
||||
Level = level;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Domain behavior: Check if lesson is at beginner level.
|
||||
/// </summary>
|
||||
public bool IsBeginnerLevel() => Level <= 2;
|
||||
|
||||
/// <summary>
|
||||
/// Domain behavior: Check if lesson is at advanced level.
|
||||
/// </summary>
|
||||
public bool IsAdvancedLevel() => Level >= 4;
|
||||
}
|
||||
56
GermanApp/Domain/Exceptions/DomainException.cs
Normal file
56
GermanApp/Domain/Exceptions/DomainException.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
|
||||
namespace GermanApp.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Base exception for domain layer errors.
|
||||
/// All domain-specific exceptions should inherit from this.
|
||||
/// </summary>
|
||||
public class DomainException : Exception
|
||||
{
|
||||
public DomainException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public DomainException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a business rule validation fails.
|
||||
/// </summary>
|
||||
public class ValidationException : DomainException
|
||||
{
|
||||
public ValidationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ValidationException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when an entity is not found.
|
||||
/// </summary>
|
||||
public class NotFoundException : DomainException
|
||||
{
|
||||
public NotFoundException(string entityName, int id) : base($"{entityName} with id {id} not found")
|
||||
{
|
||||
}
|
||||
|
||||
public NotFoundException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when attempting to perform an invalid operation.
|
||||
/// </summary>
|
||||
public class InvalidOperationException : DomainException
|
||||
{
|
||||
public InvalidOperationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
63
GermanApp/Domain/Interfaces/IRepository.cs
Normal file
63
GermanApp/Domain/Interfaces/IRepository.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using GermanApp.Domain.Entities;
|
||||
|
||||
namespace GermanApp.Domain.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Generic repository interface for domain entities.
|
||||
/// All repositories should implement this interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">The entity type</typeparam>
|
||||
/// <typeparam name="TId">The identifier type (usually int or Guid)</typeparam>
|
||||
public interface IRepository<TEntity, TId> where TEntity : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an entity by its identifier.
|
||||
/// </summary>
|
||||
Task<TEntity?> GetByIdAsync(TId id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all entities.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<TEntity>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new entity.
|
||||
/// </summary>
|
||||
Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing entity.
|
||||
/// </summary>
|
||||
Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an entity.
|
||||
/// </summary>
|
||||
Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an entity with the given identifier exists.
|
||||
/// </summary>
|
||||
Task<bool> ExistsAsync(TId id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for Lesson entities.
|
||||
/// </summary>
|
||||
public interface ILessonRepository : IRepository<Lesson, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets lessons by level.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<Lesson>> GetByLevelAsync(int level, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets beginner-level lessons.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<Lesson>> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets advanced-level lessons.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<Lesson>> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
50
GermanApp/Domain/ValueObjects/GermanWord.cs
Normal file
50
GermanApp/Domain/ValueObjects/GermanWord.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using GermanApp.Domain.Exceptions;
|
||||
|
||||
namespace GermanApp.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a German word with its translation and metadata.
|
||||
/// This is a value object - it has no identity and is defined by its attributes.
|
||||
/// </summary>
|
||||
public record GermanWord
|
||||
{
|
||||
public string Value { get; }
|
||||
public string Translation { get; }
|
||||
public string? Article { get; } // der, die, das, or null for verbs/adjectives
|
||||
public string PartOfSpeech { get; }
|
||||
|
||||
public GermanWord(string value, string translation, string? article, string partOfSpeech)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new DomainException("German word cannot be empty");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(translation))
|
||||
throw new DomainException("Translation cannot be empty");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(partOfSpeech))
|
||||
throw new DomainException("Part of speech is required");
|
||||
|
||||
if (article != null && !IsValidArticle(article))
|
||||
throw new DomainException("Invalid article. Must be 'der', 'die', or 'das'");
|
||||
|
||||
Value = value.Trim();
|
||||
Translation = translation.Trim();
|
||||
Article = article?.Trim();
|
||||
PartOfSpeech = partOfSpeech.Trim();
|
||||
}
|
||||
|
||||
private static bool IsValidArticle(string article) =>
|
||||
article.Equals("der", StringComparison.OrdinalIgnoreCase) ||
|
||||
article.Equals("die", StringComparison.OrdinalIgnoreCase) ||
|
||||
article.Equals("das", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Check if this word has a definite article.
|
||||
/// </summary>
|
||||
public bool HasArticle() => Article != null;
|
||||
|
||||
/// <summary>
|
||||
/// Get the word with its article (if applicable).
|
||||
/// </summary>
|
||||
public string GetWithArticle() => Article != null ? $"{Article} {Value}" : Value;
|
||||
}
|
||||
24
GermanApp/GermanApp.csproj
Normal file
24
GermanApp/GermanApp.csproj
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.16" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
GermanApp/GermanApp.http
Normal file
6
GermanApp/GermanApp.http
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
@GermanApp_HostAddress = http://localhost:5235
|
||||
|
||||
GET {{GermanApp_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
54
GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs
Normal file
54
GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using GermanApp.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GermanApp.Infrastructure.Data.DbContext;
|
||||
|
||||
/// <summary>
|
||||
/// Entity Framework Core database context.
|
||||
/// This is part of the Infrastructure layer.
|
||||
/// </summary>
|
||||
public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
// DbSets for domain entities
|
||||
public DbSet<Lesson> Lessons { get; set; } = null!;
|
||||
|
||||
// Note: Value objects are not stored directly as entities.
|
||||
// They are owned by entities and stored as part of the entity's data.
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Configure Lesson entity
|
||||
modelBuilder.Entity<Lesson>(builder =>
|
||||
{
|
||||
builder.HasKey(l => l.Id);
|
||||
builder.Property(l => l.Title).IsRequired().HasMaxLength(200);
|
||||
builder.Property(l => l.Description).IsRequired().HasMaxLength(2000);
|
||||
builder.Property(l => l.Level).IsRequired();
|
||||
builder.Property(l => l.CreatedAt).IsRequired();
|
||||
builder.Property(l => l.UpdatedAt).IsRequired(false);
|
||||
|
||||
// Value object: GermanWord would be configured as an owned entity
|
||||
// builder.OwnsMany(l => l.Words, wordBuilder =>
|
||||
// {
|
||||
// wordBuilder.Property(w => w.Value).HasColumnName("WordValue");
|
||||
// wordBuilder.Property(w => w.Translation).HasColumnName("Translation");
|
||||
// wordBuilder.Property(w => w.Article).HasColumnName("Article");
|
||||
// wordBuilder.Property(w => w.PartOfSpeech).HasColumnName("PartOfSpeech");
|
||||
// });
|
||||
});
|
||||
|
||||
// Seed data (optional) - Note: For EF Core, we need to set properties directly
|
||||
// In a real application, use migrations or a separate seeding mechanism
|
||||
// modelBuilder.Entity<Lesson>().HasData(
|
||||
// new { Id = 1, Title = "Greetings", Description = "Basic German greetings", Level = 1, CreatedAt = DateTime.UtcNow },
|
||||
// new { Id = 2, Title = "Numbers", Description = "German numbers 1-100", Level = 1, CreatedAt = DateTime.UtcNow },
|
||||
// new { Id = 3, Title = "Grammar Basics", Description = "Basic German grammar", Level = 2, CreatedAt = DateTime.UtcNow }
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
using GermanApp.Domain.Entities;
|
||||
using GermanApp.Domain.Interfaces;
|
||||
using GermanApp.Infrastructure.Data.DbContext;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GermanApp.Infrastructure.Data.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Entity Framework Core implementation of ILessonRepository.
|
||||
/// This is part of the Infrastructure layer.
|
||||
/// </summary>
|
||||
public class LessonRepository : ILessonRepository
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
|
||||
public LessonRepository(AppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Lesson?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Lessons
|
||||
.FirstOrDefaultAsync(l => l.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Lesson>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Lessons
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Lesson> AddAsync(Lesson entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _context.Lessons.AddAsync(entity, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return entity;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Lesson entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Lessons.Update(entity);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Lesson entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Lessons.Remove(entity);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Lessons
|
||||
.AnyAsync(l => l.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Lesson>> GetByLevelAsync(int level, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Lessons
|
||||
.Where(l => l.Level == level)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Lesson>> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Lessons
|
||||
.Where(l => l.Level <= 2)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Lesson>> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Lessons
|
||||
.Where(l => l.Level >= 4)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
140
GermanApp/Presentation/Endpoints/LessonsEndpoints.cs
Normal file
140
GermanApp/Presentation/Endpoints/LessonsEndpoints.cs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
using GermanApp.Application.DTOs;
|
||||
using GermanApp.Application.UseCases.Commands;
|
||||
using GermanApp.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GermanApp.Presentation.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal API endpoints for Lesson resources.
|
||||
/// This is part of the Presentation layer.
|
||||
/// </summary>
|
||||
public static class LessonsEndpoints
|
||||
{
|
||||
public static void MapLessonsEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/lessons");
|
||||
|
||||
// GET /api/lessons
|
||||
group.MapGet("/", async ([FromServices] ILessonRepository repository) =>
|
||||
{
|
||||
var lessons = await repository.GetAllAsync();
|
||||
return Results.Ok(lessons.Select(l => l.ToDto()));
|
||||
})
|
||||
.WithName("GetAllLessons")
|
||||
.WithOpenApi(operation => new(operation)
|
||||
{
|
||||
Summary = "Get all lessons",
|
||||
Description = "Retrieves all available German lessons"
|
||||
});
|
||||
|
||||
// GET /api/lessons/{id}
|
||||
group.MapGet("/{id}", async ([FromRoute] int id, [FromServices] ILessonRepository repository) =>
|
||||
{
|
||||
var lesson = await repository.GetByIdAsync(id);
|
||||
return lesson is null ? Results.NotFound() : Results.Ok(lesson.ToDto());
|
||||
})
|
||||
.WithName("GetLessonById")
|
||||
.WithOpenApi(operation => new(operation)
|
||||
{
|
||||
Summary = "Get lesson by ID",
|
||||
Description = "Retrieves a specific lesson by its identifier"
|
||||
});
|
||||
|
||||
// GET /api/lessons/beginner
|
||||
group.MapGet("/beginner", async ([FromServices] ILessonRepository repository) =>
|
||||
{
|
||||
var lessons = await repository.GetBeginnerLessonsAsync();
|
||||
return Results.Ok(lessons.Select(l => l.ToDto()));
|
||||
})
|
||||
.WithName("GetBeginnerLessons")
|
||||
.WithOpenApi(operation => new(operation)
|
||||
{
|
||||
Summary = "Get beginner lessons",
|
||||
Description = "Retrieves all lessons at beginner level (A1-A2)"
|
||||
});
|
||||
|
||||
// GET /api/lessons/advanced
|
||||
group.MapGet("/advanced", async ([FromServices] ILessonRepository repository) =>
|
||||
{
|
||||
var lessons = await repository.GetAdvancedLessonsAsync();
|
||||
return Results.Ok(lessons.Select(l => l.ToDto()));
|
||||
})
|
||||
.WithName("GetAdvancedLessons")
|
||||
.WithOpenApi(operation => new(operation)
|
||||
{
|
||||
Summary = "Get advanced lessons",
|
||||
Description = "Retrieves all lessons at advanced level (B2-C1)"
|
||||
});
|
||||
|
||||
// GET /api/lessons/level/{level}
|
||||
group.MapGet("/level/{level}", async ([FromRoute] int level, [FromServices] ILessonRepository repository) =>
|
||||
{
|
||||
var lessons = await repository.GetByLevelAsync(level);
|
||||
return Results.Ok(lessons.Select(l => l.ToDto()));
|
||||
})
|
||||
.WithName("GetLessonsByLevel")
|
||||
.WithOpenApi(operation => new(operation)
|
||||
{
|
||||
Summary = "Get lessons by level",
|
||||
Description = "Retrieves all lessons at a specific proficiency level"
|
||||
});
|
||||
|
||||
// POST /api/lessons
|
||||
group.MapPost("/", async (
|
||||
[FromBody] CreateLessonDto dto,
|
||||
[FromServices] ICommandHandler<CreateLessonCommand, LessonDto> handler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var command = new CreateLessonCommand(dto);
|
||||
var result = await handler.Handle(command, cancellationToken);
|
||||
return Results.Created($"/api/lessons/{result.Id}", result);
|
||||
})
|
||||
.WithName("CreateLesson")
|
||||
.WithOpenApi(operation => new(operation)
|
||||
{
|
||||
Summary = "Create a new lesson",
|
||||
Description = "Creates a new German lesson"
|
||||
});
|
||||
|
||||
// PUT /api/lessons/{id}
|
||||
group.MapPut("/{id}", async (
|
||||
[FromRoute] int id,
|
||||
[FromBody] UpdateLessonDto dto,
|
||||
[FromServices] ILessonRepository repository) =>
|
||||
{
|
||||
var existingLesson = await repository.GetByIdAsync(id);
|
||||
if (existingLesson is null)
|
||||
return Results.NotFound();
|
||||
|
||||
// Map DTO to entity
|
||||
existingLesson.Update(dto.Title, dto.Description, dto.Level);
|
||||
await repository.UpdateAsync(existingLesson);
|
||||
|
||||
return Results.Ok(existingLesson.ToDto());
|
||||
})
|
||||
.WithName("UpdateLesson")
|
||||
.WithOpenApi(operation => new(operation)
|
||||
{
|
||||
Summary = "Update a lesson",
|
||||
Description = "Updates an existing lesson"
|
||||
});
|
||||
|
||||
// DELETE /api/lessons/{id}
|
||||
group.MapDelete("/{id}", async ([FromRoute] int id, [FromServices] ILessonRepository repository) =>
|
||||
{
|
||||
var lesson = await repository.GetByIdAsync(id);
|
||||
if (lesson is null)
|
||||
return Results.NotFound();
|
||||
|
||||
await repository.DeleteAsync(lesson);
|
||||
return Results.NoContent();
|
||||
})
|
||||
.WithName("DeleteLesson")
|
||||
.WithOpenApi(operation => new(operation)
|
||||
{
|
||||
Summary = "Delete a lesson",
|
||||
Description = "Deletes a lesson by its identifier"
|
||||
});
|
||||
}
|
||||
}
|
||||
99
GermanApp/Program.cs
Normal file
99
GermanApp/Program.cs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
using GermanApp.Application.DTOs;
|
||||
using GermanApp.Application.UseCases.Commands;
|
||||
using GermanApp.Domain.Interfaces;
|
||||
using GermanApp.Infrastructure.Data.DbContext;
|
||||
using GermanApp.Infrastructure.Data.Repositories;
|
||||
using GermanApp.Presentation.Endpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// ============================================
|
||||
// INFRASTRUCTURE LAYER - Database & External Services
|
||||
// ============================================
|
||||
|
||||
// Add DbContext with SQLite (can be changed to SQL Server, PostgreSQL, etc.)
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
{
|
||||
// Using SQLite for development - configure in appsettings.json for production
|
||||
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? "Data Source=germanapp.db");
|
||||
|
||||
// Enable sensitive data logging in development
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
options.EnableSensitiveDataLogging();
|
||||
options.EnableDetailedErrors();
|
||||
}
|
||||
});
|
||||
|
||||
// Register repositories (Infrastructure implementations of Domain interfaces)
|
||||
builder.Services.AddScoped<ILessonRepository, LessonRepository>();
|
||||
|
||||
// ============================================
|
||||
// APPLICATION LAYER - Use Cases & Services
|
||||
// ============================================
|
||||
|
||||
// Register command handlers
|
||||
builder.Services.AddScoped<ICommandHandler<CreateLessonCommand, LessonDto>, CreateLessonCommandHandler>();
|
||||
|
||||
// ============================================
|
||||
// PRESENTATION LAYER - API
|
||||
// ============================================
|
||||
|
||||
// Add services for Minimal APIs
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
// Initialize database (in development)
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
dbContext.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
// Map Clean Architecture endpoints
|
||||
app.MapLessonsEndpoints();
|
||||
|
||||
// Keep original WeatherForecast endpoint for reference
|
||||
app.MapGet("/weatherforecast", () =>
|
||||
{
|
||||
var summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
var forecast = Enumerable.Range(1, 5).Select(index =>
|
||||
new WeatherForecast
|
||||
(
|
||||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
Random.Shared.Next(-20, 55),
|
||||
summaries[Random.Shared.Next(summaries.Length)]
|
||||
))
|
||||
.ToArray();
|
||||
return forecast;
|
||||
})
|
||||
.WithName("GetWeatherForecast");
|
||||
|
||||
app.Run();
|
||||
|
||||
// Existing record for reference
|
||||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
||||
{
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
14
GermanApp/Properties/launchSettings.json
Normal file
14
GermanApp/Properties/launchSettings.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5235",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
GermanApp/appsettings.Development.json
Normal file
8
GermanApp/appsettings.Development.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
GermanApp/appsettings.json
Normal file
9
GermanApp/appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue