From d90e4792d6e7e3e895684775297eca0f3175b486 Mon Sep 17 00:00:00 2001 From: Lasse Rune Hansen Date: Sun, 31 May 2026 18:14:51 +0200 Subject: [PATCH] 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 --- GermanApp/Application/DTOs/LessonDto.cs | 60 ++++++++ .../UseCases/Commands/CreateLessonCommand.cs | 65 ++++++++ GermanApp/Domain/Entities/Lesson.cs | 58 ++++++++ .../Domain/Exceptions/DomainException.cs | 56 +++++++ GermanApp/Domain/Interfaces/IRepository.cs | 63 ++++++++ GermanApp/Domain/ValueObjects/GermanWord.cs | 50 +++++++ GermanApp/GermanApp.csproj | 24 +++ GermanApp/GermanApp.http | 6 + .../Data/DbContext/AppDbContext.cs | 54 +++++++ .../Data/Repositories/LessonRepository.cs | 82 ++++++++++ .../Endpoints/LessonsEndpoints.cs | 140 ++++++++++++++++++ GermanApp/Program.cs | 99 +++++++++++++ GermanApp/Properties/launchSettings.json | 14 ++ GermanApp/appsettings.Development.json | 8 + GermanApp/appsettings.json | 9 ++ 15 files changed, 788 insertions(+) create mode 100644 GermanApp/Application/DTOs/LessonDto.cs create mode 100644 GermanApp/Application/UseCases/Commands/CreateLessonCommand.cs create mode 100644 GermanApp/Domain/Entities/Lesson.cs create mode 100644 GermanApp/Domain/Exceptions/DomainException.cs create mode 100644 GermanApp/Domain/Interfaces/IRepository.cs create mode 100644 GermanApp/Domain/ValueObjects/GermanWord.cs create mode 100644 GermanApp/GermanApp.csproj create mode 100644 GermanApp/GermanApp.http create mode 100644 GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs create mode 100644 GermanApp/Infrastructure/Data/Repositories/LessonRepository.cs create mode 100644 GermanApp/Presentation/Endpoints/LessonsEndpoints.cs create mode 100644 GermanApp/Program.cs create mode 100644 GermanApp/Properties/launchSettings.json create mode 100644 GermanApp/appsettings.Development.json create mode 100644 GermanApp/appsettings.json diff --git a/GermanApp/Application/DTOs/LessonDto.cs b/GermanApp/Application/DTOs/LessonDto.cs new file mode 100644 index 0000000..68517a8 --- /dev/null +++ b/GermanApp/Application/DTOs/LessonDto.cs @@ -0,0 +1,60 @@ +using GermanApp.Domain.Entities; + +namespace GermanApp.Application.DTOs; + +/// +/// Data Transfer Object for Lesson - used for API responses. +/// This is a read-only representation of a Lesson entity. +/// +public record LessonDto( + int Id, + string Title, + string Description, + int Level, + string LevelDescription, + DateTime CreatedAt, + DateTime? UpdatedAt); + +/// +/// Data Transfer Object for creating a new Lesson. +/// +public record CreateLessonDto( + string Title, + string Description, + int Level); + +/// +/// Data Transfer Object for updating an existing Lesson. +/// +public record UpdateLessonDto( + string Title, + string Description, + int Level); + +/// +/// Extension methods for mapping between Lesson entity and DTOs. +/// +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); +} diff --git a/GermanApp/Application/UseCases/Commands/CreateLessonCommand.cs b/GermanApp/Application/UseCases/Commands/CreateLessonCommand.cs new file mode 100644 index 0000000..7d4739d --- /dev/null +++ b/GermanApp/Application/UseCases/Commands/CreateLessonCommand.cs @@ -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; + +/// +/// Command for creating a new lesson. +/// This follows the CQRS pattern - commands represent write operations. +/// +public record CreateLessonCommand(CreateLessonDto LessonData) : ICommand; + +/// +/// Handler for the CreateLessonCommand. +/// +public class CreateLessonCommandHandler : ICommandHandler +{ + private readonly ILessonRepository _lessonRepository; + + public CreateLessonCommandHandler(ILessonRepository lessonRepository) + { + _lessonRepository = lessonRepository; + } + + public async Task 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(); + } +} + +/// +/// Generic command interface. +/// +/// The result type +public interface ICommand +{ +} + +/// +/// Generic command handler interface. +/// +/// The command type +/// The result type +public interface ICommandHandler + where TCommand : ICommand +{ + Task Handle(TCommand command, CancellationToken cancellationToken); +} + +// Note: For MediatR integration, you would implement IRequestHandler +// instead of the custom interfaces above. diff --git a/GermanApp/Domain/Entities/Lesson.cs b/GermanApp/Domain/Entities/Lesson.cs new file mode 100644 index 0000000..7d07beb --- /dev/null +++ b/GermanApp/Domain/Entities/Lesson.cs @@ -0,0 +1,58 @@ +namespace GermanApp.Domain.Entities; + +/// +/// Represents a German language lesson in the system. +/// +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 Words { get; private set; } + + /// + /// Constructor for EF Core deserialization. + /// + private Lesson() { } + + /// + /// Factory method to create a new lesson. + /// + public static Lesson Create(string title, string description, int level) + { + return new Lesson + { + Title = title, + Description = description, + Level = level, + CreatedAt = DateTime.UtcNow + }; + } + + /// + /// Updates the lesson details. + /// + public void Update(string title, string description, int level) + { + Title = title; + Description = description; + Level = level; + UpdatedAt = DateTime.UtcNow; + } + + /// + /// Domain behavior: Check if lesson is at beginner level. + /// + public bool IsBeginnerLevel() => Level <= 2; + + /// + /// Domain behavior: Check if lesson is at advanced level. + /// + public bool IsAdvancedLevel() => Level >= 4; +} diff --git a/GermanApp/Domain/Exceptions/DomainException.cs b/GermanApp/Domain/Exceptions/DomainException.cs new file mode 100644 index 0000000..fc26f63 --- /dev/null +++ b/GermanApp/Domain/Exceptions/DomainException.cs @@ -0,0 +1,56 @@ +using System; + +namespace GermanApp.Domain.Exceptions; + +/// +/// Base exception for domain layer errors. +/// All domain-specific exceptions should inherit from this. +/// +public class DomainException : Exception +{ + public DomainException(string message) : base(message) + { + } + + public DomainException(string message, Exception innerException) : base(message, innerException) + { + } +} + +/// +/// Exception thrown when a business rule validation fails. +/// +public class ValidationException : DomainException +{ + public ValidationException(string message) : base(message) + { + } + + public ValidationException(string message, Exception innerException) : base(message, innerException) + { + } +} + +/// +/// Exception thrown when an entity is not found. +/// +public class NotFoundException : DomainException +{ + public NotFoundException(string entityName, int id) : base($"{entityName} with id {id} not found") + { + } + + public NotFoundException(string message) : base(message) + { + } +} + +/// +/// Exception thrown when attempting to perform an invalid operation. +/// +public class InvalidOperationException : DomainException +{ + public InvalidOperationException(string message) : base(message) + { + } +} diff --git a/GermanApp/Domain/Interfaces/IRepository.cs b/GermanApp/Domain/Interfaces/IRepository.cs new file mode 100644 index 0000000..ecfe7fc --- /dev/null +++ b/GermanApp/Domain/Interfaces/IRepository.cs @@ -0,0 +1,63 @@ +using GermanApp.Domain.Entities; + +namespace GermanApp.Domain.Interfaces; + +/// +/// Generic repository interface for domain entities. +/// All repositories should implement this interface. +/// +/// The entity type +/// The identifier type (usually int or Guid) +public interface IRepository where TEntity : class +{ + /// + /// Gets an entity by its identifier. + /// + Task GetByIdAsync(TId id, CancellationToken cancellationToken = default); + + /// + /// Gets all entities. + /// + Task> GetAllAsync(CancellationToken cancellationToken = default); + + /// + /// Adds a new entity. + /// + Task AddAsync(TEntity entity, CancellationToken cancellationToken = default); + + /// + /// Updates an existing entity. + /// + Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default); + + /// + /// Deletes an entity. + /// + Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default); + + /// + /// Checks if an entity with the given identifier exists. + /// + Task ExistsAsync(TId id, CancellationToken cancellationToken = default); +} + +/// +/// Repository interface for Lesson entities. +/// +public interface ILessonRepository : IRepository +{ + /// + /// Gets lessons by level. + /// + Task> GetByLevelAsync(int level, CancellationToken cancellationToken = default); + + /// + /// Gets beginner-level lessons. + /// + Task> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default); + + /// + /// Gets advanced-level lessons. + /// + Task> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default); +} diff --git a/GermanApp/Domain/ValueObjects/GermanWord.cs b/GermanApp/Domain/ValueObjects/GermanWord.cs new file mode 100644 index 0000000..48bb8aa --- /dev/null +++ b/GermanApp/Domain/ValueObjects/GermanWord.cs @@ -0,0 +1,50 @@ +using GermanApp.Domain.Exceptions; + +namespace GermanApp.Domain.ValueObjects; + +/// +/// Represents a German word with its translation and metadata. +/// This is a value object - it has no identity and is defined by its attributes. +/// +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); + + /// + /// Check if this word has a definite article. + /// + public bool HasArticle() => Article != null; + + /// + /// Get the word with its article (if applicable). + /// + public string GetWithArticle() => Article != null ? $"{Article} {Value}" : Value; +} diff --git a/GermanApp/GermanApp.csproj b/GermanApp/GermanApp.csproj new file mode 100644 index 0000000..824b0c5 --- /dev/null +++ b/GermanApp/GermanApp.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/GermanApp/GermanApp.http b/GermanApp/GermanApp.http new file mode 100644 index 0000000..26eba35 --- /dev/null +++ b/GermanApp/GermanApp.http @@ -0,0 +1,6 @@ +@GermanApp_HostAddress = http://localhost:5235 + +GET {{GermanApp_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs b/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs new file mode 100644 index 0000000..df79381 --- /dev/null +++ b/GermanApp/Infrastructure/Data/DbContext/AppDbContext.cs @@ -0,0 +1,54 @@ +using GermanApp.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace GermanApp.Infrastructure.Data.DbContext; + +/// +/// Entity Framework Core database context. +/// This is part of the Infrastructure layer. +/// +public class AppDbContext : Microsoft.EntityFrameworkCore.DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) + { + } + + // DbSets for domain entities + public DbSet 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(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().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 } + // ); + } +} diff --git a/GermanApp/Infrastructure/Data/Repositories/LessonRepository.cs b/GermanApp/Infrastructure/Data/Repositories/LessonRepository.cs new file mode 100644 index 0000000..1b9a017 --- /dev/null +++ b/GermanApp/Infrastructure/Data/Repositories/LessonRepository.cs @@ -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; + +/// +/// Entity Framework Core implementation of ILessonRepository. +/// This is part of the Infrastructure layer. +/// +public class LessonRepository : ILessonRepository +{ + private readonly AppDbContext _context; + + public LessonRepository(AppDbContext context) + { + _context = context; + } + + public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) + { + return await _context.Lessons + .FirstOrDefaultAsync(l => l.Id == id, cancellationToken); + } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return await _context.Lessons + .AsNoTracking() + .ToListAsync(cancellationToken); + } + + public async Task 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 ExistsAsync(int id, CancellationToken cancellationToken = default) + { + return await _context.Lessons + .AnyAsync(l => l.Id == id, cancellationToken); + } + + public async Task> GetByLevelAsync(int level, CancellationToken cancellationToken = default) + { + return await _context.Lessons + .Where(l => l.Level == level) + .AsNoTracking() + .ToListAsync(cancellationToken); + } + + public async Task> GetBeginnerLessonsAsync(CancellationToken cancellationToken = default) + { + return await _context.Lessons + .Where(l => l.Level <= 2) + .AsNoTracking() + .ToListAsync(cancellationToken); + } + + public async Task> GetAdvancedLessonsAsync(CancellationToken cancellationToken = default) + { + return await _context.Lessons + .Where(l => l.Level >= 4) + .AsNoTracking() + .ToListAsync(cancellationToken); + } +} diff --git a/GermanApp/Presentation/Endpoints/LessonsEndpoints.cs b/GermanApp/Presentation/Endpoints/LessonsEndpoints.cs new file mode 100644 index 0000000..4f40eb3 --- /dev/null +++ b/GermanApp/Presentation/Endpoints/LessonsEndpoints.cs @@ -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; + +/// +/// Minimal API endpoints for Lesson resources. +/// This is part of the Presentation layer. +/// +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 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" + }); + } +} diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs new file mode 100644 index 0000000..0133d3f --- /dev/null +++ b/GermanApp/Program.cs @@ -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(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(); + +// ============================================ +// APPLICATION LAYER - Use Cases & Services +// ============================================ + +// Register command handlers +builder.Services.AddScoped, 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(); + 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); +} diff --git a/GermanApp/Properties/launchSettings.json b/GermanApp/Properties/launchSettings.json new file mode 100644 index 0000000..0e3da15 --- /dev/null +++ b/GermanApp/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/GermanApp/appsettings.Development.json b/GermanApp/appsettings.Development.json new file mode 100644 index 0000000..ff66ba6 --- /dev/null +++ b/GermanApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/GermanApp/appsettings.json b/GermanApp/appsettings.json new file mode 100644 index 0000000..4d56694 --- /dev/null +++ b/GermanApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}