diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Contracts/ReserveTicketsRequest.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Contracts/ReserveTicketsRequest.cs new file mode 100644 index 0000000000..43839a1b5c --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Contracts/ReserveTicketsRequest.cs @@ -0,0 +1,5 @@ +using System.ComponentModel.DataAnnotations; + +namespace EventTicketing.Api.Contracts; + +public sealed record ReserveTicketsRequest([property: Range(1, 20)] int Quantity); diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Endpoints/EventEndpoints.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Endpoints/EventEndpoints.cs new file mode 100644 index 0000000000..0f649dc290 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Endpoints/EventEndpoints.cs @@ -0,0 +1,37 @@ +using EventTicketing.Api.Contracts; +using EventTicketing.Api.Extensions; +using EventTicketing.Application.Events; + +namespace EventTicketing.Api.Endpoints; + +public static class EventEndpoints +{ + public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder app) + { + var events = app.MapGroup("/api/events"); + + events.MapGet("/{eventId:int}", async ( + int eventId, + GetEventAvailabilityHandler handler, + CancellationToken cancellationToken) => + { + var result = await handler.HandleAsync(new GetEventAvailabilityQuery(eventId), cancellationToken); + + return result.IsSuccess ? Results.Ok(result.Value) : result.ToProblem(); + }); + + events.MapPost("/{eventId:int}/reservations", async ( + int eventId, + ReserveTicketsRequest request, + ReserveTicketsHandler handler, + CancellationToken cancellationToken) => + { + var command = new ReserveTicketsCommand(eventId, request.Quantity); + var result = await handler.HandleAsync(command, cancellationToken); + + return result.IsSuccess ? Results.Ok(result.Value) : result.ToProblem(); + }); + + return app; + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.csproj b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.csproj new file mode 100644 index 0000000000..08002b12d5 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Extensions/ResultExtensions.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Extensions/ResultExtensions.cs new file mode 100644 index 0000000000..8e65ecdaba --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Extensions/ResultExtensions.cs @@ -0,0 +1,25 @@ +using EventTicketing.Domain.Common; + +namespace EventTicketing.Api.Extensions; + +public static class ResultExtensions +{ + public static IResult ToProblem(this Result result) + { + if (result.IsSuccess) + throw new InvalidOperationException("A successful result is not a problem."); + + var statusCode = result.Error.Type switch + { + ErrorType.Validation => StatusCodes.Status400BadRequest, + ErrorType.NotFound => StatusCodes.Status404NotFound, + ErrorType.Conflict => StatusCodes.Status409Conflict, + _ => StatusCodes.Status500InternalServerError + }; + + return TypedResults.Problem( + statusCode: statusCode, + title: result.Error.Code, + detail: result.Error.Description); + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Program.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Program.cs new file mode 100644 index 0000000000..fcd3298901 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Program.cs @@ -0,0 +1,27 @@ +using EventTicketing.Api.Endpoints; +using EventTicketing.Application; +using EventTicketing.Infrastructure; +using EventTicketing.Infrastructure.Persistence; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddApplication(); +builder.Services.AddInfrastructure(builder.Configuration.GetConnectionString("Ticketing")!); + +builder.Services.AddProblemDetails(); +builder.Services.AddValidation(); +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +app.UseExceptionHandler(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + await app.Services.SeedDatabaseAsync(); +} + +app.MapEventEndpoints(); + +app.Run(); diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Properties/launchSettings.json b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Properties/launchSettings.json new file mode 100644 index 0000000000..4fa2de2c5e --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/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:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/appsettings.json b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/appsettings.Development.json similarity index 80% rename from csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/appsettings.json rename to csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/appsettings.Development.json index 10f68b8c8b..0c208ae918 100644 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/appsettings.json +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/appsettings.Development.json @@ -4,6 +4,5 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } - }, - "AllowedHosts": "*" + } } diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/appsettings.json b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/appsettings.json new file mode 100644 index 0000000000..f0d3f98f07 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "Ticketing": "Data Source=tickets.db" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Abstractions/IUnitOfWork.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Abstractions/IUnitOfWork.cs new file mode 100644 index 0000000000..c56b8fcf61 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Abstractions/IUnitOfWork.cs @@ -0,0 +1,6 @@ +namespace EventTicketing.Application.Abstractions; + +public interface IUnitOfWork +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/DependencyInjection.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/DependencyInjection.cs new file mode 100644 index 0000000000..66c8325f41 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/DependencyInjection.cs @@ -0,0 +1,15 @@ +using EventTicketing.Application.Events; +using Microsoft.Extensions.DependencyInjection; + +namespace EventTicketing.Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplication(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/ToDoApp.Persistence.csproj b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/EventTicketing.Application.csproj similarity index 54% rename from csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/ToDoApp.Persistence.csproj rename to csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/EventTicketing.Application.csproj index 5173ed2f34..af1bc6ab45 100644 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/ToDoApp.Persistence.csproj +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/EventTicketing.Application.csproj @@ -1,18 +1,17 @@  - - net8.0 - enable - enable - - - - + - + + + net10.0 + enable + enable + + diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/GetEventAvailability.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/GetEventAvailability.cs new file mode 100644 index 0000000000..b1ffb87d30 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/GetEventAvailability.cs @@ -0,0 +1,21 @@ +using EventTicketing.Domain.Common; +using EventTicketing.Domain.Events; + +namespace EventTicketing.Application.Events; + +public sealed record GetEventAvailabilityQuery(int EventId); + +public sealed record EventAvailabilityResponse(int EventId, string Name, int Capacity, int TicketsLeft); + +public sealed class GetEventAvailabilityHandler(IEventReadRepository reads) +{ + public async Task> HandleAsync( + GetEventAvailabilityQuery query, CancellationToken cancellationToken = default) + { + var availability = await reads.GetAvailabilityAsync(query.EventId, cancellationToken); + if (availability is null) + return EventErrors.NotFound(query.EventId); + + return availability; + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/IEventReadRepository.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/IEventReadRepository.cs new file mode 100644 index 0000000000..cda14d952d --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/IEventReadRepository.cs @@ -0,0 +1,7 @@ +namespace EventTicketing.Application.Events; + +public interface IEventReadRepository +{ + Task GetAvailabilityAsync( + int eventId, CancellationToken cancellationToken = default); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/IEventRepository.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/IEventRepository.cs new file mode 100644 index 0000000000..024f94fe70 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/IEventRepository.cs @@ -0,0 +1,8 @@ +using EventTicketing.Domain.Events; + +namespace EventTicketing.Application.Events; + +public interface IEventRepository +{ + Task GetByIdAsync(int eventId, CancellationToken cancellationToken = default); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/ReserveTickets.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/ReserveTickets.cs new file mode 100644 index 0000000000..a00c951839 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/ReserveTickets.cs @@ -0,0 +1,28 @@ +using EventTicketing.Application.Abstractions; +using EventTicketing.Domain.Common; +using EventTicketing.Domain.Events; + +namespace EventTicketing.Application.Events; + +public sealed record ReserveTicketsCommand(int EventId, int Quantity); + +public sealed record ReservationResponse(int EventId, int TicketsReserved, int TicketsLeft); + +public sealed class ReserveTicketsHandler(IEventRepository events, IUnitOfWork unitOfWork) +{ + public async Task> HandleAsync( + ReserveTicketsCommand command, CancellationToken cancellationToken = default) + { + var ev = await events.GetByIdAsync(command.EventId, cancellationToken); + if (ev is null) + return EventErrors.NotFound(command.EventId); + + var reservation = ev.Reserve(command.Quantity); + if (reservation.IsFailure) + return reservation.Error; + + await unitOfWork.SaveChangesAsync(cancellationToken); + + return new ReservationResponse(ev.Id, command.Quantity, ev.TicketsLeft); + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.ArchitectureTests/DependencyRuleTests.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.ArchitectureTests/DependencyRuleTests.cs new file mode 100644 index 0000000000..76f3357484 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.ArchitectureTests/DependencyRuleTests.cs @@ -0,0 +1,50 @@ +using EventTicketing.Application.Events; +using EventTicketing.Domain.Events; +using EventTicketing.Infrastructure.Persistence; +using NetArchTest.Rules; +using TestResult = NetArchTest.Rules.TestResult; + +namespace EventTicketing.ArchitectureTests; + +public class DependencyRuleTests +{ + private const string Application = "EventTicketing.Application"; + private const string Infrastructure = "EventTicketing.Infrastructure"; + private const string Api = "EventTicketing.Api"; + + [Fact] + public void Domain_DependsOnNoOtherLayer() + { + var result = Types.InAssembly(typeof(Event).Assembly) + .ShouldNot() + .HaveDependencyOnAny(Application, Infrastructure, Api) + .GetResult(); + + Assert.True(result.IsSuccessful, Describe(result)); + } + + [Fact] + public void Application_DoesNotDependOnInfrastructureOrTheWeb() + { + var result = Types.InAssembly(typeof(ReserveTicketsHandler).Assembly) + .ShouldNot() + .HaveDependencyOnAny(Infrastructure, Api, "Microsoft.EntityFrameworkCore", "Microsoft.AspNetCore") + .GetResult(); + + Assert.True(result.IsSuccessful, Describe(result)); + } + + [Fact] + public void Infrastructure_DoesNotDependOnTheApi() + { + var result = Types.InAssembly(typeof(TicketingDbContext).Assembly) + .ShouldNot() + .HaveDependencyOn(Api) + .GetResult(); + + Assert.True(result.IsSuccessful, Describe(result)); + } + + private static string Describe(TestResult result) => + "Offending types: " + string.Join(", ", result.FailingTypeNames ?? []); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.ArchitectureTests/EventTicketing.ArchitectureTests.csproj b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.ArchitectureTests/EventTicketing.ArchitectureTests.csproj new file mode 100644 index 0000000000..1e027d8a6d --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.ArchitectureTests/EventTicketing.ArchitectureTests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/DomainException.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/DomainException.cs new file mode 100644 index 0000000000..0ea30629b5 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/DomainException.cs @@ -0,0 +1,3 @@ +namespace EventTicketing.Domain.Common; + +public sealed class DomainException(string message) : Exception(message); diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/Error.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/Error.cs new file mode 100644 index 0000000000..c0e7687a2a --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/Error.cs @@ -0,0 +1,23 @@ +namespace EventTicketing.Domain.Common; + +public enum ErrorType +{ + Failure, + Validation, + NotFound, + Conflict +} + +public sealed record Error(string Code, string Description, ErrorType Type) +{ + public static readonly Error None = new(string.Empty, string.Empty, ErrorType.Failure); + + public static Error Validation(string code, string description) => + new(code, description, ErrorType.Validation); + + public static Error NotFound(string code, string description) => + new(code, description, ErrorType.NotFound); + + public static Error Conflict(string code, string description) => + new(code, description, ErrorType.Conflict); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/Result.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/Result.cs new file mode 100644 index 0000000000..67147a661b --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/Result.cs @@ -0,0 +1,43 @@ +namespace EventTicketing.Domain.Common; + +public class Result +{ + protected Result(bool isSuccess, Error error) + { + if (isSuccess && error != Error.None) + throw new ArgumentException("A successful result cannot carry an error.", nameof(error)); + + if (!isSuccess && error == Error.None) + throw new ArgumentException("A failed result needs an error.", nameof(error)); + + IsSuccess = isSuccess; + Error = error; + } + + public bool IsSuccess { get; } + public bool IsFailure => !IsSuccess; + public Error Error { get; } + + public static Result Success() => new(true, Error.None); + public static Result Failure(Error error) => new(false, error); + + public static implicit operator Result(Error error) => Failure(error); +} + +public sealed class Result : Result +{ + private readonly TValue? _value; + + private Result(TValue? value, bool isSuccess, Error error) + : base(isSuccess, error) => _value = value; + + public TValue Value => IsSuccess + ? _value! + : throw new InvalidOperationException("A failed result has no value."); + + public static implicit operator Result(TValue value) => + new(value, true, Error.None); + + public static implicit operator Result(Error error) => + new(default, false, error); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Domain/ToDoApp.Domain.csproj b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/EventTicketing.Domain.csproj similarity index 77% rename from csharp-architectural-patterns/CleanArchitecture/ToDoApp.Domain/ToDoApp.Domain.csproj rename to csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/EventTicketing.Domain.csproj index fa71b7ae6a..b760144708 100644 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Domain/ToDoApp.Domain.csproj +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/EventTicketing.Domain.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/Event.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/Event.cs new file mode 100644 index 0000000000..4b4caa21c8 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/Event.cs @@ -0,0 +1,43 @@ +using EventTicketing.Domain.Common; + +namespace EventTicketing.Domain.Events; + +public sealed class Event +{ + private Event(string name, int capacity) + { + Name = name; + Capacity = capacity; + } + + public int Id { get; private set; } + public string Name { get; private set; } + public int Capacity { get; private set; } + public int TicketsSold { get; private set; } + + public int TicketsLeft => Capacity - TicketsSold; + + public static Event Create(string name, int capacity) + { + if (string.IsNullOrWhiteSpace(name)) + throw new DomainException("An event needs a name."); + + if (capacity <= 0) + throw new DomainException("Capacity must be positive."); + + return new Event(name, capacity); + } + + public Result Reserve(int quantity) + { + if (quantity <= 0) + throw new DomainException("Quantity must be positive."); + + if (quantity > TicketsLeft) + return EventErrors.SoldOut(TicketsLeft, quantity); + + TicketsSold += quantity; + + return Result.Success(); + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/EventErrors.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/EventErrors.cs new file mode 100644 index 0000000000..c3f9286a7c --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/EventErrors.cs @@ -0,0 +1,12 @@ +using EventTicketing.Domain.Common; + +namespace EventTicketing.Domain.Events; + +public static class EventErrors +{ + public static Error NotFound(int eventId) => + Error.NotFound("Event.NotFound", $"Event {eventId} was not found."); + + public static Error SoldOut(int ticketsLeft, int requested) => + Error.Conflict("Event.SoldOut", $"Only {ticketsLeft} tickets left, {requested} requested."); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/DependencyInjection.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000000..050854fe79 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/DependencyInjection.cs @@ -0,0 +1,23 @@ +using EventTicketing.Application.Abstractions; +using EventTicketing.Application.Events; +using EventTicketing.Infrastructure.Events; +using EventTicketing.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace EventTicketing.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, string connectionString) + { + services.AddDbContext(options => options.UseSqlite(connectionString)); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/EventTicketing.Infrastructure.csproj b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/EventTicketing.Infrastructure.csproj new file mode 100644 index 0000000000..0a49afecdd --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/EventTicketing.Infrastructure.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventReadRepository.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventReadRepository.cs new file mode 100644 index 0000000000..9db8ad92cf --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventReadRepository.cs @@ -0,0 +1,17 @@ +using EventTicketing.Application.Events; +using EventTicketing.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace EventTicketing.Infrastructure.Events; + +public sealed class EventReadRepository(TicketingDbContext dbContext) : IEventReadRepository +{ + public Task GetAvailabilityAsync( + int eventId, CancellationToken cancellationToken = default) => + dbContext.Events + .AsNoTracking() + .Where(e => e.Id == eventId) + .Select(e => new EventAvailabilityResponse( + e.Id, e.Name, e.Capacity, e.Capacity - e.TicketsSold)) + .FirstOrDefaultAsync(cancellationToken); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventRepository.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventRepository.cs new file mode 100644 index 0000000000..0309acfbab --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventRepository.cs @@ -0,0 +1,12 @@ +using EventTicketing.Application.Events; +using EventTicketing.Domain.Events; +using EventTicketing.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace EventTicketing.Infrastructure.Events; + +public sealed class EventRepository(TicketingDbContext dbContext) : IEventRepository +{ + public Task GetByIdAsync(int eventId, CancellationToken cancellationToken = default) => + dbContext.Events.FirstOrDefaultAsync(e => e.Id == eventId, cancellationToken); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/DatabaseSeeder.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/DatabaseSeeder.cs new file mode 100644 index 0000000000..90fde28419 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/DatabaseSeeder.cs @@ -0,0 +1,25 @@ +using EventTicketing.Domain.Events; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace EventTicketing.Infrastructure.Persistence; + +public static class DatabaseSeeder +{ + public static async Task SeedDatabaseAsync(this IServiceProvider services) + { + using var scope = services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + await dbContext.Database.EnsureCreatedAsync(); + + if (!await dbContext.Events.AnyAsync()) + { + dbContext.Events.AddRange( + Event.Create("Clean Architecture Live", capacity: 100), + Event.Create("Tiny Jazz Club Night", capacity: 2)); + + await dbContext.SaveChangesAsync(); + } + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/TicketingDbContext.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/TicketingDbContext.cs new file mode 100644 index 0000000000..bb9e6806b2 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/TicketingDbContext.cs @@ -0,0 +1,19 @@ +using EventTicketing.Domain.Events; +using Microsoft.EntityFrameworkCore; + +namespace EventTicketing.Infrastructure.Persistence; + +public sealed class TicketingDbContext(DbContextOptions options) + : DbContext(options) +{ + public DbSet Events => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(builder => + { + builder.HasKey(e => e.Id); + builder.Property(e => e.Name).HasMaxLength(200); + }); + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/UnitOfWork.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/UnitOfWork.cs new file mode 100644 index 0000000000..dddcbcdef5 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/UnitOfWork.cs @@ -0,0 +1,9 @@ +using EventTicketing.Application.Abstractions; + +namespace EventTicketing.Infrastructure.Persistence; + +public sealed class UnitOfWork(TicketingDbContext dbContext) : IUnitOfWork +{ + public Task SaveChangesAsync(CancellationToken cancellationToken = default) => + dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/Fakes.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/Fakes.cs new file mode 100644 index 0000000000..a2b91f2c22 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/Fakes.cs @@ -0,0 +1,22 @@ +using EventTicketing.Application.Abstractions; +using EventTicketing.Application.Events; +using EventTicketing.Domain.Events; + +namespace EventTicketing.UnitTests.Application; + +internal sealed class FakeEventRepository(Event? ev) : IEventRepository +{ + public Task GetByIdAsync(int eventId, CancellationToken cancellationToken = default) => + Task.FromResult(ev); +} + +internal sealed class FakeUnitOfWork : IUnitOfWork +{ + public int SaveCount { get; private set; } + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + SaveCount++; + return Task.CompletedTask; + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/ReserveTicketsHandlerTests.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/ReserveTicketsHandlerTests.cs new file mode 100644 index 0000000000..12a26bd66b --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/ReserveTicketsHandlerTests.cs @@ -0,0 +1,49 @@ +using EventTicketing.Application.Events; +using EventTicketing.Domain.Events; + +namespace EventTicketing.UnitTests.Application; + +public class ReserveTicketsHandlerTests +{ + [Fact] + public async Task HandleAsync_WhenTicketsAreAvailable_ReservesAndSaves() + { + var unitOfWork = new FakeUnitOfWork(); + var ev = Event.Create("Clean Architecture Live", capacity: 100); + var handler = new ReserveTicketsHandler(new FakeEventRepository(ev), unitOfWork); + + var result = await handler.HandleAsync( + new ReserveTicketsCommand(EventId: 1, Quantity: 2), TestContext.Current.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.Equal(98, result.Value.TicketsLeft); + Assert.Equal(1, unitOfWork.SaveCount); + } + + [Fact] + public async Task HandleAsync_WhenEventIsSoldOut_ReturnsConflictAndSavesNothing() + { + var unitOfWork = new FakeUnitOfWork(); + var ev = Event.Create("Clean Architecture Live", capacity: 1); + var handler = new ReserveTicketsHandler(new FakeEventRepository(ev), unitOfWork); + + var result = await handler.HandleAsync( + new ReserveTicketsCommand(EventId: 1, Quantity: 2), TestContext.Current.CancellationToken); + + Assert.True(result.IsFailure); + Assert.Equal("Event.SoldOut", result.Error.Code); + Assert.Equal(0, unitOfWork.SaveCount); + } + + [Fact] + public async Task HandleAsync_WhenEventDoesNotExist_ReturnsNotFound() + { + var handler = new ReserveTicketsHandler(new FakeEventRepository(null), new FakeUnitOfWork()); + + var result = await handler.HandleAsync( + new ReserveTicketsCommand(EventId: 42, Quantity: 2), TestContext.Current.CancellationToken); + + Assert.True(result.IsFailure); + Assert.Equal("Event.NotFound", result.Error.Code); + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Domain/EventTests.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Domain/EventTests.cs new file mode 100644 index 0000000000..ada11ff2aa --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Domain/EventTests.cs @@ -0,0 +1,38 @@ +using EventTicketing.Domain.Common; +using EventTicketing.Domain.Events; + +namespace EventTicketing.UnitTests.Domain; + +public class EventTests +{ + [Fact] + public void Reserve_WhenEnoughTicketsAreLeft_SellsThem() + { + var ev = Event.Create("Clean Architecture Live", capacity: 100); + + var result = ev.Reserve(3); + + Assert.True(result.IsSuccess); + Assert.Equal(97, ev.TicketsLeft); + } + + [Fact] + public void Reserve_WhenTooFewTicketsAreLeft_ReturnsSoldOutAndSellsNothing() + { + var ev = Event.Create("Clean Architecture Live", capacity: 2); + + var result = ev.Reserve(3); + + Assert.True(result.IsFailure); + Assert.Equal("Event.SoldOut", result.Error.Code); + Assert.Equal(0, ev.TicketsSold); + } + + [Fact] + public void Reserve_WithNonPositiveQuantity_Throws() + { + var ev = Event.Create("Clean Architecture Live", capacity: 100); + + Assert.Throws(() => ev.Reserve(0)); + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/EventTicketing.UnitTests.csproj b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/EventTicketing.UnitTests.csproj new file mode 100644 index 0000000000..5fdfd0c23f --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/EventTicketing.UnitTests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.sln b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.sln new file mode 100644 index 0000000000..629ee5d067 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.sln @@ -0,0 +1,104 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Domain", "EventTicketing.Domain\EventTicketing.Domain.csproj", "{159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Application", "EventTicketing.Application\EventTicketing.Application.csproj", "{A446F8F5-A9D2-4B20-B400-634C8601F953}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Infrastructure", "EventTicketing.Infrastructure\EventTicketing.Infrastructure.csproj", "{7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Api", "EventTicketing.Api\EventTicketing.Api.csproj", "{CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.UnitTests", "EventTicketing.UnitTests\EventTicketing.UnitTests.csproj", "{DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.ArchitectureTests", "EventTicketing.ArchitectureTests\EventTicketing.ArchitectureTests.csproj", "{2EA7392E-A820-4312-84E1-5F35C413019E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Debug|x64.ActiveCfg = Debug|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Debug|x64.Build.0 = Debug|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Debug|x86.ActiveCfg = Debug|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Debug|x86.Build.0 = Debug|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Release|Any CPU.Build.0 = Release|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Release|x64.ActiveCfg = Release|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Release|x64.Build.0 = Release|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Release|x86.ActiveCfg = Release|Any CPU + {159B8B27-7ECF-4DEE-AA8E-975ABCDE12C9}.Release|x86.Build.0 = Release|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Debug|x64.ActiveCfg = Debug|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Debug|x64.Build.0 = Debug|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Debug|x86.ActiveCfg = Debug|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Debug|x86.Build.0 = Debug|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Release|Any CPU.Build.0 = Release|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Release|x64.ActiveCfg = Release|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Release|x64.Build.0 = Release|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Release|x86.ActiveCfg = Release|Any CPU + {A446F8F5-A9D2-4B20-B400-634C8601F953}.Release|x86.Build.0 = Release|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Debug|x64.ActiveCfg = Debug|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Debug|x64.Build.0 = Debug|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Debug|x86.ActiveCfg = Debug|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Debug|x86.Build.0 = Debug|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Release|Any CPU.Build.0 = Release|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Release|x64.ActiveCfg = Release|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Release|x64.Build.0 = Release|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Release|x86.ActiveCfg = Release|Any CPU + {7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}.Release|x86.Build.0 = Release|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Debug|x64.ActiveCfg = Debug|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Debug|x64.Build.0 = Debug|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Debug|x86.Build.0 = Debug|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Release|Any CPU.Build.0 = Release|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Release|x64.ActiveCfg = Release|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Release|x64.Build.0 = Release|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Release|x86.ActiveCfg = Release|Any CPU + {CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}.Release|x86.Build.0 = Release|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Debug|x64.Build.0 = Debug|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Debug|x86.ActiveCfg = Debug|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Debug|x86.Build.0 = Debug|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Release|Any CPU.Build.0 = Release|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Release|x64.ActiveCfg = Release|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Release|x64.Build.0 = Release|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Release|x86.ActiveCfg = Release|Any CPU + {DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}.Release|x86.Build.0 = Release|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Debug|x64.ActiveCfg = Debug|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Debug|x64.Build.0 = Debug|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Debug|x86.ActiveCfg = Debug|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Debug|x86.Build.0 = Debug|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Release|Any CPU.Build.0 = Release|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Release|x64.ActiveCfg = Release|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Release|x64.Build.0 = Release|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Release|x86.ActiveCfg = Release|Any CPU + {2EA7392E-A820-4312-84E1-5F35C413019E}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/Controllers/ToDoItemController.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/Controllers/ToDoItemController.cs deleted file mode 100644 index ad3775ce93..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/Controllers/ToDoItemController.cs +++ /dev/null @@ -1,25 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Mvc; -using ToDoApp.Application.Commands.CreateToDo; -using ToDoApp.Application.Queries.ToDoItem; - -namespace ToDoApp.API.Controllers; - -[Route("api/[controller]")] -[ApiController] -public class ToDoItemController(IMediator mediator) : ControllerBase -{ - [HttpGet] - public async Task Get() - { - return Ok(await mediator.Send(new ToDoItemQuery())); - } - - [HttpPost] - public async Task Post([FromBody] CreateToDoItemCommand command) - { - await mediator.Send(command); - - return Created(); - } -} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/Program.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/Program.cs deleted file mode 100644 index 458cd1c224..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/Program.cs +++ /dev/null @@ -1,29 +0,0 @@ - -using ToDoApp.Domain.Interfaces; -using ToDoApp.Persistence; -using ToDoApp.Application.IoC; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddControllers(); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -builder.Services.AddApplicationDependencies(); -builder.Services.AddSingleton(); - -var app = builder.Build(); - -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -app.UseHttpsRedirection(); - -app.UseAuthorization(); - -app.MapControllers(); - -app.Run(); \ No newline at end of file diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/Properties/launchSettings.json b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/Properties/launchSettings.json deleted file mode 100644 index 3de382f131..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/Properties/launchSettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:7170;http://localhost:5157", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/ToDoApp.API.csproj b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/ToDoApp.API.csproj deleted file mode 100644 index 7a0d5f1ff9..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/ToDoApp.API.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net8.0 - enable - enable - true - - - - - - - - - - - - - diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/CreateTodoItemCommandHandlerTests.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/CreateTodoItemCommandHandlerTests.cs deleted file mode 100644 index 1f9c6e43e3..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/CreateTodoItemCommandHandlerTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using ToDoApp.Application.Commands.CreateToDo; -using ToDoApp.Domain.Entities; - -namespace ToDoApp.Application.Tests; - -public class CreateToDoItemCommandHandlerTests -{ - [Fact] - public void GivenCreateToDoItemCommandHandler_WhenHandleCalled_ThenCreateNewToDoItem() - { - // Arrange - var toDoRepositoryMock = new Mock(); - toDoRepositoryMock.Setup(x => x.CreateAsync(It.IsAny())) - .ReturnsAsync(1); - var createToDoItemCommandHandler = new CreateToDoItemCommandHandler(toDoRepositoryMock.Object); - - var createToDoItemCommand = new CreateToDoItemCommand - { - Description = "Test Description" - }; - - // Act - var result = createToDoItemCommandHandler.Handle(createToDoItemCommand, CancellationToken.None).Result; - - // Assert - Assert.Equal(1, result); - } -} \ No newline at end of file diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/GlobalUsings.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/GlobalUsings.cs deleted file mode 100644 index 4ff6ad3448..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/GlobalUsings.cs +++ /dev/null @@ -1,3 +0,0 @@ -global using Xunit; -global using Moq; -global using ToDoApp.Domain.Interfaces; \ No newline at end of file diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/ToDoApp.Application.Tests.csproj b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/ToDoApp.Application.Tests.csproj deleted file mode 100644 index 84532e8f70..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/ToDoApp.Application.Tests.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/ToDoItemQueryHandlerTests.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/ToDoItemQueryHandlerTests.cs deleted file mode 100644 index c93d4a670a..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application.Tests/ToDoItemQueryHandlerTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using ToDoApp.Application.Queries.ToDoItem; - -namespace ToDoApp.Application.Tests; - -public class ToDoItemQueryHandlerTests -{ - [Fact] - public async Task GivenToDoItemQueryHandler_WhenHandleCalled_ThenReturnToDoItems() - { - // Arrange - var mockRepository = new Mock(); - mockRepository.Setup(x => x.GetAllAsync()) - .ReturnsAsync( - [ - new() { Description = "Item 1" }, - new() { Description = "Item 2" } - ]); - - var handler = new ToDoItemQueryHandler(mockRepository.Object); - var query = new ToDoItemQuery(); - - // Act - var result = await handler.Handle(query, CancellationToken.None); - - // Assert - Assert.Equal(2, result.Count); - Assert.Equal("Item 1", result[0].Description); - Assert.Equal("Item 2", result[^1].Description); - } -} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Commands/CreateToDo/CreateToDoItemCommand.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Commands/CreateToDo/CreateToDoItemCommand.cs deleted file mode 100644 index 92e5a804d9..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Commands/CreateToDo/CreateToDoItemCommand.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MediatR; - -namespace ToDoApp.Application.Commands.CreateToDo; - -public class CreateToDoItemCommand : IRequest -{ - public required string Description { get; set; } -} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Commands/CreateToDo/CreateTodoItemCommandHandler.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Commands/CreateToDo/CreateTodoItemCommandHandler.cs deleted file mode 100644 index 8f2cb57b7a..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Commands/CreateToDo/CreateTodoItemCommandHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; -using ToDoApp.Domain.Entities; -using ToDoApp.Domain.Interfaces; - -namespace ToDoApp.Application.Commands.CreateToDo; - -public class CreateToDoItemCommandHandler(IToDoRepository toDoRepository) - : IRequestHandler -{ - public Task Handle( - CreateToDoItemCommand request, CancellationToken cancellationToken) - { - var item = new ToDoItem - { - Description = request.Description - }; - - return toDoRepository.CreateAsync(item); - } -} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/IoC/ServiceCollectionExtensions.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/IoC/ServiceCollectionExtensions.cs deleted file mode 100644 index 20a4b4eac6..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/IoC/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace ToDoApp.Application.IoC; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddApplicationDependencies(this IServiceCollection services) - { - services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(AppDomain.CurrentDomain.GetAssemblies())); - - return services; - } -} \ No newline at end of file diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Queries/ToDoItem/ToDoItemQuery.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Queries/ToDoItem/ToDoItemQuery.cs deleted file mode 100644 index 706a453433..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Queries/ToDoItem/ToDoItemQuery.cs +++ /dev/null @@ -1,7 +0,0 @@ -using MediatR; -namespace ToDoApp.Application.Queries.ToDoItem; - -public class ToDoItemQuery - : IRequest> -{ -} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Queries/ToDoItem/ToDoItemQueryHandler.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Queries/ToDoItem/ToDoItemQueryHandler.cs deleted file mode 100644 index 784c302f94..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/Queries/ToDoItem/ToDoItemQueryHandler.cs +++ /dev/null @@ -1,14 +0,0 @@ -using MediatR; -using ToDoApp.Domain.Interfaces; - -namespace ToDoApp.Application.Queries.ToDoItem; - -public class ToDoItemQueryHandler(IToDoRepository toDoRepository) - : IRequestHandler> -{ - public Task> Handle( - ToDoItemQuery request, CancellationToken cancellationToken) - { - return toDoRepository.GetAllAsync(); - } -} \ No newline at end of file diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/ToDoApp.Application.csproj b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/ToDoApp.Application.csproj deleted file mode 100644 index d127c92f63..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Application/ToDoApp.Application.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Domain/Entities/ToDoItem.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Domain/Entities/ToDoItem.cs deleted file mode 100644 index ed4089f00f..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Domain/Entities/ToDoItem.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ToDoApp.Domain.Entities; - -public class ToDoItem -{ - public int Id { get; set; } - public required string Description { get; set; } - public bool IsDone { get; set; } -} \ No newline at end of file diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Domain/Interfaces/IToDoRepository.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Domain/Interfaces/IToDoRepository.cs deleted file mode 100644 index e5a7b9d1ec..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Domain/Interfaces/IToDoRepository.cs +++ /dev/null @@ -1,9 +0,0 @@ -using ToDoApp.Domain.Entities; - -namespace ToDoApp.Domain.Interfaces; - -public interface IToDoRepository -{ - Task> GetAllAsync(); - Task CreateAsync(ToDoItem item); -} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Infrastructure/ToDoApp.Infrastructure.csproj b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Infrastructure/ToDoApp.Infrastructure.csproj deleted file mode 100644 index fa71b7ae6a..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Infrastructure/ToDoApp.Infrastructure.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net8.0 - enable - enable - - - diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/InMemoryToDoRepository.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/InMemoryToDoRepository.cs deleted file mode 100644 index 9db30cc000..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/InMemoryToDoRepository.cs +++ /dev/null @@ -1,21 +0,0 @@ -using ToDoApp.Domain.Entities; -using ToDoApp.Domain.Interfaces; - -namespace ToDoApp.Persistence; - -public class InMemoryToDoRepository : IToDoRepository -{ - private static readonly List _items = []; - - public Task CreateAsync(ToDoItem item) - { - _items.Add(item); - - return Task.FromResult(item.Id); - } - - public Task> GetAllAsync() - { - return Task.FromResult(_items); - } -} \ No newline at end of file diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/SqlTodoRepository.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/SqlTodoRepository.cs deleted file mode 100644 index 4215e690ed..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/SqlTodoRepository.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using ToDoApp.Domain.Entities; -using ToDoApp.Domain.Interfaces; - -namespace ToDoApp.Persistence; - -public class SqlToDoRepository : IToDoRepository -{ - private readonly ToDoDbContext _context; - - public SqlToDoRepository(ToDoDbContext context) - { - _context = context; - } - - public Task CreateAsync(ToDoItem item) - { - _context.ToDoItems.Add(item); - - return _context.SaveChangesAsync(); - } - - public Task> GetAllAsync() - { - return _context.ToDoItems.ToListAsync(); - } -} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/ToDoDbContext.cs b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/ToDoDbContext.cs deleted file mode 100644 index e9f82a24f7..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.Persistence/ToDoDbContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using ToDoApp.Domain.Entities; - -namespace ToDoApp.Persistence; - -public class ToDoDbContext : DbContext -{ - public ToDoDbContext(DbContextOptions options) - : base(options) - { - } - - public DbSet ToDoItems { get; set; } -} diff --git a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.sln b/csharp-architectural-patterns/CleanArchitecture/ToDoApp.sln deleted file mode 100644 index e0cce71573..0000000000 --- a/csharp-architectural-patterns/CleanArchitecture/ToDoApp.sln +++ /dev/null @@ -1,71 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.8.34309.116 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ToDoApp.API", "ToDoApp.API\ToDoApp.API.csproj", "{FEE4EF92-E8AC-464F-B63C-DDDAABD1A0D5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Presentation", "Presentation", "{AEE51D5C-AAC9-41D5-98FE-1C9EC4FEB36B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{8F778E12-2BD5-489A-BF4C-D1E90ECDF4FD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{753910C3-3BA3-4933-AF53-62631C7E521F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ToDoApp.Domain", "ToDoApp.Domain\ToDoApp.Domain.csproj", "{9C7A1444-55A8-4666-A2A8-232FF90A27EF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ToDoApp.Application", "ToDoApp.Application\ToDoApp.Application.csproj", "{24041317-18D4-4351-A84F-1F1C83438EC2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ToDoApp.Persistence", "ToDoApp.Persistence\ToDoApp.Persistence.csproj", "{8205DAAB-E1E2-4098-B9F7-03B9900BDC41}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ToDoApp.Infrastructure", "ToDoApp.Infrastructure\ToDoApp.Infrastructure.csproj", "{9CC7CB49-58D3-42EB-A1B9-A021CF621910}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8BFD18FA-5AC2-4A02-AA0E-77F65C079165}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ToDoApp.Application.Tests", "ToDoApp.Application.Tests\ToDoApp.Application.Tests.csproj", "{DA33F6F0-BF91-4DEF-80BE-8B2E2DDC4CC9}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FEE4EF92-E8AC-464F-B63C-DDDAABD1A0D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FEE4EF92-E8AC-464F-B63C-DDDAABD1A0D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FEE4EF92-E8AC-464F-B63C-DDDAABD1A0D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FEE4EF92-E8AC-464F-B63C-DDDAABD1A0D5}.Release|Any CPU.Build.0 = Release|Any CPU - {9C7A1444-55A8-4666-A2A8-232FF90A27EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9C7A1444-55A8-4666-A2A8-232FF90A27EF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9C7A1444-55A8-4666-A2A8-232FF90A27EF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9C7A1444-55A8-4666-A2A8-232FF90A27EF}.Release|Any CPU.Build.0 = Release|Any CPU - {24041317-18D4-4351-A84F-1F1C83438EC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {24041317-18D4-4351-A84F-1F1C83438EC2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {24041317-18D4-4351-A84F-1F1C83438EC2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {24041317-18D4-4351-A84F-1F1C83438EC2}.Release|Any CPU.Build.0 = Release|Any CPU - {8205DAAB-E1E2-4098-B9F7-03B9900BDC41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8205DAAB-E1E2-4098-B9F7-03B9900BDC41}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8205DAAB-E1E2-4098-B9F7-03B9900BDC41}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8205DAAB-E1E2-4098-B9F7-03B9900BDC41}.Release|Any CPU.Build.0 = Release|Any CPU - {9CC7CB49-58D3-42EB-A1B9-A021CF621910}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9CC7CB49-58D3-42EB-A1B9-A021CF621910}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9CC7CB49-58D3-42EB-A1B9-A021CF621910}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9CC7CB49-58D3-42EB-A1B9-A021CF621910}.Release|Any CPU.Build.0 = Release|Any CPU - {DA33F6F0-BF91-4DEF-80BE-8B2E2DDC4CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DA33F6F0-BF91-4DEF-80BE-8B2E2DDC4CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DA33F6F0-BF91-4DEF-80BE-8B2E2DDC4CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DA33F6F0-BF91-4DEF-80BE-8B2E2DDC4CC9}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {FEE4EF92-E8AC-464F-B63C-DDDAABD1A0D5} = {AEE51D5C-AAC9-41D5-98FE-1C9EC4FEB36B} - {9C7A1444-55A8-4666-A2A8-232FF90A27EF} = {8F778E12-2BD5-489A-BF4C-D1E90ECDF4FD} - {24041317-18D4-4351-A84F-1F1C83438EC2} = {8F778E12-2BD5-489A-BF4C-D1E90ECDF4FD} - {8205DAAB-E1E2-4098-B9F7-03B9900BDC41} = {753910C3-3BA3-4933-AF53-62631C7E521F} - {9CC7CB49-58D3-42EB-A1B9-A021CF621910} = {753910C3-3BA3-4933-AF53-62631C7E521F} - {DA33F6F0-BF91-4DEF-80BE-8B2E2DDC4CC9} = {8BFD18FA-5AC2-4A02-AA0E-77F65C079165} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {6B81A701-CA9A-4EEB-8583-C471B446F564} - EndGlobalSection -EndGlobal