From 5d5d13a65d74476047e806aa7dc0460cdd701615 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Thu, 24 Sep 2026 20:13:06 +0200 Subject: [PATCH 1/2] Clean Architecture: simplify the EventTicketing sample to follow the step-by-step article One entity, one command, one query and one repository, built with the article's own steps on .NET 10. - Domain: Event, EventErrors, Result - Application: IEventRepository, ReserveTickets, GetEventAvailability, DependencyInjection - Infrastructure: TicketingDbContext (seed data with HasData), EventRepository, DependencyInjection - Api: EventEndpoints, ResultExtensions, Program, EventTicketing.Api.http (launch profile on port 5000) - Tests: 4 unit tests and 2 architecture tests (NetArchTest.Rules) Removed: IUnitOfWork, the read repository, DatabaseSeeder, DomainException, OpenAPI and the feature folders. --- .../Contracts/ReserveTicketsRequest.cs | 5 - .../{Endpoints => }/EventEndpoints.cs | 13 +- .../EventTicketing.Api.csproj | 4 - .../EventTicketing.Api.http | 28 ++++ .../Extensions/ResultExtensions.cs | 25 --- .../EventTicketing.Api/Program.cs | 10 +- .../Properties/launchSettings.json | 11 +- .../EventTicketing.Api/ResultExtensions.cs | 21 +++ .../Abstractions/IUnitOfWork.cs | 6 - .../DependencyInjection.cs | 1 - .../Events/GetEventAvailability.cs | 21 --- .../Events/IEventReadRepository.cs | 7 - .../Events/IEventRepository.cs | 8 - .../GetEventAvailability.cs | 20 +++ .../IEventRepository.cs | 12 ++ .../{Events => }/ReserveTickets.cs | 14 +- .../DependencyRuleTests.cs | 33 +--- .../EventTicketing.ArchitectureTests.csproj | 2 +- .../Common/DomainException.cs | 3 - .../EventTicketing.Domain/Common/Error.cs | 23 --- .../EventTicketing.Domain/Common/Result.cs | 43 ----- .../{Events => }/Event.cs | 14 +- .../EventTicketing.Domain/EventErrors.cs | 10 ++ .../Events/EventErrors.cs | 12 -- .../EventTicketing.Domain/Result.cs | 39 +++++ .../DependencyInjection.cs | 8 +- .../EventRepository.cs | 21 +++ .../Events/EventReadRepository.cs | 17 -- .../Events/EventRepository.cs | 12 -- .../Persistence/DatabaseSeeder.cs | 25 --- .../Persistence/UnitOfWork.cs | 9 - .../{Persistence => }/TicketingDbContext.cs | 9 +- .../Application/Fakes.cs | 22 --- .../Application/ReserveTicketsHandlerTests.cs | 49 ------ .../Domain/EventTests.cs | 38 ----- .../EventTicketing.UnitTests/EventTests.cs | 29 ++++ .../ReserveTicketsHandlerTests.cs | 52 ++++++ .../CleanArchitecture/EventTicketing.sln | 156 +++++++++--------- 38 files changed, 355 insertions(+), 477 deletions(-) delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Contracts/ReserveTicketsRequest.cs rename csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/{Endpoints => }/EventEndpoints.cs (78%) create mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.http delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Extensions/ResultExtensions.cs create mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/ResultExtensions.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Abstractions/IUnitOfWork.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/GetEventAvailability.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/IEventReadRepository.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/IEventRepository.cs create mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/GetEventAvailability.cs create mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/IEventRepository.cs rename csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/{Events => }/ReserveTickets.cs (68%) delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/DomainException.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/Error.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/Result.cs rename csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/{Events => }/Event.cs (65%) create mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/EventErrors.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/EventErrors.cs create mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Result.cs create mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/EventRepository.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventReadRepository.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventRepository.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/DatabaseSeeder.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/UnitOfWork.cs rename csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/{Persistence => }/TicketingDbContext.cs (58%) delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/Fakes.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/ReserveTicketsHandlerTests.cs delete mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Domain/EventTests.cs create mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/EventTests.cs create mode 100644 csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/ReserveTicketsHandlerTests.cs diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Contracts/ReserveTicketsRequest.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Contracts/ReserveTicketsRequest.cs deleted file mode 100644 index 43839a1b5..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Contracts/ReserveTicketsRequest.cs +++ /dev/null @@ -1,5 +0,0 @@ -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/EventEndpoints.cs similarity index 78% rename from csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Endpoints/EventEndpoints.cs rename to csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventEndpoints.cs index 0f649dc29..a345a61d2 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Endpoints/EventEndpoints.cs +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventEndpoints.cs @@ -1,12 +1,13 @@ -using EventTicketing.Api.Contracts; -using EventTicketing.Api.Extensions; -using EventTicketing.Application.Events; +using System.ComponentModel.DataAnnotations; +using EventTicketing.Application; -namespace EventTicketing.Api.Endpoints; +namespace EventTicketing.Api; + +public sealed record ReserveTicketsRequest([property: Range(1, 20)] int Quantity); public static class EventEndpoints { - public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder app) + public static void MapEventEndpoints(this IEndpointRouteBuilder app) { var events = app.MapGroup("/api/events"); @@ -31,7 +32,5 @@ public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder 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 index 08002b12d..3f3c38cb2 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.csproj +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.csproj @@ -5,10 +5,6 @@ - - - - net10.0 enable diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.http b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.http new file mode 100644 index 000000000..7cab3964c --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.http @@ -0,0 +1,28 @@ +@baseUrl = http://localhost:5000 + +GET {{baseUrl}}/api/events/1 + +### + +POST {{baseUrl}}/api/events/1/reservations +Content-Type: application/json + +{ "quantity": 3 } + +### + +POST {{baseUrl}}/api/events/2/reservations +Content-Type: application/json + +{ "quantity": 3 } + +### + +POST {{baseUrl}}/api/events/1/reservations +Content-Type: application/json + +{ "quantity": 0 } + +### + +GET {{baseUrl}}/api/events/99 diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Extensions/ResultExtensions.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Extensions/ResultExtensions.cs deleted file mode 100644 index 8e65ecdab..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Extensions/ResultExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 index fcd329890..4befcf897 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Program.cs +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Program.cs @@ -1,7 +1,6 @@ -using EventTicketing.Api.Endpoints; +using EventTicketing.Api; using EventTicketing.Application; using EventTicketing.Infrastructure; -using EventTicketing.Infrastructure.Persistence; var builder = WebApplication.CreateBuilder(args); @@ -10,16 +9,15 @@ builder.Services.AddProblemDetails(); builder.Services.AddValidation(); -builder.Services.AddOpenApi(); var app = builder.Build(); app.UseExceptionHandler(); -if (app.Environment.IsDevelopment()) +using (var scope = app.Services.CreateScope()) { - app.MapOpenApi(); - await app.Services.SeedDatabaseAsync(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); } app.MapEventEndpoints(); diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Properties/launchSettings.json b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Properties/launchSettings.json index 4fa2de2c5..b0ac7136a 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Properties/launchSettings.json +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/Properties/launchSettings.json @@ -4,11 +4,20 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7052;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/ResultExtensions.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/ResultExtensions.cs new file mode 100644 index 000000000..f893963bd --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/ResultExtensions.cs @@ -0,0 +1,21 @@ +using EventTicketing.Domain; + +namespace EventTicketing.Api; + +public static class ResultExtensions +{ + public static IResult ToProblem(this Result result) + { + var error = result.Error + ?? throw new InvalidOperationException("A successful result is not a problem."); + + var statusCode = error.Type switch + { + ErrorType.NotFound => StatusCodes.Status404NotFound, + ErrorType.Conflict => StatusCodes.Status409Conflict, + _ => StatusCodes.Status500InternalServerError + }; + + return TypedResults.Problem(statusCode: statusCode, title: error.Code, detail: error.Description); + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Abstractions/IUnitOfWork.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Abstractions/IUnitOfWork.cs deleted file mode 100644 index c56b8fcf6..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Abstractions/IUnitOfWork.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 index 66c8325f4..9e37c77a4 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/DependencyInjection.cs +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/DependencyInjection.cs @@ -1,4 +1,3 @@ -using EventTicketing.Application.Events; using Microsoft.Extensions.DependencyInjection; namespace EventTicketing.Application; diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/GetEventAvailability.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/GetEventAvailability.cs deleted file mode 100644 index b1ffb87d3..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/GetEventAvailability.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index cda14d952..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/IEventReadRepository.cs +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 024f94fe7..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/IEventRepository.cs +++ /dev/null @@ -1,8 +0,0 @@ -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/GetEventAvailability.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/GetEventAvailability.cs new file mode 100644 index 000000000..c38c2d44b --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/GetEventAvailability.cs @@ -0,0 +1,20 @@ +using EventTicketing.Domain; + +namespace EventTicketing.Application; + +public sealed record GetEventAvailabilityQuery(int EventId); + +public sealed record EventAvailability(int EventId, string Name, int TicketsLeft); + +public sealed class GetEventAvailabilityHandler(IEventRepository events) +{ + public async Task> HandleAsync( + GetEventAvailabilityQuery query, CancellationToken cancellationToken = default) + { + var availability = await events.GetAvailabilityAsync(query.EventId, cancellationToken); + if (availability is null) + return EventErrors.NotFound(query.EventId); + + return availability; + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/IEventRepository.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/IEventRepository.cs new file mode 100644 index 000000000..1e2d895e8 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/IEventRepository.cs @@ -0,0 +1,12 @@ +using EventTicketing.Domain; + +namespace EventTicketing.Application; + +public interface IEventRepository +{ + Task GetByIdAsync(int eventId, CancellationToken cancellationToken = default); + + Task GetAvailabilityAsync(int eventId, CancellationToken cancellationToken = default); + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/ReserveTickets.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/ReserveTickets.cs similarity index 68% rename from csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/ReserveTickets.cs rename to csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/ReserveTickets.cs index a00c95183..56ec774a1 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/Events/ReserveTickets.cs +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Application/ReserveTickets.cs @@ -1,14 +1,12 @@ -using EventTicketing.Application.Abstractions; -using EventTicketing.Domain.Common; -using EventTicketing.Domain.Events; +using EventTicketing.Domain; -namespace EventTicketing.Application.Events; +namespace EventTicketing.Application; 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 sealed class ReserveTicketsHandler(IEventRepository events) { public async Task> HandleAsync( ReserveTicketsCommand command, CancellationToken cancellationToken = default) @@ -18,10 +16,10 @@ public async Task> HandleAsync( return EventErrors.NotFound(command.EventId); var reservation = ev.Reserve(command.Quantity); - if (reservation.IsFailure) - return reservation.Error; + if (!reservation.IsSuccess) + return reservation.Error!; - await unitOfWork.SaveChangesAsync(cancellationToken); + await events.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 index 76f335748..76a3edb32 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.ArchitectureTests/DependencyRuleTests.cs +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.ArchitectureTests/DependencyRuleTests.cs @@ -1,26 +1,20 @@ -using EventTicketing.Application.Events; -using EventTicketing.Domain.Events; -using EventTicketing.Infrastructure.Persistence; +using EventTicketing.Application; +using EventTicketing.Domain; 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) + .HaveDependencyOnAny("EventTicketing.Application", "EventTicketing.Infrastructure", "EventTicketing.Api") .GetResult(); - Assert.True(result.IsSuccessful, Describe(result)); + Assert.True(result.IsSuccessful, "Offending types: " + string.Join(", ", result.FailingTypeNames ?? [])); } [Fact] @@ -28,23 +22,10 @@ public void Application_DoesNotDependOnInfrastructureOrTheWeb() { var result = Types.InAssembly(typeof(ReserveTicketsHandler).Assembly) .ShouldNot() - .HaveDependencyOnAny(Infrastructure, Api, "Microsoft.EntityFrameworkCore", "Microsoft.AspNetCore") + .HaveDependencyOnAny("EventTicketing.Infrastructure", "EventTicketing.Api", + "Microsoft.EntityFrameworkCore", "Microsoft.AspNetCore") .GetResult(); - Assert.True(result.IsSuccessful, Describe(result)); + Assert.True(result.IsSuccessful, "Offending types: " + string.Join(", ", result.FailingTypeNames ?? [])); } - - [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 index 1e027d8a6..788e2905f 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.ArchitectureTests/EventTicketing.ArchitectureTests.csproj +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.ArchitectureTests/EventTicketing.ArchitectureTests.csproj @@ -20,7 +20,7 @@ - + diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/DomainException.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/DomainException.cs deleted file mode 100644 index 0ea30629b..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/DomainException.cs +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index c0e7687a2..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/Error.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 67147a661..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Common/Result.cs +++ /dev/null @@ -1,43 +0,0 @@ -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/EventTicketing.Domain/Events/Event.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Event.cs similarity index 65% rename from csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/Event.cs rename to csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Event.cs index 4b4caa21c..8c2b7c2ed 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/Event.cs +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Event.cs @@ -1,6 +1,4 @@ -using EventTicketing.Domain.Common; - -namespace EventTicketing.Domain.Events; +namespace EventTicketing.Domain; public sealed class Event { @@ -19,19 +17,15 @@ private Event(string name, int capacity) 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."); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); return new Event(name, capacity); } public Result Reserve(int quantity) { - if (quantity <= 0) - throw new DomainException("Quantity must be positive."); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); if (quantity > TicketsLeft) return EventErrors.SoldOut(TicketsLeft, quantity); diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/EventErrors.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/EventErrors.cs new file mode 100644 index 000000000..24d232a0c --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/EventErrors.cs @@ -0,0 +1,10 @@ +namespace EventTicketing.Domain; + +public static class EventErrors +{ + public static Error NotFound(int eventId) => + new("Event.NotFound", $"Event {eventId} was not found.", ErrorType.NotFound); + + public static Error SoldOut(int ticketsLeft, int requested) => + new("Event.SoldOut", $"Only {ticketsLeft} tickets left, {requested} requested.", ErrorType.Conflict); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/EventErrors.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/EventErrors.cs deleted file mode 100644 index c3f9286a7..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Events/EventErrors.cs +++ /dev/null @@ -1,12 +0,0 @@ -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.Domain/Result.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Result.cs new file mode 100644 index 000000000..7f406bb58 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Domain/Result.cs @@ -0,0 +1,39 @@ +namespace EventTicketing.Domain; + +public enum ErrorType +{ + NotFound, + Conflict +} + +public sealed record Error(string Code, string Description, ErrorType Type); + +public class Result +{ + protected Result(Error? error) => Error = error; + + public Error? Error { get; } + public bool IsSuccess => Error is null; + + public static Result Success() => new(null); + + public static implicit operator Result(Error error) => new(error); +} + +public sealed class Result : Result +{ + private readonly TValue? _value; + + private Result(TValue? value, Error? error) + : base(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, null); + + public static implicit operator Result(Error error) => + new(default, error); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/DependencyInjection.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/DependencyInjection.cs index 050854fe7..dc5b86a5b 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/DependencyInjection.cs +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/DependencyInjection.cs @@ -1,7 +1,4 @@ -using EventTicketing.Application.Abstractions; -using EventTicketing.Application.Events; -using EventTicketing.Infrastructure.Events; -using EventTicketing.Infrastructure.Persistence; +using EventTicketing.Application; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -13,10 +10,7 @@ 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/EventRepository.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/EventRepository.cs new file mode 100644 index 000000000..dea7a45f1 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/EventRepository.cs @@ -0,0 +1,21 @@ +using EventTicketing.Application; +using EventTicketing.Domain; +using Microsoft.EntityFrameworkCore; + +namespace EventTicketing.Infrastructure; + +public sealed class EventRepository(TicketingDbContext dbContext) : IEventRepository +{ + public Task GetByIdAsync(int eventId, CancellationToken cancellationToken = default) => + dbContext.Events.FirstOrDefaultAsync(e => e.Id == eventId, cancellationToken); + + public Task GetAvailabilityAsync(int eventId, CancellationToken cancellationToken = default) => + dbContext.Events + .AsNoTracking() + .Where(e => e.Id == eventId) + .Select(e => new EventAvailability(e.Id, e.Name, e.Capacity - e.TicketsSold)) + .FirstOrDefaultAsync(cancellationToken); + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) => + dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventReadRepository.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventReadRepository.cs deleted file mode 100644 index 9db8ad92c..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventReadRepository.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 0309acfba..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Events/EventRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 90fde2841..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/DatabaseSeeder.cs +++ /dev/null @@ -1,25 +0,0 @@ -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/UnitOfWork.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/UnitOfWork.cs deleted file mode 100644 index dddcbcdef..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/UnitOfWork.cs +++ /dev/null @@ -1,9 +0,0 @@ -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.Infrastructure/Persistence/TicketingDbContext.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/TicketingDbContext.cs similarity index 58% rename from csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/TicketingDbContext.cs rename to csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/TicketingDbContext.cs index bb9e6806b..93502dbf3 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/Persistence/TicketingDbContext.cs +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Infrastructure/TicketingDbContext.cs @@ -1,7 +1,7 @@ -using EventTicketing.Domain.Events; +using EventTicketing.Domain; using Microsoft.EntityFrameworkCore; -namespace EventTicketing.Infrastructure.Persistence; +namespace EventTicketing.Infrastructure; public sealed class TicketingDbContext(DbContextOptions options) : DbContext(options) @@ -12,8 +12,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(builder => { - builder.HasKey(e => e.Id); builder.Property(e => e.Name).HasMaxLength(200); + + builder.HasData( + new { Id = 1, Name = "Clean Architecture Live", Capacity = 100, TicketsSold = 0 }, + new { Id = 2, Name = "Tiny Jazz Club Night", Capacity = 2, TicketsSold = 0 }); }); } } diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/Fakes.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/Fakes.cs deleted file mode 100644 index a2b91f2c2..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/Fakes.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 12a26bd66..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Application/ReserveTicketsHandlerTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index ada11ff2a..000000000 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/Domain/EventTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -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/EventTests.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/EventTests.cs new file mode 100644 index 000000000..e838af944 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/EventTests.cs @@ -0,0 +1,29 @@ +using EventTicketing.Domain; + +namespace EventTicketing.UnitTests; + +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("Tiny Jazz Club Night", capacity: 2); + + var result = ev.Reserve(3); + + Assert.False(result.IsSuccess); + Assert.Equal("Event.SoldOut", result.Error!.Code); + Assert.Equal(0, ev.TicketsSold); + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/ReserveTicketsHandlerTests.cs b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/ReserveTicketsHandlerTests.cs new file mode 100644 index 000000000..8d04af3b5 --- /dev/null +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.UnitTests/ReserveTicketsHandlerTests.cs @@ -0,0 +1,52 @@ +using EventTicketing.Application; +using EventTicketing.Domain; + +namespace EventTicketing.UnitTests; + +public class ReserveTicketsHandlerTests +{ + [Fact] + public async Task HandleAsync_WhenTicketsAreAvailable_ReservesAndSaves() + { + var events = new FakeEventRepository(Event.Create("Clean Architecture Live", capacity: 100)); + var handler = new ReserveTicketsHandler(events); + + 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, events.SaveCount); + } + + [Fact] + public async Task HandleAsync_WhenEventIsSoldOut_SavesNothing() + { + var events = new FakeEventRepository(Event.Create("Tiny Jazz Club Night", capacity: 1)); + var handler = new ReserveTicketsHandler(events); + + var result = await handler.HandleAsync( + new ReserveTicketsCommand(EventId: 1, Quantity: 2), TestContext.Current.CancellationToken); + + Assert.False(result.IsSuccess); + Assert.Equal("Event.SoldOut", result.Error!.Code); + Assert.Equal(0, events.SaveCount); + } +} + +internal sealed class FakeEventRepository(Event? ev) : IEventRepository +{ + public int SaveCount { get; private set; } + + public Task GetByIdAsync(int eventId, CancellationToken cancellationToken = default) => + Task.FromResult(ev); + + public Task GetAvailabilityAsync(int eventId, CancellationToken cancellationToken = default) => + Task.FromResult(null); + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + SaveCount++; + return Task.CompletedTask; + } +} diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.sln b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.sln index 629ee5d06..7deab074b 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.sln +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.sln @@ -3,17 +3,17 @@ 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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Domain", "EventTicketing.Domain\EventTicketing.Domain.csproj", "{747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Application", "EventTicketing.Application\EventTicketing.Application.csproj", "{A446F8F5-A9D2-4B20-B400-634C8601F953}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Application", "EventTicketing.Application\EventTicketing.Application.csproj", "{414CB068-6303-4C6B-88C3-110DF13AB824}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Infrastructure", "EventTicketing.Infrastructure\EventTicketing.Infrastructure.csproj", "{7E3F5A24-15AC-4C27-8E6C-AC8EFB695138}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Infrastructure", "EventTicketing.Infrastructure\EventTicketing.Infrastructure.csproj", "{77F9DCE0-A684-4F19-A12D-D96498877407}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Api", "EventTicketing.Api\EventTicketing.Api.csproj", "{CDDE590D-17CF-4039-A4E5-8A1EC7A245F7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Api", "EventTicketing.Api\EventTicketing.Api.csproj", "{C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.UnitTests", "EventTicketing.UnitTests\EventTicketing.UnitTests.csproj", "{DE452D9A-440B-44EE-8D6F-CE2D89B5A6E4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.UnitTests", "EventTicketing.UnitTests\EventTicketing.UnitTests.csproj", "{B766BC5E-484B-4F1B-9B7E-69FA58302923}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.ArchitectureTests", "EventTicketing.ArchitectureTests\EventTicketing.ArchitectureTests.csproj", "{2EA7392E-A820-4312-84E1-5F35C413019E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.ArchitectureTests", "EventTicketing.ArchitectureTests\EventTicketing.ArchitectureTests.csproj", "{59EC3582-12E8-4164-B40F-17B4D15B8F61}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,78 +25,78 @@ Global 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 + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Debug|x64.ActiveCfg = Debug|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Debug|x64.Build.0 = Debug|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Debug|x86.ActiveCfg = Debug|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Debug|x86.Build.0 = Debug|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Release|Any CPU.Build.0 = Release|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Release|x64.ActiveCfg = Release|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Release|x64.Build.0 = Release|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Release|x86.ActiveCfg = Release|Any CPU + {747A3FF7-C320-4377-BBC9-3BFDEFDF66F3}.Release|x86.Build.0 = Release|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Debug|Any CPU.Build.0 = Debug|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Debug|x64.ActiveCfg = Debug|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Debug|x64.Build.0 = Debug|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Debug|x86.ActiveCfg = Debug|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Debug|x86.Build.0 = Debug|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Release|Any CPU.ActiveCfg = Release|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Release|Any CPU.Build.0 = Release|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Release|x64.ActiveCfg = Release|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Release|x64.Build.0 = Release|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Release|x86.ActiveCfg = Release|Any CPU + {414CB068-6303-4C6B-88C3-110DF13AB824}.Release|x86.Build.0 = Release|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Debug|x64.ActiveCfg = Debug|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Debug|x64.Build.0 = Debug|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Debug|x86.ActiveCfg = Debug|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Debug|x86.Build.0 = Debug|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Release|Any CPU.Build.0 = Release|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Release|x64.ActiveCfg = Release|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Release|x64.Build.0 = Release|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Release|x86.ActiveCfg = Release|Any CPU + {77F9DCE0-A684-4F19-A12D-D96498877407}.Release|x86.Build.0 = Release|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Debug|x64.Build.0 = Debug|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Debug|x86.Build.0 = Debug|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Release|Any CPU.Build.0 = Release|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Release|x64.ActiveCfg = Release|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Release|x64.Build.0 = Release|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Release|x86.ActiveCfg = Release|Any CPU + {C3EC8948-3309-4EF1-B5CC-58B242B0BAFA}.Release|x86.Build.0 = Release|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Debug|x64.ActiveCfg = Debug|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Debug|x64.Build.0 = Debug|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Debug|x86.ActiveCfg = Debug|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Debug|x86.Build.0 = Debug|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Release|Any CPU.Build.0 = Release|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Release|x64.ActiveCfg = Release|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Release|x64.Build.0 = Release|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Release|x86.ActiveCfg = Release|Any CPU + {B766BC5E-484B-4F1B-9B7E-69FA58302923}.Release|x86.Build.0 = Release|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Debug|x64.ActiveCfg = Debug|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Debug|x64.Build.0 = Debug|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Debug|x86.ActiveCfg = Debug|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Debug|x86.Build.0 = Debug|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Release|Any CPU.Build.0 = Release|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Release|x64.ActiveCfg = Release|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Release|x64.Build.0 = Release|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Release|x86.ActiveCfg = Release|Any CPU + {59EC3582-12E8-4164-B40F-17B4D15B8F61}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From aaaf1b1f087c85d3f4214404454d3e1686dfaaca Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Thu, 24 Sep 2026 20:16:17 +0200 Subject: [PATCH 2/2] Clean Architecture: use full URLs in the .http file --- .../EventTicketing.Api/EventTicketing.Api.http | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.http b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.http index 7cab3964c..2801b7444 100644 --- a/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.http +++ b/csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/EventTicketing.Api.http @@ -1,28 +1,26 @@ -@baseUrl = http://localhost:5000 - -GET {{baseUrl}}/api/events/1 +GET http://localhost:5000/api/events/1 ### -POST {{baseUrl}}/api/events/1/reservations +POST http://localhost:5000/api/events/1/reservations Content-Type: application/json { "quantity": 3 } ### -POST {{baseUrl}}/api/events/2/reservations +POST http://localhost:5000/api/events/2/reservations Content-Type: application/json { "quantity": 3 } ### -POST {{baseUrl}}/api/events/1/reservations +POST http://localhost:5000/api/events/1/reservations Content-Type: application/json { "quantity": 0 } ### -GET {{baseUrl}}/api/events/99 +GET http://localhost:5000/api/events/99