From c72a7c65ee739681fa4f8bdf525d37920bfcd41c Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 27 Sep 2026 19:23:37 +0200 Subject: [PATCH] Vertical Slice Architecture: rebuild the sample as an event ticketing API on net10.0 Replace the net8.0 games-and-consoles sample (MediatR 12.2.0, AutoMapper 13.0.1, EF Core InMemory, Swashbuckle, a shared ServiceManager) with the article's build: one minimal API project with three feature slices, an Event entity that guards its capacity rule, a Result type mapped to problem details, built-in validation, and EF Core SQLite 10.0.12. Tests run the slices against SQLite in memory and an architecture test keeps features from depending on each other. Classic EventTicketing.sln for the CI, launch profile on port 5000 so the .http file works from an IDE, and a folder .gitignore for the SQLite file. Removes the NU1903 advisory on AutoMapper 13.0.1 and the nullability warnings. --- .../VerticalSliceArchitecture/.gitignore | 3 + .../EventTicketing.Tests.csproj | 26 ++++ .../ReserveTicketsTests.cs | 55 ++++++++ .../EventTicketing.Tests/SliceTests.cs | 35 +++++ .../EventTicketing.sln | 48 +++++++ .../EventTicketing/Data/TicketingDbContext.cs | 22 ++++ .../EventTicketing/Domain/Event.cs | 37 ++++++ .../EventTicketing/Domain/EventErrors.cs | 10 ++ .../EventTicketing/Domain/Result.cs | 39 ++++++ .../EventTicketing/EventTicketing.csproj | 13 ++ .../EventTicketing/EventTicketing.http | 37 ++++++ .../Features/Events/GetEventAvailability.cs | 39 ++++++ .../Features/Events/ListAvailableEvents.cs | 17 +++ .../Features/Reservations/ReserveTickets.cs | 44 +++++++ .../EventTicketing/Program.cs | 33 +++++ .../Properties/launchSettings.json | 22 +--- .../EventTicketing/ResultExtensions.cs | 21 +++ .../appsettings.Development.json | 0 .../appsettings.json | 3 + .../Tests/ConsoleServiceTest.cs | 63 --------- .../Tests/GameServiceTest.cs | 124 ------------------ .../Tests/Tests.csproj | 29 ---- .../VerticalSliceArchitecture/Tests/Usings.cs | 6 - .../VerticalSliceArchitecture.sln | 31 ----- .../Data/DataContext.cs | 12 -- .../VerticalSliceArchitecture/Data/Seed.cs | 61 --------- .../VerticalSliceArchitecture/Domain/Game.cs | 11 -- .../Domain/GameConsole.cs | 10 -- .../Features/Consoles/ConsoleService.cs | 29 ---- .../Features/Consoles/ConsolesController.cs | 27 ---- .../Features/Consoles/GetAllConsoles.cs | 41 ------ .../Features/Consoles/IConsoleService.cs | 10 -- .../Features/Consoles/MapperProfile.cs | 13 -- .../Features/Games/AddGameToConsole.cs | 64 --------- .../Exceptions/NoConsoleExistsException.cs | 7 - .../Games/Exceptions/NoGameExistsException.cs | 14 -- .../Features/Games/GameService.cs | 40 ------ .../Features/Games/GamesController.cs | 120 ----------------- .../Features/Games/GetAllGamesForConsole.cs | 48 ------- .../Features/Games/IGameService.cs | 12 -- .../Features/Games/MapperProfile.cs | 14 -- .../Features/Games/RemoveGameFromConsole.cs | 44 ------- .../Features/Games/UpdateGameForConsole.cs | 48 ------- .../VerticalSliceArchitecture/Program.cs | 46 ------- .../ServiceManager/IServiceManager.cs | 12 -- .../ServiceManager/ServiceManager.cs | 45 ------- .../VerticalSliceArchitecture.csproj | 16 --- 47 files changed, 489 insertions(+), 1012 deletions(-) create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/.gitignore create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/EventTicketing.Tests.csproj create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/ReserveTicketsTests.cs create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/SliceTests.cs create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.sln create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Data/TicketingDbContext.cs create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Domain/Event.cs create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Domain/EventErrors.cs create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Domain/Result.cs create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/EventTicketing.csproj create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/EventTicketing.http create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Events/GetEventAvailability.cs create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Events/ListAvailableEvents.cs create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Reservations/ReserveTickets.cs create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Program.cs rename csharp-architectural-patterns/VerticalSliceArchitecture/{VerticalSliceArchitecture => EventTicketing}/Properties/launchSettings.json (50%) create mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/ResultExtensions.cs rename csharp-architectural-patterns/VerticalSliceArchitecture/{VerticalSliceArchitecture => EventTicketing}/appsettings.Development.json (100%) rename csharp-architectural-patterns/VerticalSliceArchitecture/{VerticalSliceArchitecture => EventTicketing}/appsettings.json (66%) delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/Tests/ConsoleServiceTest.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/Tests/GameServiceTest.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/Tests/Tests.csproj delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/Tests/Usings.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture.sln delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Data/DataContext.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Data/Seed.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Domain/Game.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Domain/GameConsole.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/ConsoleService.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/ConsolesController.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/GetAllConsoles.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/IConsoleService.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/MapperProfile.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/AddGameToConsole.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/Exceptions/NoConsoleExistsException.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/Exceptions/NoGameExistsException.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GameService.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GamesController.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GetAllGamesForConsole.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/IGameService.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/MapperProfile.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/RemoveGameFromConsole.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/UpdateGameForConsole.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Program.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/ServiceManager/IServiceManager.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/ServiceManager/ServiceManager.cs delete mode 100644 csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/VerticalSliceArchitecture.csproj diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/.gitignore b/csharp-architectural-patterns/VerticalSliceArchitecture/.gitignore new file mode 100644 index 0000000000..1998dc0e60 --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/.gitignore @@ -0,0 +1,3 @@ +tickets.db +tickets.db-shm +tickets.db-wal diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/EventTicketing.Tests.csproj b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/EventTicketing.Tests.csproj new file mode 100644 index 0000000000..9530da9bd1 --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/EventTicketing.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/ReserveTicketsTests.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/ReserveTicketsTests.cs new file mode 100644 index 0000000000..76b88cb16d --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/ReserveTicketsTests.cs @@ -0,0 +1,55 @@ +using EventTicketing.Data; +using EventTicketing.Features.Reservations; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace EventTicketing.Tests; + +public sealed class ReserveTicketsTests : IDisposable +{ + private readonly SqliteConnection _connection = new("Data Source=:memory:"); + + public ReserveTicketsTests() => _connection.Open(); + + public void Dispose() => _connection.Dispose(); + + private TicketingDbContext CreateDbContext() + { + var options = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .Options; + + var dbContext = new TicketingDbContext(options); + dbContext.Database.EnsureCreated(); + + return dbContext; + } + + [Fact] + public async Task HandleAsync_WhenTicketsAreAvailable_SavesTheReservation() + { + var handler = new ReserveTickets.Handler(CreateDbContext()); + + var result = await handler.HandleAsync(eventId: 1, quantity: 3, TestContext.Current.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.Equal(97, result.Value.TicketsLeft); + + var saved = await CreateDbContext().Events.SingleAsync(e => e.Id == 1, TestContext.Current.CancellationToken); + Assert.Equal(3, saved.TicketsSold); + } + + [Fact] + public async Task HandleAsync_WhenTheEventIsSoldOut_SavesNothing() + { + var handler = new ReserveTickets.Handler(CreateDbContext()); + + var result = await handler.HandleAsync(eventId: 2, quantity: 3, TestContext.Current.CancellationToken); + + Assert.False(result.IsSuccess); + Assert.Equal("Event.SoldOut", result.Error!.Code); + + var saved = await CreateDbContext().Events.SingleAsync(e => e.Id == 2, TestContext.Current.CancellationToken); + Assert.Equal(0, saved.TicketsSold); + } +} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/SliceTests.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/SliceTests.cs new file mode 100644 index 0000000000..9bdd84d73e --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.Tests/SliceTests.cs @@ -0,0 +1,35 @@ +using EventTicketing.Features.Reservations; +using NetArchTest.Rules; + +namespace EventTicketing.Tests; + +public class SliceTests +{ + [Fact] + public void Features_DoNotDependOnEachOther() + { + var assembly = typeof(ReserveTickets).Assembly; + + var features = assembly.GetTypes() + .Select(t => t.Namespace) + .OfType() + .Where(ns => ns.StartsWith("EventTicketing.Features.")) + .Distinct() + .ToArray(); + + foreach (var feature in features) + { + var otherFeatures = features.Where(f => f != feature).ToArray(); + + var result = Types.InAssembly(assembly) + .That() + .ResideInNamespace(feature) + .ShouldNot() + .HaveDependencyOnAny(otherFeatures) + .GetResult(); + + Assert.True(result.IsSuccessful, + $"{feature} depends on another feature: " + string.Join(", ", result.FailingTypeNames ?? [])); + } + } +} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.sln b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.sln new file mode 100644 index 0000000000..9eb5cb74d7 --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing.sln @@ -0,0 +1,48 @@ + +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", "EventTicketing\EventTicketing.csproj", "{5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventTicketing.Tests", "EventTicketing.Tests\EventTicketing.Tests.csproj", "{4B09EE23-0672-4FE0-8078-430028A5351E}" +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 + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Debug|x64.ActiveCfg = Debug|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Debug|x64.Build.0 = Debug|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Debug|x86.ActiveCfg = Debug|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Debug|x86.Build.0 = Debug|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Release|Any CPU.Build.0 = Release|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Release|x64.ActiveCfg = Release|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Release|x64.Build.0 = Release|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Release|x86.ActiveCfg = Release|Any CPU + {5FAF63F8-89C3-4EEE-A3A0-DCB365CA69B6}.Release|x86.Build.0 = Release|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Debug|x64.ActiveCfg = Debug|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Debug|x64.Build.0 = Debug|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Debug|x86.ActiveCfg = Debug|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Debug|x86.Build.0 = Debug|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Release|Any CPU.Build.0 = Release|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Release|x64.ActiveCfg = Release|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Release|x64.Build.0 = Release|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Release|x86.ActiveCfg = Release|Any CPU + {4B09EE23-0672-4FE0-8078-430028A5351E}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Data/TicketingDbContext.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Data/TicketingDbContext.cs new file mode 100644 index 0000000000..33431ebab6 --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Data/TicketingDbContext.cs @@ -0,0 +1,22 @@ +using EventTicketing.Domain; +using Microsoft.EntityFrameworkCore; + +namespace EventTicketing.Data; + +public sealed class TicketingDbContext(DbContextOptions options) + : DbContext(options) +{ + public DbSet Events => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(builder => + { + builder.Property(e => e.Name).HasMaxLength(200); + + builder.HasData( + new { Id = 1, Name = "Vertical Slice Live", Capacity = 100, TicketsSold = 0 }, + new { Id = 2, Name = "Tiny Jazz Club Night", Capacity = 2, TicketsSold = 0 }); + }); + } +} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Domain/Event.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Domain/Event.cs new file mode 100644 index 0000000000..8c2b7c2eda --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Domain/Event.cs @@ -0,0 +1,37 @@ +namespace EventTicketing.Domain; + +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) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); + + return new Event(name, capacity); + } + + public Result Reserve(int quantity) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); + + if (quantity > TicketsLeft) + return EventErrors.SoldOut(TicketsLeft, quantity); + + TicketsSold += quantity; + + return Result.Success(); + } +} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Domain/EventErrors.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Domain/EventErrors.cs new file mode 100644 index 0000000000..24d232a0ca --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/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/VerticalSliceArchitecture/EventTicketing/Domain/Result.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Domain/Result.cs new file mode 100644 index 0000000000..7f406bb586 --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/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/VerticalSliceArchitecture/EventTicketing/EventTicketing.csproj b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/EventTicketing.csproj new file mode 100644 index 0000000000..9361cc4861 --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/EventTicketing.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/EventTicketing.http b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/EventTicketing.http new file mode 100644 index 0000000000..09a304f51a --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/EventTicketing.http @@ -0,0 +1,37 @@ +GET http://localhost:5000/api/events/1 + +### + +POST http://localhost:5000/api/events/1/reservations +Content-Type: application/json + +{ "quantity": 3 } + +### + +POST http://localhost:5000/api/events/2/reservations +Content-Type: application/json + +{ "quantity": 3 } + +### + +POST http://localhost:5000/api/events/1/reservations +Content-Type: application/json + +{ "quantity": 0 } + +### + +GET http://localhost:5000/api/events/99 + +### + +POST http://localhost:5000/api/events/2/reservations +Content-Type: application/json + +{ "quantity": 2 } + +### + +GET http://localhost:5000/api/events diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Events/GetEventAvailability.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Events/GetEventAvailability.cs new file mode 100644 index 0000000000..90df16bb21 --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Events/GetEventAvailability.cs @@ -0,0 +1,39 @@ +using EventTicketing.Data; +using EventTicketing.Domain; +using Microsoft.EntityFrameworkCore; + +namespace EventTicketing.Features.Events; + +public static class GetEventAvailability +{ + public sealed record Response(int EventId, string Name, int TicketsLeft); + + public sealed class Handler(TicketingDbContext dbContext) + { + public async Task> HandleAsync( + int eventId, CancellationToken cancellationToken = default) + { + var availability = await dbContext.Events + .AsNoTracking() + .Where(e => e.Id == eventId) + .Select(e => new Response(e.Id, e.Name, e.Capacity - e.TicketsSold)) + .FirstOrDefaultAsync(cancellationToken); + + if (availability is null) + return EventErrors.NotFound(eventId); + + return availability; + } + } + + public static void MapEndpoint(IEndpointRouteBuilder app) => + app.MapGet("/{eventId:int}", async ( + int eventId, + Handler handler, + CancellationToken cancellationToken) => + { + var result = await handler.HandleAsync(eventId, cancellationToken); + + return result.IsSuccess ? Results.Ok(result.Value) : result.ToProblem(); + }); +} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Events/ListAvailableEvents.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Events/ListAvailableEvents.cs new file mode 100644 index 0000000000..15bb2ac737 --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Events/ListAvailableEvents.cs @@ -0,0 +1,17 @@ +using EventTicketing.Data; +using Microsoft.EntityFrameworkCore; + +namespace EventTicketing.Features.Events; + +public static class ListAvailableEvents +{ + public sealed record AvailableEvent(int EventId, string Name, int TicketsLeft); + + public static void MapEndpoint(IEndpointRouteBuilder app) => + app.MapGet("/", async (TicketingDbContext dbContext, CancellationToken cancellationToken) => + await dbContext.Events + .AsNoTracking() + .Where(e => e.TicketsSold < e.Capacity) + .Select(e => new AvailableEvent(e.Id, e.Name, e.Capacity - e.TicketsSold)) + .ToListAsync(cancellationToken)); +} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Reservations/ReserveTickets.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Reservations/ReserveTickets.cs new file mode 100644 index 0000000000..b9efe5949e --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Features/Reservations/ReserveTickets.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.DataAnnotations; +using EventTicketing.Data; +using EventTicketing.Domain; +using Microsoft.EntityFrameworkCore; + +namespace EventTicketing.Features.Reservations; + +public static class ReserveTickets +{ + public sealed record Request([property: Range(1, 20)] int Quantity); + + public sealed record Response(int EventId, int TicketsReserved, int TicketsLeft); + + public sealed class Handler(TicketingDbContext dbContext) + { + public async Task> HandleAsync( + int eventId, int quantity, CancellationToken cancellationToken = default) + { + var ev = await dbContext.Events.FirstOrDefaultAsync(e => e.Id == eventId, cancellationToken); + if (ev is null) + return EventErrors.NotFound(eventId); + + var reservation = ev.Reserve(quantity); + if (!reservation.IsSuccess) + return reservation.Error!; + + await dbContext.SaveChangesAsync(cancellationToken); + + return new Response(ev.Id, quantity, ev.TicketsLeft); + } + } + + public static void MapEndpoint(IEndpointRouteBuilder app) => + app.MapPost("/{eventId:int}/reservations", async ( + int eventId, + Request request, + Handler handler, + CancellationToken cancellationToken) => + { + var result = await handler.HandleAsync(eventId, request.Quantity, cancellationToken); + + return result.IsSuccess ? Results.Ok(result.Value) : result.ToProblem(); + }); +} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Program.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Program.cs new file mode 100644 index 0000000000..a6cac08a41 --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Program.cs @@ -0,0 +1,33 @@ +using EventTicketing.Data; +using EventTicketing.Features.Events; +using EventTicketing.Features.Reservations; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("Ticketing"))); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddProblemDetails(); +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.UseExceptionHandler(); + +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); +} + +var events = app.MapGroup("/api/events"); + +GetEventAvailability.MapEndpoint(events); +ReserveTickets.MapEndpoint(events); +ListAvailableEvents.MapEndpoint(events); + +app.Run(); diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Properties/launchSettings.json b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Properties/launchSettings.json similarity index 50% rename from csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Properties/launchSettings.json rename to csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Properties/launchSettings.json index b3107716ee..b0ac7136aa 100644 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Properties/launchSettings.json +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/Properties/launchSettings.json @@ -1,28 +1,20 @@ -{ +{ "$schema": "https://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:47567", - "sslPort": 0 - } - }, "profiles": { - "VerticalSliceArchitecture": { + "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:5216", + "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "IIS Express": { - "commandName": "IISExpress", + "https": { + "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "swagger", + "applicationUrl": "https://localhost:7052;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/ResultExtensions.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/ResultExtensions.cs new file mode 100644 index 0000000000..6b97d710fb --- /dev/null +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/ResultExtensions.cs @@ -0,0 +1,21 @@ +using EventTicketing.Domain; + +namespace EventTicketing; + +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/VerticalSliceArchitecture/VerticalSliceArchitecture/appsettings.Development.json b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/appsettings.Development.json similarity index 100% rename from csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/appsettings.Development.json rename to csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/appsettings.Development.json diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/appsettings.json b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/appsettings.json similarity index 66% rename from csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/appsettings.json rename to csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/appsettings.json index 10f68b8c8b..f0d3f98f07 100644 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/appsettings.json +++ b/csharp-architectural-patterns/VerticalSliceArchitecture/EventTicketing/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "Ticketing": "Data Source=tickets.db" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/ConsoleServiceTest.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/ConsoleServiceTest.cs deleted file mode 100644 index 54b0885dd1..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/ConsoleServiceTest.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Tests -{ - public class ConsoleServiceTest - { - private DataContext _context; - - [Fact] - public async Task WhenGetAllConsoles_ThenConsolesAreReturnedSuccessfully() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase("consoleDB1") - .Options; - - _context = new DataContext(options); - - _context.Consoles.Add(new GameConsole - { - Id = 1, - Name = "Xbox Series X", - Manufacturer = "Microsoft" - }); - - await _context.SaveChangesAsync(); - - //When - var consoles = await new ConsoleService(_context).GetAllConsolesAsync(); - - //Then - Assert.NotNull(consoles); - - Assert.True(consoles.Any()); - } - - [Fact] - public async Task WhenGetConsoleById_ThenSpecificConsoleIsReturned() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase("consoleDB2") - .Options; - - _context = new DataContext(options); - - _context.Consoles.Add(new GameConsole - { - Id = 1, - Name = "Xbox Series X", - Manufacturer = "Microsoft" - }); - - await _context.SaveChangesAsync(); - - //When - var console = await new ConsoleService(_context).GetConsoleByIdAsync(1); - - await _context.SaveChangesAsync(); - - //Then - Assert.NotNull(console); - - Assert.True(console.Id == 1); - } - } -} \ No newline at end of file diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/GameServiceTest.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/GameServiceTest.cs deleted file mode 100644 index bd663da02e..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/GameServiceTest.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace Tests -{ - public class GameServiceTest - { - private DataContext _context; - - [Fact] - public async Task WhenGameIsAdded_GameCreatedSuccessfully() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase("gameDB1") - .Options; - - _context = new DataContext(options); - - _context.Consoles.Add(new GameConsole - { - Id = 1, - Name = "Xbox Series X", - Manufacturer = "Microsoft" - }); - - //When - new GameService(_context).AddGameToConsole(1, new Game { Id = 10, Name = "Fighting Simulator", Publisher = "Best Fights Corp" }); - - await _context.SaveChangesAsync(); - - //Then - Assert.True(await _context.Games.FirstOrDefaultAsync() != null); - } - - [Fact] - public async Task WhenGetAllGames_ThenGamesAreReturnedSuccessfully() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase("gameDB2") - .Options; - - _context = new DataContext(options); - - _context.Consoles.Add(new GameConsole - { - Id = 1, - Name = "Xbox Series X", - Manufacturer = "Microsoft" - }); - - new GameService(_context).AddGameToConsole(1, new Game { Id = 10, Name = "Fighting Simulator", Publisher = "Best Fights Corp" }); - - await _context.SaveChangesAsync(); - - //When - var games = await new GameService(_context).GetAllGamesAsync(1); - - //Then - Assert.NotNull(games); - - Assert.True(games.Any()); - } - - [Fact] - public async Task WhenGetGameByID_ThenSpecifiedGameIsReturnedSuccessfully() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase("gameDB3") - .Options; - - _context = new DataContext(options); - - _context.Consoles.Add(new GameConsole - { - Id = 1, - Name = "Xbox Series X", - Manufacturer = "Microsoft" - }); - - new GameService(_context).AddGameToConsole(1, new Game { Id = 12, Name = "Running Simulator", Publisher = "Best Runs Corp" }); - - await _context.SaveChangesAsync(); - - //When - var game = await new GameService(_context).GetGameAsync(1, 12); - - //Then - Assert.NotNull(game); - - Assert.True(game.Id == 12); - } - - [Fact] - public async Task WhenDeletingAGame_ThenSpecifiedGameIsDeletedSuccessfully() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase("gameDB4") - .Options; - - _context = new DataContext(options); - - _context.Consoles.Add(new GameConsole - { - Id = 1, - Name = "Xbox Series X", - Manufacturer = "Microsoft" - }); - - var newGame = new Game { Id = 13, Name = "Game Dev Simulator", Publisher = "Worst Games Corp" }; - - new GameService(_context).AddGameToConsole(1, newGame); - - await _context.SaveChangesAsync(); - - //When - new GameService(_context).DeleteGame(newGame); - - await _context.SaveChangesAsync(); - - //Then - - var deletedGame = await _context.Games.FirstOrDefaultAsync(x => x.Id == 13); - - Assert.Null(deletedGame); - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/Tests.csproj b/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/Tests.csproj deleted file mode 100644 index 0f65eb1ff5..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net8.0 - enable - enable - - false - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/Usings.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/Usings.cs deleted file mode 100644 index 26a7334fba..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/Tests/Usings.cs +++ /dev/null @@ -1,6 +0,0 @@ -global using Microsoft.EntityFrameworkCore; -global using VerticalSliceArchitecture.Data; -global using VerticalSliceArchitecture.Domain; -global using VerticalSliceArchitecture.Features.Consoles; -global using VerticalSliceArchitecture.Features.Games; -global using Xunit; diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture.sln b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture.sln deleted file mode 100644 index 0c9400484e..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture.sln +++ /dev/null @@ -1,31 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.2.32516.85 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VerticalSliceArchitecture", "VerticalSliceArchitecture\VerticalSliceArchitecture.csproj", "{B461B03F-664F-422A-993B-79AC4C6FCF1D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{0E1829EA-90EC-47FC-B7F9-47B4C31AA54B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B461B03F-664F-422A-993B-79AC4C6FCF1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B461B03F-664F-422A-993B-79AC4C6FCF1D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B461B03F-664F-422A-993B-79AC4C6FCF1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B461B03F-664F-422A-993B-79AC4C6FCF1D}.Release|Any CPU.Build.0 = Release|Any CPU - {0E1829EA-90EC-47FC-B7F9-47B4C31AA54B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0E1829EA-90EC-47FC-B7F9-47B4C31AA54B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E1829EA-90EC-47FC-B7F9-47B4C31AA54B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E1829EA-90EC-47FC-B7F9-47B4C31AA54B}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {938CAD25-5CDA-4BEB-A011-3EA89D816224} - EndGlobalSection -EndGlobal diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Data/DataContext.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Data/DataContext.cs deleted file mode 100644 index fff48a5d33..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Data/DataContext.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using VerticalSliceArchitecture.Domain; - -namespace VerticalSliceArchitecture.Data -{ - public class DataContext : DbContext - { - public DataContext(DbContextOptions options) : base(options) { } - public DbSet Consoles { get; set; } - public DbSet Games { get; set; } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Data/Seed.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Data/Seed.cs deleted file mode 100644 index 81df2bb323..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Data/Seed.cs +++ /dev/null @@ -1,61 +0,0 @@ -using VerticalSliceArchitecture.Domain; - -namespace VerticalSliceArchitecture.Data -{ - public class Seed - { - public void SeedData(DataContext context) - { - //Seeding Consoles - - context.Consoles.Add(new GameConsole - { - Id = 1, - Name = "Xbox Series X", - Manufacturer = "Microsoft" - }); - - context.Consoles.Add(new GameConsole - { - Id = 2, - Name = "PlayStation 5", - Manufacturer = "Sony" - }); - - context.Consoles.Add(new GameConsole - { - Id = 3, - Name = "Nintendo Switch", - Manufacturer = "Nintendo" - }); - - //Seeding Games - - context.Games.Add(new Game - { - Id = 10, - Name = "Horizon Forbidden West", - Publisher = "Sony Interactive Entertainment", - ConsoleId = 2 - }); - - context.Games.Add(new Game - { - Id = 11, - Name = "Animal Crossing: New Horizons", - Publisher = "Nintendo", - ConsoleId = 3 - }); - - context.Games.Add(new Game - { - Id = 12, - Name = "Halo Infinite", - Publisher = "Xbox Game Studios", - ConsoleId = 1 - }); - - context.SaveChanges(); - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Domain/Game.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Domain/Game.cs deleted file mode 100644 index 6648a7771f..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Domain/Game.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace VerticalSliceArchitecture.Domain -{ - public class Game - { - public int Id { get; set; } - public string? Name { get; set; } - public string? Publisher { get; set; } - public int ConsoleId { get; set; } - public GameConsole? Console { get; set; } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Domain/GameConsole.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Domain/GameConsole.cs deleted file mode 100644 index 8372cb0d7e..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Domain/GameConsole.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace VerticalSliceArchitecture.Domain -{ - public class GameConsole - { - public int Id { get; set; } - public string? Name { get; set; } - public string? Manufacturer { get; set; } - public ICollection? Games { get; set; } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/ConsoleService.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/ConsoleService.cs deleted file mode 100644 index 8f795e3243..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/ConsoleService.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using VerticalSliceArchitecture.Data; -using VerticalSliceArchitecture.Domain; - -namespace VerticalSliceArchitecture.Features.Consoles -{ - public class ConsoleService : IConsoleService - { - private readonly DataContext _context; - - public ConsoleService(DataContext context) - { - _context = context; - } - - public async Task> GetAllConsolesAsync() - { - return await _context.Consoles - .OrderBy(x => x.Id) - .ToListAsync(); - } - - public async Task GetConsoleByIdAsync(int consoleId) - { - return await _context.Consoles - .FirstOrDefaultAsync(x => x.Id == consoleId); - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/ConsolesController.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/ConsolesController.cs deleted file mode 100644 index c75a8c6d88..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/ConsolesController.cs +++ /dev/null @@ -1,27 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Mvc; - -namespace VerticalSliceArchitecture.Features.Consoles -{ - [Route("api/[controller]")] - [ApiController] - public class ConsolesController : ControllerBase - { - private readonly IMediator _mediator; - public ConsolesController(IMediator mediator) - { - _mediator = mediator; - } - - [HttpGet] - public async Task>> GetConsolesAsync() - { - var result = await _mediator.Send(new GetAllConsoles.GetConsolesQuery()); - - if (result is null) - return NotFound(); - - return Ok(result); - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/GetAllConsoles.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/GetAllConsoles.cs deleted file mode 100644 index 9857427aed..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/GetAllConsoles.cs +++ /dev/null @@ -1,41 +0,0 @@ -using AutoMapper; -using MediatR; -using VerticalSliceArchitecture.ServiceManager; - -namespace VerticalSliceArchitecture.Features.Consoles -{ - public class GetAllConsoles - { - //Input - public class GetConsolesQuery : IRequest> { } - - //Output - public class ConsoleResult - { - public int Id { get; set; } - public string? Name { get; set; } - public string? Manufacturer { get; set; } - } - - //Handler - public class Handler : IRequestHandler> - { - private readonly IServiceManager _serviceManager; - private readonly IMapper _mapper; - - public Handler(IServiceManager serviceManager, IMapper mapper) - { - _serviceManager = serviceManager; - _mapper = mapper; - } - - public async Task> Handle(GetConsolesQuery request, CancellationToken cancellationToken) - { - var consoles = await _serviceManager.Console.GetAllConsolesAsync(); - var results = _mapper.Map>(consoles); - - return results; - } - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/IConsoleService.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/IConsoleService.cs deleted file mode 100644 index 18b3f810ac..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/IConsoleService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using VerticalSliceArchitecture.Domain; - -namespace VerticalSliceArchitecture.Features.Consoles -{ - public interface IConsoleService - { - Task> GetAllConsolesAsync(); - Task GetConsoleByIdAsync(int consoleId); - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/MapperProfile.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/MapperProfile.cs deleted file mode 100644 index e18ebbff8a..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Consoles/MapperProfile.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AutoMapper; -using VerticalSliceArchitecture.Domain; - -namespace VerticalSliceArchitecture.Features.Consoles -{ - public class MapperProfile : Profile - { - public MapperProfile() - { - CreateMap(); - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/AddGameToConsole.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/AddGameToConsole.cs deleted file mode 100644 index 57907817fd..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/AddGameToConsole.cs +++ /dev/null @@ -1,64 +0,0 @@ -using AutoMapper; -using MediatR; -using VerticalSliceArchitecture.Domain; -using VerticalSliceArchitecture.Features.Games.Exceptions; -using VerticalSliceArchitecture.ServiceManager; - -namespace VerticalSliceArchitecture.Features.Games -{ - public class AddGameToConsole - { - //Input - public class AddGameCommand : IRequest - { - public string? Name { get; set; } - public string? Publisher { get; set; } - public int ConsoleId { get; set; } - } - - //Output - public class GameResult - { - public int Id { get; set; } - public string? Name { get; set; } - public string? Publisher { get; set; } - public int ConsoleId { get; set; } - } - - //Handler - public class Handler : IRequestHandler - { - private readonly IServiceManager _serviceManager; - private readonly IMapper _mapper; - - public Handler(IServiceManager serviceManager, IMapper mapper) - { - _serviceManager = serviceManager; - _mapper = mapper; - } - - public async Task Handle(AddGameCommand request, CancellationToken cancellationToken) - { - var console = await _serviceManager.Console.GetConsoleByIdAsync(request.ConsoleId); - - if (console is null) - throw new NoConsoleExistsException(request.ConsoleId); - - var game = new Game() - { - Name = request.Name, - Publisher = request.Publisher, - ConsoleId = request.ConsoleId - }; - - _serviceManager.Game.AddGameToConsole(request.ConsoleId, game); - - await _serviceManager.SaveAsync(); - - var result = _mapper.Map(game); - - return result; - } - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/Exceptions/NoConsoleExistsException.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/Exceptions/NoConsoleExistsException.cs deleted file mode 100644 index 0611c69373..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/Exceptions/NoConsoleExistsException.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace VerticalSliceArchitecture.Features.Games.Exceptions -{ - public class NoConsoleExistsException : Exception - { - public NoConsoleExistsException(int consoleId) : base($"Console with id: {consoleId} doesn't exist.") { } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/Exceptions/NoGameExistsException.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/Exceptions/NoGameExistsException.cs deleted file mode 100644 index 98fb65fceb..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/Exceptions/NoGameExistsException.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace VerticalSliceArchitecture.Features.Games.Exceptions -{ - public class NoGameExistsException : Exception - { - public NoGameExistsException(int consoleId, int gameId) : base($"Game with id: {gameId} doesn't exist for console id {consoleId}") - { - ConsoleId = consoleId; - GameId = gameId; - } - - public int ConsoleId { get; set; } - public int GameId { get; set; } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GameService.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GameService.cs deleted file mode 100644 index 36ce4932bf..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GameService.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using VerticalSliceArchitecture.Data; -using VerticalSliceArchitecture.Domain; - -namespace VerticalSliceArchitecture.Features.Games -{ - public class GameService : IGameService - { - private readonly DataContext _context; - - public GameService(DataContext context) - { - _context = context; - } - public void AddGameToConsole(int consoleId, Game game) - { - game.ConsoleId = consoleId; - - _context.Games.Add(game); - } - - public void DeleteGame(Game game) - { - _context.Games.Remove(game); - } - - public async Task> GetAllGamesAsync(int consoleId) - { - return await _context.Games - .Where(x => x.ConsoleId == consoleId) - .ToListAsync(); - } - - public async Task GetGameAsync(int consoleId, int gameId) - { - return await _context.Games - .FirstOrDefaultAsync(x => x.ConsoleId == consoleId && x.Id == gameId); - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GamesController.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GamesController.cs deleted file mode 100644 index 92097d8b28..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GamesController.cs +++ /dev/null @@ -1,120 +0,0 @@ -using MediatR; -using Microsoft.AspNetCore.Mvc; -using VerticalSliceArchitecture.Features.Games.Exceptions; -using static VerticalSliceArchitecture.Features.Games.GetAllGamesForConsole; -using static VerticalSliceArchitecture.Features.Games.RemoveGameFromConsole; -using static VerticalSliceArchitecture.Features.Games.UpdateGameForConsole; - -namespace VerticalSliceArchitecture.Features.Games -{ - [Route("api/[controller]")] - [ApiController] - public class GamesController : ControllerBase - { - private readonly IMediator _mediator; - - public GamesController(IMediator mediator) - { - _mediator = mediator; - } - - [HttpGet(Name = "GetGamesForConsole")] - public async Task>> GetGamesForConsole(int consoleId) - { - try - { - var query = new GetGamesQuery - { - ConsoleId = consoleId - }; - - var result = await _mediator.Send(query); - - return Ok(result); - } - catch (NoConsoleExistsException ex) - { - return Conflict(new - { - ex.Message - }); - } - } - - [HttpPost] - public async Task AddGame(AddGameToConsole.AddGameCommand command) - { - try - { - var result = await _mediator.Send(command); - - return CreatedAtRoute("GetGamesForConsole", new { consoleId = result.ConsoleId }, result); - } - catch (NoConsoleExistsException ex) - { - return Conflict(new - { - ex.Message - }); - } - } - - [HttpPut] - public async Task UpdateGameForConsole(int consoleId, UpdateGameCommand command) - { - try - { - command.ConsoleId = consoleId; - - await _mediator.Send(command); - - return NoContent(); - } - catch (NoConsoleExistsException ex) - { - return Conflict(new - { - ex.Message - }); - } - catch (NoGameExistsException ex) - { - return Conflict(new - { - ex.Message, - ex.ConsoleId, - ex.GameId - }); - } - } - - [HttpDelete] - public async Task RemoveGameFromConsole(int consoleId, RemoveGameCommand command) - { - try - { - command.ConsoleId = consoleId; - - await _mediator.Send(command); - - return NoContent(); - } - catch (NoConsoleExistsException ex) - { - return Conflict(new - { - ex.Message - }); - } - catch (NoGameExistsException ex) - { - return Conflict(new - { - ex.Message, - ex.ConsoleId, - ex.GameId - }); - } - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GetAllGamesForConsole.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GetAllGamesForConsole.cs deleted file mode 100644 index 01f966e4da..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/GetAllGamesForConsole.cs +++ /dev/null @@ -1,48 +0,0 @@ -using AutoMapper; -using MediatR; -using VerticalSliceArchitecture.Features.Games.Exceptions; -using VerticalSliceArchitecture.ServiceManager; - -namespace VerticalSliceArchitecture.Features.Games -{ - public class GetAllGamesForConsole - { - public class GetGamesQuery : IRequest> - { - public int ConsoleId { get; set; } - } - - public class GameResult - { - public int Id { get; set; } - public string? Name { get; set; } - public string? Publisher { get; set; } - } - - public class Handler : IRequestHandler> - { - private readonly IServiceManager _serviceManager; - private readonly IMapper _mapper; - - public Handler(IServiceManager serviceManager, IMapper mapper) - { - _serviceManager = serviceManager; - _mapper = mapper; - } - - public async Task> Handle(GetGamesQuery request, CancellationToken cancellationToken) - { - var console = await _serviceManager.Console.GetConsoleByIdAsync(request.ConsoleId); - - if (console is null) - throw new NoConsoleExistsException(request.ConsoleId); - - var games = await _serviceManager.Game.GetAllGamesAsync(console.Id); - - var result = _mapper.Map>(games); - - return result; - } - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/IGameService.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/IGameService.cs deleted file mode 100644 index eead99ea99..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/IGameService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using VerticalSliceArchitecture.Domain; - -namespace VerticalSliceArchitecture.Features.Games -{ - public interface IGameService - { - Task> GetAllGamesAsync(int consoleId); - Task GetGameAsync(int consoleId, int gameId); - void AddGameToConsole(int consoleId, Game game); - void DeleteGame(Game game); - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/MapperProfile.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/MapperProfile.cs deleted file mode 100644 index 0c6f42f952..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/MapperProfile.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AutoMapper; -using VerticalSliceArchitecture.Domain; - -namespace VerticalSliceArchitecture.Features.Games -{ - public class MapperProfile : Profile - { - public MapperProfile() - { - CreateMap(); - CreateMap(); - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/RemoveGameFromConsole.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/RemoveGameFromConsole.cs deleted file mode 100644 index 1e5867c216..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/RemoveGameFromConsole.cs +++ /dev/null @@ -1,44 +0,0 @@ -using MediatR; -using VerticalSliceArchitecture.Features.Games.Exceptions; -using VerticalSliceArchitecture.ServiceManager; - -namespace VerticalSliceArchitecture.Features.Games -{ - public class RemoveGameFromConsole - { - public class RemoveGameCommand : IRequest - { - public int ConsoleId { get; set; } - public int GameId { get; set; } - } - - public class Handler : IRequestHandler - { - private readonly IServiceManager _serviceManager; - - public Handler(IServiceManager serviceManager) - { - _serviceManager = serviceManager; - } - - public async Task Handle(RemoveGameCommand request, CancellationToken cancellationToken) - { - var console = await _serviceManager.Console.GetConsoleByIdAsync(request.ConsoleId); - - if (console is null) - throw new NoConsoleExistsException(request.ConsoleId); - - var game = await _serviceManager.Game.GetGameAsync(request.ConsoleId, request.GameId); - - if (game is null) - throw new NoGameExistsException(request.ConsoleId, request.GameId); - - _serviceManager.Game.DeleteGame(game); - - await _serviceManager.SaveAsync(); - - return; - } - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/UpdateGameForConsole.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/UpdateGameForConsole.cs deleted file mode 100644 index 472ac9d6ad..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Features/Games/UpdateGameForConsole.cs +++ /dev/null @@ -1,48 +0,0 @@ -using AutoMapper; -using MediatR; -using VerticalSliceArchitecture.Features.Games.Exceptions; -using VerticalSliceArchitecture.ServiceManager; - -namespace VerticalSliceArchitecture.Features.Games -{ - public class UpdateGameForConsole - { - public class UpdateGameCommand : IRequest - { - public int Id { get; set; } - public string? Name { get; set; } - public string? Publisher { get; set; } - public int ConsoleId { get; set; } - } - - public class Handler : IRequestHandler - { - private readonly IServiceManager _serviceManager; - - public Handler(IServiceManager serviceManager) - { - _serviceManager = serviceManager; - } - - public async Task Handle(UpdateGameCommand request, CancellationToken cancellationToken) - { - var console = await _serviceManager.Console.GetConsoleByIdAsync(request.ConsoleId); - - if (console is null) - throw new NoConsoleExistsException(request.ConsoleId); - - var game = await _serviceManager.Game.GetGameAsync(request.ConsoleId, request.Id); - - if (game is null) - throw new NoGameExistsException(request.ConsoleId, request.Id); - - game.Name = request.Name!; - game.Publisher = request.Publisher!; - - await _serviceManager.SaveAsync(); - - return; - } - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Program.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Program.cs deleted file mode 100644 index 6016bbdfc0..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/Program.cs +++ /dev/null @@ -1,46 +0,0 @@ -using MediatR; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using VerticalSliceArchitecture.Data; -using VerticalSliceArchitecture.ServiceManager; - -var builder = WebApplication.CreateBuilder(args); - -// Add services to the container. - -builder.Services.AddAutoMapper(typeof(Program).Assembly); -builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); - -builder.Services.AddDbContext(options => -{ - options.UseInMemoryDatabase("GamingDB") - .ConfigureWarnings(builder => builder.Ignore(InMemoryEventId.TransactionIgnoredWarning)); -}); - -builder.Services.AddScoped(); - -builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -using (var scope = app.Services.CreateScope()) -{ - var dataContext = scope.ServiceProvider.GetRequiredService(); - new Seed().SeedData(dataContext); -} - -app.UseAuthorization(); - -app.MapControllers(); - -app.Run(); diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/ServiceManager/IServiceManager.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/ServiceManager/IServiceManager.cs deleted file mode 100644 index ba4e3aec7a..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/ServiceManager/IServiceManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -using VerticalSliceArchitecture.Features.Consoles; -using VerticalSliceArchitecture.Features.Games; - -namespace VerticalSliceArchitecture.ServiceManager -{ - public interface IServiceManager - { - IConsoleService Console { get; } - IGameService Game { get; } - Task SaveAsync(); - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/ServiceManager/ServiceManager.cs b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/ServiceManager/ServiceManager.cs deleted file mode 100644 index 1d888665c4..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/ServiceManager/ServiceManager.cs +++ /dev/null @@ -1,45 +0,0 @@ -using VerticalSliceArchitecture.Data; -using VerticalSliceArchitecture.Features.Consoles; -using VerticalSliceArchitecture.Features.Games; - -namespace VerticalSliceArchitecture.ServiceManager -{ - public class ServiceManager : IServiceManager - { - private readonly DataContext _context; - private IConsoleService _consoleService; - private IGameService _gameService; - - public ServiceManager(DataContext context) - { - _context = context; - } - - public IConsoleService Console - { - get - { - if (_consoleService == null) - _consoleService = new ConsoleService(_context); - - return _consoleService; - } - } - - public IGameService Game - { - get - { - if (_gameService == null) - _gameService = new GameService(_context); - - return _gameService; - } - } - - public Task SaveAsync() - { - return _context.SaveChangesAsync(); - } - } -} diff --git a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/VerticalSliceArchitecture.csproj b/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/VerticalSliceArchitecture.csproj deleted file mode 100644 index 365dcd1c6a..0000000000 --- a/csharp-architectural-patterns/VerticalSliceArchitecture/VerticalSliceArchitecture/VerticalSliceArchitecture.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - -