From d3dfa4fc05b87a94aea701cba4cf97a191b0fc14 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sun, 10 Nov 2024 23:33:31 +0000 Subject: [PATCH 01/24] Initial commit --- .gitignore | 335 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Brewery.sln | 8 ++ 2 files changed, 343 insertions(+) create mode 100644 .gitignore create mode 100644 Brewery.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b395a49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,335 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ + +.vscode/ + +.DS_Store \ No newline at end of file diff --git a/Brewery.sln b/Brewery.sln new file mode 100644 index 0000000..a55ff74 --- /dev/null +++ b/Brewery.sln @@ -0,0 +1,8 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection +EndGlobal From 64017e1685d2cb4c1210f82766e5fa463ea4d52a Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sun, 10 Nov 2024 23:42:27 +0000 Subject: [PATCH 02/24] add Brewery Layers .Api .Infrastructure, .Application, .Domain --- Brewery.sln | 36 +++++++++++++++++++ src/Brewery.Api/Brewery.Api.csproj | 14 ++++++++ src/Brewery.Api/Program.cs | 6 ++++ .../Properties/launchSettings.json | 23 ++++++++++++ src/Brewery.Api/appsettings.Development.json | 8 +++++ src/Brewery.Api/appsettings.json | 9 +++++ .../Brewery.Application.csproj | 9 +++++ src/Brewery.Domain/Brewery.Domain.csproj | 9 +++++ .../Brewery.Infrastructure.csproj | 9 +++++ 9 files changed, 123 insertions(+) create mode 100644 src/Brewery.Api/Brewery.Api.csproj create mode 100644 src/Brewery.Api/Program.cs create mode 100644 src/Brewery.Api/Properties/launchSettings.json create mode 100644 src/Brewery.Api/appsettings.Development.json create mode 100644 src/Brewery.Api/appsettings.json create mode 100644 src/Brewery.Application/Brewery.Application.csproj create mode 100644 src/Brewery.Domain/Brewery.Domain.csproj create mode 100644 src/Brewery.Infrastructure/Brewery.Infrastructure.csproj diff --git a/Brewery.sln b/Brewery.sln index a55ff74..5b7b15a 100644 --- a/Brewery.sln +++ b/Brewery.sln @@ -1,8 +1,44 @@  Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{13BABC4E-2013-45BE-B5F2-37374813BFF3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FD1F28D7-23D0-4225-814C-549E308E4C9D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Api", "src\Brewery.Api\Brewery.Api.csproj", "{CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Domain", "src\Brewery.Domain\Brewery.Domain.csproj", "{89613C40-A24A-4972-909F-8E840056349A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Application", "src\Brewery.Application\Brewery.Application.csproj", "{6DA2F545-AD92-4EF3-8638-D42F1F8C5833}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Infrastructure", "src\Brewery.Infrastructure\Brewery.Infrastructure.csproj", "{F0FFE48E-371C-423C-9DB7-22BF810F6AA4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + {89613C40-A24A-4972-909F-8E840056349A} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + {6DA2F545-AD92-4EF3-8638-D42F1F8C5833} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + {F0FFE48E-371C-423C-9DB7-22BF810F6AA4} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}.Release|Any CPU.Build.0 = Release|Any CPU + {89613C40-A24A-4972-909F-8E840056349A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89613C40-A24A-4972-909F-8E840056349A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89613C40-A24A-4972-909F-8E840056349A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89613C40-A24A-4972-909F-8E840056349A}.Release|Any CPU.Build.0 = Release|Any CPU + {6DA2F545-AD92-4EF3-8638-D42F1F8C5833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DA2F545-AD92-4EF3-8638-D42F1F8C5833}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DA2F545-AD92-4EF3-8638-D42F1F8C5833}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DA2F545-AD92-4EF3-8638-D42F1F8C5833}.Release|Any CPU.Build.0 = Release|Any CPU + {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection EndGlobal diff --git a/src/Brewery.Api/Brewery.Api.csproj b/src/Brewery.Api/Brewery.Api.csproj new file mode 100644 index 0000000..3d173c2 --- /dev/null +++ b/src/Brewery.Api/Brewery.Api.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/src/Brewery.Api/Program.cs b/src/Brewery.Api/Program.cs new file mode 100644 index 0000000..99b89a4 --- /dev/null +++ b/src/Brewery.Api/Program.cs @@ -0,0 +1,6 @@ +var builder = WebApplication.CreateBuilder(args); + +var app = builder.Build(); +app.MapGet("/", () => "Hello from Brewery Api!"); + +app.Run(); diff --git a/src/Brewery.Api/Properties/launchSettings.json b/src/Brewery.Api/Properties/launchSettings.json new file mode 100644 index 0000000..f3973c1 --- /dev/null +++ b/src/Brewery.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:37980", + "sslPort": 44349 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "/", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Brewery.Api/appsettings.Development.json b/src/Brewery.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Brewery.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Brewery.Api/appsettings.json b/src/Brewery.Api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/Brewery.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Brewery.Application/Brewery.Application.csproj b/src/Brewery.Application/Brewery.Application.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/src/Brewery.Application/Brewery.Application.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Brewery.Domain/Brewery.Domain.csproj b/src/Brewery.Domain/Brewery.Domain.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/src/Brewery.Domain/Brewery.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + From 6f7a475388ec2c82a0d280982ee5189e3abebfbc Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sun, 10 Nov 2024 23:44:10 +0000 Subject: [PATCH 03/24] add project references --- src/Brewery.Api/Brewery.Api.csproj | 4 ++++ src/Brewery.Application/Brewery.Application.csproj | 4 ++++ src/Brewery.Infrastructure/Brewery.Infrastructure.csproj | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/src/Brewery.Api/Brewery.Api.csproj b/src/Brewery.Api/Brewery.Api.csproj index 3d173c2..70d1467 100644 --- a/src/Brewery.Api/Brewery.Api.csproj +++ b/src/Brewery.Api/Brewery.Api.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/Brewery.Application/Brewery.Application.csproj b/src/Brewery.Application/Brewery.Application.csproj index 3a63532..55df28b 100644 --- a/src/Brewery.Application/Brewery.Application.csproj +++ b/src/Brewery.Application/Brewery.Application.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj index 3a63532..84fde08 100644 --- a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj +++ b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj @@ -6,4 +6,8 @@ enable + + + + From 32d358bf0ade0347c2fadfb8ef644ebcfad166dd Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sun, 10 Nov 2024 23:56:43 +0000 Subject: [PATCH 04/24] add Beer, Brewer, Brewery, Wholesaler --- src/Brewery.Domain/Entities/Beer.cs | 13 ++++++++ src/Brewery.Domain/Entities/Brewer.cs | 38 +++++++++++++++++++++++ src/Brewery.Domain/Entities/Brewery.cs | 6 ++++ src/Brewery.Domain/Entities/Wholesaler.cs | 6 ++++ 4 files changed, 63 insertions(+) create mode 100644 src/Brewery.Domain/Entities/Beer.cs create mode 100644 src/Brewery.Domain/Entities/Brewer.cs create mode 100644 src/Brewery.Domain/Entities/Brewery.cs create mode 100644 src/Brewery.Domain/Entities/Wholesaler.cs diff --git a/src/Brewery.Domain/Entities/Beer.cs b/src/Brewery.Domain/Entities/Beer.cs new file mode 100644 index 0000000..6519439 --- /dev/null +++ b/src/Brewery.Domain/Entities/Beer.cs @@ -0,0 +1,13 @@ +namespace Brewery.Domain.Entities; + +public class Beer +{ + public Guid Id { get; set; } + public Guid BrewerId { get; set; } + + public Beer(Guid id, Guid brewerId) + { + Id = id; + } + +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Brewer.cs b/src/Brewery.Domain/Entities/Brewer.cs new file mode 100644 index 0000000..8fe192a --- /dev/null +++ b/src/Brewery.Domain/Entities/Brewer.cs @@ -0,0 +1,38 @@ +namespace Brewery.Domain.Entities; + +public class Brewer +{ + public IEnumerable Beers => _beers; + public Guid Id { get; private set; } + public string Name { get; private set; } + private readonly List _beers = new List(); + + public Brewer(Guid id) + { + Id = id; + } + + public void ChangeName(string name) + => Name = name; + + public void AddBeer(Beer beer) + => _beers.Add(beer); + + public void DeleteBeer(Beer beer) + => _beers.Remove(beer); + + public void UpdateBeer(Beer beer) + { + var exisitingBeer = _beers.First(beer => beer.Id == beer.Id); + + } + + + public static Brewer Create(Guid id, string name) + { + var brewer = new Brewer(id); + brewer.ChangeName(name); + + return brewer; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Brewery.cs b/src/Brewery.Domain/Entities/Brewery.cs new file mode 100644 index 0000000..ca6c8b8 --- /dev/null +++ b/src/Brewery.Domain/Entities/Brewery.cs @@ -0,0 +1,6 @@ +namespace Brewery.Domain.Entities; + +public class Brewery +{ + +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Wholesaler.cs b/src/Brewery.Domain/Entities/Wholesaler.cs new file mode 100644 index 0000000..a2ef44b --- /dev/null +++ b/src/Brewery.Domain/Entities/Wholesaler.cs @@ -0,0 +1,6 @@ +namespace Brewery.Domain.Entities; + +public class Wholesaler +{ + +} \ No newline at end of file From 18ea2c4ea3bc0aefe8a63c1ac14e0fbfa5da5beb Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Mon, 11 Nov 2024 07:00:57 +0000 Subject: [PATCH 05/24] add ICommand, ICommandHandler, ICommandDispatcher --- src/Brewery.Domain/Entities/Brewer.cs | 5 ++--- src/Brewery.Infrastructure/Commands/ICommand.cs | 6 ++++++ src/Brewery.Infrastructure/Commands/ICommandDispatcher.cs | 6 ++++++ src/Brewery.Infrastructure/Commands/ICommandHandler.cs | 6 ++++++ 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/Brewery.Infrastructure/Commands/ICommand.cs create mode 100644 src/Brewery.Infrastructure/Commands/ICommandDispatcher.cs create mode 100644 src/Brewery.Infrastructure/Commands/ICommandHandler.cs diff --git a/src/Brewery.Domain/Entities/Brewer.cs b/src/Brewery.Domain/Entities/Brewer.cs index 8fe192a..728de77 100644 --- a/src/Brewery.Domain/Entities/Brewer.cs +++ b/src/Brewery.Domain/Entities/Brewer.cs @@ -23,11 +23,10 @@ public void DeleteBeer(Beer beer) public void UpdateBeer(Beer beer) { - var exisitingBeer = _beers.First(beer => beer.Id == beer.Id); + var exisitingBeer = _beers.First(b => b.Id == beer.Id); } - - + public static Brewer Create(Guid id, string name) { var brewer = new Brewer(id); diff --git a/src/Brewery.Infrastructure/Commands/ICommand.cs b/src/Brewery.Infrastructure/Commands/ICommand.cs new file mode 100644 index 0000000..d6e7f24 --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/ICommand.cs @@ -0,0 +1,6 @@ +namespace Brewery.Infrastructure.Commands; + +public interface ICommand +{ + +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/ICommandDispatcher.cs b/src/Brewery.Infrastructure/Commands/ICommandDispatcher.cs new file mode 100644 index 0000000..4c74d4f --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/ICommandDispatcher.cs @@ -0,0 +1,6 @@ +namespace Brewery.Infrastructure.Commands; + +public interface ICommandDispatcher +{ + Task DispatchAsync(TCommand command) where TCommand : ICommand; +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/ICommandHandler.cs b/src/Brewery.Infrastructure/Commands/ICommandHandler.cs new file mode 100644 index 0000000..917a660 --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/ICommandHandler.cs @@ -0,0 +1,6 @@ +namespace Brewery.Infrastructure.Commands; + +public interface ICommandHandler where TCommand : class, ICommand +{ + Task HandleAsync(TCommand command); +} \ No newline at end of file From 4e6cc5b98e5a0265f4f56861f68f8ab707157b87 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Tue, 12 Nov 2024 22:58:31 +0000 Subject: [PATCH 06/24] add Migrations, CQRS --- Brewery.sln | 7 + .../Brewery.Abstractions.csproj | 13 ++ src/Brewery.Abstractions/Commands/ICommand.cs | 6 + .../Commands/ICommandDispatcher.cs | 4 +- .../Commands/ICommandHandler.cs | 2 +- .../Exceptions/BreweryException.cs | 11 ++ .../Postgres/PostgresOptions.cs | 6 + src/Brewery.Abstractions/Queries/IQuery.cs | 8 ++ .../Queries/IQueryDispatcher.cs | 6 + .../Queries/IQueryHandler.cs | 6 + src/Brewery.Api/AssemblyLoader.cs | 27 ++++ src/Brewery.Api/Brewery.Api.csproj | 4 + src/Brewery.Api/BreweryApi.rest | 1 + src/Brewery.Api/Controllers/BaseController.cs | 18 +++ .../Controllers/BreweryController.cs | 43 ++++++ src/Brewery.Api/Program.cs | 7 +- src/Brewery.Api/appsettings.json | 5 +- .../Commands/AddBrewery.cs | 5 + .../Commands/Handlers/AddBreweryHandler.cs | 26 ++++ src/Brewery.Application/DTO/BeerDto.cs | 7 + src/Brewery.Application/DTO/BreweryDto.cs | 7 + .../Queries/BrowseBeers.cs | 6 + src/Brewery.Application/Queries/GetBrewery.cs | 6 + src/Brewery.Domain/Brewery.Domain.csproj | 4 + src/Brewery.Domain/Entities/Beer.cs | 26 +++- src/Brewery.Domain/Entities/Brewery.cs | 26 +++- .../Exceptions/InvalidUnitPriceException.cs | 13 ++ .../Repositories/IBreweryRepository.cs | 9 ++ .../Brewery.Infrastructure.csproj | 8 +- .../Commands/CommandDispatcher.cs | 23 ++++ .../Commands/Extensions.cs | 20 +++ .../Commands/ICommand.cs | 6 - .../EF/BreweryDbContext.cs | 23 ++++ src/Brewery.Infrastructure/EF/Extensions.cs | 23 ++++ ...Beer,Brewery,Brewer,Wholesaler.Designer.cs | 124 ++++++++++++++++++ ...05505_AddBeer,Brewery,Brewer,Wholesaler.cs | 114 ++++++++++++++++ .../BreweryDbContextModelSnapshot.cs | 121 +++++++++++++++++ .../EF/Repositories/BreweryRepository.cs | 38 ++++++ src/Brewery.Infrastructure/Extensions.cs | 62 +++++++++ .../Middleware/EndpointsInfoMiddleware.cs | 34 +++++ .../Queries/Extensions.cs | 19 +++ .../Queries/QueryDispatcher.cs | 24 ++++ .../Services/AppInitializer.cs | 28 ++++ 43 files changed, 960 insertions(+), 16 deletions(-) create mode 100644 src/Brewery.Abstractions/Brewery.Abstractions.csproj create mode 100644 src/Brewery.Abstractions/Commands/ICommand.cs rename src/{Brewery.Infrastructure => Brewery.Abstractions}/Commands/ICommandDispatcher.cs (61%) rename src/{Brewery.Infrastructure => Brewery.Abstractions}/Commands/ICommandHandler.cs (72%) create mode 100644 src/Brewery.Abstractions/Exceptions/BreweryException.cs create mode 100644 src/Brewery.Abstractions/Postgres/PostgresOptions.cs create mode 100644 src/Brewery.Abstractions/Queries/IQuery.cs create mode 100644 src/Brewery.Abstractions/Queries/IQueryDispatcher.cs create mode 100644 src/Brewery.Abstractions/Queries/IQueryHandler.cs create mode 100644 src/Brewery.Api/AssemblyLoader.cs create mode 100644 src/Brewery.Api/BreweryApi.rest create mode 100644 src/Brewery.Api/Controllers/BaseController.cs create mode 100644 src/Brewery.Api/Controllers/BreweryController.cs create mode 100644 src/Brewery.Application/Commands/AddBrewery.cs create mode 100644 src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs create mode 100644 src/Brewery.Application/DTO/BeerDto.cs create mode 100644 src/Brewery.Application/DTO/BreweryDto.cs create mode 100644 src/Brewery.Application/Queries/BrowseBeers.cs create mode 100644 src/Brewery.Application/Queries/GetBrewery.cs create mode 100644 src/Brewery.Domain/Exceptions/InvalidUnitPriceException.cs create mode 100644 src/Brewery.Domain/Repositories/IBreweryRepository.cs create mode 100644 src/Brewery.Infrastructure/Commands/CommandDispatcher.cs create mode 100644 src/Brewery.Infrastructure/Commands/Extensions.cs delete mode 100644 src/Brewery.Infrastructure/Commands/ICommand.cs create mode 100644 src/Brewery.Infrastructure/EF/BreweryDbContext.cs create mode 100644 src/Brewery.Infrastructure/EF/Extensions.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.Designer.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs create mode 100644 src/Brewery.Infrastructure/EF/Repositories/BreweryRepository.cs create mode 100644 src/Brewery.Infrastructure/Extensions.cs create mode 100644 src/Brewery.Infrastructure/Middleware/EndpointsInfoMiddleware.cs create mode 100644 src/Brewery.Infrastructure/Queries/Extensions.cs create mode 100644 src/Brewery.Infrastructure/Queries/QueryDispatcher.cs create mode 100644 src/Brewery.Infrastructure/Services/AppInitializer.cs diff --git a/Brewery.sln b/Brewery.sln index 5b7b15a..9cba4fb 100644 --- a/Brewery.sln +++ b/Brewery.sln @@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Application", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Infrastructure", "src\Brewery.Infrastructure\Brewery.Infrastructure.csproj", "{F0FFE48E-371C-423C-9DB7-22BF810F6AA4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Abstractions", "src\Brewery.Abstractions\Brewery.Abstractions.csproj", "{AD694077-201B-49C3-9537-FE96BA598E82}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -22,6 +24,7 @@ Global {89613C40-A24A-4972-909F-8E840056349A} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} {6DA2F545-AD92-4EF3-8638-D42F1F8C5833} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} {F0FFE48E-371C-423C-9DB7-22BF810F6AA4} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + {AD694077-201B-49C3-9537-FE96BA598E82} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -40,5 +43,9 @@ Global {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Debug|Any CPU.Build.0 = Debug|Any CPU {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Release|Any CPU.ActiveCfg = Release|Any CPU {F0FFE48E-371C-423C-9DB7-22BF810F6AA4}.Release|Any CPU.Build.0 = Release|Any CPU + {AD694077-201B-49C3-9537-FE96BA598E82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD694077-201B-49C3-9537-FE96BA598E82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD694077-201B-49C3-9537-FE96BA598E82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD694077-201B-49C3-9537-FE96BA598E82}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Brewery.Abstractions/Brewery.Abstractions.csproj b/src/Brewery.Abstractions/Brewery.Abstractions.csproj new file mode 100644 index 0000000..49a0f4e --- /dev/null +++ b/src/Brewery.Abstractions/Brewery.Abstractions.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Brewery.Abstractions/Commands/ICommand.cs b/src/Brewery.Abstractions/Commands/ICommand.cs new file mode 100644 index 0000000..d3c6799 --- /dev/null +++ b/src/Brewery.Abstractions/Commands/ICommand.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Commands; + +public interface ICommand +{ + +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/ICommandDispatcher.cs b/src/Brewery.Abstractions/Commands/ICommandDispatcher.cs similarity index 61% rename from src/Brewery.Infrastructure/Commands/ICommandDispatcher.cs rename to src/Brewery.Abstractions/Commands/ICommandDispatcher.cs index 4c74d4f..b391652 100644 --- a/src/Brewery.Infrastructure/Commands/ICommandDispatcher.cs +++ b/src/Brewery.Abstractions/Commands/ICommandDispatcher.cs @@ -1,6 +1,6 @@ -namespace Brewery.Infrastructure.Commands; +namespace Brewery.Abstractions.Commands; public interface ICommandDispatcher { - Task DispatchAsync(TCommand command) where TCommand : ICommand; + Task DispatchAsync(TCommand command) where TCommand : class, ICommand; } \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/ICommandHandler.cs b/src/Brewery.Abstractions/Commands/ICommandHandler.cs similarity index 72% rename from src/Brewery.Infrastructure/Commands/ICommandHandler.cs rename to src/Brewery.Abstractions/Commands/ICommandHandler.cs index 917a660..89c3a31 100644 --- a/src/Brewery.Infrastructure/Commands/ICommandHandler.cs +++ b/src/Brewery.Abstractions/Commands/ICommandHandler.cs @@ -1,4 +1,4 @@ -namespace Brewery.Infrastructure.Commands; +namespace Brewery.Abstractions.Commands; public interface ICommandHandler where TCommand : class, ICommand { diff --git a/src/Brewery.Abstractions/Exceptions/BreweryException.cs b/src/Brewery.Abstractions/Exceptions/BreweryException.cs new file mode 100644 index 0000000..6e4dcaf --- /dev/null +++ b/src/Brewery.Abstractions/Exceptions/BreweryException.cs @@ -0,0 +1,11 @@ +namespace Brewery.Abstractions.Exceptions; + +public class BreweryException : Exception +{ + public string Message { get; set; } + + public BreweryException(string message) + { + Message = message; + } +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Postgres/PostgresOptions.cs b/src/Brewery.Abstractions/Postgres/PostgresOptions.cs new file mode 100644 index 0000000..004fd11 --- /dev/null +++ b/src/Brewery.Abstractions/Postgres/PostgresOptions.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Postgres; + +public class PostgresOptions +{ + public string ConnectionString { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Queries/IQuery.cs b/src/Brewery.Abstractions/Queries/IQuery.cs new file mode 100644 index 0000000..cdd0be3 --- /dev/null +++ b/src/Brewery.Abstractions/Queries/IQuery.cs @@ -0,0 +1,8 @@ +namespace Brewery.Abstractions.Queries; + +public interface IQuery +{ + +} + +public interface IQuery : IQuery; \ No newline at end of file diff --git a/src/Brewery.Abstractions/Queries/IQueryDispatcher.cs b/src/Brewery.Abstractions/Queries/IQueryDispatcher.cs new file mode 100644 index 0000000..ccabad4 --- /dev/null +++ b/src/Brewery.Abstractions/Queries/IQueryDispatcher.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Queries; + +public interface IQueryDispatcher +{ + Task QueryAsync(IQuery query); +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Queries/IQueryHandler.cs b/src/Brewery.Abstractions/Queries/IQueryHandler.cs new file mode 100644 index 0000000..5dc5bac --- /dev/null +++ b/src/Brewery.Abstractions/Queries/IQueryHandler.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Queries; + +public interface IQueryHandler where TQuery : IQuery +{ + Task QueryAsync(TQuery query); +} \ No newline at end of file diff --git a/src/Brewery.Api/AssemblyLoader.cs b/src/Brewery.Api/AssemblyLoader.cs new file mode 100644 index 0000000..62b9c87 --- /dev/null +++ b/src/Brewery.Api/AssemblyLoader.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace Brewery.Api; + +public static class AssemblyLoader +{ + public static List GetAssemblies() + { + var assemblies = AppDomain.CurrentDomain + .GetAssemblies() + .OrderBy(assembly => assembly.Location) + .ToList(); + + var locations = assemblies + .Where(a => !a.IsDynamic) + .Select(a => a.Location) + .ToArray(); + + var files = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll") + .Where(f => !locations.Contains(f, StringComparer.InvariantCultureIgnoreCase)) + .ToList(); + + files.ForEach(f => assemblies.Add(AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(f)))); + + return assemblies; + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Brewery.Api.csproj b/src/Brewery.Api/Brewery.Api.csproj index 70d1467..3ef5b5f 100644 --- a/src/Brewery.Api/Brewery.Api.csproj +++ b/src/Brewery.Api/Brewery.Api.csproj @@ -8,6 +8,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Brewery.Api/BreweryApi.rest b/src/Brewery.Api/BreweryApi.rest new file mode 100644 index 0000000..2fcba0d --- /dev/null +++ b/src/Brewery.Api/BreweryApi.rest @@ -0,0 +1 @@ +@url = http://localhost:5000/brewery \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BaseController.cs b/src/Brewery.Api/Controllers/BaseController.cs new file mode 100644 index 0000000..076369f --- /dev/null +++ b/src/Brewery.Api/Controllers/BaseController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +[ApiController] +[Route("/[controller]")] +public class BaseController : ControllerBase +{ + public ActionResult OkOrNotFound(TModel model) + { + if (model is null) + { + return NotFound(); + } + + return Ok(model); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BreweryController.cs b/src/Brewery.Api/Controllers/BreweryController.cs new file mode 100644 index 0000000..105ae20 --- /dev/null +++ b/src/Brewery.Api/Controllers/BreweryController.cs @@ -0,0 +1,43 @@ +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class BreweryController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + private readonly IQueryDispatcher _queryDispatcher; + + public BreweryController(ICommandDispatcher commandDispatcher, + IQueryDispatcher queryDispatcher) + { + _commandDispatcher = commandDispatcher; + _queryDispatcher = queryDispatcher; + } + + [HttpGet("{breweryId:guid}")] + public async Task> Get(Guid breweryId) + { + var brewery = await _queryDispatcher.QueryAsync(new GetBrewery(breweryId)); + return OkOrNotFound(brewery); + } + + [HttpGet("{breweryId:guid}/beers")] + public async Task>> Browse(Guid breweryId) + { + var beersDto = await _queryDispatcher + .QueryAsync(new BrowseBeers(breweryId)); + return Ok(beersDto); + } + + [HttpPost] + public async Task Post(AddBrewery command) + { + await _commandDispatcher.DispatchAsync(command with { Id = Guid.NewGuid() }); + return CreatedAtAction(nameof(Get), new { breweryId = command.Id }, null); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Program.cs b/src/Brewery.Api/Program.cs index 99b89a4..0cb5116 100644 --- a/src/Brewery.Api/Program.cs +++ b/src/Brewery.Api/Program.cs @@ -1,6 +1,11 @@ +using Brewery.Api; +using Brewery.Infrastructure; + var builder = WebApplication.CreateBuilder(args); +var assemblies = AssemblyLoader.GetAssemblies(); +builder.Services.AddInfrastructure(assemblies); var app = builder.Build(); -app.MapGet("/", () => "Hello from Brewery Api!"); +app.UseInfrastructure(); app.Run(); diff --git a/src/Brewery.Api/appsettings.json b/src/Brewery.Api/appsettings.json index 10f68b8..ce7c425 100644 --- a/src/Brewery.Api/appsettings.json +++ b/src/Brewery.Api/appsettings.json @@ -5,5 +5,8 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "postgres": { + "connectionString": "Host=localhost;Database=Brewery;Username=postgres;Password=czcz" + } } diff --git a/src/Brewery.Application/Commands/AddBrewery.cs b/src/Brewery.Application/Commands/AddBrewery.cs new file mode 100644 index 0000000..102d4b7 --- /dev/null +++ b/src/Brewery.Application/Commands/AddBrewery.cs @@ -0,0 +1,5 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddBrewery(Guid Id, string Name) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs new file mode 100644 index 0000000..e6a2184 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs @@ -0,0 +1,26 @@ +using Brewery.Abstractions.Commands; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddBreweryHandler : ICommandHandler +{ + private readonly IBreweryRepository _breweryRepository; + + public AddBreweryHandler(IBreweryRepository breweryRepository) + { + _breweryRepository = breweryRepository; + } + + public async Task HandleAsync(AddBrewery command) + { + var brewery = await _breweryRepository.GetBreweryById(command.Id); + if (brewery is not null) + { + + } + + brewery = Domain.Entities.Brewery.Create(command.Id, command.Name); + await _breweryRepository.AddBrewery(brewery); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/BeerDto.cs b/src/Brewery.Application/DTO/BeerDto.cs new file mode 100644 index 0000000..3f764a6 --- /dev/null +++ b/src/Brewery.Application/DTO/BeerDto.cs @@ -0,0 +1,7 @@ +namespace Brewery.Application.DTO; + +public class BeerDto +{ + public string Name { get; set; } + public decimal UnitPrice { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/BreweryDto.cs b/src/Brewery.Application/DTO/BreweryDto.cs new file mode 100644 index 0000000..7a5a44a --- /dev/null +++ b/src/Brewery.Application/DTO/BreweryDto.cs @@ -0,0 +1,7 @@ +namespace Brewery.Application.DTO; + +public class BreweryDto +{ + public Guid Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/Queries/BrowseBeers.cs b/src/Brewery.Application/Queries/BrowseBeers.cs new file mode 100644 index 0000000..3eac452 --- /dev/null +++ b/src/Brewery.Application/Queries/BrowseBeers.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record BrowseBeers(Guid BreweryId) : IQuery>; \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetBrewery.cs b/src/Brewery.Application/Queries/GetBrewery.cs new file mode 100644 index 0000000..7901def --- /dev/null +++ b/src/Brewery.Application/Queries/GetBrewery.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record GetBrewery(Guid Id) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Domain/Brewery.Domain.csproj b/src/Brewery.Domain/Brewery.Domain.csproj index 3a63532..8877d85 100644 --- a/src/Brewery.Domain/Brewery.Domain.csproj +++ b/src/Brewery.Domain/Brewery.Domain.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/src/Brewery.Domain/Entities/Beer.cs b/src/Brewery.Domain/Entities/Beer.cs index 6519439..6bfd4e4 100644 --- a/src/Brewery.Domain/Entities/Beer.cs +++ b/src/Brewery.Domain/Entities/Beer.cs @@ -1,13 +1,33 @@ -namespace Brewery.Domain.Entities; +using Brewery.Abstractions.Exceptions; +using Brewery.Domain.Exceptions; + +namespace Brewery.Domain.Entities; public class Beer { - public Guid Id { get; set; } - public Guid BrewerId { get; set; } + public Guid Id { get; private set; } + public Guid BrewerId { get; private set; } + public decimal UnitPrice { get; private set; } public Beer(Guid id, Guid brewerId) { Id = id; } + public void SetPrice(decimal unitPrice) + { + if (unitPrice <= 0) + { + throw new InvalidUnitPriceException(unitPrice); + } + + UnitPrice = unitPrice; + } + + public static Beer Create(Guid id, Guid brewerId) + { + var beer = new Beer(id, brewerId); + + return beer; + } } \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Brewery.cs b/src/Brewery.Domain/Entities/Brewery.cs index ca6c8b8..68276b5 100644 --- a/src/Brewery.Domain/Entities/Brewery.cs +++ b/src/Brewery.Domain/Entities/Brewery.cs @@ -2,5 +2,29 @@ public class Brewery { - + private List _beers = new List(); + public Guid Id { get; private set; } + public string Name { get; private set; } + //public Guid BrewerId { get; private set; } + public Brewer Brewer { get; private set; } + public IEnumerable Beers => _beers; + + public Brewery(Guid id) + { + Id = id; + } + + public void ChangeName(string name) + { + Name = name; + } + + public static Brewery Create(Guid id, string name) + { + var brewery = new Brewery(id); + brewery.ChangeName(name); + + return brewery; + } + } \ No newline at end of file diff --git a/src/Brewery.Domain/Exceptions/InvalidUnitPriceException.cs b/src/Brewery.Domain/Exceptions/InvalidUnitPriceException.cs new file mode 100644 index 0000000..89616da --- /dev/null +++ b/src/Brewery.Domain/Exceptions/InvalidUnitPriceException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class InvalidUnitPriceException : BreweryException +{ + public decimal Price { get; } + public InvalidUnitPriceException(decimal price) + : base($"Unit Price of '{price}' is invalid.") + { + Price = price; + } +} diff --git a/src/Brewery.Domain/Repositories/IBreweryRepository.cs b/src/Brewery.Domain/Repositories/IBreweryRepository.cs new file mode 100644 index 0000000..fbb0961 --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBreweryRepository.cs @@ -0,0 +1,9 @@ +namespace Brewery.Domain.Repositories; + +public interface IBreweryRepository +{ + Task AddBrewery(Entities.Brewery brewery); + Task UpdateBrewery(Entities.Brewery brewery); + Task DeleteBrewery(Entities.Brewery brewery); + Task GetBreweryById(Guid breweryId); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj index 84fde08..2a58f42 100644 --- a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj +++ b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj @@ -5,9 +5,15 @@ enable enable - + + + + + + + diff --git a/src/Brewery.Infrastructure/Commands/CommandDispatcher.cs b/src/Brewery.Infrastructure/Commands/CommandDispatcher.cs new file mode 100644 index 0000000..5d00eae --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/CommandDispatcher.cs @@ -0,0 +1,23 @@ +using Brewery.Abstractions.Commands; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Commands; + +public class CommandDispatcher : ICommandDispatcher +{ + private readonly IServiceProvider _serviceProvider; + + public CommandDispatcher(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task DispatchAsync(TCommand command) where TCommand : class, ICommand + { + using var scope = _serviceProvider.CreateScope(); + var handler = scope.ServiceProvider + .GetService>(); + + await handler.HandleAsync(command); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/Extensions.cs b/src/Brewery.Infrastructure/Commands/Extensions.cs new file mode 100644 index 0000000..48798ae --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/Extensions.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using Brewery.Abstractions.Commands; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Commands; + +public static class Extensions +{ + public static IServiceCollection AddCommands(this IServiceCollection services, IList assemblies) + { + services.AddSingleton(); + + services.Scan(a => a.FromAssemblies(assemblies) + .AddClasses(c => c.AssignableTo(typeof(ICommandHandler<>))) + .AsImplementedInterfaces() + .WithScopedLifetime()); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/ICommand.cs b/src/Brewery.Infrastructure/Commands/ICommand.cs deleted file mode 100644 index d6e7f24..0000000 --- a/src/Brewery.Infrastructure/Commands/ICommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Brewery.Infrastructure.Commands; - -public interface ICommand -{ - -} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs new file mode 100644 index 0000000..68ff90a --- /dev/null +++ b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs @@ -0,0 +1,23 @@ +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF; + +public class BreweryDbContext : DbContext +{ + public DbSet Breweries { get; set; } + public DbSet Beers { get; set; } + public DbSet Brewers { get; set; } + + public BreweryDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); + modelBuilder.HasDefaultSchema("brewery"); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Extensions.cs b/src/Brewery.Infrastructure/EF/Extensions.cs new file mode 100644 index 0000000..43641b8 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Extensions.cs @@ -0,0 +1,23 @@ +using Brewery.Abstractions.Postgres; +using Brewery.Domain.Repositories; +using Brewery.Infrastructure.EF.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.EF; + +public static class Extensions +{ + internal static IServiceCollection AddEF(this IServiceCollection services) + { + services.AddScoped(); + + var postgresOptions = services.GetOptions("postgres"); + services.AddDbContext(options => + { + options.UseNpgsql(postgresOptions.ConnectionString); + }); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.Designer.cs new file mode 100644 index 0000000..d48c668 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.Designer.cs @@ -0,0 +1,124 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241111205505_AddBeer,Brewery,Brewer,Wholesaler")] + partial class AddBeerBreweryBrewerWholesaler + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.HasIndex("BreweryId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Beers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", "Brewer") + .WithMany() + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Brewer"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Beers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.cs b/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.cs new file mode 100644 index 0000000..657f0b2 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.cs @@ -0,0 +1,114 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class AddBeerBreweryBrewerWholesaler : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "brewery"); + + migrationBuilder.CreateTable( + name: "Brewers", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Brewers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Breweries", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + BrewerId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Breweries", x => x.Id); + table.ForeignKey( + name: "FK_Breweries_Brewers_BrewerId", + column: x => x.BrewerId, + principalSchema: "brewery", + principalTable: "Brewers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Beers", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BrewerId = table.Column(type: "uuid", nullable: false), + UnitPrice = table.Column(type: "numeric", nullable: false), + BreweryId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Beers", x => x.Id); + table.ForeignKey( + name: "FK_Beers_Breweries_BreweryId", + column: x => x.BreweryId, + principalSchema: "brewery", + principalTable: "Breweries", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Beers_Brewers_BrewerId", + column: x => x.BrewerId, + principalSchema: "brewery", + principalTable: "Brewers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Beers_BrewerId", + schema: "brewery", + table: "Beers", + column: "BrewerId"); + + migrationBuilder.CreateIndex( + name: "IX_Beers_BreweryId", + schema: "brewery", + table: "Beers", + column: "BreweryId"); + + migrationBuilder.CreateIndex( + name: "IX_Breweries_BrewerId", + schema: "brewery", + table: "Breweries", + column: "BrewerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Beers", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "Breweries", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "Brewers", + schema: "brewery"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs new file mode 100644 index 0000000..4353041 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs @@ -0,0 +1,121 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + partial class BreweryDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.HasIndex("BreweryId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Beers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", "Brewer") + .WithMany() + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Brewer"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Beers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Repositories/BreweryRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BreweryRepository.cs new file mode 100644 index 0000000..0d55682 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BreweryRepository.cs @@ -0,0 +1,38 @@ +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BreweryRepository : IBreweryRepository +{ + private readonly DbSet _breweries; + private readonly BreweryDbContext _dbContext; + + public BreweryRepository(BreweryDbContext dbContext) + { + _breweries = dbContext.Breweries; + _dbContext = dbContext; + } + + public async Task AddBrewery(Domain.Entities.Brewery brewery) + { + await _breweries.AddAsync(brewery); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateBrewery(Domain.Entities.Brewery brewery) + { + _breweries.Update(brewery); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteBrewery(Domain.Entities.Brewery brewery) + { + _breweries.Remove(brewery); + await _dbContext.SaveChangesAsync(); + } + + public Task GetBreweryById(Guid breweryId) + => _breweries + .SingleOrDefaultAsync(b => b.Id == breweryId); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Extensions.cs b/src/Brewery.Infrastructure/Extensions.cs new file mode 100644 index 0000000..81d0c01 --- /dev/null +++ b/src/Brewery.Infrastructure/Extensions.cs @@ -0,0 +1,62 @@ +using System.Reflection; +using Brewery.Infrastructure.Commands; +using Brewery.Infrastructure.EF; +using Brewery.Infrastructure.Middleware; +using Brewery.Infrastructure.Queries; +using Brewery.Infrastructure.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure; + +public static class Extensions +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services, IList assemblies) + { + services.AddControllers(); + services.AddHostedService(); + services.AddSingleton(); + services.AddEF(); + services.AddCommands(assemblies); + services.AddQueries(assemblies); + + return services; + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseRouting(); + app.UseEndpoints(e => + { + e.MapControllers(); + e.MapGet("/", () => "Hello from Brewery Api!"); + e.MapGet("/endpoints", async (HttpContext context, EndpointsInfoMiddleware middleware) => + { + await middleware.InvokeAsync(context, _ => Task.CompletedTask); + }); + }); + + return app; + } + + public static TOptions GetOptions(this IServiceCollection services, string sectionName) + where TOptions : class, new() + { + using var serviceProvider = services.BuildServiceProvider(); + var configuration = serviceProvider.GetRequiredService(); + + return configuration.GetOptions(sectionName); + } + + public static TOptions GetOptions(this IConfiguration configuration, string sectionName) + where TOptions : class, new() + { + var options = new TOptions(); + var section = configuration.GetSection(sectionName); + section.Bind(options); + + return options; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Middleware/EndpointsInfoMiddleware.cs b/src/Brewery.Infrastructure/Middleware/EndpointsInfoMiddleware.cs new file mode 100644 index 0000000..151022c --- /dev/null +++ b/src/Brewery.Infrastructure/Middleware/EndpointsInfoMiddleware.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Brewery.Infrastructure.Middleware; + +public class EndpointsInfoMiddleware : IMiddleware +{ + private readonly EndpointDataSource _endpointDataSource; + + public EndpointsInfoMiddleware(EndpointDataSource endpointDataSource) + { + _endpointDataSource = endpointDataSource; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var endpoints = _endpointDataSource.Endpoints + .Select(e => + { + var routeEndpoint = e as RouteEndpoint; + + return new + { + Endpoint = e.DisplayName, + Methods = string.Join(", ", e.Metadata.OfType() + .Select(m => string.Join(", ", m.HttpMethods))), + Route = routeEndpoint?.RoutePattern?.RawText, + }; + }) + .ToArray(); + + await context.Response.WriteAsJsonAsync(endpoints); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Queries/Extensions.cs b/src/Brewery.Infrastructure/Queries/Extensions.cs new file mode 100644 index 0000000..cfe2959 --- /dev/null +++ b/src/Brewery.Infrastructure/Queries/Extensions.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using Brewery.Abstractions.Queries; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Queries; + +public static class Extensions +{ + public static IServiceCollection AddQueries(this IServiceCollection services, IList assemblies) + { + services.AddSingleton(); + services.Scan(a => a.FromAssemblies(assemblies) + .AddClasses(c => c.AssignableTo(typeof(IQueryHandler<,>))) + .AsImplementedInterfaces() + .WithScopedLifetime()); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Queries/QueryDispatcher.cs b/src/Brewery.Infrastructure/Queries/QueryDispatcher.cs new file mode 100644 index 0000000..d57895e --- /dev/null +++ b/src/Brewery.Infrastructure/Queries/QueryDispatcher.cs @@ -0,0 +1,24 @@ +using Brewery.Abstractions.Queries; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Queries; + +public class QueryDispatcher : IQueryDispatcher +{ + private readonly IServiceProvider _serviceProvider; + + public QueryDispatcher(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public Task QueryAsync(IQuery query) + { + using var scope = _serviceProvider.CreateScope(); + var handlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); + var handler = scope.ServiceProvider.GetRequiredService(handlerType); + + return (Task)handlerType.GetMethod(nameof(IQueryHandler,TResult>.QueryAsync)) + ?.Invoke(handler, new[] { query }); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Services/AppInitializer.cs b/src/Brewery.Infrastructure/Services/AppInitializer.cs new file mode 100644 index 0000000..3aa00d7 --- /dev/null +++ b/src/Brewery.Infrastructure/Services/AppInitializer.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Brewery.Infrastructure.Services; + +public class AppInitializer : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + + public AppInitializer(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var dbContextTypes = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(t => t.GetTypes()) + .Where(t => typeof(DbContext).IsAssignableFrom(t) && !t.IsInterface && t != typeof(DbContext)); + using var scope = _serviceProvider.CreateScope(); + foreach (var dbType in dbContextTypes) + { + var dbContext = scope.ServiceProvider.GetService(dbType) as DbContext; + await dbContext.Database.MigrateAsync(); + } + } +} \ No newline at end of file From e67694a73d046007be49fe34c51fd73ea1f11a8b Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Wed, 13 Nov 2024 00:03:21 +0000 Subject: [PATCH 07/24] add BeerQueries and Commands --- src/Brewery.Api/BreweryApi.rest | 30 ++++- src/Brewery.Api/Controllers/BeerController.cs | 47 ++++++++ .../Controllers/BreweryController.cs | 4 +- src/Brewery.Application/Commands/AddBeer.cs | 8 ++ .../Commands/AddBrewery.cs | 5 +- .../Commands/DeleteBeer.cs | 5 + .../Commands/Handlers/AddBeerHandler.cs | 29 +++++ .../Commands/Handlers/DeleteBeerHandler.cs | 26 ++++ .../Commands/Handlers/UpdateBeerHandler.cs | 6 + .../Commands/UpdateBeer.cs | 5 + src/Brewery.Application/DTO/BeerDto.cs | 7 +- src/Brewery.Application/DTO/Extensions.cs | 17 +++ .../Exceptions/BeerAlreadyExistException.cs | 13 ++ .../Exceptions/BeerNotFoundException.cs | 13 ++ .../Queries/BrowseBeersByBrewery.cs | 6 + .../Queries/{BrowseBeers.cs => GetBeer.cs} | 2 +- src/Brewery.Domain/Entities/Beer.cs | 12 +- src/Brewery.Domain/Entities/Brewery.cs | 2 +- .../Repositories/IBeerRepository.cs | 11 ++ .../Brewery.Infrastructure.csproj | 1 - .../20241112230230_EditBrewery.Designer.cs | 108 +++++++++++++++++ .../Migrations/20241112230230_EditBrewery.cs | 58 +++++++++ .../20241112233804_EditBeer.Designer.cs | 112 ++++++++++++++++++ .../EF/Migrations/20241112233804_EditBeer.cs | 31 +++++ .../BreweryDbContextModelSnapshot.cs | 20 +--- .../Handlers/BrowseBeersByBreweryHandler.cs | 27 +++++ .../EF/Queries/Handlers/GetBreweryHandler.cs | 31 +++++ .../Queries/QueryDispatcher.cs | 4 +- 28 files changed, 611 insertions(+), 29 deletions(-) create mode 100644 src/Brewery.Api/Controllers/BeerController.cs create mode 100644 src/Brewery.Application/Commands/AddBeer.cs create mode 100644 src/Brewery.Application/Commands/DeleteBeer.cs create mode 100644 src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs create mode 100644 src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs create mode 100644 src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs create mode 100644 src/Brewery.Application/Commands/UpdateBeer.cs create mode 100644 src/Brewery.Application/DTO/Extensions.cs create mode 100644 src/Brewery.Application/Exceptions/BeerAlreadyExistException.cs create mode 100644 src/Brewery.Application/Exceptions/BeerNotFoundException.cs create mode 100644 src/Brewery.Application/Queries/BrowseBeersByBrewery.cs rename src/Brewery.Application/Queries/{BrowseBeers.cs => GetBeer.cs} (60%) create mode 100644 src/Brewery.Domain/Repositories/IBeerRepository.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.Designer.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.Designer.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.cs create mode 100644 src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs create mode 100644 src/Brewery.Infrastructure/EF/Queries/Handlers/GetBreweryHandler.cs diff --git a/src/Brewery.Api/BreweryApi.rest b/src/Brewery.Api/BreweryApi.rest index 2fcba0d..cd81454 100644 --- a/src/Brewery.Api/BreweryApi.rest +++ b/src/Brewery.Api/BreweryApi.rest @@ -1 +1,29 @@ -@url = http://localhost:5000/brewery \ No newline at end of file +@url = http://localhost:5000 + +@breweryId = d730c80b-5680-4058-a64e-892c501cb287 + +### +GET {{url}}/beer/{{breweryId}} + +### +POST {{url}}/beer +Content-Type: application/json + +{ + "name": "beer 1", + "unitPrice": 3.50 +} + +### +GET {{url}}/brewery/{{breweryId}} + +### +GET {{url}}/brewery/{{breweryId}}/beers + +### +POST {{url}}/brewery +Content-Type: application/json + +{ + "name": "brewery 2" +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BeerController.cs b/src/Brewery.Api/Controllers/BeerController.cs new file mode 100644 index 0000000..feae30f --- /dev/null +++ b/src/Brewery.Api/Controllers/BeerController.cs @@ -0,0 +1,47 @@ +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.Commands.Handlers; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class BeerController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + private readonly IQueryDispatcher _queryDispatcher; + + public BeerController(ICommandDispatcher commandDispatcher, + IQueryDispatcher queryDispatcher) + { + _commandDispatcher = commandDispatcher; + _queryDispatcher = queryDispatcher; + } + + [HttpGet("{beerId:guid}")] + public async Task> GetBeer(Guid beerId) + => OkOrNotFound(await _queryDispatcher.QueryAsync(new GetBeer(beerId))); + + [HttpPost] + public async Task AddBeer(AddBeer command) + { + await _commandDispatcher.DispatchAsync(command); + return CreatedAtAction(nameof(GetBeer), new { beerId = command.Id }, null); + } + + [HttpPut("{beerId:guid}")] + public async Task UpdateBeer(UpdateBeer command, Guid beerId) + { + await _commandDispatcher.DispatchAsync(command with { Id = beerId }); + return NoContent(); + } + + [HttpDelete("{beerId:guid}")] + public async Task DeleteBeer(Guid beerId) + { + await _commandDispatcher.DispatchAsync(new DeleteBeer(beerId)); + return NoContent(); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BreweryController.cs b/src/Brewery.Api/Controllers/BreweryController.cs index 105ae20..dcf326f 100644 --- a/src/Brewery.Api/Controllers/BreweryController.cs +++ b/src/Brewery.Api/Controllers/BreweryController.cs @@ -30,14 +30,14 @@ public async Task> Get(Guid breweryId) public async Task>> Browse(Guid breweryId) { var beersDto = await _queryDispatcher - .QueryAsync(new BrowseBeers(breweryId)); + .QueryAsync(new BrowseBeersByBrewery(breweryId)); return Ok(beersDto); } [HttpPost] public async Task Post(AddBrewery command) { - await _commandDispatcher.DispatchAsync(command with { Id = Guid.NewGuid() }); + await _commandDispatcher.DispatchAsync(command); return CreatedAtAction(nameof(Get), new { breweryId = command.Id }, null); } } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBeer.cs b/src/Brewery.Application/Commands/AddBeer.cs new file mode 100644 index 0000000..5aa53e2 --- /dev/null +++ b/src/Brewery.Application/Commands/AddBeer.cs @@ -0,0 +1,8 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddBeer(Guid BrewerId, string Name, decimal UnitPrice) : ICommand +{ + public Guid Id { get; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBrewery.cs b/src/Brewery.Application/Commands/AddBrewery.cs index 102d4b7..0f15023 100644 --- a/src/Brewery.Application/Commands/AddBrewery.cs +++ b/src/Brewery.Application/Commands/AddBrewery.cs @@ -2,4 +2,7 @@ namespace Brewery.Application.Commands; -public record AddBrewery(Guid Id, string Name) : ICommand; \ No newline at end of file +public record AddBrewery(string Name) : ICommand +{ + public Guid Id { get; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/DeleteBeer.cs b/src/Brewery.Application/Commands/DeleteBeer.cs new file mode 100644 index 0000000..288f0cf --- /dev/null +++ b/src/Brewery.Application/Commands/DeleteBeer.cs @@ -0,0 +1,5 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record DeleteBeer(Guid Id) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs new file mode 100644 index 0000000..f5bb9fd --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs @@ -0,0 +1,29 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddBeerHandler : ICommandHandler +{ + private readonly IBeerRepository _beerRepository; + + public AddBeerHandler(IBeerRepository beerRepository) + { + _beerRepository = beerRepository; + } + + public async Task HandleAsync(AddBeer command) + { + var beer = await _beerRepository.GetBeerById(command.Id); + if (beer is not null) + { + throw new BeerAlreadyExistException(command.Id); + } + + beer = Beer.Create(command.Id, command.BrewerId, command.Name, command.UnitPrice); + + await _beerRepository.AddAsync(beer); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs new file mode 100644 index 0000000..4301d10 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs @@ -0,0 +1,26 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class DeleteBeerHandler : ICommandHandler +{ + private readonly IBeerRepository _beerRepositry; + + public DeleteBeerHandler(IBeerRepository beerRepositry) + { + _beerRepositry = beerRepositry; + } + + public async Task HandleAsync(DeleteBeer command) + { + var beer = await _beerRepositry.GetBeerById(command.Id); + if (beer is null) + { + throw new BeerNotFoundException(command.Id); + } + + await _beerRepositry.DeleteAsync(beer); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs new file mode 100644 index 0000000..ca9b1cc --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs @@ -0,0 +1,6 @@ +namespace Brewery.Application.Commands.Handlers; + +public class UpdateBeerHandler +{ + +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/UpdateBeer.cs b/src/Brewery.Application/Commands/UpdateBeer.cs new file mode 100644 index 0000000..90c250b --- /dev/null +++ b/src/Brewery.Application/Commands/UpdateBeer.cs @@ -0,0 +1,5 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record UpdateBeer(Guid Id, string Name, decimal UnitPrice) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/DTO/BeerDto.cs b/src/Brewery.Application/DTO/BeerDto.cs index 3f764a6..fc4c07c 100644 --- a/src/Brewery.Application/DTO/BeerDto.cs +++ b/src/Brewery.Application/DTO/BeerDto.cs @@ -1,7 +1,10 @@ -namespace Brewery.Application.DTO; +using Brewery.Domain.Entities; + +namespace Brewery.Application.DTO; public class BeerDto { + public Guid Id { get; set; } public string Name { get; set; } public decimal UnitPrice { get; set; } -} \ No newline at end of file +} diff --git a/src/Brewery.Application/DTO/Extensions.cs b/src/Brewery.Application/DTO/Extensions.cs new file mode 100644 index 0000000..ca684dd --- /dev/null +++ b/src/Brewery.Application/DTO/Extensions.cs @@ -0,0 +1,17 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Application.DTO; + +public static class Extensions +{ + public static BeerDto AsDto(this Beer beer) + { + var beerDto = new BeerDto + { + Id = beer.Id, + Name = beer.Name, + }; + + return beerDto; + } +} diff --git a/src/Brewery.Application/Exceptions/BeerAlreadyExistException.cs b/src/Brewery.Application/Exceptions/BeerAlreadyExistException.cs new file mode 100644 index 0000000..51c6ac9 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerAlreadyExistException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerAlreadyExistException : BreweryException +{ + public Guid BeerId { get; } + public BeerAlreadyExistException(Guid beerId) + : base($"Beer with id '{beerId}' already exists.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerNotFoundException.cs b/src/Brewery.Application/Exceptions/BeerNotFoundException.cs new file mode 100644 index 0000000..cee18ba --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerNotFoundException : BreweryException +{ + public Guid BeerId { get; } + public BeerNotFoundException(Guid beerId) + : base($"Beer with id '{beerId}' was not found.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Queries/BrowseBeersByBrewery.cs b/src/Brewery.Application/Queries/BrowseBeersByBrewery.cs new file mode 100644 index 0000000..8707c79 --- /dev/null +++ b/src/Brewery.Application/Queries/BrowseBeersByBrewery.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record BrowseBeersByBrewery(Guid BreweryId) : IQuery>; \ No newline at end of file diff --git a/src/Brewery.Application/Queries/BrowseBeers.cs b/src/Brewery.Application/Queries/GetBeer.cs similarity index 60% rename from src/Brewery.Application/Queries/BrowseBeers.cs rename to src/Brewery.Application/Queries/GetBeer.cs index 3eac452..5b64fad 100644 --- a/src/Brewery.Application/Queries/BrowseBeers.cs +++ b/src/Brewery.Application/Queries/GetBeer.cs @@ -3,4 +3,4 @@ namespace Brewery.Application.Queries; -public record BrowseBeers(Guid BreweryId) : IQuery>; \ No newline at end of file +public record GetBeer(Guid Id) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Beer.cs b/src/Brewery.Domain/Entities/Beer.cs index 6bfd4e4..f9fb774 100644 --- a/src/Brewery.Domain/Entities/Beer.cs +++ b/src/Brewery.Domain/Entities/Beer.cs @@ -7,13 +7,19 @@ public class Beer { public Guid Id { get; private set; } public Guid BrewerId { get; private set; } + public string Name { get; private set; } public decimal UnitPrice { get; private set; } public Beer(Guid id, Guid brewerId) { Id = id; } - + + public void ChangeName(string name) + { + Name = name; + } + public void SetPrice(decimal unitPrice) { if (unitPrice <= 0) @@ -24,9 +30,11 @@ public void SetPrice(decimal unitPrice) UnitPrice = unitPrice; } - public static Beer Create(Guid id, Guid brewerId) + public static Beer Create(Guid id, Guid brewerId, string name, decimal unitPrice) { var beer = new Beer(id, brewerId); + beer.ChangeName(name); + beer.SetPrice(unitPrice); return beer; } diff --git a/src/Brewery.Domain/Entities/Brewery.cs b/src/Brewery.Domain/Entities/Brewery.cs index 68276b5..1aaf789 100644 --- a/src/Brewery.Domain/Entities/Brewery.cs +++ b/src/Brewery.Domain/Entities/Brewery.cs @@ -6,7 +6,7 @@ public class Brewery public Guid Id { get; private set; } public string Name { get; private set; } //public Guid BrewerId { get; private set; } - public Brewer Brewer { get; private set; } + //public Brewer Brewer { get; private set; } public IEnumerable Beers => _beers; public Brewery(Guid id) diff --git a/src/Brewery.Domain/Repositories/IBeerRepository.cs b/src/Brewery.Domain/Repositories/IBeerRepository.cs new file mode 100644 index 0000000..2de516a --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBeerRepository.cs @@ -0,0 +1,11 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IBeerRepository +{ + Task AddAsync(Beer beer); + Task UpdateAsync(Beer beer); + Task DeleteAsync(Beer beer); + Task GetBeerById(Guid id); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj index 2a58f42..762e47b 100644 --- a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj +++ b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj @@ -3,7 +3,6 @@ net8.0 enable - enable diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.Designer.cs new file mode 100644 index 0000000..654fbea --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.Designer.cs @@ -0,0 +1,108 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241112230230_EditBrewery")] + partial class EditBrewery + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.HasIndex("BreweryId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Beers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Beers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.cs b/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.cs new file mode 100644 index 0000000..d49e866 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class EditBrewery : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Breweries_Brewers_BrewerId", + schema: "brewery", + table: "Breweries"); + + migrationBuilder.DropIndex( + name: "IX_Breweries_BrewerId", + schema: "brewery", + table: "Breweries"); + + migrationBuilder.DropColumn( + name: "BrewerId", + schema: "brewery", + table: "Breweries"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BrewerId", + schema: "brewery", + table: "Breweries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateIndex( + name: "IX_Breweries_BrewerId", + schema: "brewery", + table: "Breweries", + column: "BrewerId"); + + migrationBuilder.AddForeignKey( + name: "FK_Breweries_Brewers_BrewerId", + schema: "brewery", + table: "Breweries", + column: "BrewerId", + principalSchema: "brewery", + principalTable: "Brewers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.Designer.cs new file mode 100644 index 0000000..3d4773c --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.Designer.cs @@ -0,0 +1,112 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241112233804_EditBeer")] + partial class EditBeer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.HasIndex("BreweryId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Beers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Beers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.cs new file mode 100644 index 0000000..58cf06b --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class EditBeer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Name", + schema: "brewery", + table: "Beers", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Name", + schema: "brewery", + table: "Beers"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs index 4353041..1c246fd 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs @@ -35,6 +35,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("BreweryId") .HasColumnType("uuid"); + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + b.Property("UnitPrice") .HasColumnType("numeric"); @@ -68,17 +72,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("BrewerId") - .HasColumnType("uuid"); - b.Property("Name") .IsRequired() .HasColumnType("text"); b.HasKey("Id"); - b.HasIndex("BrewerId"); - b.ToTable("Breweries", "brewery"); }); @@ -95,17 +94,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("BreweryId"); }); - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.HasOne("Brewery.Domain.Entities.Brewer", "Brewer") - .WithMany() - .HasForeignKey("BrewerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Brewer"); - }); - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => { b.Navigation("Beers"); diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs new file mode 100644 index 0000000..4710c10 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs @@ -0,0 +1,27 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class BrowseBeersByBreweryHandler : IQueryHandler> +{ + private readonly DbSet _breweries; + + public BrowseBeersByBreweryHandler(BreweryDbContext dbContext) + { + _breweries = dbContext.Breweries; + } + public async Task> QueryAsync(BrowseBeersByBrewery query) + { + var brewery = await _breweries + .AsNoTracking() + .Include(b => b.Beers) + .SingleOrDefaultAsync(b => b.Id == query.BreweryId); + + return brewery is not null + ? brewery?.Beers.Select(b => b.AsDto()) + : Enumerable.Empty(); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBreweryHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBreweryHandler.cs new file mode 100644 index 0000000..2902e7d --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBreweryHandler.cs @@ -0,0 +1,31 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetBreweryHandler : IQueryHandler +{ + private readonly DbSet _breweries; + + public GetBreweryHandler(BreweryDbContext dbContext) + { + _breweries = dbContext.Breweries; + } + + public async Task QueryAsync(GetBrewery query) + { + var brewery = await _breweries + .AsNoTracking() + .SingleOrDefaultAsync(b => b.Id == query.Id); + + return brewery is not null + ? new BreweryDto + { + Id = brewery.Id, + Name = brewery.Name, + } + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Queries/QueryDispatcher.cs b/src/Brewery.Infrastructure/Queries/QueryDispatcher.cs index d57895e..f1e6bde 100644 --- a/src/Brewery.Infrastructure/Queries/QueryDispatcher.cs +++ b/src/Brewery.Infrastructure/Queries/QueryDispatcher.cs @@ -12,13 +12,13 @@ public QueryDispatcher(IServiceProvider serviceProvider) _serviceProvider = serviceProvider; } - public Task QueryAsync(IQuery query) + public async Task QueryAsync(IQuery query) { using var scope = _serviceProvider.CreateScope(); var handlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); var handler = scope.ServiceProvider.GetRequiredService(handlerType); - return (Task)handlerType.GetMethod(nameof(IQueryHandler,TResult>.QueryAsync)) + return await (Task)handlerType.GetMethod(nameof(IQueryHandler,TResult>.QueryAsync)) ?.Invoke(handler, new[] { query }); } } \ No newline at end of file From 2853c164eac26043399fbe9f79b310bc18fe1d79 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Wed, 13 Nov 2024 14:43:42 +0000 Subject: [PATCH 08/24] add BeerController, UpdateBeerHandler --- src/Brewery.Api/Controllers/BeerController.cs | 1 - .../Controllers/BrewerController.cs | 6 ++ .../Commands/Handlers/UpdateBeerHandler.cs | 39 +++++++- .../Commands/UpdateBeer.cs | 2 +- src/Brewery.Domain/Entities/Brewery.cs | 8 +- src/Brewery.Infrastructure/EF/Extensions.cs | 1 + ...20241113144018_EditBeer,Brewer.Designer.cs | 98 +++++++++++++++++++ .../20241113144018_EditBeer,Brewer.cs | 56 +++++++++++ .../BreweryDbContextModelSnapshot.cs | 14 --- .../Handlers/BrowseBeersByBreweryHandler.cs | 17 ++-- .../EF/Repositories/BeerRepository.cs | 38 +++++++ 11 files changed, 248 insertions(+), 32 deletions(-) create mode 100644 src/Brewery.Api/Controllers/BrewerController.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.Designer.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.cs create mode 100644 src/Brewery.Infrastructure/EF/Repositories/BeerRepository.cs diff --git a/src/Brewery.Api/Controllers/BeerController.cs b/src/Brewery.Api/Controllers/BeerController.cs index feae30f..29fd638 100644 --- a/src/Brewery.Api/Controllers/BeerController.cs +++ b/src/Brewery.Api/Controllers/BeerController.cs @@ -1,7 +1,6 @@ using Brewery.Abstractions.Commands; using Brewery.Abstractions.Queries; using Brewery.Application.Commands; -using Brewery.Application.Commands.Handlers; using Brewery.Application.DTO; using Brewery.Application.Queries; using Microsoft.AspNetCore.Mvc; diff --git a/src/Brewery.Api/Controllers/BrewerController.cs b/src/Brewery.Api/Controllers/BrewerController.cs new file mode 100644 index 0000000..6ee3bd7 --- /dev/null +++ b/src/Brewery.Api/Controllers/BrewerController.cs @@ -0,0 +1,6 @@ +namespace Brewery.Api.Controllers; + +public class BrewerController +{ + +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs index ca9b1cc..4079aa0 100644 --- a/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs @@ -1,6 +1,37 @@ -namespace Brewery.Application.Commands.Handlers; +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Repositories; -public class UpdateBeerHandler +namespace Brewery.Application.Commands.Handlers; + +public record UpdateBeerHandler : ICommandHandler { - -} \ No newline at end of file + private readonly IBeerRepository _beerRepository; + + public UpdateBeerHandler(IBeerRepository beerRepository) + { + _beerRepository = beerRepository; + } + + public async Task HandleAsync(UpdateBeer command) + { + var beer = await _beerRepository.GetBeerById(command.Id); + if (beer is null) + { + throw new BeerNotFoundException(command.Id); + } + + if (!string.IsNullOrEmpty(command.Name)) + { + beer.ChangeName(command.Name); + } + + if (command.UnitPrice != 0) + { + beer.SetPrice(command.UnitPrice); + } + + await _beerRepository.UpdateAsync(beer); + } +} + diff --git a/src/Brewery.Application/Commands/UpdateBeer.cs b/src/Brewery.Application/Commands/UpdateBeer.cs index 90c250b..e0ee265 100644 --- a/src/Brewery.Application/Commands/UpdateBeer.cs +++ b/src/Brewery.Application/Commands/UpdateBeer.cs @@ -2,4 +2,4 @@ namespace Brewery.Application.Commands; -public record UpdateBeer(Guid Id, string Name, decimal UnitPrice) : ICommand; \ No newline at end of file +public record UpdateBeer(Guid Id, string Name = null, decimal UnitPrice = default) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Brewery.cs b/src/Brewery.Domain/Entities/Brewery.cs index 1aaf789..661d84d 100644 --- a/src/Brewery.Domain/Entities/Brewery.cs +++ b/src/Brewery.Domain/Entities/Brewery.cs @@ -2,12 +2,10 @@ public class Brewery { - private List _beers = new List(); public Guid Id { get; private set; } public string Name { get; private set; } - //public Guid BrewerId { get; private set; } - //public Brewer Brewer { get; private set; } - public IEnumerable Beers => _beers; + // public Guid BrewerId { get; private set; } + // public Brewer Brewer { get; private set; } public Brewery(Guid id) { @@ -18,6 +16,8 @@ public void ChangeName(string name) { Name = name; } + + public static Brewery Create(Guid id, string name) { diff --git a/src/Brewery.Infrastructure/EF/Extensions.cs b/src/Brewery.Infrastructure/EF/Extensions.cs index 43641b8..6717801 100644 --- a/src/Brewery.Infrastructure/EF/Extensions.cs +++ b/src/Brewery.Infrastructure/EF/Extensions.cs @@ -11,6 +11,7 @@ public static class Extensions internal static IServiceCollection AddEF(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); var postgresOptions = services.GetOptions("postgres"); services.AddDbContext(options => diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.Designer.cs new file mode 100644 index 0000000..341b04a --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.Designer.cs @@ -0,0 +1,98 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241113144018_EditBeer,Brewer")] + partial class EditBeerBrewer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("Beers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.cs new file mode 100644 index 0000000..88eca26 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class EditBeerBrewer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Beers_Breweries_BreweryId", + schema: "brewery", + table: "Beers"); + + migrationBuilder.DropIndex( + name: "IX_Beers_BreweryId", + schema: "brewery", + table: "Beers"); + + migrationBuilder.DropColumn( + name: "BreweryId", + schema: "brewery", + table: "Beers"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BreweryId", + schema: "brewery", + table: "Beers", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Beers_BreweryId", + schema: "brewery", + table: "Beers", + column: "BreweryId"); + + migrationBuilder.AddForeignKey( + name: "FK_Beers_Breweries_BreweryId", + schema: "brewery", + table: "Beers", + column: "BreweryId", + principalSchema: "brewery", + principalTable: "Breweries", + principalColumn: "Id"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs index 1c246fd..d8fca0f 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs @@ -32,9 +32,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("BrewerId") .HasColumnType("uuid"); - b.Property("BreweryId") - .HasColumnType("uuid"); - b.Property("Name") .IsRequired() .HasColumnType("text"); @@ -46,8 +43,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("BrewerId"); - b.HasIndex("BreweryId"); - b.ToTable("Beers", "brewery"); }); @@ -88,21 +83,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("BrewerId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - - b.HasOne("Brewery.Domain.Entities.Brewery", null) - .WithMany("Beers") - .HasForeignKey("BreweryId"); }); modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => { b.Navigation("Beers"); }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.Navigation("Beers"); - }); #pragma warning restore 612, 618 } } diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs index 4710c10..89666bf 100644 --- a/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs @@ -15,13 +15,14 @@ public BrowseBeersByBreweryHandler(BreweryDbContext dbContext) } public async Task> QueryAsync(BrowseBeersByBrewery query) { - var brewery = await _breweries - .AsNoTracking() - .Include(b => b.Beers) - .SingleOrDefaultAsync(b => b.Id == query.BreweryId); - - return brewery is not null - ? brewery?.Beers.Select(b => b.AsDto()) - : Enumerable.Empty(); + throw new NotImplementedException(); + // var brewery = await _breweries + // .AsNoTracking() + // //.Include(b => b.Beers) + // .SingleOrDefaultAsync(b => b.Id == query.BreweryId); + // + // return brewery is not null + // ? brewery?.Beers.Select(b => b.AsDto()) + // : Enumerable.Empty(); } } \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/BeerRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BeerRepository.cs new file mode 100644 index 0000000..9f46055 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BeerRepository.cs @@ -0,0 +1,38 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BeerRepository : IBeerRepository +{ + private readonly DbSet _beers; + private readonly BreweryDbContext _dbContext; + + public BeerRepository(BreweryDbContext dbContext) + { + _beers = dbContext.Beers; + _dbContext = dbContext; + } + + public async Task AddAsync(Beer beer) + { + await _beers.AddAsync(beer); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateAsync(Beer beer) + { + _beers.Update(beer); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteAsync(Beer beer) + { + _beers.Remove(beer); + await _dbContext.SaveChangesAsync(); + } + + public Task GetBeerById(Guid id) + => _beers.SingleOrDefaultAsync(beer => beer.Id == id); +} \ No newline at end of file From 86b3378983a4931e6169ea118e3f2df22ee1d39d Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Wed, 13 Nov 2024 18:48:06 +0000 Subject: [PATCH 09/24] add ExceptionMiddleware --- .../Exceptions/ExceptionResponse.cs | 6 +++ .../Exceptions/IExceptionToResponseMapper.cs | 6 +++ src/Brewery.Api/BreweryApi.rest | 32 +++++++++++++++- src/Brewery.Api/Controllers/BeerController.cs | 4 +- .../Controllers/BrewerController.cs | 21 +++++++++- src/Brewery.Application/Commands/AddBrewer.cs | 8 ++++ .../Commands/DeleteBeer.cs | 2 +- .../Commands/Handlers/AddBeerHandler.cs | 15 ++++++-- .../Commands/Handlers/AddBrewerHandler.cs | 28 ++++++++++++++ .../Commands/Handlers/DeleteBeerHandler.cs | 17 ++++++--- .../Commands/Handlers/UpdateBeerHandler.cs | 16 +++++++- .../Commands/UpdateBeer.cs | 2 +- src/Brewery.Application/DTO/BeerDto.cs | 1 + src/Brewery.Application/DTO/BrewerDto.cs | 7 ++++ src/Brewery.Application/DTO/Extensions.cs | 13 +++++++ .../BeerDoesNotBelongToBrewerException.cs | 15 ++++++++ .../Exceptions/BrewerAlreadyExistException.cs | 13 +++++++ .../Exceptions/BrewerNotFoundException.cs | 13 +++++++ src/Brewery.Application/Queries/GetBrewer.cs | 6 +++ src/Brewery.Domain/Entities/Beer.cs | 1 + src/Brewery.Domain/Entities/Brewer.cs | 1 + src/Brewery.Domain/Entities/Brewery.cs | 16 ++++++-- .../Repositories/IBrewerRepository.cs | 9 +++++ .../Brewery.Infrastructure.csproj | 1 + .../EF/BreweryDbContext.cs | 3 +- src/Brewery.Infrastructure/EF/Extensions.cs | 5 ++- .../EF/Queries/Handlers/GetBeerHandler.cs | 27 +++++++++++++ .../EF/Queries/Handlers/GetBrewerHandler.cs | 27 +++++++++++++ .../EF/Repositories/BrewerRepository.cs | 38 +++++++++++++++++++ .../Exceptions/ExceptionMiddleware.cs | 29 ++++++++++++++ .../Exceptions/ExceptionToResponseMapper.cs | 22 +++++++++++ .../Exceptions/Extensions.cs | 24 ++++++++++++ src/Brewery.Infrastructure/Extensions.cs | 3 ++ 33 files changed, 407 insertions(+), 24 deletions(-) create mode 100644 src/Brewery.Abstractions/Exceptions/ExceptionResponse.cs create mode 100644 src/Brewery.Abstractions/Exceptions/IExceptionToResponseMapper.cs create mode 100644 src/Brewery.Application/Commands/AddBrewer.cs create mode 100644 src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs create mode 100644 src/Brewery.Application/DTO/BrewerDto.cs create mode 100644 src/Brewery.Application/Exceptions/BeerDoesNotBelongToBrewerException.cs create mode 100644 src/Brewery.Application/Exceptions/BrewerAlreadyExistException.cs create mode 100644 src/Brewery.Application/Exceptions/BrewerNotFoundException.cs create mode 100644 src/Brewery.Application/Queries/GetBrewer.cs create mode 100644 src/Brewery.Domain/Repositories/IBrewerRepository.cs create mode 100644 src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerHandler.cs create mode 100644 src/Brewery.Infrastructure/EF/Queries/Handlers/GetBrewerHandler.cs create mode 100644 src/Brewery.Infrastructure/EF/Repositories/BrewerRepository.cs create mode 100644 src/Brewery.Infrastructure/Exceptions/ExceptionMiddleware.cs create mode 100644 src/Brewery.Infrastructure/Exceptions/ExceptionToResponseMapper.cs create mode 100644 src/Brewery.Infrastructure/Exceptions/Extensions.cs diff --git a/src/Brewery.Abstractions/Exceptions/ExceptionResponse.cs b/src/Brewery.Abstractions/Exceptions/ExceptionResponse.cs new file mode 100644 index 0000000..1959c00 --- /dev/null +++ b/src/Brewery.Abstractions/Exceptions/ExceptionResponse.cs @@ -0,0 +1,6 @@ +using System.Net; + +namespace Brewery.Abstractions.Exceptions; + +public record ExceptionResponse(Error Error, HttpStatusCode HttpStatusCode); +public record Error(string Code, string Message); \ No newline at end of file diff --git a/src/Brewery.Abstractions/Exceptions/IExceptionToResponseMapper.cs b/src/Brewery.Abstractions/Exceptions/IExceptionToResponseMapper.cs new file mode 100644 index 0000000..5bfab72 --- /dev/null +++ b/src/Brewery.Abstractions/Exceptions/IExceptionToResponseMapper.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Exceptions; + +public interface IExceptionToResponseMapper +{ + ExceptionResponse Map(Exception exception); +} \ No newline at end of file diff --git a/src/Brewery.Api/BreweryApi.rest b/src/Brewery.Api/BreweryApi.rest index cd81454..5671003 100644 --- a/src/Brewery.Api/BreweryApi.rest +++ b/src/Brewery.Api/BreweryApi.rest @@ -1,19 +1,49 @@ @url = http://localhost:5000 +@beerId = fd9c1c15-5425-4f46-85c1-ee43236b2e83 +@brewerId = 03464b8d-a27a-4134-996d-a5d97d1eafa5 @breweryId = d730c80b-5680-4058-a64e-892c501cb287 -### +### List all beers by brewery GET {{url}}/beer/{{breweryId}} ### +GET {{url}}/beer/{{beerId}} + +### Brewer adds beer POST {{url}}/beer Content-Type: application/json { + "brewerId": "{{brewerId}}", "name": "beer 1", "unitPrice": 3.50 } +### Brewer updates beer +PUT {{url}}/beer/{{beerId}} +Content-Type: application/json + +{ + "brewerId": "{{brewerId}}", + //name": , + "unitPrice": 3.00 +} + +### Brewer deletes beer +DELETE {{url}}/beer + +### +GET {{url}}/brewer/{{brewerId}} + +### +POST {{url}}/brewer +Content-Type: application/json + +{ + "name": "brewski" +} + ### GET {{url}}/brewery/{{breweryId}} diff --git a/src/Brewery.Api/Controllers/BeerController.cs b/src/Brewery.Api/Controllers/BeerController.cs index 29fd638..8b95a33 100644 --- a/src/Brewery.Api/Controllers/BeerController.cs +++ b/src/Brewery.Api/Controllers/BeerController.cs @@ -38,9 +38,9 @@ public async Task UpdateBeer(UpdateBeer command, Guid beerId) } [HttpDelete("{beerId:guid}")] - public async Task DeleteBeer(Guid beerId) + public async Task DeleteBeer(DeleteBeer command, Guid beerId) { - await _commandDispatcher.DispatchAsync(new DeleteBeer(beerId)); + await _commandDispatcher.DispatchAsync(command with { BeerId = beerId } ); return NoContent(); } } \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BrewerController.cs b/src/Brewery.Api/Controllers/BrewerController.cs index 6ee3bd7..5e73261 100644 --- a/src/Brewery.Api/Controllers/BrewerController.cs +++ b/src/Brewery.Api/Controllers/BrewerController.cs @@ -1,6 +1,23 @@ -namespace Brewery.Api.Controllers; +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Mvc; -public class BrewerController +namespace Brewery.Api.Controllers; + +public class BrewerController(ICommandDispatcher commandDispatcher, IQueryDispatcher queryDispatcher) + : BaseController { + [HttpGet("{brewerId:guid}")] + public async Task> Get(Guid brewerId) + => Ok(await queryDispatcher.QueryAsync(new GetBrewer(brewerId))); + [HttpPost] + public async Task AddBrewer(AddBrewer command) + { + await commandDispatcher.DispatchAsync(command); + return CreatedAtAction(nameof(Get), new { brewerId = command.Id }, null); + } } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBrewer.cs b/src/Brewery.Application/Commands/AddBrewer.cs new file mode 100644 index 0000000..71fc6be --- /dev/null +++ b/src/Brewery.Application/Commands/AddBrewer.cs @@ -0,0 +1,8 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddBrewer(string Name) : ICommand +{ + public Guid Id { get; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/DeleteBeer.cs b/src/Brewery.Application/Commands/DeleteBeer.cs index 288f0cf..00ecc0a 100644 --- a/src/Brewery.Application/Commands/DeleteBeer.cs +++ b/src/Brewery.Application/Commands/DeleteBeer.cs @@ -2,4 +2,4 @@ namespace Brewery.Application.Commands; -public record DeleteBeer(Guid Id) : ICommand; \ No newline at end of file +public record DeleteBeer(Guid BeerId, Guid BrewerId) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs index f5bb9fd..dd0d517 100644 --- a/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs @@ -8,21 +8,30 @@ namespace Brewery.Application.Commands.Handlers; public class AddBeerHandler : ICommandHandler { private readonly IBeerRepository _beerRepository; - - public AddBeerHandler(IBeerRepository beerRepository) + private readonly IBrewerRepository _brewerRepository; + + public AddBeerHandler(IBeerRepository beerRepository, + IBrewerRepository brewerRepository) { _beerRepository = beerRepository; + _brewerRepository = brewerRepository; } public async Task HandleAsync(AddBeer command) { + var brewer = await _brewerRepository.GetBrewer(command.BrewerId); + if (brewer is null) + { + throw new BrewerNotFoundException(command.BrewerId); + } + var beer = await _beerRepository.GetBeerById(command.Id); if (beer is not null) { throw new BeerAlreadyExistException(command.Id); } - beer = Beer.Create(command.Id, command.BrewerId, command.Name, command.UnitPrice); + beer = Beer.Create(command.Id, brewer.Id, command.Name, command.UnitPrice); await _beerRepository.AddAsync(beer); } diff --git a/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs new file mode 100644 index 0000000..4b9bad2 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs @@ -0,0 +1,28 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddBrewerHandler : ICommandHandler +{ + private readonly IBrewerRepository _brewerRepository; + + public AddBrewerHandler(IBrewerRepository brewerRepository) + { + _brewerRepository = brewerRepository; + } + + public async Task HandleAsync(AddBrewer command) + { + var brewer = await _brewerRepository.GetBrewer(command.Id); + if (brewer is not null) + { + throw new BrewerAlreadyExistException(command.Id); + } + + brewer = Brewer.Create(command.Id, command.Name); + await _brewerRepository.AddBrewer(brewer); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs index 4301d10..ed7b213 100644 --- a/src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/DeleteBeerHandler.cs @@ -6,21 +6,26 @@ namespace Brewery.Application.Commands.Handlers; public class DeleteBeerHandler : ICommandHandler { - private readonly IBeerRepository _beerRepositry; + private readonly IBeerRepository _beerRepository; public DeleteBeerHandler(IBeerRepository beerRepositry) { - _beerRepositry = beerRepositry; + _beerRepository = beerRepositry; } public async Task HandleAsync(DeleteBeer command) { - var beer = await _beerRepositry.GetBeerById(command.Id); + var beer = await _beerRepository.GetBeerById(command.BeerId); if (beer is null) { - throw new BeerNotFoundException(command.Id); + throw new BeerNotFoundException(command.BeerId); } - - await _beerRepositry.DeleteAsync(beer); + + if (beer.BrewerId != command.BrewerId) + { + throw new BeerDoesNotBelongToBrewerException(beer.Id, beer.BrewerId); + } + + await _beerRepository.DeleteAsync(beer); } } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs index 4079aa0..493befc 100644 --- a/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs @@ -7,20 +7,34 @@ namespace Brewery.Application.Commands.Handlers; public record UpdateBeerHandler : ICommandHandler { private readonly IBeerRepository _beerRepository; + private readonly IBrewerRepository _brewerRepository; - public UpdateBeerHandler(IBeerRepository beerRepository) + public UpdateBeerHandler(IBeerRepository beerRepository, + IBrewerRepository brewerRepository) { _beerRepository = beerRepository; + _brewerRepository = brewerRepository; } public async Task HandleAsync(UpdateBeer command) { + var brewer = await _brewerRepository.GetBrewer(command.BrewerId); + if (brewer is null) + { + throw new BrewerNotFoundException(command.BrewerId); + } + var beer = await _beerRepository.GetBeerById(command.Id); if (beer is null) { throw new BeerNotFoundException(command.Id); } + if (beer.BrewerId != brewer.Id) + { + throw new BeerDoesNotBelongToBrewerException(beer.Id, brewer.Id); + } + if (!string.IsNullOrEmpty(command.Name)) { beer.ChangeName(command.Name); diff --git a/src/Brewery.Application/Commands/UpdateBeer.cs b/src/Brewery.Application/Commands/UpdateBeer.cs index e0ee265..671119c 100644 --- a/src/Brewery.Application/Commands/UpdateBeer.cs +++ b/src/Brewery.Application/Commands/UpdateBeer.cs @@ -2,4 +2,4 @@ namespace Brewery.Application.Commands; -public record UpdateBeer(Guid Id, string Name = null, decimal UnitPrice = default) : ICommand; \ No newline at end of file +public record UpdateBeer(Guid Id, Guid BrewerId, string Name = null, decimal UnitPrice = default) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/DTO/BeerDto.cs b/src/Brewery.Application/DTO/BeerDto.cs index fc4c07c..b6f8b1f 100644 --- a/src/Brewery.Application/DTO/BeerDto.cs +++ b/src/Brewery.Application/DTO/BeerDto.cs @@ -5,6 +5,7 @@ namespace Brewery.Application.DTO; public class BeerDto { public Guid Id { get; set; } + public Guid BrewerId { get; set; } public string Name { get; set; } public decimal UnitPrice { get; set; } } diff --git a/src/Brewery.Application/DTO/BrewerDto.cs b/src/Brewery.Application/DTO/BrewerDto.cs new file mode 100644 index 0000000..bf57bbc --- /dev/null +++ b/src/Brewery.Application/DTO/BrewerDto.cs @@ -0,0 +1,7 @@ +namespace Brewery.Application.DTO; + +public class BrewerDto +{ + public Guid Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/Extensions.cs b/src/Brewery.Application/DTO/Extensions.cs index ca684dd..8421fcb 100644 --- a/src/Brewery.Application/DTO/Extensions.cs +++ b/src/Brewery.Application/DTO/Extensions.cs @@ -9,9 +9,22 @@ public static BeerDto AsDto(this Beer beer) var beerDto = new BeerDto { Id = beer.Id, + BrewerId = beer.BrewerId, Name = beer.Name, + UnitPrice = beer.UnitPrice, }; return beerDto; } + + public static BrewerDto AsDto(this Brewer brewer) + { + var brewerDto = new BrewerDto + { + Id = brewer.Id, + Name = brewer.Name, + }; + + return brewerDto; + } } diff --git a/src/Brewery.Application/Exceptions/BeerDoesNotBelongToBrewerException.cs b/src/Brewery.Application/Exceptions/BeerDoesNotBelongToBrewerException.cs new file mode 100644 index 0000000..e96ea12 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerDoesNotBelongToBrewerException.cs @@ -0,0 +1,15 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerDoesNotBelongToBrewerException : BreweryException +{ + public Guid BeerId { get; } + public Guid BrewerId { get; } + public BeerDoesNotBelongToBrewerException(Guid beerId, Guid brewerId) + : base($"Beer with id {beerId} does not belong to brewer with id '{brewerId}'") + { + BeerId = beerId; + BrewerId = brewerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BrewerAlreadyExistException.cs b/src/Brewery.Application/Exceptions/BrewerAlreadyExistException.cs new file mode 100644 index 0000000..d01d22c --- /dev/null +++ b/src/Brewery.Application/Exceptions/BrewerAlreadyExistException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BrewerAlreadyExistException : BreweryException +{ + public Guid BrewerId { get; } + public BrewerAlreadyExistException(Guid brewerId) + : base($"Brewer with id '{brewerId}' already exists.") + { + BrewerId = brewerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BrewerNotFoundException.cs b/src/Brewery.Application/Exceptions/BrewerNotFoundException.cs new file mode 100644 index 0000000..7cc43e5 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BrewerNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BrewerNotFoundException : BreweryException +{ + public Guid BrewerId { get; } + public BrewerNotFoundException(Guid brewerId) + : base($"Brewer with id '{brewerId}' was not found.") + { + BrewerId = brewerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetBrewer.cs b/src/Brewery.Application/Queries/GetBrewer.cs new file mode 100644 index 0000000..ead9465 --- /dev/null +++ b/src/Brewery.Application/Queries/GetBrewer.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record GetBrewer(Guid BrewerId) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Beer.cs b/src/Brewery.Domain/Entities/Beer.cs index f9fb774..57cae63 100644 --- a/src/Brewery.Domain/Entities/Beer.cs +++ b/src/Brewery.Domain/Entities/Beer.cs @@ -13,6 +13,7 @@ public class Beer public Beer(Guid id, Guid brewerId) { Id = id; + BrewerId = brewerId; } public void ChangeName(string name) diff --git a/src/Brewery.Domain/Entities/Brewer.cs b/src/Brewery.Domain/Entities/Brewer.cs index 728de77..61a7c3e 100644 --- a/src/Brewery.Domain/Entities/Brewer.cs +++ b/src/Brewery.Domain/Entities/Brewer.cs @@ -4,6 +4,7 @@ public class Brewer { public IEnumerable Beers => _beers; public Guid Id { get; private set; } + public Guid? BreweryId { get; private set; } public string Name { get; private set; } private readonly List _beers = new List(); diff --git a/src/Brewery.Domain/Entities/Brewery.cs b/src/Brewery.Domain/Entities/Brewery.cs index 661d84d..dc0e9ca 100644 --- a/src/Brewery.Domain/Entities/Brewery.cs +++ b/src/Brewery.Domain/Entities/Brewery.cs @@ -2,10 +2,10 @@ public class Brewery { + private readonly List _brewers; public Guid Id { get; private set; } public string Name { get; private set; } - // public Guid BrewerId { get; private set; } - // public Brewer Brewer { get; private set; } + public IEnumerable Brewers => _brewers; public Brewery(Guid id) { @@ -16,8 +16,16 @@ public void ChangeName(string name) { Name = name; } - - + + public void AddBrewer(Brewer brewer) + { + _brewers.Add(brewer); + } + + public void RemoveBrewer(Brewer brewer) + { + _brewers.Remove(brewer); + } public static Brewery Create(Guid id, string name) { diff --git a/src/Brewery.Domain/Repositories/IBrewerRepository.cs b/src/Brewery.Domain/Repositories/IBrewerRepository.cs new file mode 100644 index 0000000..e870953 --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBrewerRepository.cs @@ -0,0 +1,9 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IBrewerRepository +{ + Task AddBrewer(Brewer brewer); + Task GetBrewer(Guid brewerId); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj index 762e47b..c58e56a 100644 --- a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj +++ b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs index 68ff90a..aba135f 100644 --- a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs +++ b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs @@ -5,9 +5,10 @@ namespace Brewery.Infrastructure.EF; public class BreweryDbContext : DbContext { - public DbSet Breweries { get; set; } + public DbSet Beers { get; set; } public DbSet Brewers { get; set; } + public DbSet Breweries { get; set; } public BreweryDbContext(DbContextOptions options) : base(options) diff --git a/src/Brewery.Infrastructure/EF/Extensions.cs b/src/Brewery.Infrastructure/EF/Extensions.cs index 6717801..45030cf 100644 --- a/src/Brewery.Infrastructure/EF/Extensions.cs +++ b/src/Brewery.Infrastructure/EF/Extensions.cs @@ -10,9 +10,10 @@ public static class Extensions { internal static IServiceCollection AddEF(this IServiceCollection services) { - services.AddScoped(); services.AddScoped(); - + services.AddScoped(); + services.AddScoped(); + var postgresOptions = services.GetOptions("postgres"); services.AddDbContext(options => { diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerHandler.cs new file mode 100644 index 0000000..cb9cd9d --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerHandler.cs @@ -0,0 +1,27 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetBeerHandler : IQueryHandler +{ + private readonly DbSet _beers; + + public GetBeerHandler(BreweryDbContext dbContext) + { + _beers = dbContext.Beers; + } + + public async Task QueryAsync(GetBeer query) + { + var beer = await _beers.AsNoTracking() + .SingleOrDefaultAsync(beer => beer.Id == query.Id); + + return beer is not null + ? beer.AsDto() + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBrewerHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBrewerHandler.cs new file mode 100644 index 0000000..cd79497 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBrewerHandler.cs @@ -0,0 +1,27 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetBrewerHandler : IQueryHandler +{ + private readonly DbSet _brewers; + + public GetBrewerHandler(BreweryDbContext dbContext) + { + _brewers = dbContext.Brewers; + } + public async Task QueryAsync(GetBrewer query) + { + var brewer = await _brewers + .AsNoTracking() + .SingleOrDefaultAsync(b => b.Id == query.BrewerId); + + return brewer is not null + ? brewer.AsDto() + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/BrewerRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BrewerRepository.cs new file mode 100644 index 0000000..d5620f5 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BrewerRepository.cs @@ -0,0 +1,38 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BrewerRepository : IBrewerRepository +{ + private readonly DbSet _brewers; + private readonly BreweryDbContext _dbContext; + + public BrewerRepository(BreweryDbContext dbContext) + { + _brewers = dbContext.Brewers; + _dbContext = dbContext; + } + + public async Task AddBrewer(Brewer brewer) + { + await _brewers.AddAsync(brewer); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateAsync(Brewer brewer) + { + _brewers.Update(brewer); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteAsync(Brewer brewer) + { + _brewers.Remove(brewer); + await _dbContext.SaveChangesAsync(); + } + + public Task GetBrewer(Guid brewerId) + => _brewers.SingleOrDefaultAsync(_brewers => _brewers.Id == brewerId); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Exceptions/ExceptionMiddleware.cs b/src/Brewery.Infrastructure/Exceptions/ExceptionMiddleware.cs new file mode 100644 index 0000000..9419f0e --- /dev/null +++ b/src/Brewery.Infrastructure/Exceptions/ExceptionMiddleware.cs @@ -0,0 +1,29 @@ +using System.Net; +using Brewery.Abstractions.Exceptions; +using Microsoft.AspNetCore.Http; + +namespace Brewery.Infrastructure.Exceptions; + +public class ExceptionMiddleware : IMiddleware +{ + private readonly IExceptionToResponseMapper _exceptionToResponseMapper; + + public ExceptionMiddleware(IExceptionToResponseMapper exceptionToResponseMapper) + { + _exceptionToResponseMapper = exceptionToResponseMapper; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + try + { + await next(context); + } + catch (Exception ex) + { + var response = _exceptionToResponseMapper.Map(ex); + context.Response.StatusCode = (int)response.HttpStatusCode; + await context.Response.WriteAsJsonAsync(response.Error); + } + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/src/Brewery.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 0000000..a28e8bb --- /dev/null +++ b/src/Brewery.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,22 @@ +using System.Net; +using Brewery.Abstractions.Exceptions; +using Humanizer; + +namespace Brewery.Infrastructure.Exceptions; + +public class ExceptionToResponseMapper : IExceptionToResponseMapper +{ + public ExceptionResponse Map(Exception exception) + => exception switch + { + BreweryException breweryException => new ExceptionResponse( + new Error(GetErrorCode(exception), breweryException.Message), + HttpStatusCode.BadRequest), + _ => new ExceptionResponse( + new Error("error", "There was an error."), + HttpStatusCode.InternalServerError) + }; + + private static string GetErrorCode(Exception exception) + => exception.GetType().Name.Underscore().Replace("_exception", string.Empty); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Exceptions/Extensions.cs b/src/Brewery.Infrastructure/Exceptions/Extensions.cs new file mode 100644 index 0000000..6441f82 --- /dev/null +++ b/src/Brewery.Infrastructure/Exceptions/Extensions.cs @@ -0,0 +1,24 @@ +using Brewery.Abstractions.Exceptions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Exceptions; + +public static class Extensions +{ + public static IServiceCollection AddExceptionHanding(this IServiceCollection services) + { + services.AddSingleton(); + services.AddScoped(); + + return services; + } + + public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder app) + { + app.UseMiddleware(); + + return app; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Extensions.cs b/src/Brewery.Infrastructure/Extensions.cs index 81d0c01..fb9c6b2 100644 --- a/src/Brewery.Infrastructure/Extensions.cs +++ b/src/Brewery.Infrastructure/Extensions.cs @@ -1,6 +1,7 @@ using System.Reflection; using Brewery.Infrastructure.Commands; using Brewery.Infrastructure.EF; +using Brewery.Infrastructure.Exceptions; using Brewery.Infrastructure.Middleware; using Brewery.Infrastructure.Queries; using Brewery.Infrastructure.Services; @@ -15,6 +16,7 @@ public static class Extensions { public static IServiceCollection AddInfrastructure(this IServiceCollection services, IList assemblies) { + services.AddExceptionHanding(); services.AddControllers(); services.AddHostedService(); services.AddSingleton(); @@ -27,6 +29,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) { + app.UseExceptionHandling(); app.UseRouting(); app.UseEndpoints(e => { From e60a83611e5e121636a0a976d9028953462ce610 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Thu, 14 Nov 2024 15:53:43 +0000 Subject: [PATCH 10/24] edit Wholesaler --- src/Brewery.Api/BreweryApi.rest | 14 ++- src/Brewery.Application/Commands/AddBrewer.cs | 2 +- .../Commands/Handlers/AddBrewerHandler.cs | 19 ++- .../Exceptions/BreweryNotFoundException.cs | 13 ++ src/Brewery.Domain/Entities/Brewer.cs | 7 +- src/Brewery.Domain/Entities/Brewery.cs | 2 +- src/Brewery.Domain/Entities/Wholesaler.cs | 4 + ...41113185859_EditBrewer,Brewery.Designer.cs | 115 ++++++++++++++++++ .../20241113185859_EditBrewer,Brewery.cs | 56 +++++++++ .../BreweryDbContextModelSnapshot.cs | 17 +++ .../Handlers/BrowseBeersByBreweryHandler.cs | 20 +-- 11 files changed, 248 insertions(+), 21 deletions(-) create mode 100644 src/Brewery.Application/Exceptions/BreweryNotFoundException.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.Designer.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.cs diff --git a/src/Brewery.Api/BreweryApi.rest b/src/Brewery.Api/BreweryApi.rest index 5671003..0850a01 100644 --- a/src/Brewery.Api/BreweryApi.rest +++ b/src/Brewery.Api/BreweryApi.rest @@ -26,12 +26,17 @@ Content-Type: application/json { "brewerId": "{{brewerId}}", - //name": , - "unitPrice": 3.00 + "name": "super beer 1", + "unitPrice": 8.00 } ### Brewer deletes beer -DELETE {{url}}/beer +DELETE {{url}}/beer/{{beerId}} +Content-Type: application/json + +{ + "brewerId": "{{brewerId}}" +} ### GET {{url}}/brewer/{{brewerId}} @@ -41,7 +46,8 @@ POST {{url}}/brewer Content-Type: application/json { - "name": "brewski" + "name": "brewski 2", + "breweryId": "{{breweryId}}" } ### diff --git a/src/Brewery.Application/Commands/AddBrewer.cs b/src/Brewery.Application/Commands/AddBrewer.cs index 71fc6be..a064ea2 100644 --- a/src/Brewery.Application/Commands/AddBrewer.cs +++ b/src/Brewery.Application/Commands/AddBrewer.cs @@ -2,7 +2,7 @@ namespace Brewery.Application.Commands; -public record AddBrewer(string Name) : ICommand +public record AddBrewer(string Name, Guid BreweryId = default) : ICommand { public Guid Id { get; } = Guid.NewGuid(); } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs index 4b9bad2..cabb020 100644 --- a/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs @@ -8,10 +8,13 @@ namespace Brewery.Application.Commands.Handlers; public class AddBrewerHandler : ICommandHandler { private readonly IBrewerRepository _brewerRepository; + private readonly IBreweryRepository _breweryRepository; - public AddBrewerHandler(IBrewerRepository brewerRepository) + public AddBrewerHandler(IBrewerRepository brewerRepository, + IBreweryRepository breweryRepository) { _brewerRepository = brewerRepository; + _breweryRepository = breweryRepository; } public async Task HandleAsync(AddBrewer command) @@ -21,8 +24,20 @@ public async Task HandleAsync(AddBrewer command) { throw new BrewerAlreadyExistException(command.Id); } - + brewer = Brewer.Create(command.Id, command.Name); + if (command.BreweryId != Guid.Empty) + { + var brewery = await _breweryRepository.GetBreweryById(command.BreweryId); + if (brewery is null) + { + throw new BreweryNotFoundException(command.BreweryId); + } + + brewer.ChangeBreweryId(brewery.Id); + } + + await _brewerRepository.AddBrewer(brewer); } } \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BreweryNotFoundException.cs b/src/Brewery.Application/Exceptions/BreweryNotFoundException.cs new file mode 100644 index 0000000..4dd090e --- /dev/null +++ b/src/Brewery.Application/Exceptions/BreweryNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BreweryNotFoundException : BreweryException +{ + public Guid BreweryId { get; } + public BreweryNotFoundException(Guid breweryId) + : base($"Brewery with id '{breweryId}' was not found.") + { + BreweryId = breweryId; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Brewer.cs b/src/Brewery.Domain/Entities/Brewer.cs index 61a7c3e..9b9a606 100644 --- a/src/Brewery.Domain/Entities/Brewer.cs +++ b/src/Brewery.Domain/Entities/Brewer.cs @@ -22,12 +22,11 @@ public void AddBeer(Beer beer) public void DeleteBeer(Beer beer) => _beers.Remove(beer); - public void UpdateBeer(Beer beer) + public void ChangeBreweryId(Guid breweryId) { - var exisitingBeer = _beers.First(b => b.Id == beer.Id); - + BreweryId = breweryId; } - + public static Brewer Create(Guid id, string name) { var brewer = new Brewer(id); diff --git a/src/Brewery.Domain/Entities/Brewery.cs b/src/Brewery.Domain/Entities/Brewery.cs index dc0e9ca..59a9528 100644 --- a/src/Brewery.Domain/Entities/Brewery.cs +++ b/src/Brewery.Domain/Entities/Brewery.cs @@ -2,7 +2,7 @@ public class Brewery { - private readonly List _brewers; + private readonly List _brewers = new List(); public Guid Id { get; private set; } public string Name { get; private set; } public IEnumerable Brewers => _brewers; diff --git a/src/Brewery.Domain/Entities/Wholesaler.cs b/src/Brewery.Domain/Entities/Wholesaler.cs index a2ef44b..55f467b 100644 --- a/src/Brewery.Domain/Entities/Wholesaler.cs +++ b/src/Brewery.Domain/Entities/Wholesaler.cs @@ -2,5 +2,9 @@ public class Wholesaler { + private readonly HashSet _beers = new(); + public Guid Id { get; private set; } + public string Name { get; private set; } + public IEnumerable Beers => _beers; } \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.Designer.cs new file mode 100644 index 0000000..05e7c59 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.Designer.cs @@ -0,0 +1,115 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241113185859_EditBrewer,Brewery")] + partial class EditBrewerBrewery + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.cs b/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.cs new file mode 100644 index 0000000..fb7eb10 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class EditBrewerBrewery : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BreweryId", + schema: "brewery", + table: "Brewers", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Brewers_BreweryId", + schema: "brewery", + table: "Brewers", + column: "BreweryId"); + + migrationBuilder.AddForeignKey( + name: "FK_Brewers_Breweries_BreweryId", + schema: "brewery", + table: "Brewers", + column: "BreweryId", + principalSchema: "brewery", + principalTable: "Breweries", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Brewers_Breweries_BreweryId", + schema: "brewery", + table: "Brewers"); + + migrationBuilder.DropIndex( + name: "IX_Brewers_BreweryId", + schema: "brewery", + table: "Brewers"); + + migrationBuilder.DropColumn( + name: "BreweryId", + schema: "brewery", + table: "Brewers"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs index d8fca0f..b731f94 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs @@ -52,12 +52,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("BreweryId") + .HasColumnType("uuid"); + b.Property("Name") .IsRequired() .HasColumnType("text"); b.HasKey("Id"); + b.HasIndex("BreweryId"); + b.ToTable("Brewers", "brewery"); }); @@ -85,10 +90,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => { b.Navigation("Beers"); }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs index 89666bf..03f60e6 100644 --- a/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/BrowseBeersByBreweryHandler.cs @@ -15,14 +15,16 @@ public BrowseBeersByBreweryHandler(BreweryDbContext dbContext) } public async Task> QueryAsync(BrowseBeersByBrewery query) { - throw new NotImplementedException(); - // var brewery = await _breweries - // .AsNoTracking() - // //.Include(b => b.Beers) - // .SingleOrDefaultAsync(b => b.Id == query.BreweryId); - // - // return brewery is not null - // ? brewery?.Beers.Select(b => b.AsDto()) - // : Enumerable.Empty(); + var brewery = await _breweries + .AsNoTracking() + .Include(b => b.Brewers) + .ThenInclude(b => b.Beers) + .SingleOrDefaultAsync(b => b.Id == query.BreweryId); + + return brewery is not null + ? brewery?.Brewers + .SelectMany(b => b.Beers) + .Select(b => b.AsDto()) + : Enumerable.Empty(); } } \ No newline at end of file From 3ccec29a3f37c04b04a7b7cf92225bb4f8ee8960 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Thu, 14 Nov 2024 20:14:59 +0000 Subject: [PATCH 11/24] add Sale, Wholesaler, Repositories, Exceptions, Commands --- .../Controllers/WholesalerController.cs | 39 +++++++++++++++ .../Commands/AddBeerSale.cs | 6 +++ .../Commands/AddBeerSaleHandler.cs | 43 ++++++++++++++++ .../Commands/AddWholesaler.cs | 8 +++ src/Brewery.Application/DTO/Extensions.cs | 7 +++ src/Brewery.Application/DTO/WholesalerDto.cs | 7 +++ .../Exceptions/BeerSaleNotFoundException.cs | 13 +++++ ...ExcessiveBeerSaleOrderQuantityException.cs | 13 +++++ .../Exceptions/WholesalerNotFoundException.cs | 13 +++++ .../Queries/GetWholesaler.cs | 7 +++ src/Brewery.Domain/Entities/Sale.cs | 49 +++++++++++++++++++ src/Brewery.Domain/Entities/Wholesaler.cs | 29 ++++++++++- .../InvalidBeerQuantityException.cs | 13 +++++ .../InvalidBeerToBeRestockedException.cs | 13 +++++ .../NotEnoughBeerToSellException.cs | 11 +++++ .../Repositories/ISaleRepository.cs | 11 +++++ .../Repositories/IWholesalerRepository.cs | 10 ++++ .../EF/BreweryDbContext.cs | 2 + src/Brewery.Infrastructure/EF/Extensions.cs | 2 + .../Queries/Handlers/GetWholesalerHandler.cs | 27 ++++++++++ .../EF/Repositories/SaleRepository.cs | 32 ++++++++++++ .../EF/Repositories/WholesalerRepository.cs | 32 ++++++++++++ 22 files changed, 385 insertions(+), 2 deletions(-) create mode 100644 src/Brewery.Api/Controllers/WholesalerController.cs create mode 100644 src/Brewery.Application/Commands/AddBeerSale.cs create mode 100644 src/Brewery.Application/Commands/AddBeerSaleHandler.cs create mode 100644 src/Brewery.Application/Commands/AddWholesaler.cs create mode 100644 src/Brewery.Application/DTO/WholesalerDto.cs create mode 100644 src/Brewery.Application/Exceptions/BeerSaleNotFoundException.cs create mode 100644 src/Brewery.Application/Exceptions/ExcessiveBeerSaleOrderQuantityException.cs create mode 100644 src/Brewery.Application/Exceptions/WholesalerNotFoundException.cs create mode 100644 src/Brewery.Application/Queries/GetWholesaler.cs create mode 100644 src/Brewery.Domain/Entities/Sale.cs create mode 100644 src/Brewery.Domain/Exceptions/InvalidBeerQuantityException.cs create mode 100644 src/Brewery.Domain/Exceptions/InvalidBeerToBeRestockedException.cs create mode 100644 src/Brewery.Domain/Exceptions/NotEnoughBeerToSellException.cs create mode 100644 src/Brewery.Domain/Repositories/ISaleRepository.cs create mode 100644 src/Brewery.Domain/Repositories/IWholesalerRepository.cs create mode 100644 src/Brewery.Infrastructure/EF/Queries/Handlers/GetWholesalerHandler.cs create mode 100644 src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs create mode 100644 src/Brewery.Infrastructure/EF/Repositories/WholesalerRepository.cs diff --git a/src/Brewery.Api/Controllers/WholesalerController.cs b/src/Brewery.Api/Controllers/WholesalerController.cs new file mode 100644 index 0000000..3dfebe4 --- /dev/null +++ b/src/Brewery.Api/Controllers/WholesalerController.cs @@ -0,0 +1,39 @@ +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class WholesalerController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + private readonly IQueryDispatcher _queryDispatcher; + + public WholesalerController(ICommandDispatcher commandDispatcher, IQueryDispatcher queryDispatcher) + { + _commandDispatcher = commandDispatcher; + _queryDispatcher = queryDispatcher; + } + + [HttpGet("{wholesalerId:guid}")] + public async Task> Get(Guid wholesalerId) + => Ok(_queryDispatcher.QueryAsync(new GetWholesaler(wholesalerId))); + + [HttpPost] + public async Task Post(AddWholesaler command) + { + await _commandDispatcher.DispatchAsync(command); + return CreatedAtAction(nameof(Get), new { wholesaleerId = command.Id }, null); + } + + [HttpPost("{wholesalerId:guid}/sale")] + public async Task AddSale(AddBeerSale command) + { + await _commandDispatcher.DispatchAsync(command); + return NoContent(); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBeerSale.cs b/src/Brewery.Application/Commands/AddBeerSale.cs new file mode 100644 index 0000000..8b93224 --- /dev/null +++ b/src/Brewery.Application/Commands/AddBeerSale.cs @@ -0,0 +1,6 @@ + +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddBeerSale(Guid WholesalerId, Guid BeerId, int Quantity) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBeerSaleHandler.cs b/src/Brewery.Application/Commands/AddBeerSaleHandler.cs new file mode 100644 index 0000000..a8e65c3 --- /dev/null +++ b/src/Brewery.Application/Commands/AddBeerSaleHandler.cs @@ -0,0 +1,43 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands; + +public class AddBeerSaleHandler : ICommandHandler +{ + private readonly IWholesalerRepository _wholesalerRepository; + private readonly ISaleRepository _saleRepository; + + public AddBeerSaleHandler(IWholesalerRepository wholesalerRepository, + ISaleRepository saleRepository) + { + _wholesalerRepository = wholesalerRepository; + _saleRepository = saleRepository; + } + + public async Task HandleAsync(AddBeerSale command) + { + var wholesaler = await _wholesalerRepository.GetWholesaler(command.WholesalerId); + if (wholesaler is null) + { + throw new WholesalerNotFoundException(command.WholesalerId); + } + + var beerSale = await _saleRepository.GetSaleByBeerId(command.BeerId); + if (beerSale is null) + { + throw new BeerSaleNotFoundException(command.BeerId); + } + + if (beerSale.Quantity < command.Quantity) + { + throw new ExcessiveBeerSaleOrderQuantityException(command.Quantity); + } + + beerSale.TakeBeer(command.Quantity); + wholesaler.AddBeerSale(beerSale); + + await _wholesalerRepository + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddWholesaler.cs b/src/Brewery.Application/Commands/AddWholesaler.cs new file mode 100644 index 0000000..6a5a92a --- /dev/null +++ b/src/Brewery.Application/Commands/AddWholesaler.cs @@ -0,0 +1,8 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public class AddWholesaler(string Name) : ICommand +{ + public Guid Id => Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/Extensions.cs b/src/Brewery.Application/DTO/Extensions.cs index 8421fcb..c2b6685 100644 --- a/src/Brewery.Application/DTO/Extensions.cs +++ b/src/Brewery.Application/DTO/Extensions.cs @@ -27,4 +27,11 @@ public static BrewerDto AsDto(this Brewer brewer) return brewerDto; } + + public static WholesalerDto AsDto(this Wholesaler wholesaler) + => new WholesalerDto + { + Id = wholesaler.Id, + Name = wholesaler.Name, + }; } diff --git a/src/Brewery.Application/DTO/WholesalerDto.cs b/src/Brewery.Application/DTO/WholesalerDto.cs new file mode 100644 index 0000000..4a429ff --- /dev/null +++ b/src/Brewery.Application/DTO/WholesalerDto.cs @@ -0,0 +1,7 @@ +namespace Brewery.Application.DTO; + +public class WholesalerDto +{ + public Guid Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerSaleNotFoundException.cs b/src/Brewery.Application/Exceptions/BeerSaleNotFoundException.cs new file mode 100644 index 0000000..07d83fe --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerSaleNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerSaleNotFoundException : BreweryException +{ + public Guid BeerId { get; } + public BeerSaleNotFoundException(Guid beerId) + : base($"Sale for beer with id '{beerId}' was not found.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/ExcessiveBeerSaleOrderQuantityException.cs b/src/Brewery.Application/Exceptions/ExcessiveBeerSaleOrderQuantityException.cs new file mode 100644 index 0000000..59a24bd --- /dev/null +++ b/src/Brewery.Application/Exceptions/ExcessiveBeerSaleOrderQuantityException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class ExcessiveBeerSaleOrderQuantityException : BreweryException +{ + public int Quantity { get; } + public ExcessiveBeerSaleOrderQuantityException(int quantity) + : base($"Order quantity of '{quantity}' beers is excessive. Not enough stock.") + { + Quantity = quantity; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/WholesalerNotFoundException.cs b/src/Brewery.Application/Exceptions/WholesalerNotFoundException.cs new file mode 100644 index 0000000..5601a49 --- /dev/null +++ b/src/Brewery.Application/Exceptions/WholesalerNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class WholesalerNotFoundException : BreweryException +{ + public Guid WholesalerId { get; } + public WholesalerNotFoundException(Guid wholesalerId) + : base($"Wholesaler with id '{wholesalerId}' was not found.") + { + WholesalerId = wholesalerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetWholesaler.cs b/src/Brewery.Application/Queries/GetWholesaler.cs new file mode 100644 index 0000000..f508c49 --- /dev/null +++ b/src/Brewery.Application/Queries/GetWholesaler.cs @@ -0,0 +1,7 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Domain.Entities; + +namespace Brewery.Application.Queries; + +public record GetWholesaler(Guid WholesalerId) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Sale.cs b/src/Brewery.Domain/Entities/Sale.cs new file mode 100644 index 0000000..c8fa29e --- /dev/null +++ b/src/Brewery.Domain/Entities/Sale.cs @@ -0,0 +1,49 @@ +using Brewery.Domain.Exceptions; + +namespace Brewery.Domain.Entities; + +public class Sale +{ + public Guid Id { get; private set; } + public Guid BeerId { get; private set; } + public int Quantity { get; private set; } + + public Sale(Guid id, Guid beerId) + { + Id = id; + BeerId = beerId; + } + + public void RestockBeer(Guid beerId, int quantity) + { + if (beerId != BeerId) + { + throw new InvalidBeerToBeRestockedException(beerId); + } + + if (quantity <= 0) + { + throw new InvalidBeerQuantityException(quantity); + } + + Quantity += quantity; + } + + public void TakeBeer(int quantity) + { + if (Quantity < quantity) + { + throw new NotEnoughBeerToSellException(quantity); + } + + Quantity -= quantity; + } + + public static Sale Create(Guid id, Guid beerId, int quantity) + { + var sale = new Sale(id, beerId); + sale.RestockBeer(beerId, quantity); + + return sale; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Wholesaler.cs b/src/Brewery.Domain/Entities/Wholesaler.cs index 55f467b..f9a6bac 100644 --- a/src/Brewery.Domain/Entities/Wholesaler.cs +++ b/src/Brewery.Domain/Entities/Wholesaler.cs @@ -2,9 +2,34 @@ public class Wholesaler { - private readonly HashSet _beers = new(); public Guid Id { get; private set; } public string Name { get; private set; } - public IEnumerable Beers => _beers; + private readonly HashSet _beers = new(); + public IEnumerable Beers => _beers; + + public Wholesaler(Guid id) + { + Id = id; + } + public void ChangeName(string name) + => Name = name; + + public void AddBeerSale(Sale sale) + { + _beers.Add(sale); + } + + public void RemoveBeerSale(Sale sale) + { + _beers.Remove(sale); + } + + public static Wholesaler Create(Guid id, string name) + { + var wholesaler = new Wholesaler(id); + wholesaler.ChangeName(name); + + return wholesaler; + } } \ No newline at end of file diff --git a/src/Brewery.Domain/Exceptions/InvalidBeerQuantityException.cs b/src/Brewery.Domain/Exceptions/InvalidBeerQuantityException.cs new file mode 100644 index 0000000..4202550 --- /dev/null +++ b/src/Brewery.Domain/Exceptions/InvalidBeerQuantityException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class InvalidBeerQuantityException : BreweryException +{ + public int Quantity { get; } + public InvalidBeerQuantityException(int quantity) + : base($"Quantity of '{quantity}' is invalid.") + { + Quantity = quantity; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Exceptions/InvalidBeerToBeRestockedException.cs b/src/Brewery.Domain/Exceptions/InvalidBeerToBeRestockedException.cs new file mode 100644 index 0000000..97e8fd8 --- /dev/null +++ b/src/Brewery.Domain/Exceptions/InvalidBeerToBeRestockedException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class InvalidBeerToBeRestockedException : BreweryException +{ + public Guid BeerId { get; } + public InvalidBeerToBeRestockedException(Guid beerId) + : base($"Beer with '{beerId}' is invalid to be restocked.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Exceptions/NotEnoughBeerToSellException.cs b/src/Brewery.Domain/Exceptions/NotEnoughBeerToSellException.cs new file mode 100644 index 0000000..601a568 --- /dev/null +++ b/src/Brewery.Domain/Exceptions/NotEnoughBeerToSellException.cs @@ -0,0 +1,11 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class NotEnoughBeerToSellException : BreweryException +{ + public NotEnoughBeerToSellException(int quantity) + : base($"Not enough beer to sell. Quantity of '{quantity}' is excessive.") + { + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/ISaleRepository.cs b/src/Brewery.Domain/Repositories/ISaleRepository.cs new file mode 100644 index 0000000..ed34be3 --- /dev/null +++ b/src/Brewery.Domain/Repositories/ISaleRepository.cs @@ -0,0 +1,11 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface ISaleRepository +{ + Task AddAsync(Sale sale); + Task DeleteAsync(Sale sale); + Task GetSaleByBeerId(Guid beerId); + +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IWholesalerRepository.cs b/src/Brewery.Domain/Repositories/IWholesalerRepository.cs new file mode 100644 index 0000000..8c5f4fb --- /dev/null +++ b/src/Brewery.Domain/Repositories/IWholesalerRepository.cs @@ -0,0 +1,10 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IWholesalerRepository +{ + Task AddWholesaler(Wholesaler wholesaler); + Task UpdateWholesaler(Wholesaler wholesaler); + Task GetWholesaler(Guid id); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs index aba135f..00e51e4 100644 --- a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs +++ b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs @@ -9,6 +9,8 @@ public class BreweryDbContext : DbContext public DbSet Beers { get; set; } public DbSet Brewers { get; set; } public DbSet Breweries { get; set; } + public DbSet Wholesalers { get; set; } + public DbSet Sales { get; set; } public BreweryDbContext(DbContextOptions options) : base(options) diff --git a/src/Brewery.Infrastructure/EF/Extensions.cs b/src/Brewery.Infrastructure/EF/Extensions.cs index 45030cf..7360ac4 100644 --- a/src/Brewery.Infrastructure/EF/Extensions.cs +++ b/src/Brewery.Infrastructure/EF/Extensions.cs @@ -13,6 +13,8 @@ internal static IServiceCollection AddEF(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); var postgresOptions = services.GetOptions("postgres"); services.AddDbContext(options => diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetWholesalerHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetWholesalerHandler.cs new file mode 100644 index 0000000..28e3b91 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetWholesalerHandler.cs @@ -0,0 +1,27 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetWholesalerHandler : IQueryHandler +{ + private readonly DbSet _wholesalers; + + public GetWholesalerHandler(BreweryDbContext dbContext) + { + _wholesalers = dbContext.Wholesalers; + } + public async Task QueryAsync(GetWholesaler query) + { + var wholesaler = await _wholesalers + .AsNoTracking() + .SingleOrDefaultAsync(w => w.Id == query.WholesalerId); + + return wholesaler is not null + ? wholesaler.AsDto() + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs new file mode 100644 index 0000000..33fe816 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs @@ -0,0 +1,32 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class SaleRepository : ISaleRepository +{ + private readonly DbSet _sales; + private readonly BreweryDbContext _dbContext; + + public SaleRepository(BreweryDbContext dbContext) + { + _sales = _dbContext.Sales; + _dbContext = dbContext; + } + + public async Task AddAsync(Sale sale) + { + await _sales.AddAsync(sale); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteAsync(Sale sale) + { + _sales.Remove(sale); + await _dbContext.SaveChangesAsync(); + } + + public Task GetSaleByBeerId(Guid beerId) + => _sales.SingleOrDefaultAsync(s => s.BeerId == beerId); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/WholesalerRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/WholesalerRepository.cs new file mode 100644 index 0000000..181dd83 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/WholesalerRepository.cs @@ -0,0 +1,32 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class WholesalerRepository : IWholesalerRepository +{ + private readonly DbSet _wholesalers; + private readonly BreweryDbContext _dbContext; + + public WholesalerRepository(BreweryDbContext dbContext) + { + _wholesalers = dbContext.Wholesalers; + _dbContext = dbContext; + } + + public async Task AddWholesaler(Wholesaler wholesaler) + { + await _wholesalers.AddAsync(wholesaler); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateWholesaler(Wholesaler wholesaler) + { + _wholesalers.Update(wholesaler); + await _dbContext.SaveChangesAsync(); + } + + public Task GetWholesaler(Guid id) + => _wholesalers.SingleOrDefaultAsync(w => w.Id == id); +} \ No newline at end of file From 72ec7d6410cbb84658239c2ed9511654b44f4033 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Fri, 15 Nov 2024 20:12:44 +0000 Subject: [PATCH 12/24] add BeerStock, Repository, CommandHandler --- .../Controllers/BeerStockController.cs | 11 + .../Controllers/WholesalerController.cs | 11 +- .../Commands/AddBeerSale.cs | 3 +- .../Commands/AddBeerSaleHandler.cs | 43 ---- .../Commands/AddBeerStock.cs | 8 + .../Commands/AddWholesaler.cs | 4 +- .../Commands/Handlers/AddBeerSaleHandler.cs | 52 +++++ .../Commands/Handlers/AddBeerStockHandler.cs | 25 +++ .../Commands/Handlers/AddWholesalerHandler.cs | 28 +++ .../BeerSaleAlreadyExistException.cs | 15 ++ ...eerStockNotEnoughToFulfilOrderException.cs | 15 ++ .../Exceptions/BeerStockNotFoundException.cs | 13 ++ .../WholesaleAlreadyExistException.cs | 13 ++ .../Entities/{Sale.cs => BeerSale.cs} | 12 +- src/Brewery.Domain/Entities/BeerStock.cs | 49 +++++ src/Brewery.Domain/Entities/Brewer.cs | 8 + src/Brewery.Domain/Entities/Wholesaler.cs | 12 +- .../NotEnoughBeerToSellException.cs | 11 - .../NotEnoughBeerToTakeException.cs | 11 + .../Repositories/IBeerStockRepository.cs | 11 + .../Repositories/ISaleRepository.cs | 7 +- .../EF/BreweryDbContext.cs | 3 +- src/Brewery.Infrastructure/EF/Extensions.cs | 1 + ...41115144513_AddWholesaler,Sale.Designer.cs | 164 +++++++++++++++ .../20241115144513_AddWholesaler,Sale.cs | 67 ++++++ .../20241115200357_AddBeerStock.Designer.cs | 181 ++++++++++++++++ .../Migrations/20241115200357_AddBeerStock.cs | 99 +++++++++ .../20241115200807_EditBrewer.Designer.cs | 195 ++++++++++++++++++ .../Migrations/20241115200807_EditBrewer.cs | 56 +++++ .../BreweryDbContextModelSnapshot.cs | 80 +++++++ .../EF/Repositories/BeerStockRepository.cs | 38 ++++ .../EF/Repositories/SaleRepository.cs | 22 +- 32 files changed, 1181 insertions(+), 87 deletions(-) create mode 100644 src/Brewery.Api/Controllers/BeerStockController.cs delete mode 100644 src/Brewery.Application/Commands/AddBeerSaleHandler.cs create mode 100644 src/Brewery.Application/Commands/AddBeerStock.cs create mode 100644 src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs create mode 100644 src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs create mode 100644 src/Brewery.Application/Commands/Handlers/AddWholesalerHandler.cs create mode 100644 src/Brewery.Application/Exceptions/BeerSaleAlreadyExistException.cs create mode 100644 src/Brewery.Application/Exceptions/BeerStockNotEnoughToFulfilOrderException.cs create mode 100644 src/Brewery.Application/Exceptions/BeerStockNotFoundException.cs create mode 100644 src/Brewery.Application/Exceptions/WholesaleAlreadyExistException.cs rename src/Brewery.Domain/Entities/{Sale.cs => BeerSale.cs} (73%) create mode 100644 src/Brewery.Domain/Entities/BeerStock.cs delete mode 100644 src/Brewery.Domain/Exceptions/NotEnoughBeerToSellException.cs create mode 100644 src/Brewery.Domain/Exceptions/NotEnoughBeerToTakeException.cs create mode 100644 src/Brewery.Domain/Repositories/IBeerStockRepository.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.Designer.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.Designer.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.Designer.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.cs create mode 100644 src/Brewery.Infrastructure/EF/Repositories/BeerStockRepository.cs diff --git a/src/Brewery.Api/Controllers/BeerStockController.cs b/src/Brewery.Api/Controllers/BeerStockController.cs new file mode 100644 index 0000000..4ca1985 --- /dev/null +++ b/src/Brewery.Api/Controllers/BeerStockController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class BeerStockController : BaseController +{ + [HttpPost] + public async Task Post(AddBeerStock addBeerStock) + + +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/WholesalerController.cs b/src/Brewery.Api/Controllers/WholesalerController.cs index 3dfebe4..39e7265 100644 --- a/src/Brewery.Api/Controllers/WholesalerController.cs +++ b/src/Brewery.Api/Controllers/WholesalerController.cs @@ -3,7 +3,6 @@ using Brewery.Application.Commands; using Brewery.Application.DTO; using Brewery.Application.Queries; -using Brewery.Domain.Entities; using Microsoft.AspNetCore.Mvc; namespace Brewery.Api.Controllers; @@ -21,19 +20,21 @@ public WholesalerController(ICommandDispatcher commandDispatcher, IQueryDispatch [HttpGet("{wholesalerId:guid}")] public async Task> Get(Guid wholesalerId) - => Ok(_queryDispatcher.QueryAsync(new GetWholesaler(wholesalerId))); + { + return Ok(await _queryDispatcher.QueryAsync(new GetWholesaler(wholesalerId))); + } [HttpPost] public async Task Post(AddWholesaler command) { await _commandDispatcher.DispatchAsync(command); - return CreatedAtAction(nameof(Get), new { wholesaleerId = command.Id }, null); + return CreatedAtAction(nameof(Get), new { wholesalerId = command.Id }, null); } [HttpPost("{wholesalerId:guid}/sale")] - public async Task AddSale(AddBeerSale command) + public async Task AddSale(AddBeerSale command, Guid wholesalerId) { - await _commandDispatcher.DispatchAsync(command); + await _commandDispatcher.DispatchAsync(command with { WholesalerId = wholesalerId }); return NoContent(); } } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBeerSale.cs b/src/Brewery.Application/Commands/AddBeerSale.cs index 8b93224..b0fc8f6 100644 --- a/src/Brewery.Application/Commands/AddBeerSale.cs +++ b/src/Brewery.Application/Commands/AddBeerSale.cs @@ -1,5 +1,4 @@ - -using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Commands; namespace Brewery.Application.Commands; diff --git a/src/Brewery.Application/Commands/AddBeerSaleHandler.cs b/src/Brewery.Application/Commands/AddBeerSaleHandler.cs deleted file mode 100644 index a8e65c3..0000000 --- a/src/Brewery.Application/Commands/AddBeerSaleHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Brewery.Abstractions.Commands; -using Brewery.Application.Exceptions; -using Brewery.Domain.Repositories; - -namespace Brewery.Application.Commands; - -public class AddBeerSaleHandler : ICommandHandler -{ - private readonly IWholesalerRepository _wholesalerRepository; - private readonly ISaleRepository _saleRepository; - - public AddBeerSaleHandler(IWholesalerRepository wholesalerRepository, - ISaleRepository saleRepository) - { - _wholesalerRepository = wholesalerRepository; - _saleRepository = saleRepository; - } - - public async Task HandleAsync(AddBeerSale command) - { - var wholesaler = await _wholesalerRepository.GetWholesaler(command.WholesalerId); - if (wholesaler is null) - { - throw new WholesalerNotFoundException(command.WholesalerId); - } - - var beerSale = await _saleRepository.GetSaleByBeerId(command.BeerId); - if (beerSale is null) - { - throw new BeerSaleNotFoundException(command.BeerId); - } - - if (beerSale.Quantity < command.Quantity) - { - throw new ExcessiveBeerSaleOrderQuantityException(command.Quantity); - } - - beerSale.TakeBeer(command.Quantity); - wholesaler.AddBeerSale(beerSale); - - await _wholesalerRepository - } -} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBeerStock.cs b/src/Brewery.Application/Commands/AddBeerStock.cs new file mode 100644 index 0000000..2d52bdc --- /dev/null +++ b/src/Brewery.Application/Commands/AddBeerStock.cs @@ -0,0 +1,8 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record AddBeerStock(Guid BeerId, Guid BrewerId, int Quantity) : ICommand +{ + public Guid Id { get; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddWholesaler.cs b/src/Brewery.Application/Commands/AddWholesaler.cs index 6a5a92a..d0b90dd 100644 --- a/src/Brewery.Application/Commands/AddWholesaler.cs +++ b/src/Brewery.Application/Commands/AddWholesaler.cs @@ -2,7 +2,7 @@ namespace Brewery.Application.Commands; -public class AddWholesaler(string Name) : ICommand +public record AddWholesaler(string Name) : ICommand { - public Guid Id => Guid.NewGuid(); + public Guid Id { get; } = Guid.NewGuid(); } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs new file mode 100644 index 0000000..a3aac80 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs @@ -0,0 +1,52 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddBeerSaleHandler : ICommandHandler +{ + private readonly IWholesalerRepository _wholesalerRepository; + private readonly IBeerStockRepository _beerStockRepository; + + public AddBeerSaleHandler(IWholesalerRepository wholesalerRepository, + IBeerStockRepository beerStockRepository) + { + _wholesalerRepository = wholesalerRepository; + _beerStockRepository = beerStockRepository; + } + + public async Task HandleAsync(AddBeerSale command) + { + var wholesaler = await _wholesalerRepository.GetWholesaler(command.WholesalerId); + if (wholesaler is null) + { + throw new WholesalerNotFoundException(command.WholesalerId); + } + + var beerSale = wholesaler.BeerSales.SingleOrDefault(b => b.Id == command.BeerId); + if (beerSale is not null) + { + throw new BeerSaleAlreadyExistException(command.WholesalerId, command.BeerId); + } + + var beerStock = await _beerStockRepository.GetBeerStock(command.BeerId); + if (beerStock is null) + { + throw new BeerStockNotFoundException(command.BeerId); + } + + if (beerStock.Quantity < command.Quantity) + { + throw new BeerStockNotEnoughToFulfilOrderException(command.BeerId, command.Quantity); + } + + beerSale = BeerSale.Create(command.WholesalerId, command.BeerId, 50); + beerStock.TakeForBeerSale(beerSale.Quantity); + wholesaler.AddBeerSale(beerSale); + + await _beerStockRepository.UpdateBeerStock(beerStock); + await _wholesalerRepository.UpdateWholesaler(wholesaler); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs new file mode 100644 index 0000000..2fe3a22 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs @@ -0,0 +1,25 @@ +using Brewery.Abstractions.Commands; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddBeerStockHandler : ICommandHandler +{ + private readonly IBeerStockRepository _beerStockRepository; + private readonly IBreweryRepository _breweryRepository; + private readonly IBeerRepository _beerRepository; + + public AddBeerStockHandler(IBeerStockRepository beerStockRepository, + IBreweryRepository breweryRepository, + IBeerRepository beerRepository) + { + _beerStockRepository = beerStockRepository; + _breweryRepository = breweryRepository; + _beerRepository = beerRepository; + } + + public Task HandleAsync(AddBeerStock command) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddWholesalerHandler.cs b/src/Brewery.Application/Commands/Handlers/AddWholesalerHandler.cs new file mode 100644 index 0000000..ec1d624 --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/AddWholesalerHandler.cs @@ -0,0 +1,28 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; + +namespace Brewery.Application.Commands.Handlers; + +public class AddWholesalerHandler : ICommandHandler +{ + private readonly IWholesalerRepository _wholesalerRepository; + + public AddWholesalerHandler(IWholesalerRepository wholesalerRepository) + { + _wholesalerRepository = wholesalerRepository; + } + + public async Task HandleAsync(AddWholesaler command) + { + var wholesaler = await _wholesalerRepository.GetWholesaler(command.Id); + if (wholesaler is not null) + { + throw new WholesaleAlreadyExistException(command.Id); + } + + wholesaler = Wholesaler.Create(command.Id, command.Name); + await _wholesalerRepository.AddWholesaler(wholesaler); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerSaleAlreadyExistException.cs b/src/Brewery.Application/Exceptions/BeerSaleAlreadyExistException.cs new file mode 100644 index 0000000..f6fcbce --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerSaleAlreadyExistException.cs @@ -0,0 +1,15 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerSaleAlreadyExistException : BreweryException +{ + public Guid WholesalerId { get; } + public Guid BeerId { get; } + public BeerSaleAlreadyExistException(Guid wholesalerId, Guid beerId) + : base($"Wholsaler with id '{wholesalerId}' already offers beer sale with id '{beerId}'.") + { + WholesalerId = wholesalerId; + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerStockNotEnoughToFulfilOrderException.cs b/src/Brewery.Application/Exceptions/BeerStockNotEnoughToFulfilOrderException.cs new file mode 100644 index 0000000..2268465 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerStockNotEnoughToFulfilOrderException.cs @@ -0,0 +1,15 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerStockNotEnoughToFulfilOrderException : BreweryException +{ + public Guid BeerId { get; } + public int Quantity { get; } + public BeerStockNotEnoughToFulfilOrderException(Guid beerId, int quantity) + : base($"Beer stock for beer with id '{beerId}' is not enough. Sale quantity of '{quantity}' is excessive.") + { + BeerId = beerId; + Quantity = quantity; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerStockNotFoundException.cs b/src/Brewery.Application/Exceptions/BeerStockNotFoundException.cs new file mode 100644 index 0000000..4832853 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerStockNotFoundException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerStockNotFoundException : BreweryException +{ + public Guid BeerId { get; } + public BeerStockNotFoundException(Guid beerId) + : base($"Beer stock for beer with id '{beerId}' was not found.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/WholesaleAlreadyExistException.cs b/src/Brewery.Application/Exceptions/WholesaleAlreadyExistException.cs new file mode 100644 index 0000000..5db53f8 --- /dev/null +++ b/src/Brewery.Application/Exceptions/WholesaleAlreadyExistException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class WholesaleAlreadyExistException : BreweryException +{ + public Guid WholesalerId { get; } + public WholesaleAlreadyExistException(Guid wholesalerId) + : base($"Wholesaler with id '{wholesalerId}' already exists.") + { + WholesalerId = wholesalerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Sale.cs b/src/Brewery.Domain/Entities/BeerSale.cs similarity index 73% rename from src/Brewery.Domain/Entities/Sale.cs rename to src/Brewery.Domain/Entities/BeerSale.cs index c8fa29e..91f2981 100644 --- a/src/Brewery.Domain/Entities/Sale.cs +++ b/src/Brewery.Domain/Entities/BeerSale.cs @@ -2,13 +2,13 @@ namespace Brewery.Domain.Entities; -public class Sale +public class BeerSale { public Guid Id { get; private set; } public Guid BeerId { get; private set; } public int Quantity { get; private set; } - public Sale(Guid id, Guid beerId) + public BeerSale(Guid id, Guid beerId) { Id = id; BeerId = beerId; @@ -29,19 +29,19 @@ public void RestockBeer(Guid beerId, int quantity) Quantity += quantity; } - public void TakeBeer(int quantity) + public void SellBeer(int quantity) { if (Quantity < quantity) { - throw new NotEnoughBeerToSellException(quantity); + throw new NotEnoughBeerToTakeException(quantity); } Quantity -= quantity; } - public static Sale Create(Guid id, Guid beerId, int quantity) + public static BeerSale Create(Guid id, Guid beerId, int quantity) { - var sale = new Sale(id, beerId); + var sale = new BeerSale(id, beerId); sale.RestockBeer(beerId, quantity); return sale; diff --git a/src/Brewery.Domain/Entities/BeerStock.cs b/src/Brewery.Domain/Entities/BeerStock.cs new file mode 100644 index 0000000..4e4fab7 --- /dev/null +++ b/src/Brewery.Domain/Entities/BeerStock.cs @@ -0,0 +1,49 @@ +using Brewery.Domain.Exceptions; + +namespace Brewery.Domain.Entities; + +public class BeerStock +{ + public Guid Id { get; private set; } + public Guid BeerId { get; private set; } + public int Quantity { get; private set; } + + public BeerStock(Guid id, Guid beerId) + { + Id = id; + BeerId = beerId; + } + + public void RestockBeer(Guid beerId, int quantity) + { + if (beerId != BeerId) + { + throw new InvalidBeerToBeRestockedException(beerId); + } + + if (quantity <= 0) + { + throw new InvalidBeerQuantityException(quantity); + } + + Quantity += quantity; + } + + public void TakeForBeerSale(int quantity) + { + if (Quantity < quantity) + { + throw new NotEnoughBeerToTakeException(quantity); + } + + Quantity -= quantity; + } + + public static BeerStock Create(Guid id, Guid beerId, int quantity) + { + var beerStock = new BeerStock(id, beerId); + beerStock.RestockBeer(beerId, quantity); + + return beerStock; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Brewer.cs b/src/Brewery.Domain/Entities/Brewer.cs index 9b9a606..6a15c9d 100644 --- a/src/Brewery.Domain/Entities/Brewer.cs +++ b/src/Brewery.Domain/Entities/Brewer.cs @@ -3,10 +3,12 @@ public class Brewer { public IEnumerable Beers => _beers; + public IEnumerable BeerStocks => _beerStocks; public Guid Id { get; private set; } public Guid? BreweryId { get; private set; } public string Name { get; private set; } private readonly List _beers = new List(); + private readonly List _beerStocks = new List(); public Brewer(Guid id) { @@ -21,6 +23,12 @@ public void AddBeer(Beer beer) public void DeleteBeer(Beer beer) => _beers.Remove(beer); + + public void AddBeerStock(BeerStock stock) + => _beerStocks.Add(stock); + + public void DeleteBeerStock(BeerStock stock) + => _beerStocks.Remove(stock); public void ChangeBreweryId(Guid breweryId) { diff --git a/src/Brewery.Domain/Entities/Wholesaler.cs b/src/Brewery.Domain/Entities/Wholesaler.cs index f9a6bac..b9fed76 100644 --- a/src/Brewery.Domain/Entities/Wholesaler.cs +++ b/src/Brewery.Domain/Entities/Wholesaler.cs @@ -4,8 +4,8 @@ public class Wholesaler { public Guid Id { get; private set; } public string Name { get; private set; } - private readonly HashSet _beers = new(); - public IEnumerable Beers => _beers; + private readonly HashSet _beerSales = new(); + public IEnumerable BeerSales => _beerSales; public Wholesaler(Guid id) { @@ -15,14 +15,14 @@ public Wholesaler(Guid id) public void ChangeName(string name) => Name = name; - public void AddBeerSale(Sale sale) + public void AddBeerSale(BeerSale beerSale) { - _beers.Add(sale); + _beerSales.Add(beerSale); } - public void RemoveBeerSale(Sale sale) + public void RemoveBeerSale(BeerSale beerSale) { - _beers.Remove(sale); + _beerSales.Remove(beerSale); } public static Wholesaler Create(Guid id, string name) diff --git a/src/Brewery.Domain/Exceptions/NotEnoughBeerToSellException.cs b/src/Brewery.Domain/Exceptions/NotEnoughBeerToSellException.cs deleted file mode 100644 index 601a568..0000000 --- a/src/Brewery.Domain/Exceptions/NotEnoughBeerToSellException.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Brewery.Abstractions.Exceptions; - -namespace Brewery.Domain.Exceptions; - -public class NotEnoughBeerToSellException : BreweryException -{ - public NotEnoughBeerToSellException(int quantity) - : base($"Not enough beer to sell. Quantity of '{quantity}' is excessive.") - { - } -} \ No newline at end of file diff --git a/src/Brewery.Domain/Exceptions/NotEnoughBeerToTakeException.cs b/src/Brewery.Domain/Exceptions/NotEnoughBeerToTakeException.cs new file mode 100644 index 0000000..47b214f --- /dev/null +++ b/src/Brewery.Domain/Exceptions/NotEnoughBeerToTakeException.cs @@ -0,0 +1,11 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class NotEnoughBeerToTakeException : BreweryException +{ + public NotEnoughBeerToTakeException(int quantity) + : base($"Not enough beer to take. Quantity of '{quantity}' is excessive.") + { + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IBeerStockRepository.cs b/src/Brewery.Domain/Repositories/IBeerStockRepository.cs new file mode 100644 index 0000000..1de40ca --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBeerStockRepository.cs @@ -0,0 +1,11 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IBeerStockRepository +{ + Task AddBeerStock(BeerStock stock); + Task UpdateBeerStock(BeerStock stock); + Task DeleteBeerStock(BeerStock stock); + Task GetBeerStock(Guid beerId); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/ISaleRepository.cs b/src/Brewery.Domain/Repositories/ISaleRepository.cs index ed34be3..2cc982a 100644 --- a/src/Brewery.Domain/Repositories/ISaleRepository.cs +++ b/src/Brewery.Domain/Repositories/ISaleRepository.cs @@ -4,8 +4,9 @@ namespace Brewery.Domain.Repositories; public interface ISaleRepository { - Task AddAsync(Sale sale); - Task DeleteAsync(Sale sale); - Task GetSaleByBeerId(Guid beerId); + Task AddAsync(BeerSale beerSale); + Task UpdateAsync(BeerSale beerSale); + Task DeleteAsync(BeerSale beerSale); + Task GetSaleByBeerId(Guid beerId); } \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs index 00e51e4..ad03e98 100644 --- a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs +++ b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs @@ -10,7 +10,8 @@ public class BreweryDbContext : DbContext public DbSet Brewers { get; set; } public DbSet Breweries { get; set; } public DbSet Wholesalers { get; set; } - public DbSet Sales { get; set; } + public DbSet BeerSales { get; set; } + public DbSet BeerStocks { get; set; } public BreweryDbContext(DbContextOptions options) : base(options) diff --git a/src/Brewery.Infrastructure/EF/Extensions.cs b/src/Brewery.Infrastructure/EF/Extensions.cs index 7360ac4..625e419 100644 --- a/src/Brewery.Infrastructure/EF/Extensions.cs +++ b/src/Brewery.Infrastructure/EF/Extensions.cs @@ -15,6 +15,7 @@ internal static IServiceCollection AddEF(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); var postgresOptions = services.GetOptions("postgres"); services.AddDbContext(options => diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.Designer.cs new file mode 100644 index 0000000..656b804 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.Designer.cs @@ -0,0 +1,164 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241115144513_AddWholesaler,Sale")] + partial class AddWholesalerSale + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("WholesalerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WholesalerId"); + + b.ToTable("Sales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Wholesalers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Sale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("Beers") + .HasForeignKey("WholesalerId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Navigation("Beers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.cs new file mode 100644 index 0000000..1b74003 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class AddWholesalerSale : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Wholesalers", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Wholesalers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Sales", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BeerId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + WholesalerId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Sales", x => x.Id); + table.ForeignKey( + name: "FK_Sales_Wholesalers_WholesalerId", + column: x => x.WholesalerId, + principalSchema: "brewery", + principalTable: "Wholesalers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Sales_WholesalerId", + schema: "brewery", + table: "Sales", + column: "WholesalerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Sales", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "Wholesalers", + schema: "brewery"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.Designer.cs new file mode 100644 index 0000000..dfed9e4 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.Designer.cs @@ -0,0 +1,181 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241115200357_AddBeerStock")] + partial class AddBeerStock + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("WholesalerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WholesalerId"); + + b.ToTable("BeerSales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("BeerStocks", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Wholesalers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("BeerSales") + .HasForeignKey("WholesalerId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Navigation("BeerSales"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.cs new file mode 100644 index 0000000..679d928 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class AddBeerStock : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Sales", + schema: "brewery"); + + migrationBuilder.CreateTable( + name: "BeerSales", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BeerId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + WholesalerId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BeerSales", x => x.Id); + table.ForeignKey( + name: "FK_BeerSales_Wholesalers_WholesalerId", + column: x => x.WholesalerId, + principalSchema: "brewery", + principalTable: "Wholesalers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "BeerStocks", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BeerId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeerStocks", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_BeerSales_WholesalerId", + schema: "brewery", + table: "BeerSales", + column: "WholesalerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BeerSales", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "BeerStocks", + schema: "brewery"); + + migrationBuilder.CreateTable( + name: "Sales", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BeerId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + WholesalerId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Sales", x => x.Id); + table.ForeignKey( + name: "FK_Sales_Wholesalers_WholesalerId", + column: x => x.WholesalerId, + principalSchema: "brewery", + principalTable: "Wholesalers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Sales_WholesalerId", + schema: "brewery", + table: "Sales", + column: "WholesalerId"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.Designer.cs new file mode 100644 index 0000000..37190ac --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.Designer.cs @@ -0,0 +1,195 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241115200807_EditBrewer")] + partial class EditBrewer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("WholesalerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WholesalerId"); + + b.ToTable("BeerSales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("BeerStocks", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Wholesalers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("BeerSales") + .HasForeignKey("WholesalerId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("BeerStocks") + .HasForeignKey("BrewerId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("BeerStocks"); + + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Navigation("BeerSales"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.cs new file mode 100644 index 0000000..2d9fd2a --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class EditBrewer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BrewerId", + schema: "brewery", + table: "BeerStocks", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_BeerStocks_BrewerId", + schema: "brewery", + table: "BeerStocks", + column: "BrewerId"); + + migrationBuilder.AddForeignKey( + name: "FK_BeerStocks_Brewers_BrewerId", + schema: "brewery", + table: "BeerStocks", + column: "BrewerId", + principalSchema: "brewery", + principalTable: "Brewers", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BeerStocks_Brewers_BrewerId", + schema: "brewery", + table: "BeerStocks"); + + migrationBuilder.DropIndex( + name: "IX_BeerStocks_BrewerId", + schema: "brewery", + table: "BeerStocks"); + + migrationBuilder.DropColumn( + name: "BrewerId", + schema: "brewery", + table: "BeerStocks"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs index b731f94..2c46b9e 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs @@ -46,6 +46,50 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Beers", "brewery"); }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("WholesalerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WholesalerId"); + + b.ToTable("BeerSales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("BeerStocks", "brewery"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => { b.Property("Id") @@ -81,6 +125,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Breweries", "brewery"); }); + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Wholesalers", "brewery"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => { b.HasOne("Brewery.Domain.Entities.Brewer", null) @@ -90,6 +149,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("BeerSales") + .HasForeignKey("WholesalerId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("BeerStocks") + .HasForeignKey("BrewerId"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => { b.HasOne("Brewery.Domain.Entities.Brewery", null) @@ -99,6 +172,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => { + b.Navigation("BeerStocks"); + b.Navigation("Beers"); }); @@ -106,6 +181,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("Brewers"); }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Navigation("BeerSales"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Brewery.Infrastructure/EF/Repositories/BeerStockRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BeerStockRepository.cs new file mode 100644 index 0000000..05e293c --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BeerStockRepository.cs @@ -0,0 +1,38 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BeerStockRepository : IBeerStockRepository +{ + private readonly DbSet _beerStocks; + private readonly BreweryDbContext _dbContext; + + public BeerStockRepository(BreweryDbContext dbContext) + { + _beerStocks = dbContext.BeerStocks; + _dbContext = dbContext; + } + + public async Task AddBeerStock(BeerStock stock) + { + await _beerStocks.AddAsync(stock); + await _dbContext.SaveChangesAsync(); + } + + public async Task UpdateBeerStock(BeerStock stock) + { + _beerStocks.Update(stock); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteBeerStock(BeerStock stock) + { + _beerStocks.Remove(stock); + await _dbContext.SaveChangesAsync(); + } + + public Task GetBeerStock(Guid beerId) + => _beerStocks.SingleOrDefaultAsync(b => b.BeerId == beerId); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs index 33fe816..0611b9b 100644 --- a/src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs +++ b/src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs @@ -6,27 +6,33 @@ namespace Brewery.Infrastructure.EF.Repositories; public class SaleRepository : ISaleRepository { - private readonly DbSet _sales; + private readonly DbSet _sales; private readonly BreweryDbContext _dbContext; public SaleRepository(BreweryDbContext dbContext) { - _sales = _dbContext.Sales; + _sales = _dbContext.BeerSales; _dbContext = dbContext; } - public async Task AddAsync(Sale sale) + public async Task AddAsync(BeerSale beerSale) { - await _sales.AddAsync(sale); + await _sales.AddAsync(beerSale); await _dbContext.SaveChangesAsync(); } - - public async Task DeleteAsync(Sale sale) + + public async Task UpdateAsync(BeerSale beerSale) + { + _sales.Update(beerSale); + await _dbContext.SaveChangesAsync(); + } + + public async Task DeleteAsync(BeerSale beerSale) { - _sales.Remove(sale); + _sales.Remove(beerSale); await _dbContext.SaveChangesAsync(); } - public Task GetSaleByBeerId(Guid beerId) + public Task GetSaleByBeerId(Guid beerId) => _sales.SingleOrDefaultAsync(s => s.BeerId == beerId); } \ No newline at end of file From 9ad0221b9b727ea79754c7ce55a7fbe448bcbd22 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sat, 16 Nov 2024 19:20:41 +0000 Subject: [PATCH 13/24] add DTOs, Queries, Commands --- src/Brewery.Api/BreweryApi.rest | 65 +++++++- .../Controllers/BeerQuoteController.cs | 32 ++++ .../Controllers/BeerStockController.cs | 22 ++- .../Commands/AddBeerStock.cs | 2 +- .../Commands/Handlers/AddBeerSaleHandler.cs | 34 ++-- .../Commands/Handlers/AddBeerStockHandler.cs | 32 +++- .../Commands/Handlers/RequestQuoteHandler.cs | 78 ++++++++++ .../Commands/Handlers/UpdateBeerHandler.cs | 7 +- .../Commands/RequestQuote.cs | 9 ++ src/Brewery.Application/DTO/BeerDto.cs | 1 - src/Brewery.Application/DTO/BeerOrderDto.cs | 9 ++ src/Brewery.Application/DTO/BeerQuoteDto.cs | 9 ++ src/Brewery.Application/DTO/Extensions.cs | 21 ++- .../BeerStockAlreadyExistException.cs | 13 ++ ...eerStockNotEnoughToFulfilOrderException.cs | 15 -- .../DuplicatesInRequestQuoteException.cs | 11 ++ .../NotEnoughBeerForRequestException.cs | 15 ++ .../Exceptions/OrderCannotBeEmptyException.cs | 13 ++ .../WholesalerBeerSaleNotFoundException.cs | 15 ++ .../Queries/GetBeerQuote.cs | 6 + src/Brewery.Domain/Entities/Beer.cs | 14 +- src/Brewery.Domain/Entities/BeerOrder.cs | 17 ++ src/Brewery.Domain/Entities/BeerQuote.cs | 55 +++++++ src/Brewery.Domain/Entities/BeerSale.cs | 10 +- src/Brewery.Domain/Entities/BeerStock.cs | 20 ++- .../Repositories/IBeerQuoteRepository.cs | 9 ++ ...leRepository.cs => IBeerSaleRepository.cs} | 5 +- .../ValueObjects/BeerEnquiry.cs | 7 + .../EF/BreweryDbContext.cs | 2 + .../Configuration/BeerOrderConfiguration.cs | 13 ++ src/Brewery.Infrastructure/EF/Extensions.cs | 3 +- ...Beer,Brewery,Brewer,Wholesaler.Designer.cs | 124 --------------- .../20241112230230_EditBrewery.Designer.cs | 108 ------------- .../Migrations/20241112230230_EditBrewery.cs | 58 ------- .../20241112233804_EditBeer.Designer.cs | 112 -------------- ...20241113144018_EditBeer,Brewer.Designer.cs | 98 ------------ .../20241113144018_EditBeer,Brewer.cs | 56 ------- ...41113185859_EditBrewer,Brewery.Designer.cs | 115 -------------- .../20241113185859_EditBrewer,Brewery.cs | 56 ------- .../20241115144513_AddWholesaler,Sale.cs | 67 -------- .../Migrations/20241115200357_AddBeerStock.cs | 99 ------------ .../Migrations/20241115200807_EditBrewer.cs | 56 ------- ...730_EditBeerToRemoveUnitPrice.Designer.cs} | 16 +- ...241115202730_EditBeerToRemoveUnitPrice.cs} | 113 +++++++++++--- ...163101_AddBeerOrder,BeerQuote.Designer.cs} | 86 ++++++++++- .../20241116163101_AddBeerOrder,BeerQuote.cs | 118 ++++++++++++++ ...32_EditBeerSaleToAddUnitPrice.Designer.cs} | 146 ++++++++++++++---- ...41116175232_EditBeerSaleToAddUnitPrice.cs} | 16 +- .../BreweryDbContextModelSnapshot.cs | 75 ++++++++- .../Queries/Handlers/GetBeerQuoteHandler.cs | 28 ++++ .../EF/Repositories/BeerQuoteRepository.cs | 26 ++++ ...aleRepository.cs => BeerSaleRepository.cs} | 18 +-- .../Services/AppInitializer.cs | 5 + 53 files changed, 1045 insertions(+), 1115 deletions(-) create mode 100644 src/Brewery.Api/Controllers/BeerQuoteController.cs create mode 100644 src/Brewery.Application/Commands/Handlers/RequestQuoteHandler.cs create mode 100644 src/Brewery.Application/Commands/RequestQuote.cs create mode 100644 src/Brewery.Application/DTO/BeerOrderDto.cs create mode 100644 src/Brewery.Application/DTO/BeerQuoteDto.cs create mode 100644 src/Brewery.Application/Exceptions/BeerStockAlreadyExistException.cs delete mode 100644 src/Brewery.Application/Exceptions/BeerStockNotEnoughToFulfilOrderException.cs create mode 100644 src/Brewery.Application/Exceptions/DuplicatesInRequestQuoteException.cs create mode 100644 src/Brewery.Application/Exceptions/NotEnoughBeerForRequestException.cs create mode 100644 src/Brewery.Application/Exceptions/OrderCannotBeEmptyException.cs create mode 100644 src/Brewery.Application/Exceptions/WholesalerBeerSaleNotFoundException.cs create mode 100644 src/Brewery.Application/Queries/GetBeerQuote.cs create mode 100644 src/Brewery.Domain/Entities/BeerOrder.cs create mode 100644 src/Brewery.Domain/Entities/BeerQuote.cs create mode 100644 src/Brewery.Domain/Repositories/IBeerQuoteRepository.cs rename src/Brewery.Domain/Repositories/{ISaleRepository.cs => IBeerSaleRepository.cs} (66%) create mode 100644 src/Brewery.Domain/ValueObjects/BeerEnquiry.cs create mode 100644 src/Brewery.Infrastructure/EF/Configuration/BeerOrderConfiguration.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.Designer.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.Designer.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.Designer.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.Designer.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.Designer.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.cs delete mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.cs rename src/Brewery.Infrastructure/EF/Migrations/{20241115200807_EditBrewer.Designer.cs => 20241115202730_EditBeerToRemoveUnitPrice.Designer.cs} (95%) rename src/Brewery.Infrastructure/EF/Migrations/{20241111205505_AddBeer,Brewery,Brewer,Wholesaler.cs => 20241115202730_EditBeerToRemoveUnitPrice.cs} (55%) rename src/Brewery.Infrastructure/EF/Migrations/{20241115200357_AddBeerStock.Designer.cs => 20241116163101_AddBeerOrder,BeerQuote.Designer.cs} (68%) create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.cs rename src/Brewery.Infrastructure/EF/Migrations/{20241115144513_AddWholesaler,Sale.Designer.cs => 20241116175232_EditBeerSaleToAddUnitPrice.Designer.cs} (57%) rename src/Brewery.Infrastructure/EF/Migrations/{20241112233804_EditBeer.cs => 20241116175232_EditBeerSaleToAddUnitPrice.cs} (62%) create mode 100644 src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerQuoteHandler.cs create mode 100644 src/Brewery.Infrastructure/EF/Repositories/BeerQuoteRepository.cs rename src/Brewery.Infrastructure/EF/Repositories/{SaleRepository.cs => BeerSaleRepository.cs} (55%) diff --git a/src/Brewery.Api/BreweryApi.rest b/src/Brewery.Api/BreweryApi.rest index 0850a01..2ec54ef 100644 --- a/src/Brewery.Api/BreweryApi.rest +++ b/src/Brewery.Api/BreweryApi.rest @@ -1,11 +1,16 @@ @url = http://localhost:5000 -@beerId = fd9c1c15-5425-4f46-85c1-ee43236b2e83 -@brewerId = 03464b8d-a27a-4134-996d-a5d97d1eafa5 -@breweryId = d730c80b-5680-4058-a64e-892c501cb287 +@beerId = a547da0f-15ef-487a-bf31-b28eccdec50b +@beerId2 = 4fc96048-6ee6-403f-b4c5-647aced09499 +@beerId3 = 8a7f0585-03a2-4295-91f4-99e4b6f829e1 +@beerStockId = 4f79410c-ce39-47d3-91f3-c726a26aaba4 +@brewerId = 21281174-0a8b-4097-badf-603bab276b9c +@breweryId = 1d91c9f4-61df-4787-9ed6-aa065c2ba8bc +@wholesalerId = c8c4d2a4-a85d-416b-b052-dc1f331f2237 +@beerQuoteId = f6f00152-986d-441c-8662-27e01e279a8e ### List all beers by brewery -GET {{url}}/beer/{{breweryId}} +GET {{url}}/brewery/{{breweryId}}/beers ### GET {{url}}/beer/{{beerId}} @@ -16,8 +21,7 @@ Content-Type: application/json { "brewerId": "{{brewerId}}", - "name": "beer 1", - "unitPrice": 3.50 + "name": "beer 3" } ### Brewer updates beer @@ -38,6 +42,16 @@ Content-Type: application/json "brewerId": "{{brewerId}}" } +### Brewer adds beerStock +POST {{url}}/beerstock/{{brewerId}} +Content-Type: application/json + +{ + "beerId": "{{beerId2}}", + "quantity": 100, + "unitPrice": 5 +} + ### GET {{url}}/brewer/{{brewerId}} @@ -46,7 +60,7 @@ POST {{url}}/brewer Content-Type: application/json { - "name": "brewski 2", + "name": "brewski 1", "breweryId": "{{breweryId}}" } @@ -61,5 +75,40 @@ POST {{url}}/brewery Content-Type: application/json { - "name": "brewery 2" + "name": "brewery 1" +} + +//Add wholesaler +### +POST {{url}}/wholesaler +Content-Type: application/json + +{ + "name": "wholesaler 1" +} + +//Add sale to wholesaler +### +POST {{url}}/wholesaler/{{wholesalerId}}/sale +Content-Type: application/json + +{ + "beerId": "{{beerId2}}", + "quantity": 50 +} + +### +GET {{url}}/beerQuote/{{beerQuoteId}} + +//Request quote +### +POST {{url}}/beerQuote +Content-Type: application/json + +{ + "wholesalerId": "{{wholesalerId}}", + "beersEnquiry": [ + { "beerId": "{{beerId}}", "requiredQuantity": 30 }, + { "beerId": "{{beerId2}}", "requiredQuantity": 10 } + ] } \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BeerQuoteController.cs b/src/Brewery.Api/Controllers/BeerQuoteController.cs new file mode 100644 index 0000000..cd77221 --- /dev/null +++ b/src/Brewery.Api/Controllers/BeerQuoteController.cs @@ -0,0 +1,32 @@ +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class BeerQuoteController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + private readonly IQueryDispatcher _queryDispatcher; + + public BeerQuoteController(ICommandDispatcher commandDispatcher, + IQueryDispatcher queryDispatcher) + { + _commandDispatcher = commandDispatcher; + _queryDispatcher = queryDispatcher; + } + + [HttpGet("{beerQuoteId}")] + public async Task> Get(Guid beerQuoteId) + => Ok(await _queryDispatcher.QueryAsync(new GetBeerQuote(beerQuoteId))); + + [HttpPost] + public async Task Post(RequestQuote command) + { + await _commandDispatcher.DispatchAsync(command); + return CreatedAtAction(nameof(Get), new { beerQuoteId = command.RequestQuoteId }, null); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BeerStockController.cs b/src/Brewery.Api/Controllers/BeerStockController.cs index 4ca1985..14b5f57 100644 --- a/src/Brewery.Api/Controllers/BeerStockController.cs +++ b/src/Brewery.Api/Controllers/BeerStockController.cs @@ -1,11 +1,23 @@ -using Microsoft.AspNetCore.Mvc; +using System.Security.Cryptography; +using Brewery.Abstractions.Commands; +using Brewery.Application.Commands; +using Microsoft.AspNetCore.Mvc; namespace Brewery.Api.Controllers; public class BeerStockController : BaseController { - [HttpPost] - public async Task Post(AddBeerStock addBeerStock) - - + private readonly ICommandDispatcher _commandDispatcher; + + public BeerStockController(ICommandDispatcher commandDispatcher) + { + _commandDispatcher = commandDispatcher; + } + + [HttpPost("{brewerId:guid}")] + public async Task Post(AddBeerStock addBeerStock, Guid brewerId) + { + await _commandDispatcher.DispatchAsync(addBeerStock with { BrewerId = brewerId }); + return NoContent(); + } } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBeerStock.cs b/src/Brewery.Application/Commands/AddBeerStock.cs index 2d52bdc..4c78a54 100644 --- a/src/Brewery.Application/Commands/AddBeerStock.cs +++ b/src/Brewery.Application/Commands/AddBeerStock.cs @@ -2,7 +2,7 @@ namespace Brewery.Application.Commands; -public record AddBeerStock(Guid BeerId, Guid BrewerId, int Quantity) : ICommand +public record AddBeerStock(Guid BeerId, Guid BrewerId, int Quantity, decimal UnitPrice) : ICommand { public Guid Id { get; } = Guid.NewGuid(); } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs index a3aac80..f142b8c 100644 --- a/src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/AddBeerSaleHandler.cs @@ -9,12 +9,15 @@ public class AddBeerSaleHandler : ICommandHandler { private readonly IWholesalerRepository _wholesalerRepository; private readonly IBeerStockRepository _beerStockRepository; + private readonly IBeerSaleRepository _beerSaleRepository; public AddBeerSaleHandler(IWholesalerRepository wholesalerRepository, - IBeerStockRepository beerStockRepository) + IBeerStockRepository beerStockRepository, + IBeerSaleRepository beerSaleRepository) { _wholesalerRepository = wholesalerRepository; _beerStockRepository = beerStockRepository; + _beerSaleRepository = beerSaleRepository; } public async Task HandleAsync(AddBeerSale command) @@ -25,28 +28,33 @@ public async Task HandleAsync(AddBeerSale command) throw new WholesalerNotFoundException(command.WholesalerId); } - var beerSale = wholesaler.BeerSales.SingleOrDefault(b => b.Id == command.BeerId); - if (beerSale is not null) - { - throw new BeerSaleAlreadyExistException(command.WholesalerId, command.BeerId); - } - var beerStock = await _beerStockRepository.GetBeerStock(command.BeerId); if (beerStock is null) { throw new BeerStockNotFoundException(command.BeerId); } - + + var beerSales = await _beerSaleRepository.BrowseByBeerId(command.BeerId); + var beerSale = beerSales?.SingleOrDefault(b => b.WholesalerId == wholesaler.Id); + if (beerSale is not null) + { + throw new BeerSaleAlreadyExistException(command.WholesalerId, command.BeerId); + } + if (beerStock.Quantity < command.Quantity) { - throw new BeerStockNotEnoughToFulfilOrderException(command.BeerId, command.Quantity); + throw new NotEnoughBeerForRequestException(command.BeerId, command.Quantity); } - beerSale = BeerSale.Create(command.WholesalerId, command.BeerId, 50); - beerStock.TakeForBeerSale(beerSale.Quantity); - wholesaler.AddBeerSale(beerSale); + beerSale = BeerSale.Create(Guid.NewGuid(), beerStock.BeerId, + wholesaler.Id, command.Quantity, beerStock.UnitPrice); + beerStock.TakeForBeerSale(beerSale.Quantity); await _beerStockRepository.UpdateBeerStock(beerStock); - await _wholesalerRepository.UpdateWholesaler(wholesaler); + + //wholesaler.AddBeerSale(beerSale); + await _beerSaleRepository.AddAsync(beerSale); + //await _wholesalerRepository.UpdateWholesaler(wholesaler); + // unit of work pattern? } } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs index 2fe3a22..2650cd7 100644 --- a/src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/AddBeerStockHandler.cs @@ -1,4 +1,6 @@ using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; using Brewery.Domain.Repositories; namespace Brewery.Application.Commands.Handlers; @@ -6,20 +8,40 @@ namespace Brewery.Application.Commands.Handlers; public class AddBeerStockHandler : ICommandHandler { private readonly IBeerStockRepository _beerStockRepository; - private readonly IBreweryRepository _breweryRepository; + private readonly IBrewerRepository _brewerRepository; private readonly IBeerRepository _beerRepository; public AddBeerStockHandler(IBeerStockRepository beerStockRepository, - IBreweryRepository breweryRepository, + IBrewerRepository brewerRepository, IBeerRepository beerRepository) { _beerStockRepository = beerStockRepository; - _breweryRepository = breweryRepository; + _brewerRepository = brewerRepository; _beerRepository = beerRepository; } - public Task HandleAsync(AddBeerStock command) + public async Task HandleAsync(AddBeerStock command) { - throw new NotImplementedException(); + var brewer = await _brewerRepository.GetBrewer(command.BrewerId); + if (brewer is null) + { + throw new BrewerNotFoundException(command.BrewerId); + } + + var beer = await _beerRepository.GetBeerById(command.BeerId); + if (beer is null) + { + throw new BeerNotFoundException(command.BeerId); + } + + var beerStock = await _beerStockRepository.GetBeerStock(command.BeerId); + if (beerStock is not null) + { + throw new BeerStockAlreadyExistException(command.BeerId); + } + + beerStock = BeerStock.Create(Guid.NewGuid(), brewer.Id, beer.Id, + command.Quantity, command.UnitPrice); + await _beerStockRepository.AddBeerStock(beerStock); } } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/RequestQuoteHandler.cs b/src/Brewery.Application/Commands/Handlers/RequestQuoteHandler.cs new file mode 100644 index 0000000..cb07d4f --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/RequestQuoteHandler.cs @@ -0,0 +1,78 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Brewery.Domain.ValueObjects; + +namespace Brewery.Application.Commands.Handlers; + +public class RequestQuoteHandler : ICommandHandler +{ + private readonly HashSet _beerIds = new(); + private readonly IBeerSaleRepository _beerSaleRepository; + private readonly IWholesalerRepository _wholesalerRepository; + private readonly IBeerQuoteRepository _beerQuoteRepository; + + public RequestQuoteHandler(IWholesalerRepository wholesalerRepository, + IBeerSaleRepository beerSaleRepository, + IBeerQuoteRepository beerQuoteRepository) + { + _wholesalerRepository = wholesalerRepository; + _beerSaleRepository = beerSaleRepository; + _beerQuoteRepository = beerQuoteRepository; + } + + public async Task HandleAsync(RequestQuote command) + { + var wholesaler = await _wholesalerRepository.GetWholesaler(command.WholesalerId); + if (wholesaler is null) throw new WholesalerNotFoundException(command.WholesalerId); + + CheckForDuplicates(command); + + var beerQuote = BeerQuote.Create(command.RequestQuoteId); + foreach (var enquiry in command.BeersEnquiry) + { + var beerSales = await _beerSaleRepository.BrowseByBeerId(enquiry.BeerId); + var beerSale = beerSales?.SingleOrDefault(b => b.WholesalerId == wholesaler.Id); + if (beerSale is null) + { + throw new WholesalerBeerSaleNotFoundException(wholesaler.Id, enquiry.BeerId); + } + + if (enquiry.RequiredQuantity <= 0) + { + throw new OrderCannotBeEmptyException(enquiry.BeerId); + } + + if (beerSale.Quantity < enquiry.RequiredQuantity) + { + throw new NotEnoughBeerForRequestException(beerSale.BeerId, enquiry.RequiredQuantity); + } + + var beerOrder = new BeerOrder(beerSale.BeerId, enquiry.RequiredQuantity, beerSale.UnitPrice); + beerQuote.AddBeerOrder(beerOrder); + beerSale.SellBeer(beerOrder.Quantity); + } + + beerQuote.CalculateDiscount(); + beerQuote.CalculateTotal(); + await _beerQuoteRepository.AddAsync(beerQuote); + } + + private void CheckForDuplicates(RequestQuote command) + { + _beerIds.Clear(); + foreach (var beerEnquiry in command.BeersEnquiry) + { + var beerId = beerEnquiry.BeerId; + var exists = _beerIds.TryGetValue(beerId, out _); + if (exists) + { + throw new DuplicatesInRequestQuoteException(); + } + + _beerIds.Add(beerId); + + } + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs index 493befc..564a67f 100644 --- a/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/UpdateBeerHandler.cs @@ -39,12 +39,7 @@ public async Task HandleAsync(UpdateBeer command) { beer.ChangeName(command.Name); } - - if (command.UnitPrice != 0) - { - beer.SetPrice(command.UnitPrice); - } - + await _beerRepository.UpdateAsync(beer); } } diff --git a/src/Brewery.Application/Commands/RequestQuote.cs b/src/Brewery.Application/Commands/RequestQuote.cs new file mode 100644 index 0000000..2dbcd6b --- /dev/null +++ b/src/Brewery.Application/Commands/RequestQuote.cs @@ -0,0 +1,9 @@ +using Brewery.Abstractions.Commands; +using Brewery.Domain.ValueObjects; + +namespace Brewery.Application.Commands; + +public record RequestQuote(Guid WholesalerId, IEnumerable BeersEnquiry) : ICommand +{ + public Guid RequestQuoteId { get; } = Guid.NewGuid(); +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/BeerDto.cs b/src/Brewery.Application/DTO/BeerDto.cs index b6f8b1f..908a612 100644 --- a/src/Brewery.Application/DTO/BeerDto.cs +++ b/src/Brewery.Application/DTO/BeerDto.cs @@ -7,5 +7,4 @@ public class BeerDto public Guid Id { get; set; } public Guid BrewerId { get; set; } public string Name { get; set; } - public decimal UnitPrice { get; set; } } diff --git a/src/Brewery.Application/DTO/BeerOrderDto.cs b/src/Brewery.Application/DTO/BeerOrderDto.cs new file mode 100644 index 0000000..c4df59f --- /dev/null +++ b/src/Brewery.Application/DTO/BeerOrderDto.cs @@ -0,0 +1,9 @@ +namespace Brewery.Application.DTO; + +public class BeerOrderDto +{ + public Guid BeerId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + public decimal Total { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/BeerQuoteDto.cs b/src/Brewery.Application/DTO/BeerQuoteDto.cs new file mode 100644 index 0000000..315a82f --- /dev/null +++ b/src/Brewery.Application/DTO/BeerQuoteDto.cs @@ -0,0 +1,9 @@ +namespace Brewery.Application.DTO; + +public class BeerQuoteDto +{ + public Guid Id { get; set; } + public IEnumerable BeerOrders { get; set; } + public int DiscountInPercent { get; set; } + public decimal Total { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/DTO/Extensions.cs b/src/Brewery.Application/DTO/Extensions.cs index c2b6685..30915b1 100644 --- a/src/Brewery.Application/DTO/Extensions.cs +++ b/src/Brewery.Application/DTO/Extensions.cs @@ -1,4 +1,5 @@ using Brewery.Domain.Entities; +using Brewery.Domain.ValueObjects; namespace Brewery.Application.DTO; @@ -11,7 +12,6 @@ public static BeerDto AsDto(this Beer beer) Id = beer.Id, BrewerId = beer.BrewerId, Name = beer.Name, - UnitPrice = beer.UnitPrice, }; return beerDto; @@ -34,4 +34,23 @@ public static WholesalerDto AsDto(this Wholesaler wholesaler) Id = wholesaler.Id, Name = wholesaler.Name, }; + + public static BeerOrderDto AsDto(this BeerOrder beerOrder) + => new BeerOrderDto + { + BeerId = beerOrder.BeerId, + Quantity = beerOrder.Quantity, + UnitPrice = beerOrder.UnitPrice, + Total = beerOrder.Total, + }; + + public static BeerQuoteDto AsDto(this BeerQuote beerQuote) + => new BeerQuoteDto() + { + Id = beerQuote.Id, + BeerOrders = beerQuote.BeerOrders + .Select(bo => bo.AsDto()), + Total = beerQuote.Total, + DiscountInPercent = beerQuote.DiscountInPercent, + }; } diff --git a/src/Brewery.Application/Exceptions/BeerStockAlreadyExistException.cs b/src/Brewery.Application/Exceptions/BeerStockAlreadyExistException.cs new file mode 100644 index 0000000..f07cdf3 --- /dev/null +++ b/src/Brewery.Application/Exceptions/BeerStockAlreadyExistException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class BeerStockAlreadyExistException : BreweryException +{ + public Guid BeerId { get; } + public BeerStockAlreadyExistException(Guid beerId) + : base($"Beer stock for beer with id '{beerId}' already exists.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/BeerStockNotEnoughToFulfilOrderException.cs b/src/Brewery.Application/Exceptions/BeerStockNotEnoughToFulfilOrderException.cs deleted file mode 100644 index 2268465..0000000 --- a/src/Brewery.Application/Exceptions/BeerStockNotEnoughToFulfilOrderException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Brewery.Abstractions.Exceptions; - -namespace Brewery.Application.Exceptions; - -public class BeerStockNotEnoughToFulfilOrderException : BreweryException -{ - public Guid BeerId { get; } - public int Quantity { get; } - public BeerStockNotEnoughToFulfilOrderException(Guid beerId, int quantity) - : base($"Beer stock for beer with id '{beerId}' is not enough. Sale quantity of '{quantity}' is excessive.") - { - BeerId = beerId; - Quantity = quantity; - } -} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/DuplicatesInRequestQuoteException.cs b/src/Brewery.Application/Exceptions/DuplicatesInRequestQuoteException.cs new file mode 100644 index 0000000..4f3ba08 --- /dev/null +++ b/src/Brewery.Application/Exceptions/DuplicatesInRequestQuoteException.cs @@ -0,0 +1,11 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class DuplicatesInRequestQuoteException : BreweryException +{ + public DuplicatesInRequestQuoteException() + : base($"Request quote cannot contain duplicates.") + { + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/NotEnoughBeerForRequestException.cs b/src/Brewery.Application/Exceptions/NotEnoughBeerForRequestException.cs new file mode 100644 index 0000000..2dcadae --- /dev/null +++ b/src/Brewery.Application/Exceptions/NotEnoughBeerForRequestException.cs @@ -0,0 +1,15 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class NotEnoughBeerForRequestException : BreweryException +{ + public Guid BeerId { get; } + public int Quantity { get; } + public NotEnoughBeerForRequestException(Guid beerId, int quantity) + : base($"Not enough beer with id '{beerId}'. Requested quantity of '{quantity}' is excessive.") + { + BeerId = beerId; + Quantity = quantity; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/OrderCannotBeEmptyException.cs b/src/Brewery.Application/Exceptions/OrderCannotBeEmptyException.cs new file mode 100644 index 0000000..10feb22 --- /dev/null +++ b/src/Brewery.Application/Exceptions/OrderCannotBeEmptyException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class OrderCannotBeEmptyException : BreweryException +{ + public Guid BeerId { get; } + public OrderCannotBeEmptyException(Guid beerId) + : base($"Beer order with id '{beerId}' cannot be empty.") + { + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/WholesalerBeerSaleNotFoundException.cs b/src/Brewery.Application/Exceptions/WholesalerBeerSaleNotFoundException.cs new file mode 100644 index 0000000..1cf8d77 --- /dev/null +++ b/src/Brewery.Application/Exceptions/WholesalerBeerSaleNotFoundException.cs @@ -0,0 +1,15 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class WholesalerBeerSaleNotFoundException : BreweryException +{ + public Guid WholesalerId { get; } + public Guid BeerId { get; } + public WholesalerBeerSaleNotFoundException(Guid wholesalerId, Guid beerId) + : base($"Beer sale was not found. Wholesaler with id '{wholesalerId}' does not offer beer sale with id '{beerId}'.") + { + WholesalerId = wholesalerId; + BeerId = beerId; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetBeerQuote.cs b/src/Brewery.Application/Queries/GetBeerQuote.cs new file mode 100644 index 0000000..fa6dc0f --- /dev/null +++ b/src/Brewery.Application/Queries/GetBeerQuote.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record GetBeerQuote(Guid BeerQuoteId) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/Beer.cs b/src/Brewery.Domain/Entities/Beer.cs index 57cae63..6423620 100644 --- a/src/Brewery.Domain/Entities/Beer.cs +++ b/src/Brewery.Domain/Entities/Beer.cs @@ -8,7 +8,6 @@ public class Beer public Guid Id { get; private set; } public Guid BrewerId { get; private set; } public string Name { get; private set; } - public decimal UnitPrice { get; private set; } public Beer(Guid id, Guid brewerId) { @@ -20,22 +19,11 @@ public void ChangeName(string name) { Name = name; } - - public void SetPrice(decimal unitPrice) - { - if (unitPrice <= 0) - { - throw new InvalidUnitPriceException(unitPrice); - } - - UnitPrice = unitPrice; - } - + public static Beer Create(Guid id, Guid brewerId, string name, decimal unitPrice) { var beer = new Beer(id, brewerId); beer.ChangeName(name); - beer.SetPrice(unitPrice); return beer; } diff --git a/src/Brewery.Domain/Entities/BeerOrder.cs b/src/Brewery.Domain/Entities/BeerOrder.cs new file mode 100644 index 0000000..0c6587e --- /dev/null +++ b/src/Brewery.Domain/Entities/BeerOrder.cs @@ -0,0 +1,17 @@ +namespace Brewery.Domain.Entities; + +public class BeerOrder +{ + public Guid BeerId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + public decimal Total { get; set; } + + public BeerOrder(Guid beerId, int quantity, decimal unitPrice) + { + BeerId = beerId; + Quantity = quantity; + UnitPrice = unitPrice; + Total = quantity * unitPrice; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/BeerQuote.cs b/src/Brewery.Domain/Entities/BeerQuote.cs new file mode 100644 index 0000000..13dcdbe --- /dev/null +++ b/src/Brewery.Domain/Entities/BeerQuote.cs @@ -0,0 +1,55 @@ +using Brewery.Domain.ValueObjects; + +namespace Brewery.Domain.Entities; + +public class BeerQuote +{ + public Guid Id { get; private set; } + public IEnumerable BeerOrders => _beerOrders; + private readonly HashSet _beerOrders = new HashSet(); + public int DiscountInPercent { get; private set; } + public decimal Total { get; private set; } + + public BeerQuote(Guid id) + { + Id = id; + } + + public void AddBeerOrder(BeerOrder beerOrder) + => _beerOrders.Add(beerOrder); + + public void CalculateTotal() + { + foreach (var beerOrder in _beerOrders) + { + Total += beerOrder.Total; + } + + if (DiscountInPercent is not 0) + { + Total = Total * (100 - DiscountInPercent) / 100; + } + } + + public void CalculateDiscount() + { + var beerAmount = _beerOrders.Sum(b => b.Quantity); + if (beerAmount > 20) + { + DiscountInPercent = 20; + return; + } + + if (beerAmount > 10) + { + DiscountInPercent = 10; + return; + } + + DiscountInPercent = 0; + return; + } + + public static BeerQuote Create(Guid id) + => new BeerQuote(id); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/BeerSale.cs b/src/Brewery.Domain/Entities/BeerSale.cs index 91f2981..7696231 100644 --- a/src/Brewery.Domain/Entities/BeerSale.cs +++ b/src/Brewery.Domain/Entities/BeerSale.cs @@ -6,12 +6,15 @@ public class BeerSale { public Guid Id { get; private set; } public Guid BeerId { get; private set; } + public Guid WholesalerId { get; private set; } public int Quantity { get; private set; } + public decimal UnitPrice { get; private set; } - public BeerSale(Guid id, Guid beerId) + public BeerSale(Guid id, Guid beerId, Guid wholesalerId) { Id = id; BeerId = beerId; + WholesalerId = wholesalerId; } public void RestockBeer(Guid beerId, int quantity) @@ -39,10 +42,11 @@ public void SellBeer(int quantity) Quantity -= quantity; } - public static BeerSale Create(Guid id, Guid beerId, int quantity) + public static BeerSale Create(Guid id, Guid beerId, Guid wholesalerId, int quantity, decimal unitPrice) { - var sale = new BeerSale(id, beerId); + var sale = new BeerSale(id, beerId, wholesalerId); sale.RestockBeer(beerId, quantity); + sale.UnitPrice = unitPrice; return sale; } diff --git a/src/Brewery.Domain/Entities/BeerStock.cs b/src/Brewery.Domain/Entities/BeerStock.cs index 4e4fab7..a04286f 100644 --- a/src/Brewery.Domain/Entities/BeerStock.cs +++ b/src/Brewery.Domain/Entities/BeerStock.cs @@ -6,11 +6,14 @@ public class BeerStock { public Guid Id { get; private set; } public Guid BeerId { get; private set; } + public Guid BrewerId { get; private set; } public int Quantity { get; private set; } + public decimal UnitPrice { get; private set; } - public BeerStock(Guid id, Guid beerId) + public BeerStock(Guid id, Guid brewerId, Guid beerId) { Id = id; + BrewerId = brewerId; BeerId = beerId; } @@ -38,11 +41,22 @@ public void TakeForBeerSale(int quantity) Quantity -= quantity; } + + public void SetPrice(decimal unitPrice) + { + if (unitPrice <= 0) + { + throw new InvalidUnitPriceException(unitPrice); + } + + UnitPrice = unitPrice; + } - public static BeerStock Create(Guid id, Guid beerId, int quantity) + public static BeerStock Create(Guid id, Guid brewerId, Guid beerId, int quantity, decimal unitPrice) { - var beerStock = new BeerStock(id, beerId); + var beerStock = new BeerStock(id, brewerId, beerId); beerStock.RestockBeer(beerId, quantity); + beerStock.SetPrice(unitPrice); return beerStock; } diff --git a/src/Brewery.Domain/Repositories/IBeerQuoteRepository.cs b/src/Brewery.Domain/Repositories/IBeerQuoteRepository.cs new file mode 100644 index 0000000..16975a5 --- /dev/null +++ b/src/Brewery.Domain/Repositories/IBeerQuoteRepository.cs @@ -0,0 +1,9 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IBeerQuoteRepository +{ + Task AddAsync(BeerQuote beerQuote); + Task GetBeerQuote(Guid id); +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/ISaleRepository.cs b/src/Brewery.Domain/Repositories/IBeerSaleRepository.cs similarity index 66% rename from src/Brewery.Domain/Repositories/ISaleRepository.cs rename to src/Brewery.Domain/Repositories/IBeerSaleRepository.cs index 2cc982a..f2c4994 100644 --- a/src/Brewery.Domain/Repositories/ISaleRepository.cs +++ b/src/Brewery.Domain/Repositories/IBeerSaleRepository.cs @@ -2,11 +2,10 @@ namespace Brewery.Domain.Repositories; -public interface ISaleRepository +public interface IBeerSaleRepository { Task AddAsync(BeerSale beerSale); Task UpdateAsync(BeerSale beerSale); Task DeleteAsync(BeerSale beerSale); - Task GetSaleByBeerId(Guid beerId); - + Task> BrowseByBeerId(Guid beerId); } \ No newline at end of file diff --git a/src/Brewery.Domain/ValueObjects/BeerEnquiry.cs b/src/Brewery.Domain/ValueObjects/BeerEnquiry.cs new file mode 100644 index 0000000..ceedf79 --- /dev/null +++ b/src/Brewery.Domain/ValueObjects/BeerEnquiry.cs @@ -0,0 +1,7 @@ +namespace Brewery.Domain.ValueObjects; + +public class BeerEnquiry +{ + public Guid BeerId { get; set; } + public int RequiredQuantity { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs index ad03e98..bd6fb07 100644 --- a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs +++ b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs @@ -12,6 +12,8 @@ public class BreweryDbContext : DbContext public DbSet Wholesalers { get; set; } public DbSet BeerSales { get; set; } public DbSet BeerStocks { get; set; } + public DbSet BeerOrder { get; set; } + public DbSet BeerQuotes { get; set; } public BreweryDbContext(DbContextOptions options) : base(options) diff --git a/src/Brewery.Infrastructure/EF/Configuration/BeerOrderConfiguration.cs b/src/Brewery.Infrastructure/EF/Configuration/BeerOrderConfiguration.cs new file mode 100644 index 0000000..ac729db --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Configuration/BeerOrderConfiguration.cs @@ -0,0 +1,13 @@ +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Brewery.Infrastructure.EF.Configuration; + +public class BeerOrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(b => b.BeerId); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Extensions.cs b/src/Brewery.Infrastructure/EF/Extensions.cs index 625e419..4df9b26 100644 --- a/src/Brewery.Infrastructure/EF/Extensions.cs +++ b/src/Brewery.Infrastructure/EF/Extensions.cs @@ -14,8 +14,9 @@ internal static IServiceCollection AddEF(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); var postgresOptions = services.GetOptions("postgres"); services.AddDbContext(options => diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.Designer.cs deleted file mode 100644 index d48c668..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.Designer.cs +++ /dev/null @@ -1,124 +0,0 @@ -// -using System; -using Brewery.Infrastructure.EF; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - [DbContext(typeof(BreweryDbContext))] - [Migration("20241111205505_AddBeer,Brewery,Brewer,Wholesaler")] - partial class AddBeerBreweryBrewerWholesaler - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("brewery") - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BrewerId") - .HasColumnType("uuid"); - - b.Property("BreweryId") - .HasColumnType("uuid"); - - b.Property("UnitPrice") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.HasIndex("BrewerId"); - - b.HasIndex("BreweryId"); - - b.ToTable("Beers", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Brewers", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BrewerId") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("BrewerId"); - - b.ToTable("Breweries", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => - { - b.HasOne("Brewery.Domain.Entities.Brewer", null) - .WithMany("Beers") - .HasForeignKey("BrewerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Brewery.Domain.Entities.Brewery", null) - .WithMany("Beers") - .HasForeignKey("BreweryId"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.HasOne("Brewery.Domain.Entities.Brewer", "Brewer") - .WithMany() - .HasForeignKey("BrewerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Brewer"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.Navigation("Beers"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.Navigation("Beers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.Designer.cs deleted file mode 100644 index 654fbea..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.Designer.cs +++ /dev/null @@ -1,108 +0,0 @@ -// -using System; -using Brewery.Infrastructure.EF; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - [DbContext(typeof(BreweryDbContext))] - [Migration("20241112230230_EditBrewery")] - partial class EditBrewery - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("brewery") - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BrewerId") - .HasColumnType("uuid"); - - b.Property("BreweryId") - .HasColumnType("uuid"); - - b.Property("UnitPrice") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.HasIndex("BrewerId"); - - b.HasIndex("BreweryId"); - - b.ToTable("Beers", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Brewers", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Breweries", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => - { - b.HasOne("Brewery.Domain.Entities.Brewer", null) - .WithMany("Beers") - .HasForeignKey("BrewerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Brewery.Domain.Entities.Brewery", null) - .WithMany("Beers") - .HasForeignKey("BreweryId"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.Navigation("Beers"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.Navigation("Beers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.cs b/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.cs deleted file mode 100644 index d49e866..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241112230230_EditBrewery.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - /// - public partial class EditBrewery : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Breweries_Brewers_BrewerId", - schema: "brewery", - table: "Breweries"); - - migrationBuilder.DropIndex( - name: "IX_Breweries_BrewerId", - schema: "brewery", - table: "Breweries"); - - migrationBuilder.DropColumn( - name: "BrewerId", - schema: "brewery", - table: "Breweries"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "BrewerId", - schema: "brewery", - table: "Breweries", - type: "uuid", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); - - migrationBuilder.CreateIndex( - name: "IX_Breweries_BrewerId", - schema: "brewery", - table: "Breweries", - column: "BrewerId"); - - migrationBuilder.AddForeignKey( - name: "FK_Breweries_Brewers_BrewerId", - schema: "brewery", - table: "Breweries", - column: "BrewerId", - principalSchema: "brewery", - principalTable: "Brewers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.Designer.cs deleted file mode 100644 index 3d4773c..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.Designer.cs +++ /dev/null @@ -1,112 +0,0 @@ -// -using System; -using Brewery.Infrastructure.EF; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - [DbContext(typeof(BreweryDbContext))] - [Migration("20241112233804_EditBeer")] - partial class EditBeer - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("brewery") - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BrewerId") - .HasColumnType("uuid"); - - b.Property("BreweryId") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("UnitPrice") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.HasIndex("BrewerId"); - - b.HasIndex("BreweryId"); - - b.ToTable("Beers", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Brewers", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Breweries", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => - { - b.HasOne("Brewery.Domain.Entities.Brewer", null) - .WithMany("Beers") - .HasForeignKey("BrewerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Brewery.Domain.Entities.Brewery", null) - .WithMany("Beers") - .HasForeignKey("BreweryId"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.Navigation("Beers"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.Navigation("Beers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.Designer.cs deleted file mode 100644 index 341b04a..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.Designer.cs +++ /dev/null @@ -1,98 +0,0 @@ -// -using System; -using Brewery.Infrastructure.EF; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - [DbContext(typeof(BreweryDbContext))] - [Migration("20241113144018_EditBeer,Brewer")] - partial class EditBeerBrewer - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("brewery") - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BrewerId") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("UnitPrice") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.HasIndex("BrewerId"); - - b.ToTable("Beers", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Brewers", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Breweries", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => - { - b.HasOne("Brewery.Domain.Entities.Brewer", null) - .WithMany("Beers") - .HasForeignKey("BrewerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.Navigation("Beers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.cs deleted file mode 100644 index 88eca26..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241113144018_EditBeer,Brewer.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - /// - public partial class EditBeerBrewer : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Beers_Breweries_BreweryId", - schema: "brewery", - table: "Beers"); - - migrationBuilder.DropIndex( - name: "IX_Beers_BreweryId", - schema: "brewery", - table: "Beers"); - - migrationBuilder.DropColumn( - name: "BreweryId", - schema: "brewery", - table: "Beers"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "BreweryId", - schema: "brewery", - table: "Beers", - type: "uuid", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_Beers_BreweryId", - schema: "brewery", - table: "Beers", - column: "BreweryId"); - - migrationBuilder.AddForeignKey( - name: "FK_Beers_Breweries_BreweryId", - schema: "brewery", - table: "Beers", - column: "BreweryId", - principalSchema: "brewery", - principalTable: "Breweries", - principalColumn: "Id"); - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.Designer.cs deleted file mode 100644 index 05e7c59..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.Designer.cs +++ /dev/null @@ -1,115 +0,0 @@ -// -using System; -using Brewery.Infrastructure.EF; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - [DbContext(typeof(BreweryDbContext))] - [Migration("20241113185859_EditBrewer,Brewery")] - partial class EditBrewerBrewery - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasDefaultSchema("brewery") - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BrewerId") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("UnitPrice") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.HasIndex("BrewerId"); - - b.ToTable("Beers", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BreweryId") - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("BreweryId"); - - b.ToTable("Brewers", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("Breweries", "brewery"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => - { - b.HasOne("Brewery.Domain.Entities.Brewer", null) - .WithMany("Beers") - .HasForeignKey("BrewerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.HasOne("Brewery.Domain.Entities.Brewery", null) - .WithMany("Brewers") - .HasForeignKey("BreweryId"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => - { - b.Navigation("Beers"); - }); - - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => - { - b.Navigation("Brewers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.cs b/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.cs deleted file mode 100644 index fb7eb10..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241113185859_EditBrewer,Brewery.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - /// - public partial class EditBrewerBrewery : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "BreweryId", - schema: "brewery", - table: "Brewers", - type: "uuid", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_Brewers_BreweryId", - schema: "brewery", - table: "Brewers", - column: "BreweryId"); - - migrationBuilder.AddForeignKey( - name: "FK_Brewers_Breweries_BreweryId", - schema: "brewery", - table: "Brewers", - column: "BreweryId", - principalSchema: "brewery", - principalTable: "Breweries", - principalColumn: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Brewers_Breweries_BreweryId", - schema: "brewery", - table: "Brewers"); - - migrationBuilder.DropIndex( - name: "IX_Brewers_BreweryId", - schema: "brewery", - table: "Brewers"); - - migrationBuilder.DropColumn( - name: "BreweryId", - schema: "brewery", - table: "Brewers"); - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.cs deleted file mode 100644 index 1b74003..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - /// - public partial class AddWholesalerSale : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Wholesalers", - schema: "brewery", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Wholesalers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Sales", - schema: "brewery", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - BeerId = table.Column(type: "uuid", nullable: false), - Quantity = table.Column(type: "integer", nullable: false), - WholesalerId = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Sales", x => x.Id); - table.ForeignKey( - name: "FK_Sales_Wholesalers_WholesalerId", - column: x => x.WholesalerId, - principalSchema: "brewery", - principalTable: "Wholesalers", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_Sales_WholesalerId", - schema: "brewery", - table: "Sales", - column: "WholesalerId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Sales", - schema: "brewery"); - - migrationBuilder.DropTable( - name: "Wholesalers", - schema: "brewery"); - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.cs deleted file mode 100644 index 679d928..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - /// - public partial class AddBeerStock : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Sales", - schema: "brewery"); - - migrationBuilder.CreateTable( - name: "BeerSales", - schema: "brewery", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - BeerId = table.Column(type: "uuid", nullable: false), - Quantity = table.Column(type: "integer", nullable: false), - WholesalerId = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_BeerSales", x => x.Id); - table.ForeignKey( - name: "FK_BeerSales_Wholesalers_WholesalerId", - column: x => x.WholesalerId, - principalSchema: "brewery", - principalTable: "Wholesalers", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "BeerStocks", - schema: "brewery", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - BeerId = table.Column(type: "uuid", nullable: false), - Quantity = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_BeerStocks", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_BeerSales_WholesalerId", - schema: "brewery", - table: "BeerSales", - column: "WholesalerId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "BeerSales", - schema: "brewery"); - - migrationBuilder.DropTable( - name: "BeerStocks", - schema: "brewery"); - - migrationBuilder.CreateTable( - name: "Sales", - schema: "brewery", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - BeerId = table.Column(type: "uuid", nullable: false), - Quantity = table.Column(type: "integer", nullable: false), - WholesalerId = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Sales", x => x.Id); - table.ForeignKey( - name: "FK_Sales_Wholesalers_WholesalerId", - column: x => x.WholesalerId, - principalSchema: "brewery", - principalTable: "Wholesalers", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_Sales_WholesalerId", - schema: "brewery", - table: "Sales", - column: "WholesalerId"); - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.cs deleted file mode 100644 index 2d9fd2a..0000000 --- a/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Brewery.Infrastructure.EF.Migrations -{ - /// - public partial class EditBrewer : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "BrewerId", - schema: "brewery", - table: "BeerStocks", - type: "uuid", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_BeerStocks_BrewerId", - schema: "brewery", - table: "BeerStocks", - column: "BrewerId"); - - migrationBuilder.AddForeignKey( - name: "FK_BeerStocks_Brewers_BrewerId", - schema: "brewery", - table: "BeerStocks", - column: "BrewerId", - principalSchema: "brewery", - principalTable: "Brewers", - principalColumn: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_BeerStocks_Brewers_BrewerId", - schema: "brewery", - table: "BeerStocks"); - - migrationBuilder.DropIndex( - name: "IX_BeerStocks_BrewerId", - schema: "brewery", - table: "BeerStocks"); - - migrationBuilder.DropColumn( - name: "BrewerId", - schema: "brewery", - table: "BeerStocks"); - } - } -} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.Designer.cs similarity index 95% rename from src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.Designer.cs rename to src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.Designer.cs index 37190ac..08cf86d 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/20241115200807_EditBrewer.Designer.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.Designer.cs @@ -12,8 +12,8 @@ namespace Brewery.Infrastructure.EF.Migrations { [DbContext(typeof(BreweryDbContext))] - [Migration("20241115200807_EditBrewer")] - partial class EditBrewer + [Migration("20241115202730_EditBeerToRemoveUnitPrice")] + partial class EditBeerToRemoveUnitPrice { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -39,9 +39,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("UnitPrice") - .HasColumnType("numeric"); - b.HasKey("Id"); b.HasIndex("BrewerId"); @@ -80,12 +77,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("BeerId") .HasColumnType("uuid"); - b.Property("BrewerId") + b.Property("BrewerId") .HasColumnType("uuid"); b.Property("Quantity") .HasColumnType("integer"); + b.Property("UnitPrice") + .HasColumnType("numeric"); + b.HasKey("Id"); b.HasIndex("BrewerId"); @@ -163,7 +163,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.HasOne("Brewery.Domain.Entities.Brewer", null) .WithMany("BeerStocks") - .HasForeignKey("BrewerId"); + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.cs b/src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.cs similarity index 55% rename from src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.cs rename to src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.cs index 657f0b2..187cd50 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/20241111205505_AddBeer,Brewery,Brewer,Wholesaler.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/20241115202730_EditBeerToRemoveUnitPrice.cs @@ -6,7 +6,7 @@ namespace Brewery.Infrastructure.EF.Migrations { /// - public partial class AddBeerBreweryBrewerWholesaler : Migration + public partial class EditBeerToRemoveUnitPrice : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -14,38 +14,71 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.EnsureSchema( name: "brewery"); + migrationBuilder.CreateTable( + name: "Breweries", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Breweries", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Wholesalers", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Wholesalers", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Brewers", schema: "brewery", columns: table => new { Id = table.Column(type: "uuid", nullable: false), + BreweryId = table.Column(type: "uuid", nullable: true), Name = table.Column(type: "text", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Brewers", x => x.Id); + table.ForeignKey( + name: "FK_Brewers_Breweries_BreweryId", + column: x => x.BreweryId, + principalSchema: "brewery", + principalTable: "Breweries", + principalColumn: "Id"); }); migrationBuilder.CreateTable( - name: "Breweries", + name: "BeerSales", schema: "brewery", columns: table => new { Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "text", nullable: false), - BrewerId = table.Column(type: "uuid", nullable: false) + BeerId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + WholesalerId = table.Column(type: "uuid", nullable: true) }, constraints: table => { - table.PrimaryKey("PK_Breweries", x => x.Id); + table.PrimaryKey("PK_BeerSales", x => x.Id); table.ForeignKey( - name: "FK_Breweries_Brewers_BrewerId", - column: x => x.BrewerId, + name: "FK_BeerSales_Wholesalers_WholesalerId", + column: x => x.WholesalerId, principalSchema: "brewery", - principalTable: "Brewers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); + principalTable: "Wholesalers", + principalColumn: "Id"); }); migrationBuilder.CreateTable( @@ -55,20 +88,36 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "uuid", nullable: false), BrewerId = table.Column(type: "uuid", nullable: false), - UnitPrice = table.Column(type: "numeric", nullable: false), - BreweryId = table.Column(type: "uuid", nullable: true) + Name = table.Column(type: "text", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Beers", x => x.Id); table.ForeignKey( - name: "FK_Beers_Breweries_BreweryId", - column: x => x.BreweryId, + name: "FK_Beers_Brewers_BrewerId", + column: x => x.BrewerId, principalSchema: "brewery", - principalTable: "Breweries", - principalColumn: "Id"); + principalTable: "Brewers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BeerStocks", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BeerId = table.Column(type: "uuid", nullable: false), + BrewerId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + UnitPrice = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeerStocks", x => x.Id); table.ForeignKey( - name: "FK_Beers_Brewers_BrewerId", + name: "FK_BeerStocks_Brewers_BrewerId", column: x => x.BrewerId, principalSchema: "brewery", principalTable: "Brewers", @@ -83,16 +132,22 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "BrewerId"); migrationBuilder.CreateIndex( - name: "IX_Beers_BreweryId", + name: "IX_BeerSales_WholesalerId", schema: "brewery", - table: "Beers", - column: "BreweryId"); + table: "BeerSales", + column: "WholesalerId"); migrationBuilder.CreateIndex( - name: "IX_Breweries_BrewerId", + name: "IX_BeerStocks_BrewerId", schema: "brewery", - table: "Breweries", + table: "BeerStocks", column: "BrewerId"); + + migrationBuilder.CreateIndex( + name: "IX_Brewers_BreweryId", + schema: "brewery", + table: "Brewers", + column: "BreweryId"); } /// @@ -103,12 +158,24 @@ protected override void Down(MigrationBuilder migrationBuilder) schema: "brewery"); migrationBuilder.DropTable( - name: "Breweries", + name: "BeerSales", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "BeerStocks", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "Wholesalers", schema: "brewery"); migrationBuilder.DropTable( name: "Brewers", schema: "brewery"); + + migrationBuilder.DropTable( + name: "Breweries", + schema: "brewery"); } } } diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.Designer.cs similarity index 68% rename from src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.Designer.cs rename to src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.Designer.cs index dfed9e4..98b0e00 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/20241115200357_AddBeerStock.Designer.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.Designer.cs @@ -12,8 +12,8 @@ namespace Brewery.Infrastructure.EF.Migrations { [DbContext(typeof(BreweryDbContext))] - [Migration("20241115200357_AddBeerStock")] - partial class AddBeerStock + [Migration("20241116163101_AddBeerOrder,BeerQuote")] + partial class AddBeerOrderBeerQuote { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -39,9 +39,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("UnitPrice") - .HasColumnType("numeric"); - b.HasKey("Id"); b.HasIndex("BrewerId"); @@ -49,6 +46,48 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Beers", "brewery"); }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.Property("BeerId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerQuoteId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("BeerId"); + + b.HasIndex("BeerQuoteId"); + + b.ToTable("BeerOrder", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DiscountInPercent") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("BeerQuotes", "brewery"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => { b.Property("Id") @@ -61,7 +100,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Quantity") .HasColumnType("integer"); - b.Property("WholesalerId") + b.Property("WholesalerId") .HasColumnType("uuid"); b.HasKey("Id"); @@ -80,11 +119,19 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("BeerId") .HasColumnType("uuid"); + b.Property("BrewerId") + .HasColumnType("uuid"); + b.Property("Quantity") .HasColumnType("integer"); + b.Property("UnitPrice") + .HasColumnType("numeric"); + b.HasKey("Id"); + b.HasIndex("BrewerId"); + b.ToTable("BeerStocks", "brewery"); }); @@ -147,11 +194,29 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.HasOne("Brewery.Domain.Entities.BeerQuote", null) + .WithMany("BeerOrders") + .HasForeignKey("BeerQuoteId"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => { b.HasOne("Brewery.Domain.Entities.Wholesaler", null) .WithMany("BeerSales") - .HasForeignKey("WholesalerId"); + .HasForeignKey("WholesalerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("BeerStocks") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => @@ -161,8 +226,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasForeignKey("BreweryId"); }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Navigation("BeerOrders"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => { + b.Navigation("BeerStocks"); + b.Navigation("Beers"); }); diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.cs b/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.cs new file mode 100644 index 0000000..dcd9c80 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241116163101_AddBeerOrder,BeerQuote.cs @@ -0,0 +1,118 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class AddBeerOrderBeerQuote : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BeerSales_Wholesalers_WholesalerId", + schema: "brewery", + table: "BeerSales"); + + migrationBuilder.AlterColumn( + name: "WholesalerId", + schema: "brewery", + table: "BeerSales", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "BeerQuotes", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + DiscountInPercent = table.Column(type: "integer", nullable: false), + Total = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeerQuotes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "BeerOrder", + schema: "brewery", + columns: table => new + { + BeerId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + UnitPrice = table.Column(type: "numeric", nullable: false), + Total = table.Column(type: "numeric", nullable: false), + BeerQuoteId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BeerOrder", x => x.BeerId); + table.ForeignKey( + name: "FK_BeerOrder_BeerQuotes_BeerQuoteId", + column: x => x.BeerQuoteId, + principalSchema: "brewery", + principalTable: "BeerQuotes", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_BeerOrder_BeerQuoteId", + schema: "brewery", + table: "BeerOrder", + column: "BeerQuoteId"); + + migrationBuilder.AddForeignKey( + name: "FK_BeerSales_Wholesalers_WholesalerId", + schema: "brewery", + table: "BeerSales", + column: "WholesalerId", + principalSchema: "brewery", + principalTable: "Wholesalers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BeerSales_Wholesalers_WholesalerId", + schema: "brewery", + table: "BeerSales"); + + migrationBuilder.DropTable( + name: "BeerOrder", + schema: "brewery"); + + migrationBuilder.DropTable( + name: "BeerQuotes", + schema: "brewery"); + + migrationBuilder.AlterColumn( + name: "WholesalerId", + schema: "brewery", + table: "BeerSales", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddForeignKey( + name: "FK_BeerSales_Wholesalers_WholesalerId", + schema: "brewery", + table: "BeerSales", + column: "WholesalerId", + principalSchema: "brewery", + principalTable: "Wholesalers", + principalColumn: "Id"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.Designer.cs similarity index 57% rename from src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.Designer.cs rename to src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.Designer.cs index 656b804..4abd787 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/20241115144513_AddWholesaler,Sale.Designer.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.Designer.cs @@ -12,8 +12,8 @@ namespace Brewery.Infrastructure.EF.Migrations { [DbContext(typeof(BreweryDbContext))] - [Migration("20241115144513_AddWholesaler,Sale")] - partial class AddWholesalerSale + [Migration("20241116175232_EditBeerSaleToAddUnitPrice")] + partial class EditBeerSaleToAddUnitPrice { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -39,9 +39,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("UnitPrice") - .HasColumnType("numeric"); - b.HasKey("Id"); b.HasIndex("BrewerId"); @@ -49,42 +46,49 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Beers", "brewery"); }); - modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => { - b.Property("Id") + b.Property("BeerId") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("BreweryId") + b.Property("BeerQuoteId") .HasColumnType("uuid"); - b.Property("Name") - .IsRequired() - .HasColumnType("text"); + b.Property("Quantity") + .HasColumnType("integer"); - b.HasKey("Id"); + b.Property("Total") + .HasColumnType("numeric"); - b.HasIndex("BreweryId"); + b.Property("UnitPrice") + .HasColumnType("numeric"); - b.ToTable("Brewers", "brewery"); + b.HasKey("BeerId"); + + b.HasIndex("BeerQuoteId"); + + b.ToTable("BeerOrder", "brewery"); }); - modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("Name") - .IsRequired() - .HasColumnType("text"); + b.Property("DiscountInPercent") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); b.HasKey("Id"); - b.ToTable("Breweries", "brewery"); + b.ToTable("BeerQuotes", "brewery"); }); - modelBuilder.Entity("Brewery.Domain.Entities.Sale", b => + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -96,14 +100,77 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Quantity") .HasColumnType("integer"); - b.Property("WholesalerId") + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("WholesalerId") .HasColumnType("uuid"); b.HasKey("Id"); b.HasIndex("WholesalerId"); - b.ToTable("Sales", "brewery"); + b.ToTable("BeerSales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("BeerStocks", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); }); modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => @@ -130,6 +197,31 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.HasOne("Brewery.Domain.Entities.BeerQuote", null) + .WithMany("BeerOrders") + .HasForeignKey("BeerQuoteId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("BeerSales") + .HasForeignKey("WholesalerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("BeerStocks") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => { b.HasOne("Brewery.Domain.Entities.Brewery", null) @@ -137,15 +229,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasForeignKey("BreweryId"); }); - modelBuilder.Entity("Brewery.Domain.Entities.Sale", b => + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => { - b.HasOne("Brewery.Domain.Entities.Wholesaler", null) - .WithMany("Beers") - .HasForeignKey("WholesalerId"); + b.Navigation("BeerOrders"); }); modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => { + b.Navigation("BeerStocks"); + b.Navigation("Beers"); }); @@ -156,7 +248,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => { - b.Navigation("Beers"); + b.Navigation("BeerSales"); }); #pragma warning restore 612, 618 } diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.cs similarity index 62% rename from src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.cs rename to src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.cs index 58cf06b..89f313e 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/20241112233804_EditBeer.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/20241116175232_EditBeerSaleToAddUnitPrice.cs @@ -5,27 +5,27 @@ namespace Brewery.Infrastructure.EF.Migrations { /// - public partial class EditBeer : Migration + public partial class EditBeerSaleToAddUnitPrice : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AddColumn( - name: "Name", + migrationBuilder.AddColumn( + name: "UnitPrice", schema: "brewery", - table: "Beers", - type: "text", + table: "BeerSales", + type: "numeric", nullable: false, - defaultValue: ""); + defaultValue: 0m); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( - name: "Name", + name: "UnitPrice", schema: "brewery", - table: "Beers"); + table: "BeerSales"); } } } diff --git a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs index 2c46b9e..44f3fab 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs @@ -36,9 +36,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("UnitPrice") - .HasColumnType("numeric"); - b.HasKey("Id"); b.HasIndex("BrewerId"); @@ -46,6 +43,48 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Beers", "brewery"); }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.Property("BeerId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerQuoteId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("BeerId"); + + b.HasIndex("BeerQuoteId"); + + b.ToTable("BeerOrder", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DiscountInPercent") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("BeerQuotes", "brewery"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => { b.Property("Id") @@ -58,7 +97,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Quantity") .HasColumnType("integer"); - b.Property("WholesalerId") + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("WholesalerId") .HasColumnType("uuid"); b.HasKey("Id"); @@ -77,12 +119,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("BeerId") .HasColumnType("uuid"); - b.Property("BrewerId") + b.Property("BrewerId") .HasColumnType("uuid"); b.Property("Quantity") .HasColumnType("integer"); + b.Property("UnitPrice") + .HasColumnType("numeric"); + b.HasKey("Id"); b.HasIndex("BrewerId"); @@ -149,18 +194,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.HasOne("Brewery.Domain.Entities.BeerQuote", null) + .WithMany("BeerOrders") + .HasForeignKey("BeerQuoteId"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => { b.HasOne("Brewery.Domain.Entities.Wholesaler", null) .WithMany("BeerSales") - .HasForeignKey("WholesalerId"); + .HasForeignKey("WholesalerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => { b.HasOne("Brewery.Domain.Entities.Brewer", null) .WithMany("BeerStocks") - .HasForeignKey("BrewerId"); + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => @@ -170,6 +226,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("BreweryId"); }); + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Navigation("BeerOrders"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => { b.Navigation("BeerStocks"); diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerQuoteHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerQuoteHandler.cs new file mode 100644 index 0000000..525f9a5 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetBeerQuoteHandler.cs @@ -0,0 +1,28 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetBeerQuoteHandler : IQueryHandler +{ + private readonly DbSet _beerQuotes; + + public GetBeerQuoteHandler(BreweryDbContext dbContext) + { + _beerQuotes = dbContext.BeerQuotes; + } + public async Task QueryAsync(GetBeerQuote query) + { + var beerQuote = await _beerQuotes + .AsNoTracking() + .Include(b => b.BeerOrders) + .SingleOrDefaultAsync(b => b.Id == query.BeerQuoteId); + + return beerQuote is not null + ? beerQuote.AsDto() + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/BeerQuoteRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BeerQuoteRepository.cs new file mode 100644 index 0000000..2475a58 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/BeerQuoteRepository.cs @@ -0,0 +1,26 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class BeerQuoteRepository : IBeerQuoteRepository +{ + private readonly DbSet _beerQuotes; + private readonly BreweryDbContext _dbContext; + + public BeerQuoteRepository(BreweryDbContext dbContext) + { + _beerQuotes = dbContext.BeerQuotes; + _dbContext = dbContext; + } + + public async Task AddAsync(BeerQuote beerQuote) + { + await _beerQuotes.AddAsync(beerQuote); + await _dbContext.SaveChangesAsync(); + } + + public async Task GetBeerQuote(Guid id) + => await _beerQuotes.SingleOrDefaultAsync(beerQuote => beerQuote.Id == id); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/BeerSaleRepository.cs similarity index 55% rename from src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs rename to src/Brewery.Infrastructure/EF/Repositories/BeerSaleRepository.cs index 0611b9b..b56a201 100644 --- a/src/Brewery.Infrastructure/EF/Repositories/SaleRepository.cs +++ b/src/Brewery.Infrastructure/EF/Repositories/BeerSaleRepository.cs @@ -4,35 +4,35 @@ namespace Brewery.Infrastructure.EF.Repositories; -public class SaleRepository : ISaleRepository +public class BeerSaleRepository : IBeerSaleRepository { - private readonly DbSet _sales; + private readonly DbSet _beerSales; private readonly BreweryDbContext _dbContext; - public SaleRepository(BreweryDbContext dbContext) + public BeerSaleRepository(BreweryDbContext dbContext) { - _sales = _dbContext.BeerSales; + _beerSales = dbContext.BeerSales; _dbContext = dbContext; } public async Task AddAsync(BeerSale beerSale) { - await _sales.AddAsync(beerSale); + await _beerSales.AddAsync(beerSale); await _dbContext.SaveChangesAsync(); } public async Task UpdateAsync(BeerSale beerSale) { - _sales.Update(beerSale); + _beerSales.Update(beerSale); await _dbContext.SaveChangesAsync(); } public async Task DeleteAsync(BeerSale beerSale) { - _sales.Remove(beerSale); + _beerSales.Remove(beerSale); await _dbContext.SaveChangesAsync(); } - public Task GetSaleByBeerId(Guid beerId) - => _sales.SingleOrDefaultAsync(s => s.BeerId == beerId); + public async Task> BrowseByBeerId(Guid beerId) + => await _beerSales.Where(b => b.BeerId == beerId).ToListAsync(); } \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Services/AppInitializer.cs b/src/Brewery.Infrastructure/Services/AppInitializer.cs index 3aa00d7..877801e 100644 --- a/src/Brewery.Infrastructure/Services/AppInitializer.cs +++ b/src/Brewery.Infrastructure/Services/AppInitializer.cs @@ -25,4 +25,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await dbContext.Database.MigrateAsync(); } } + + private async Task SeedDatabase() + { + + } } \ No newline at end of file From d5f7de891231048d8e6f31ba7edb52c70c0e7ef3 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sun, 17 Nov 2024 15:20:43 +0000 Subject: [PATCH 14/24] add Brewery.Tests.EndToEnd.AddBeerTests --- Brewery.sln | 14 ++++ src/Brewery.Api/Program.cs | 18 ++-- src/Brewery.Api/appsettings.test.json | 11 +++ src/Brewery.Application/Commands/AddBeer.cs | 4 +- src/Brewery.Application/Commands/AddBrewer.cs | 2 +- .../Commands/Handlers/AddBeerHandler.cs | 2 +- .../Commands/Handlers/AddBrewerHandler.cs | 1 - src/Brewery.Domain/Entities/Beer.cs | 2 +- src/Brewery.Infrastructure/Extensions.cs | 2 +- .../Services/AppInitializer.cs | 11 ++- .../Brewery.Tests.EndToEnd.csproj | 27 ++++++ .../Sync/AddBeerTests.cs | 83 +++++++++++++++++++ .../Sync/AddBrewerTests.cs | 44 ++++++++++ .../Brewery.Tests.Shared.csproj | 29 +++++++ .../Factories/BreweryAppFactory.cs | 12 +++ .../Fixtures/BreweryDbFixture.cs | 43 ++++++++++ .../Helpers/OptionsHelper.cs | 23 +++++ 17 files changed, 314 insertions(+), 14 deletions(-) create mode 100644 src/Brewery.Api/appsettings.test.json create mode 100644 tests/Brewery.Tests.EndToEnd/Brewery.Tests.EndToEnd.csproj create mode 100644 tests/Brewery.Tests.EndToEnd/Sync/AddBeerTests.cs create mode 100644 tests/Brewery.Tests.EndToEnd/Sync/AddBrewerTests.cs create mode 100644 tests/Brewery.Tests.Shared/Brewery.Tests.Shared.csproj create mode 100644 tests/Brewery.Tests.Shared/Factories/BreweryAppFactory.cs create mode 100644 tests/Brewery.Tests.Shared/Fixtures/BreweryDbFixture.cs create mode 100644 tests/Brewery.Tests.Shared/Helpers/OptionsHelper.cs diff --git a/Brewery.sln b/Brewery.sln index 9cba4fb..ccd6546 100644 --- a/Brewery.sln +++ b/Brewery.sln @@ -14,6 +14,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Infrastructure", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Abstractions", "src\Brewery.Abstractions\Brewery.Abstractions.csproj", "{AD694077-201B-49C3-9537-FE96BA598E82}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Tests.Shared", "tests\Brewery.Tests.Shared\Brewery.Tests.Shared.csproj", "{8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Brewery.Tests.EndToEnd", "tests\Brewery.Tests.EndToEnd\Brewery.Tests.EndToEnd.csproj", "{667A8402-FC19-406C-9BCA-4D194D1AD257}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,6 +29,8 @@ Global {6DA2F545-AD92-4EF3-8638-D42F1F8C5833} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} {F0FFE48E-371C-423C-9DB7-22BF810F6AA4} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} {AD694077-201B-49C3-9537-FE96BA598E82} = {13BABC4E-2013-45BE-B5F2-37374813BFF3} + {8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60} = {FD1F28D7-23D0-4225-814C-549E308E4C9D} + {667A8402-FC19-406C-9BCA-4D194D1AD257} = {FD1F28D7-23D0-4225-814C-549E308E4C9D} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {CE3C015E-3E32-4BB5-9F0B-1BD36FAF8DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -47,5 +53,13 @@ Global {AD694077-201B-49C3-9537-FE96BA598E82}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD694077-201B-49C3-9537-FE96BA598E82}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD694077-201B-49C3-9537-FE96BA598E82}.Release|Any CPU.Build.0 = Release|Any CPU + {8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EDE4ED9-E7AB-4DAB-983D-B2870AED0C60}.Release|Any CPU.Build.0 = Release|Any CPU + {667A8402-FC19-406C-9BCA-4D194D1AD257}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {667A8402-FC19-406C-9BCA-4D194D1AD257}.Debug|Any CPU.Build.0 = Debug|Any CPU + {667A8402-FC19-406C-9BCA-4D194D1AD257}.Release|Any CPU.ActiveCfg = Release|Any CPU + {667A8402-FC19-406C-9BCA-4D194D1AD257}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Brewery.Api/Program.cs b/src/Brewery.Api/Program.cs index 0cb5116..b02b9e3 100644 --- a/src/Brewery.Api/Program.cs +++ b/src/Brewery.Api/Program.cs @@ -1,11 +1,17 @@ using Brewery.Api; using Brewery.Infrastructure; -var builder = WebApplication.CreateBuilder(args); -var assemblies = AssemblyLoader.GetAssemblies(); -builder.Services.AddInfrastructure(assemblies); +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + var assemblies = AssemblyLoader.GetAssemblies(); + builder.Services.AddInfrastructure(assemblies); -var app = builder.Build(); -app.UseInfrastructure(); + var app = builder.Build(); + app.UseInfrastructure(); -app.Run(); + app.Run(); + } +} \ No newline at end of file diff --git a/src/Brewery.Api/appsettings.test.json b/src/Brewery.Api/appsettings.test.json new file mode 100644 index 0000000..60ac416 --- /dev/null +++ b/src/Brewery.Api/appsettings.test.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "postgres": { + "connectionString": "Host=localhost;Database=Brewery-test;Username=postgres;Password=czcz" + } +} diff --git a/src/Brewery.Application/Commands/AddBeer.cs b/src/Brewery.Application/Commands/AddBeer.cs index 5aa53e2..98a3583 100644 --- a/src/Brewery.Application/Commands/AddBeer.cs +++ b/src/Brewery.Application/Commands/AddBeer.cs @@ -2,7 +2,7 @@ namespace Brewery.Application.Commands; -public record AddBeer(Guid BrewerId, string Name, decimal UnitPrice) : ICommand +public record AddBeer(Guid BrewerId, string Name) : ICommand { - public Guid Id { get; } = Guid.NewGuid(); + public Guid Id { get; set; } = Guid.NewGuid(); } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/AddBrewer.cs b/src/Brewery.Application/Commands/AddBrewer.cs index a064ea2..c814f5a 100644 --- a/src/Brewery.Application/Commands/AddBrewer.cs +++ b/src/Brewery.Application/Commands/AddBrewer.cs @@ -2,7 +2,7 @@ namespace Brewery.Application.Commands; -public record AddBrewer(string Name, Guid BreweryId = default) : ICommand +public record AddBrewer(string Name, Guid BreweryId) : ICommand { public Guid Id { get; } = Guid.NewGuid(); } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs index dd0d517..30400e1 100644 --- a/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/AddBeerHandler.cs @@ -31,7 +31,7 @@ public async Task HandleAsync(AddBeer command) throw new BeerAlreadyExistException(command.Id); } - beer = Beer.Create(command.Id, brewer.Id, command.Name, command.UnitPrice); + beer = Beer.Create(command.Id, brewer.Id, command.Name); await _beerRepository.AddAsync(beer); } diff --git a/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs index cabb020..3d94479 100644 --- a/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/AddBrewerHandler.cs @@ -36,7 +36,6 @@ public async Task HandleAsync(AddBrewer command) brewer.ChangeBreweryId(brewery.Id); } - await _brewerRepository.AddBrewer(brewer); } diff --git a/src/Brewery.Domain/Entities/Beer.cs b/src/Brewery.Domain/Entities/Beer.cs index 6423620..6b50218 100644 --- a/src/Brewery.Domain/Entities/Beer.cs +++ b/src/Brewery.Domain/Entities/Beer.cs @@ -20,7 +20,7 @@ public void ChangeName(string name) Name = name; } - public static Beer Create(Guid id, Guid brewerId, string name, decimal unitPrice) + public static Beer Create(Guid id, Guid brewerId, string name) { var beer = new Beer(id, brewerId); beer.ChangeName(name); diff --git a/src/Brewery.Infrastructure/Extensions.cs b/src/Brewery.Infrastructure/Extensions.cs index fb9c6b2..b463985 100644 --- a/src/Brewery.Infrastructure/Extensions.cs +++ b/src/Brewery.Infrastructure/Extensions.cs @@ -29,7 +29,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) { - app.UseExceptionHandling(); + //app.UseExceptionHandling(); app.UseRouting(); app.UseEndpoints(e => { diff --git a/src/Brewery.Infrastructure/Services/AppInitializer.cs b/src/Brewery.Infrastructure/Services/AppInitializer.cs index 877801e..39459fa 100644 --- a/src/Brewery.Infrastructure/Services/AppInitializer.cs +++ b/src/Brewery.Infrastructure/Services/AppInitializer.cs @@ -7,17 +7,26 @@ namespace Brewery.Infrastructure.Services; public class AppInitializer : BackgroundService { private readonly IServiceProvider _serviceProvider; + private readonly IHostEnvironment _hostEnvironment; - public AppInitializer(IServiceProvider serviceProvider) + public AppInitializer(IServiceProvider serviceProvider, + IHostEnvironment hostEnvironment) { _serviceProvider = serviceProvider; + _hostEnvironment = hostEnvironment; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + if (_hostEnvironment.IsEnvironment("test")) + { + return; + } + var dbContextTypes = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(t => t.GetTypes()) .Where(t => typeof(DbContext).IsAssignableFrom(t) && !t.IsInterface && t != typeof(DbContext)); + using var scope = _serviceProvider.CreateScope(); foreach (var dbType in dbContextTypes) { diff --git a/tests/Brewery.Tests.EndToEnd/Brewery.Tests.EndToEnd.csproj b/tests/Brewery.Tests.EndToEnd/Brewery.Tests.EndToEnd.csproj new file mode 100644 index 0000000..e551d75 --- /dev/null +++ b/tests/Brewery.Tests.EndToEnd/Brewery.Tests.EndToEnd.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/tests/Brewery.Tests.EndToEnd/Sync/AddBeerTests.cs b/tests/Brewery.Tests.EndToEnd/Sync/AddBeerTests.cs new file mode 100644 index 0000000..8998688 --- /dev/null +++ b/tests/Brewery.Tests.EndToEnd/Sync/AddBeerTests.cs @@ -0,0 +1,83 @@ +using System.Net; +using System.Text; +using Brewery.Application.Commands; +using Brewery.Domain.Entities; +using Brewery.Tests.Shared.Factories; +using Brewery.Tests.Shared.Fixtures; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using Shouldly; + +namespace Brewery.Tests.EndToEnd.Sync; + +[Collection($"BreweryDbFixture")] +public class AddBeerTests : IClassFixture, IClassFixture +{ + private Task Act() => _httpClient.PostAsync("Beer", GetPayload(_command)); + + [Fact] + public async Task add_beer_endpoint_should_return_status_code_created_at_action() + { + await SeedDatabase(); + + var response = await Act(); + + response.ShouldNotBeNull(); + response.StatusCode.ShouldBe(HttpStatusCode.Created); + } + + [Fact] + public async Task add_beer_endpoint_should_return_location_header_with_correct_beer_id() + { + await SeedDatabase(); + + var response = await Act(); + + var locationHeader = response.Headers + .FirstOrDefault(h => h.Key == "Location") + .Value.First(); + locationHeader.ShouldNotBeNull(); + locationHeader.ShouldBe($"http://localhost/Beer/{_command.Id}"); + } + + [Fact] + public async Task add_beer_endpoint_should_add_beer_entity_with_given_id_to_database() + { + await SeedDatabase(); + + var response = await Act(); + + var beerFromDb = await _breweryDbFixture.BreweryDbContext.Beers.SingleOrDefaultAsync(b => b.Id == _beerId); + beerFromDb.ShouldNotBeNull(); + beerFromDb.Id.ShouldBe(_command.Id); + } + + private readonly HttpClient _httpClient; + private readonly BreweryDbFixture _breweryDbFixture; + private readonly Guid _beerId; + private readonly Guid _brewerId; + private readonly AddBeer _command; + + public AddBeerTests(BreweryAppFactory factory, BreweryDbFixture breweryDbFixture) + { + factory.Server.AllowSynchronousIO = true; + _httpClient = factory.CreateClient(); + _breweryDbFixture = breweryDbFixture; + _beerId = Guid.NewGuid(); + _brewerId = Guid.NewGuid(); + _command = new AddBeer(_brewerId, "beer 1") { Id = _beerId }; + } + + private StringContent GetPayload(AddBeer command) + { + var json = JsonConvert.SerializeObject(command); + return new StringContent(json, Encoding.UTF8, "application/json"); + } + + private async Task SeedDatabase() + { + var brewer = Brewer.Create(_brewerId, "brewer 1"); + await _breweryDbFixture.BreweryDbContext.Brewers.AddAsync(brewer); + await _breweryDbFixture.BreweryDbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/tests/Brewery.Tests.EndToEnd/Sync/AddBrewerTests.cs b/tests/Brewery.Tests.EndToEnd/Sync/AddBrewerTests.cs new file mode 100644 index 0000000..2bcb714 --- /dev/null +++ b/tests/Brewery.Tests.EndToEnd/Sync/AddBrewerTests.cs @@ -0,0 +1,44 @@ +using System.Net; +using System.Text; +using Brewery.Application.Commands; +using Brewery.Tests.Shared.Factories; +using Brewery.Tests.Shared.Fixtures; +using Newtonsoft.Json; +using Shouldly; + +namespace Brewery.Tests.EndToEnd.Sync; + +[Collection($"BreweryDbFixture")] +public class AddBrewerTests : IClassFixture, IClassFixture +{ + private async Task Act() => await _client.PostAsync("brewer", GetPayload()); + + [Fact] + public async Task add_brewer_endpoint_should_return_status_code_created_at_action() + { + await _breweryDbFixture.SeedDatabaseAsync(_breweryId); + + var response = await Act(); + + response.ShouldNotBeNull(); + response.StatusCode.ShouldBe(HttpStatusCode.Created); + } + + private readonly HttpClient _client; + private readonly BreweryDbFixture _breweryDbFixture; + private readonly Guid _breweryId; + + public AddBrewerTests(BreweryAppFactory factory, BreweryDbFixture breweryDbFixture) + { + factory.Server.AllowSynchronousIO = true; + _client = factory.CreateClient(); + _breweryDbFixture = breweryDbFixture; + _breweryId = Guid.NewGuid(); + } + + private StringContent GetPayload() + { + var json = JsonConvert.SerializeObject(new AddBrewer("brewer 1", _breweryId)); + return new StringContent(json, Encoding.UTF8, "application/json"); + } +} \ No newline at end of file diff --git a/tests/Brewery.Tests.Shared/Brewery.Tests.Shared.csproj b/tests/Brewery.Tests.Shared/Brewery.Tests.Shared.csproj new file mode 100644 index 0000000..8d26f67 --- /dev/null +++ b/tests/Brewery.Tests.Shared/Brewery.Tests.Shared.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Brewery.Tests.Shared/Factories/BreweryAppFactory.cs b/tests/Brewery.Tests.Shared/Factories/BreweryAppFactory.cs new file mode 100644 index 0000000..759ace3 --- /dev/null +++ b/tests/Brewery.Tests.Shared/Factories/BreweryAppFactory.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; + +namespace Brewery.Tests.Shared.Factories; + +public class BreweryAppFactory : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("test"); + } +} \ No newline at end of file diff --git a/tests/Brewery.Tests.Shared/Fixtures/BreweryDbFixture.cs b/tests/Brewery.Tests.Shared/Fixtures/BreweryDbFixture.cs new file mode 100644 index 0000000..95b2915 --- /dev/null +++ b/tests/Brewery.Tests.Shared/Fixtures/BreweryDbFixture.cs @@ -0,0 +1,43 @@ +using Brewery.Abstractions.Postgres; +using Brewery.Infrastructure.EF; +using Brewery.Tests.Shared.Helpers; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Tests.Shared.Fixtures; + +public class BreweryDbFixture : IAsyncLifetime +{ + public BreweryDbContext BreweryDbContext { get; private set; } + + public BreweryDbFixture() + { + + } + + public Task InitializeAsync() + { + var postgresOptions = OptionsHelper.GetOptions("postgres"); + var dbContextOptions = new DbContextOptionsBuilder() + .UseNpgsql(postgresOptions.ConnectionString) + .Options; + BreweryDbContext = new BreweryDbContext(dbContextOptions); + + BreweryDbContext.Database.EnsureDeleted(); + BreweryDbContext.Database.EnsureCreated(); + + return Task.CompletedTask; + } + + public async Task DisposeAsync() + { + await BreweryDbContext.DisposeAsync(); + } + + public async Task SeedDatabaseAsync(Guid breweryId) + { + var brewery = Domain.Entities.Brewery.Create(breweryId, "Brewery 1"); + + await BreweryDbContext.AddAsync(brewery); + await BreweryDbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/tests/Brewery.Tests.Shared/Helpers/OptionsHelper.cs b/tests/Brewery.Tests.Shared/Helpers/OptionsHelper.cs new file mode 100644 index 0000000..d960ec0 --- /dev/null +++ b/tests/Brewery.Tests.Shared/Helpers/OptionsHelper.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Configuration; + +namespace Brewery.Tests.Shared.Helpers; + +public class OptionsHelper +{ + private const string AppSettings = "appsettings.test.json"; + public static TOptions GetOptions(string sectionName) where TOptions : new() + { + TOptions options = new TOptions(); + var configuration = GetConfiguration(); + var section = configuration.GetSection(sectionName); + section.Bind(options); + + return options; + } + + private static IConfigurationRoot GetConfiguration() + => new ConfigurationBuilder() + .AddJsonFile(AppSettings) + .AddEnvironmentVariables() + .Build(); +} \ No newline at end of file From c56a806600ce6aa443141143aa2370f1edd4b056 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sun, 17 Nov 2024 15:22:05 +0000 Subject: [PATCH 15/24] Create README.md --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ + From 778188cd89cefd7b3bf336c6b700833ad8371ea7 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sun, 17 Nov 2024 15:24:45 +0000 Subject: [PATCH 16/24] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d1c8b6..c6f623c 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ - +Overview +Brewery is a RESTful API that enables breweries, wholesalers, and clients to manage and interact with a beer distribution system. The system supports functionalities for managing beer inventory, facilitating sales between brewers and wholesalers, and providing clients with quotes for bulk beer orders. The application is built using .NET Core and Entity Framework, with a focus on efficient data management and clear REST API principles. From a46dbcda74324fcbedd7ef9669da9b36b69fb4b2 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sun, 17 Nov 2024 15:31:08 +0000 Subject: [PATCH 17/24] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c6f623c..cee78da 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -Overview +## Overview Brewery is a RESTful API that enables breweries, wholesalers, and clients to manage and interact with a beer distribution system. The system supports functionalities for managing beer inventory, facilitating sales between brewers and wholesalers, and providing clients with quotes for bulk beer orders. The application is built using .NET Core and Entity Framework, with a focus on efficient data management and clear REST API principles. From 7f9b8742008f2e4497d08cb6be2c96e4dc8effdb Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Tue, 19 Nov 2024 13:27:04 +0000 Subject: [PATCH 18/24] add Auth using jwt --- src/Brewery.Abstractions/Auth/IAuthManager.cs | 7 + src/Brewery.Abstractions/Auth/JsonWebToken.cs | 12 + src/Brewery.Abstractions/Commands/ICommand.cs | 4 +- .../Commands/ICommandHandler.cs | 5 + src/Brewery.Abstractions/Contexts/IContext.cs | 8 + .../Contexts/IContextFactory.cs | 6 + .../Contexts/IIdentityContext.cs | 11 + src/Brewery.Api/BreweryApi.rest | 38 ++- .../Controllers/AccountController.cs | 46 +++ .../Controllers/BreweryController.cs | 6 +- src/Brewery.Api/Program.cs | 4 + src/Brewery.Api/appsettings.json | 9 + .../Brewery.Application.csproj | 2 +- .../Commands/CreateAccount.cs | 6 + .../Commands/Handlers/AddBreweryHandler.cs | 12 + .../Commands/Handlers/CreateAccountHandler.cs | 40 +++ .../Commands/Handlers/SignInHandler.cs | 45 +++ src/Brewery.Application/Commands/SignIn.cs | 5 + src/Brewery.Application/DTO/AccountDto.cs | 9 + .../Exceptions/EmailInUseException.cs | 13 + src/Brewery.Application/Queries/GetUser.cs | 6 + src/Brewery.Domain/Entities/User.cs | 22 ++ .../Exceptions/InvalidCredentialsException.cs | 11 + .../Repositories/IUserRepository.cs | 10 + .../Auth/AuthManager.cs | 82 +++++ .../Auth/AuthOptions.cs | 12 + src/Brewery.Infrastructure/Auth/Extensions.cs | 53 ++++ .../Brewery.Infrastructure.csproj | 1 + .../Commands/Extensions.cs | 4 + .../Contexts/Context.cs | 30 ++ .../Contexts/ContextFactory.cs | 24 ++ .../Contexts/Extensions.cs | 17 ++ .../Contexts/IdentityContext.cs | 27 ++ .../EF/BreweryDbContext.cs | 1 + .../EF/Configuration/UserConfiguration.cs | 21 ++ src/Brewery.Infrastructure/EF/Extensions.cs | 1 + .../20241119092325_AddUser.Designer.cs | 286 ++++++++++++++++++ .../EF/Migrations/20241119092325_AddUser.cs | 40 +++ .../BreweryDbContextModelSnapshot.cs | 30 ++ .../EF/Queries/Handlers/GetUserHandler.cs | 33 ++ .../EF/Repositories/UserRepository.cs | 29 ++ src/Brewery.Infrastructure/Extensions.cs | 9 +- 42 files changed, 1023 insertions(+), 14 deletions(-) create mode 100644 src/Brewery.Abstractions/Auth/IAuthManager.cs create mode 100644 src/Brewery.Abstractions/Auth/JsonWebToken.cs create mode 100644 src/Brewery.Abstractions/Contexts/IContext.cs create mode 100644 src/Brewery.Abstractions/Contexts/IContextFactory.cs create mode 100644 src/Brewery.Abstractions/Contexts/IIdentityContext.cs create mode 100644 src/Brewery.Api/Controllers/AccountController.cs create mode 100644 src/Brewery.Application/Commands/CreateAccount.cs create mode 100644 src/Brewery.Application/Commands/Handlers/CreateAccountHandler.cs create mode 100644 src/Brewery.Application/Commands/Handlers/SignInHandler.cs create mode 100644 src/Brewery.Application/Commands/SignIn.cs create mode 100644 src/Brewery.Application/DTO/AccountDto.cs create mode 100644 src/Brewery.Application/Exceptions/EmailInUseException.cs create mode 100644 src/Brewery.Application/Queries/GetUser.cs create mode 100644 src/Brewery.Domain/Entities/User.cs create mode 100644 src/Brewery.Domain/Exceptions/InvalidCredentialsException.cs create mode 100644 src/Brewery.Domain/Repositories/IUserRepository.cs create mode 100644 src/Brewery.Infrastructure/Auth/AuthManager.cs create mode 100644 src/Brewery.Infrastructure/Auth/AuthOptions.cs create mode 100644 src/Brewery.Infrastructure/Auth/Extensions.cs create mode 100644 src/Brewery.Infrastructure/Contexts/Context.cs create mode 100644 src/Brewery.Infrastructure/Contexts/ContextFactory.cs create mode 100644 src/Brewery.Infrastructure/Contexts/Extensions.cs create mode 100644 src/Brewery.Infrastructure/Contexts/IdentityContext.cs create mode 100644 src/Brewery.Infrastructure/EF/Configuration/UserConfiguration.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.Designer.cs create mode 100644 src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.cs create mode 100644 src/Brewery.Infrastructure/EF/Queries/Handlers/GetUserHandler.cs create mode 100644 src/Brewery.Infrastructure/EF/Repositories/UserRepository.cs diff --git a/src/Brewery.Abstractions/Auth/IAuthManager.cs b/src/Brewery.Abstractions/Auth/IAuthManager.cs new file mode 100644 index 0000000..6c6cc09 --- /dev/null +++ b/src/Brewery.Abstractions/Auth/IAuthManager.cs @@ -0,0 +1,7 @@ +namespace Brewery.Abstractions.Auth; + +public interface IAuthManager +{ + JsonWebToken GenerateToken(string userId, string role, string audience = null, + IDictionary> claims = null); +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Auth/JsonWebToken.cs b/src/Brewery.Abstractions/Auth/JsonWebToken.cs new file mode 100644 index 0000000..a3b096a --- /dev/null +++ b/src/Brewery.Abstractions/Auth/JsonWebToken.cs @@ -0,0 +1,12 @@ +namespace Brewery.Abstractions.Auth; + +public class JsonWebToken +{ + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public long Expires { get; set; } + public string Id { get; set; } + public string Role { get; set; } + public string Email { get; set; } + public IDictionary> Claims { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Commands/ICommand.cs b/src/Brewery.Abstractions/Commands/ICommand.cs index d3c6799..3d3badb 100644 --- a/src/Brewery.Abstractions/Commands/ICommand.cs +++ b/src/Brewery.Abstractions/Commands/ICommand.cs @@ -3,4 +3,6 @@ public interface ICommand { -} \ No newline at end of file +} + +public interface ICommand : ICommand; \ No newline at end of file diff --git a/src/Brewery.Abstractions/Commands/ICommandHandler.cs b/src/Brewery.Abstractions/Commands/ICommandHandler.cs index 89c3a31..6f0b915 100644 --- a/src/Brewery.Abstractions/Commands/ICommandHandler.cs +++ b/src/Brewery.Abstractions/Commands/ICommandHandler.cs @@ -3,4 +3,9 @@ public interface ICommandHandler where TCommand : class, ICommand { Task HandleAsync(TCommand command); +} + +public interface ICommandHandler +{ + Task HandleAsync(TCommand command); } \ No newline at end of file diff --git a/src/Brewery.Abstractions/Contexts/IContext.cs b/src/Brewery.Abstractions/Contexts/IContext.cs new file mode 100644 index 0000000..b8a3147 --- /dev/null +++ b/src/Brewery.Abstractions/Contexts/IContext.cs @@ -0,0 +1,8 @@ +namespace Brewery.Abstractions.Contexts; + +public interface IContext +{ + string RequestId { get; } + string TraceId { get; } + IIdentityContext IdentityContext { get; } +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Contexts/IContextFactory.cs b/src/Brewery.Abstractions/Contexts/IContextFactory.cs new file mode 100644 index 0000000..85d0456 --- /dev/null +++ b/src/Brewery.Abstractions/Contexts/IContextFactory.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Contexts; + +public interface IContextFactory +{ + IContext Create(); +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Contexts/IIdentityContext.cs b/src/Brewery.Abstractions/Contexts/IIdentityContext.cs new file mode 100644 index 0000000..e34ee6b --- /dev/null +++ b/src/Brewery.Abstractions/Contexts/IIdentityContext.cs @@ -0,0 +1,11 @@ +using System.Security.Claims; + +namespace Brewery.Abstractions.Contexts; + +public interface IIdentityContext +{ + bool IsAuthenticated { get; } + Guid Id { get; } + string Role { get; } + Dictionary> Claims { get; } +} diff --git a/src/Brewery.Api/BreweryApi.rest b/src/Brewery.Api/BreweryApi.rest index 2ec54ef..8c90914 100644 --- a/src/Brewery.Api/BreweryApi.rest +++ b/src/Brewery.Api/BreweryApi.rest @@ -2,13 +2,35 @@ @beerId = a547da0f-15ef-487a-bf31-b28eccdec50b @beerId2 = 4fc96048-6ee6-403f-b4c5-647aced09499 -@beerId3 = 8a7f0585-03a2-4295-91f4-99e4b6f829e1 +@beerId3 = 356907d4-9d76-4e2f-84c4-3deb5518d78d @beerStockId = 4f79410c-ce39-47d3-91f3-c726a26aaba4 @brewerId = 21281174-0a8b-4097-badf-603bab276b9c @breweryId = 1d91c9f4-61df-4787-9ed6-aa065c2ba8bc @wholesalerId = c8c4d2a4-a85d-416b-b052-dc1f331f2237 @beerQuoteId = f6f00152-986d-441c-8662-27e01e279a8e +@accessToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2ZmJiNWU4MC1kNTNmLTQwODMtYTE3OC0wOTYxYzIzZTAyODkiLCJ1bmlxdWVfbmFtZSI6IjZmYmI1ZTgwLWQ1M2YtNDA4My1hMTc4LTA5NjFjMjNlMDI4OSIsImp0aSI6ImVmMDhiMDNkLTg4ZTItNDFjNS04NWIyLTU3YTFhNzZmOTA1YSIsImlhdCI6IjE3MzIwMTMxOTA1NzYiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJ1c2VyIiwicGVybWlzc2lvbnMiOlsiYnJld2VyeSIsImJyZXdlciIsImJlZXIiXSwibmJmIjoxNzMyMDEzMTkwLCJleHAiOjE3Mzk3ODkxOTAsImlzcyI6ImJyZXdlcnkifQ.DTOkX2V3KTzeDFzBUFywHNGXp8Og_RpC1QSOUt0q2t8 + +### Sign Up +POST {{url}}/account/signUp +Content-Type: application/json + +{ + "email": "user@brewery.com", + "password": "secretBeer", + "role": "user", + "claims": { "permissions": ["brewery", "brewer", "beer"] } +} + +### Sign In +POST {{url}}/account/signIn +Content-Type: application/json + +{ + "email": "user@brewery.com", + "password": "secretBeer" +} + ### List all beers by brewery GET {{url}}/brewery/{{breweryId}}/beers @@ -25,17 +47,16 @@ Content-Type: application/json } ### Brewer updates beer -PUT {{url}}/beer/{{beerId}} +PUT {{url}}/beer/{{beerId3}} Content-Type: application/json { "brewerId": "{{brewerId}}", - "name": "super beer 1", - "unitPrice": 8.00 + "name": "super beer 3" } ### Brewer deletes beer -DELETE {{url}}/beer/{{beerId}} +DELETE {{url}}/beer/{{beerId3}} Content-Type: application/json { @@ -70,12 +91,13 @@ GET {{url}}/brewery/{{breweryId}} ### GET {{url}}/brewery/{{breweryId}}/beers +//Add Brewery ### POST {{url}}/brewery Content-Type: application/json { - "name": "brewery 1" + "name": "brewery 333" } //Add wholesaler @@ -108,7 +130,7 @@ Content-Type: application/json { "wholesalerId": "{{wholesalerId}}", "beersEnquiry": [ - { "beerId": "{{beerId}}", "requiredQuantity": 30 }, - { "beerId": "{{beerId2}}", "requiredQuantity": 10 } + { "beerId": "{{beerId}}", "requiredQuantity": 10 }, + { "beerId": "{{beerId2}}", "requiredQuantity": 100 } ] } \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/AccountController.cs b/src/Brewery.Api/Controllers/AccountController.cs new file mode 100644 index 0000000..29d3585 --- /dev/null +++ b/src/Brewery.Api/Controllers/AccountController.cs @@ -0,0 +1,46 @@ +using Brewery.Abstractions.Auth; +using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Contexts; +using Brewery.Abstractions.Queries; +using Brewery.Application.Commands; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Brewery.Api.Controllers; + +public class AccountController : BaseController +{ + private readonly ICommandDispatcher _commandDispatcher; + private readonly ICommandHandler _signInCommandHandler; + private readonly IQueryDispatcher _queryDispatcher; + private readonly IContext _context; + + public AccountController(ICommandDispatcher commandDispatcher, + ICommandHandler signInCommandHandler, + IQueryDispatcher queryDispatcher, + IContext context) + { + _commandDispatcher = commandDispatcher; + _signInCommandHandler = signInCommandHandler; + _queryDispatcher = queryDispatcher; + _context = context; + } + + [HttpGet] + //[Authorize] + public async Task> GetUser() + => OkOrNotFound(await _queryDispatcher.QueryAsync(new GetUser(_context.IdentityContext.Id))); + + [HttpPost("signUp")] + public async Task SignIn(CreateAccount command) + { + await _commandDispatcher.DispatchAsync(command with { Id = Guid.NewGuid() }); + return NoContent(); + } + + [HttpPost("signIn")] + public async Task> SignIn(SignIn command) + => OkOrNotFound(await _signInCommandHandler.HandleAsync(command)); +} \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BreweryController.cs b/src/Brewery.Api/Controllers/BreweryController.cs index dcf326f..95eadd5 100644 --- a/src/Brewery.Api/Controllers/BreweryController.cs +++ b/src/Brewery.Api/Controllers/BreweryController.cs @@ -18,14 +18,14 @@ public BreweryController(ICommandDispatcher commandDispatcher, _commandDispatcher = commandDispatcher; _queryDispatcher = queryDispatcher; } - + [HttpGet("{breweryId:guid}")] public async Task> Get(Guid breweryId) { var brewery = await _queryDispatcher.QueryAsync(new GetBrewery(breweryId)); return OkOrNotFound(brewery); } - + [HttpGet("{breweryId:guid}/beers")] public async Task>> Browse(Guid breweryId) { @@ -33,7 +33,7 @@ public async Task>> Browse(Guid breweryId) .QueryAsync(new BrowseBeersByBrewery(breweryId)); return Ok(beersDto); } - + [HttpPost] public async Task Post(AddBrewery command) { diff --git a/src/Brewery.Api/Program.cs b/src/Brewery.Api/Program.cs index b02b9e3..e155b61 100644 --- a/src/Brewery.Api/Program.cs +++ b/src/Brewery.Api/Program.cs @@ -1,5 +1,9 @@ using Brewery.Api; using Brewery.Infrastructure; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Primitives; +using Results = Microsoft.AspNetCore.Http.Results; public class Program { diff --git a/src/Brewery.Api/appsettings.json b/src/Brewery.Api/appsettings.json index ce7c425..9716366 100644 --- a/src/Brewery.Api/appsettings.json +++ b/src/Brewery.Api/appsettings.json @@ -8,5 +8,14 @@ "AllowedHosts": "*", "postgres": { "connectionString": "Host=localhost;Database=Brewery;Username=postgres;Password=czcz" + }, + "auth" : { + "IssuerSigningKey": "jrgvjirgavijpotu89fdsfdsfjngdjghnghv3498c938", + "issuer": "brewery", + "validIssuer": "brewery", + "validateIssuer": true, + "validateAudience": false, + "validateLifetime": true, + "expiry": "90:00:00" } } diff --git a/src/Brewery.Application/Brewery.Application.csproj b/src/Brewery.Application/Brewery.Application.csproj index 55df28b..35d10de 100644 --- a/src/Brewery.Application/Brewery.Application.csproj +++ b/src/Brewery.Application/Brewery.Application.csproj @@ -9,5 +9,5 @@ - + diff --git a/src/Brewery.Application/Commands/CreateAccount.cs b/src/Brewery.Application/Commands/CreateAccount.cs new file mode 100644 index 0000000..85b8c7f --- /dev/null +++ b/src/Brewery.Application/Commands/CreateAccount.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record CreateAccount(Guid Id, string Email, string Password, string Role, + Dictionary> Claims) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs b/src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs index e6a2184..5aa8c94 100644 --- a/src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/AddBreweryHandler.cs @@ -23,4 +23,16 @@ public async Task HandleAsync(AddBrewery command) brewery = Domain.Entities.Brewery.Create(command.Id, command.Name); await _breweryRepository.AddBrewery(brewery); } + + public async Task Handle(AddBrewery request, CancellationToken cancellationToken) + { + var brewery = await _breweryRepository.GetBreweryById(request.Id); + if (brewery is not null) + { + + } + + brewery = Domain.Entities.Brewery.Create(request.Id, request.Name); + await _breweryRepository.AddBrewery(brewery); + } } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/CreateAccountHandler.cs b/src/Brewery.Application/Commands/Handlers/CreateAccountHandler.cs new file mode 100644 index 0000000..449ffda --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/CreateAccountHandler.cs @@ -0,0 +1,40 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Exceptions; +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.AspNetCore.Identity; + +namespace Brewery.Application.Commands.Handlers; + +public class CreateAccountHandler : ICommandHandler +{ + private readonly IUserRepository _userRepository; + private readonly IPasswordHasher _passwordHasher; + + public CreateAccountHandler(IUserRepository userRepository, + IPasswordHasher passwordHasher) + { + _userRepository = userRepository; + _passwordHasher = passwordHasher; + } + + public async Task HandleAsync(CreateAccount command) + { + var user = await _userRepository.GetUserByEmail(command.Email); + if (user is not null) + { + throw new EmailInUseException(command.Email); + } + + var hashedPassword = _passwordHasher.HashPassword(default, command.Password); + user = new User( + command.Id, + command.Email, + hashedPassword, + command.Role is not null ? command.Role : "user", + command.Claims is not null ? command.Claims : new Dictionary>(), + DateTime.UtcNow); + + await _userRepository.AddAsync(user); + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/SignInHandler.cs b/src/Brewery.Application/Commands/Handlers/SignInHandler.cs new file mode 100644 index 0000000..f7a78ff --- /dev/null +++ b/src/Brewery.Application/Commands/Handlers/SignInHandler.cs @@ -0,0 +1,45 @@ +using Brewery.Abstractions.Auth; +using Brewery.Abstractions.Commands; +using Brewery.Domain.Entities; +using Brewery.Domain.Exceptions; +using Brewery.Domain.Repositories; +using Microsoft.AspNetCore.Identity; + +namespace Brewery.Application.Commands.Handlers; + +public class SignInHandler : ICommandHandler +{ + private readonly IUserRepository _userRepository; + private readonly IPasswordHasher _passwordHasher; + private readonly IAuthManager _authManager; + + public SignInHandler(IUserRepository userRepository, + IPasswordHasher passwordHasher, + IAuthManager authManager) + { + _userRepository = userRepository; + _passwordHasher = passwordHasher; + _authManager = authManager; + } + + public async Task HandleAsync(SignIn command) + { + var user = await _userRepository.GetUserByEmail(command.Email); + if (user is null) + { + throw new InvalidCredentialsException(); + } + + var isPasswordVerified = _passwordHasher. + VerifyHashedPassword(default, user.Password, command.Password) + == PasswordVerificationResult.Success; + if (isPasswordVerified is false) + { + throw new InvalidCredentialsException(); + } + + var jwt = _authManager.GenerateToken(user.Id.ToString(), user.Role, audience: null, user.Claims); + + return jwt; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Commands/SignIn.cs b/src/Brewery.Application/Commands/SignIn.cs new file mode 100644 index 0000000..79dc300 --- /dev/null +++ b/src/Brewery.Application/Commands/SignIn.cs @@ -0,0 +1,5 @@ +using Brewery.Abstractions.Commands; + +namespace Brewery.Application.Commands; + +public record SignIn(string Email, string Password) : ICommand; \ No newline at end of file diff --git a/src/Brewery.Application/DTO/AccountDto.cs b/src/Brewery.Application/DTO/AccountDto.cs new file mode 100644 index 0000000..e6705b6 --- /dev/null +++ b/src/Brewery.Application/DTO/AccountDto.cs @@ -0,0 +1,9 @@ +namespace Brewery.Application.DTO; + +public class AccountDto +{ + public Guid UserId { get; set; } + public string Email { get; set; } + public string Role { get; set; } + public Dictionary> Claims { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Application/Exceptions/EmailInUseException.cs b/src/Brewery.Application/Exceptions/EmailInUseException.cs new file mode 100644 index 0000000..d0b8b6b --- /dev/null +++ b/src/Brewery.Application/Exceptions/EmailInUseException.cs @@ -0,0 +1,13 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Application.Exceptions; + +public class EmailInUseException : BreweryException +{ + public string Email { get; } + public EmailInUseException(string email) + : base($"Email '{email}' already in use.") + { + Email = email; + } +} \ No newline at end of file diff --git a/src/Brewery.Application/Queries/GetUser.cs b/src/Brewery.Application/Queries/GetUser.cs new file mode 100644 index 0000000..50e7ee4 --- /dev/null +++ b/src/Brewery.Application/Queries/GetUser.cs @@ -0,0 +1,6 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; + +namespace Brewery.Application.Queries; + +public record GetUser(Guid UserId) : IQuery; \ No newline at end of file diff --git a/src/Brewery.Domain/Entities/User.cs b/src/Brewery.Domain/Entities/User.cs new file mode 100644 index 0000000..e924b4d --- /dev/null +++ b/src/Brewery.Domain/Entities/User.cs @@ -0,0 +1,22 @@ +namespace Brewery.Domain.Entities; + +public class User +{ + public Guid Id { get; set; } + public string Email { get; set; } + public string Password { get; set; } + public string Role { get; set; } + public Dictionary> Claims { get; set; } + public DateTime CreatedAt { get; set; } + + public User(Guid id, string email, string password, string role, + Dictionary> claims, DateTime createdAt) + { + Id = id; + Email = email; + Password = password; + Role = role; + Claims = claims; + CreatedAt = createdAt; + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Exceptions/InvalidCredentialsException.cs b/src/Brewery.Domain/Exceptions/InvalidCredentialsException.cs new file mode 100644 index 0000000..1e4f8a4 --- /dev/null +++ b/src/Brewery.Domain/Exceptions/InvalidCredentialsException.cs @@ -0,0 +1,11 @@ +using Brewery.Abstractions.Exceptions; + +namespace Brewery.Domain.Exceptions; + +public class InvalidCredentialsException : BreweryException +{ + public InvalidCredentialsException() : base($"Invalid sign-in credentials.") + { + + } +} \ No newline at end of file diff --git a/src/Brewery.Domain/Repositories/IUserRepository.cs b/src/Brewery.Domain/Repositories/IUserRepository.cs new file mode 100644 index 0000000..b87bb91 --- /dev/null +++ b/src/Brewery.Domain/Repositories/IUserRepository.cs @@ -0,0 +1,10 @@ +using Brewery.Domain.Entities; + +namespace Brewery.Domain.Repositories; + +public interface IUserRepository +{ + Task AddAsync(User user); + Task GetUserById(Guid id); + Task GetUserByEmail(string email); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Auth/AuthManager.cs b/src/Brewery.Infrastructure/Auth/AuthManager.cs new file mode 100644 index 0000000..ec874fc --- /dev/null +++ b/src/Brewery.Infrastructure/Auth/AuthManager.cs @@ -0,0 +1,82 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Brewery.Abstractions.Auth; +using Microsoft.IdentityModel.Tokens; + +namespace Brewery.Infrastructure.Auth; + +public class AuthManager : IAuthManager +{ + private readonly static Dictionary> EmptyClaims = new(); + private readonly AuthOptions _options; + private readonly string _issuer; + private readonly SigningCredentials _signingCredentials; + + public AuthManager(AuthOptions options) + { + _options = options; + _issuer = options.Issuer; + _signingCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.IssuerSigningKey)), + SecurityAlgorithms.HmacSha256); + } + public JsonWebToken GenerateToken(string userId, string role, + string audience = null, IDictionary> claims = null) + { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentException("User id claim (subject) cannot be empty", nameof(userId)); + } + + var now = DateTime.UtcNow; + var jwtClaims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, userId), + new Claim(JwtRegisteredClaimNames.UniqueName, userId), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeMilliseconds().ToString()) + }; + + if (!string.IsNullOrWhiteSpace(role)) + { + jwtClaims.Add(new Claim(ClaimTypes.Role, role)); + } + + if (!string.IsNullOrWhiteSpace(audience)) + { + jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Aud, audience)); + } + + if (claims?.Any() is true) + { + var customClaims = new List(); + foreach (var (claim, values) in claims) + { + customClaims.AddRange(values.Select(v => new Claim(claim, v))); + } + + jwtClaims.AddRange(customClaims); + } + + var expires = now.Add(_options.Expiry); + var jwt = new JwtSecurityToken( + _issuer, + claims: jwtClaims, + notBefore: now, + expires: expires, + signingCredentials: _signingCredentials + ); + + var token = new JwtSecurityTokenHandler().WriteToken(jwt); + + return new JsonWebToken + { + AccessToken = token, + RefreshToken = string.Empty, + Expires = new DateTimeOffset(expires).ToUnixTimeMilliseconds(), + Id = userId, + Role = role ?? string.Empty, + Claims = claims ?? EmptyClaims, + }; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Auth/AuthOptions.cs b/src/Brewery.Infrastructure/Auth/AuthOptions.cs new file mode 100644 index 0000000..e07b5d5 --- /dev/null +++ b/src/Brewery.Infrastructure/Auth/AuthOptions.cs @@ -0,0 +1,12 @@ +namespace Brewery.Infrastructure.Auth; + +public class AuthOptions +{ + public string IssuerSigningKey { get; set; } + public string Issuer { get; set; } + public string ValidIssuer { get; set; } + public bool ValidateIssuer { get; set; } + public bool ValidateAudience { get; set; } + public bool ValidateLifetime { get; set; } + public TimeSpan Expiry { get; set; } +} diff --git a/src/Brewery.Infrastructure/Auth/Extensions.cs b/src/Brewery.Infrastructure/Auth/Extensions.cs new file mode 100644 index 0000000..119e958 --- /dev/null +++ b/src/Brewery.Infrastructure/Auth/Extensions.cs @@ -0,0 +1,53 @@ +using System.Text; +using Brewery.Abstractions.Auth; +using Brewery.Domain.Entities; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace Brewery.Infrastructure.Auth; + +public static class Extensions +{ + public static IServiceCollection AddAuth(this IServiceCollection services) + { + var authOptions = services.GetOptions("auth"); + services.AddSingleton(); + services.AddSingleton, PasswordHasher>(); + + var tokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = authOptions.ValidIssuer, + ValidateIssuer = authOptions.ValidateIssuer, + ValidateAudience = authOptions.ValidateAudience, + ValidateLifetime = authOptions.ValidateLifetime, + ClockSkew = TimeSpan.Zero + + }; + if (string.IsNullOrWhiteSpace(authOptions.IssuerSigningKey)) + { + throw new ArgumentException("Missing issuer signing key.", nameof(authOptions.IssuerSigningKey)); + } + + var rawKey = Encoding.UTF8.GetBytes(authOptions.IssuerSigningKey); + tokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(rawKey); + + services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + + }).AddJwtBearer(o => + { + o.TokenValidationParameters = tokenValidationParameters; + }); + + services.AddSingleton(authOptions); + services.AddSingleton(tokenValidationParameters); + + services.AddAuthorization(); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj index c58e56a..a35ef6b 100644 --- a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj +++ b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Brewery.Infrastructure/Commands/Extensions.cs b/src/Brewery.Infrastructure/Commands/Extensions.cs index 48798ae..bc90f30 100644 --- a/src/Brewery.Infrastructure/Commands/Extensions.cs +++ b/src/Brewery.Infrastructure/Commands/Extensions.cs @@ -1,5 +1,8 @@ using System.Reflection; +using Brewery.Abstractions.Auth; using Brewery.Abstractions.Commands; +using Brewery.Application.Commands; +using Brewery.Application.Commands.Handlers; using Microsoft.Extensions.DependencyInjection; namespace Brewery.Infrastructure.Commands; @@ -14,6 +17,7 @@ public static IServiceCollection AddCommands(this IServiceCollection services, I .AddClasses(c => c.AssignableTo(typeof(ICommandHandler<>))) .AsImplementedInterfaces() .WithScopedLifetime()); + services.AddScoped, SignInHandler>(); return services; } diff --git a/src/Brewery.Infrastructure/Contexts/Context.cs b/src/Brewery.Infrastructure/Contexts/Context.cs new file mode 100644 index 0000000..e3a284e --- /dev/null +++ b/src/Brewery.Infrastructure/Contexts/Context.cs @@ -0,0 +1,30 @@ +using Brewery.Abstractions.Contexts; +using Microsoft.AspNetCore.Http; + +namespace Brewery.Infrastructure.Contexts; + +public class Context : IContext +{ + public string RequestId { get; } = $"{Guid.NewGuid():N}"; + public string TraceId { get; } + public IIdentityContext IdentityContext { get; } + + internal Context() + { + } + + public Context(IHttpContextAccessor httpContextAccessor) : + this(httpContextAccessor.HttpContext.TraceIdentifier, + new IdentityContext(httpContextAccessor.HttpContext.User)) + { + + } + + public Context(string traceId, IIdentityContext identityContext) + { + TraceId = traceId; + IdentityContext = identityContext; + } + + public static Context Empty() => new Context(); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Contexts/ContextFactory.cs b/src/Brewery.Infrastructure/Contexts/ContextFactory.cs new file mode 100644 index 0000000..55b46e8 --- /dev/null +++ b/src/Brewery.Infrastructure/Contexts/ContextFactory.cs @@ -0,0 +1,24 @@ +using Brewery.Abstractions.Contexts; +using Microsoft.AspNetCore.Http; + +namespace Brewery.Infrastructure.Contexts; + +public class ContextFactory : IContextFactory +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public ContextFactory(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public IContext Create() + { + if (_httpContextAccessor.HttpContext is null) + { + return Context.Empty(); + } + + return new Context(_httpContextAccessor); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Contexts/Extensions.cs b/src/Brewery.Infrastructure/Contexts/Extensions.cs new file mode 100644 index 0000000..dca42d6 --- /dev/null +++ b/src/Brewery.Infrastructure/Contexts/Extensions.cs @@ -0,0 +1,17 @@ +using Brewery.Abstractions.Contexts; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Contexts; + +public static class Extensions +{ + public static IServiceCollection AddContexts(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(sp => sp.GetRequiredService().Create()); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Contexts/IdentityContext.cs b/src/Brewery.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 0000000..781c0d6 --- /dev/null +++ b/src/Brewery.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,27 @@ +using System.Security.Claims; +using Brewery.Abstractions.Contexts; + +namespace Brewery.Infrastructure.Contexts; + +public class IdentityContext : IIdentityContext +{ + public bool IsAuthenticated { get; } + public Guid Id { get; } + public string Role { get; } + public Dictionary> Claims { get; } + + public IdentityContext(ClaimsPrincipal principal) + { + IsAuthenticated = principal.Identity?.IsAuthenticated is true; + Id = IsAuthenticated ? Guid.Parse(principal.Identity.Name) : Guid.Empty; + Role = IsAuthenticated ? principal.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Role)?.Value : string.Empty; + Claims = IsAuthenticated + ? principal.Claims + .GroupBy(c => c.Type) + .ToDictionary( + k => k.Key, + v => v.Select(c => c.Value)) + : new Dictionary>(); + } + +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs index bd6fb07..0caba88 100644 --- a/src/Brewery.Infrastructure/EF/BreweryDbContext.cs +++ b/src/Brewery.Infrastructure/EF/BreweryDbContext.cs @@ -14,6 +14,7 @@ public class BreweryDbContext : DbContext public DbSet BeerStocks { get; set; } public DbSet BeerOrder { get; set; } public DbSet BeerQuotes { get; set; } + public DbSet Users { get; set; } public BreweryDbContext(DbContextOptions options) : base(options) diff --git a/src/Brewery.Infrastructure/EF/Configuration/UserConfiguration.cs b/src/Brewery.Infrastructure/EF/Configuration/UserConfiguration.cs new file mode 100644 index 0000000..1ccf01d --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Configuration/UserConfiguration.cs @@ -0,0 +1,21 @@ +using System.Text.Json; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Brewery.Infrastructure.EF.Configuration; + +public class UserConfiguration : IEntityTypeConfiguration +{ + private readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + + public void Configure(EntityTypeBuilder builder) + { + builder.Property(u => u.Claims) + .HasConversion(c => JsonSerializer.Serialize(c, _jsonOptions), + json => JsonSerializer.Deserialize>>(json, _jsonOptions)); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Extensions.cs b/src/Brewery.Infrastructure/EF/Extensions.cs index 4df9b26..aa36766 100644 --- a/src/Brewery.Infrastructure/EF/Extensions.cs +++ b/src/Brewery.Infrastructure/EF/Extensions.cs @@ -17,6 +17,7 @@ internal static IServiceCollection AddEF(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); var postgresOptions = services.GetOptions("postgres"); services.AddDbContext(options => diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.Designer.cs b/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.Designer.cs new file mode 100644 index 0000000..8e39f13 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.Designer.cs @@ -0,0 +1,286 @@ +// +using System; +using Brewery.Infrastructure.EF; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + [DbContext(typeof(BreweryDbContext))] + [Migration("20241119092325_AddUser")] + partial class AddUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("brewery") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("Beers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.Property("BeerId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerQuoteId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("BeerId"); + + b.HasIndex("BeerQuoteId"); + + b.ToTable("BeerOrder", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DiscountInPercent") + .HasColumnType("integer"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("BeerQuotes", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("WholesalerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WholesalerId"); + + b.ToTable("BeerSales", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BeerId") + .HasColumnType("uuid"); + + b.Property("BrewerId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("BrewerId"); + + b.ToTable("BeerStocks", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BreweryId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BreweryId"); + + b.ToTable("Brewers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Breweries", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Wholesalers", "brewery"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Beer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("Beers") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerOrder", b => + { + b.HasOne("Brewery.Domain.Entities.BeerQuote", null) + .WithMany("BeerOrders") + .HasForeignKey("BeerQuoteId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerSale", b => + { + b.HasOne("Brewery.Domain.Entities.Wholesaler", null) + .WithMany("BeerSales") + .HasForeignKey("WholesalerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerStock", b => + { + b.HasOne("Brewery.Domain.Entities.Brewer", null) + .WithMany("BeerStocks") + .HasForeignKey("BrewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.HasOne("Brewery.Domain.Entities.Brewery", null) + .WithMany("Brewers") + .HasForeignKey("BreweryId"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.BeerQuote", b => + { + b.Navigation("BeerOrders"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewer", b => + { + b.Navigation("BeerStocks"); + + b.Navigation("Beers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Brewery", b => + { + b.Navigation("Brewers"); + }); + + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => + { + b.Navigation("BeerSales"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.cs b/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.cs new file mode 100644 index 0000000..873dd56 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Migrations/20241119092325_AddUser.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Brewery.Infrastructure.EF.Migrations +{ + /// + public partial class AddUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + schema: "brewery", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Email = table.Column(type: "text", nullable: false), + Password = table.Column(type: "text", nullable: false), + Role = table.Column(type: "text", nullable: false), + Claims = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users", + schema: "brewery"); + } + } +} diff --git a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs index 44f3fab..34aed80 100644 --- a/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs +++ b/src/Brewery.Infrastructure/EF/Migrations/BreweryDbContextModelSnapshot.cs @@ -170,6 +170,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Breweries", "brewery"); }); + modelBuilder.Entity("Brewery.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users", "brewery"); + }); + modelBuilder.Entity("Brewery.Domain.Entities.Wholesaler", b => { b.Property("Id") diff --git a/src/Brewery.Infrastructure/EF/Queries/Handlers/GetUserHandler.cs b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetUserHandler.cs new file mode 100644 index 0000000..f4ddb5e --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Queries/Handlers/GetUserHandler.cs @@ -0,0 +1,33 @@ +using Brewery.Abstractions.Queries; +using Brewery.Application.DTO; +using Brewery.Application.Queries; +using Brewery.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Queries.Handlers; + +public class GetUserHandler : IQueryHandler +{ + private readonly DbSet _users; + + public GetUserHandler(BreweryDbContext dbContext) + { + _users = dbContext.Users; + } + public async Task QueryAsync(GetUser query) + { + var user = await _users + .AsNoTracking() + .SingleOrDefaultAsync(u => u.Id == query.UserId); + + return user is not null + ? new AccountDto + { + UserId = user.Id, + Email = user.Email, + Role = user.Role, + Claims = user.Claims, + } + : null; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/Repositories/UserRepository.cs b/src/Brewery.Infrastructure/EF/Repositories/UserRepository.cs new file mode 100644 index 0000000..06c195d --- /dev/null +++ b/src/Brewery.Infrastructure/EF/Repositories/UserRepository.cs @@ -0,0 +1,29 @@ +using Brewery.Domain.Entities; +using Brewery.Domain.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.Repositories; + +public class UserRepository : IUserRepository +{ + private readonly DbSet _users; + private readonly BreweryDbContext _dbContext; + + public UserRepository(BreweryDbContext dbContext) + { + _users = dbContext.Users; + _dbContext = dbContext; + } + + public async Task AddAsync(User user) + { + await _users.AddAsync(user); + await _dbContext.SaveChangesAsync(); + } + + public Task GetUserById(Guid id) + => _users.SingleOrDefaultAsync(u => u.Id == id); + + public Task GetUserByEmail(string email) + => _users.SingleOrDefaultAsync(u => u.Email == email); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Extensions.cs b/src/Brewery.Infrastructure/Extensions.cs index b463985..f20503c 100644 --- a/src/Brewery.Infrastructure/Extensions.cs +++ b/src/Brewery.Infrastructure/Extensions.cs @@ -1,5 +1,7 @@ using System.Reflection; +using Brewery.Infrastructure.Auth; using Brewery.Infrastructure.Commands; +using Brewery.Infrastructure.Contexts; using Brewery.Infrastructure.EF; using Brewery.Infrastructure.Exceptions; using Brewery.Infrastructure.Middleware; @@ -16,6 +18,9 @@ public static class Extensions { public static IServiceCollection AddInfrastructure(this IServiceCollection services, IList assemblies) { + services.AddAuth(); + services.AddContexts(); + services.AddControllers(); services.AddExceptionHanding(); services.AddControllers(); services.AddHostedService(); @@ -29,8 +34,10 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) { - //app.UseExceptionHandling(); + app.UseAuthentication(); + app.UseExceptionHandling(); app.UseRouting(); + app.UseAuthorization(); app.UseEndpoints(e => { e.MapControllers(); From 11fa2f73905fc816513a4f428c7859d8fcac242d Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Wed, 20 Nov 2024 19:01:55 +0000 Subject: [PATCH 19/24] add RabbitMq to handle Auth --- src/Brewery.Abstractions/Commands/ICommand.cs | 6 ++- .../Messaging/IMessage.cs | 6 +++ .../Messaging/IMessagePublisher.cs | 7 +++ .../Messaging/RabbitMqOptions.cs | 6 +++ src/Brewery.Api/BreweryApi.rest | 15 +++++-- .../Controllers/AccountController.cs | 18 +++++--- .../Controllers/BreweryController.cs | 2 + src/Brewery.Api/appsettings.json | 3 ++ .../Commands/Handlers/SignInHandler.cs | 36 +++------------ src/Brewery.Infrastructure/Auth/Extensions.cs | 2 +- .../Brewery.Infrastructure.csproj | 2 + .../Commands/Extensions.cs | 2 +- src/Brewery.Infrastructure/Extensions.cs | 3 +- .../Messaging/Extensions.cs | 16 +++++++ .../Messaging/RabbitMessagePublisher.cs | 44 +++++++++++++++++++ 15 files changed, 125 insertions(+), 43 deletions(-) create mode 100644 src/Brewery.Abstractions/Messaging/IMessage.cs create mode 100644 src/Brewery.Abstractions/Messaging/IMessagePublisher.cs create mode 100644 src/Brewery.Abstractions/Messaging/RabbitMqOptions.cs create mode 100644 src/Brewery.Infrastructure/Messaging/Extensions.cs create mode 100644 src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs diff --git a/src/Brewery.Abstractions/Commands/ICommand.cs b/src/Brewery.Abstractions/Commands/ICommand.cs index 3d3badb..c5d0ca5 100644 --- a/src/Brewery.Abstractions/Commands/ICommand.cs +++ b/src/Brewery.Abstractions/Commands/ICommand.cs @@ -1,6 +1,8 @@ -namespace Brewery.Abstractions.Commands; +using Brewery.Abstractions.Messaging; -public interface ICommand +namespace Brewery.Abstractions.Commands; + +public interface ICommand : IMessage { } diff --git a/src/Brewery.Abstractions/Messaging/IMessage.cs b/src/Brewery.Abstractions/Messaging/IMessage.cs new file mode 100644 index 0000000..1f55f52 --- /dev/null +++ b/src/Brewery.Abstractions/Messaging/IMessage.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Messaging; + +public interface IMessage +{ + +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Messaging/IMessagePublisher.cs b/src/Brewery.Abstractions/Messaging/IMessagePublisher.cs new file mode 100644 index 0000000..b356084 --- /dev/null +++ b/src/Brewery.Abstractions/Messaging/IMessagePublisher.cs @@ -0,0 +1,7 @@ +namespace Brewery.Abstractions.Messaging; + +public interface IMessagePublisher +{ + Task PublishAsync(TMessage message, string exchange) + where TMessage : class, IMessage; +} \ No newline at end of file diff --git a/src/Brewery.Abstractions/Messaging/RabbitMqOptions.cs b/src/Brewery.Abstractions/Messaging/RabbitMqOptions.cs new file mode 100644 index 0000000..bf5c300 --- /dev/null +++ b/src/Brewery.Abstractions/Messaging/RabbitMqOptions.cs @@ -0,0 +1,6 @@ +namespace Brewery.Abstractions.Messaging; + +public class RabbitMqOptions +{ + public string HostName { get; set; } +} \ No newline at end of file diff --git a/src/Brewery.Api/BreweryApi.rest b/src/Brewery.Api/BreweryApi.rest index 8c90914..99c566f 100644 --- a/src/Brewery.Api/BreweryApi.rest +++ b/src/Brewery.Api/BreweryApi.rest @@ -11,6 +11,9 @@ @accessToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2ZmJiNWU4MC1kNTNmLTQwODMtYTE3OC0wOTYxYzIzZTAyODkiLCJ1bmlxdWVfbmFtZSI6IjZmYmI1ZTgwLWQ1M2YtNDA4My1hMTc4LTA5NjFjMjNlMDI4OSIsImp0aSI6ImVmMDhiMDNkLTg4ZTItNDFjNS04NWIyLTU3YTFhNzZmOTA1YSIsImlhdCI6IjE3MzIwMTMxOTA1NzYiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJ1c2VyIiwicGVybWlzc2lvbnMiOlsiYnJld2VyeSIsImJyZXdlciIsImJlZXIiXSwibmJmIjoxNzMyMDEzMTkwLCJleHAiOjE3Mzk3ODkxOTAsImlzcyI6ImJyZXdlcnkifQ.DTOkX2V3KTzeDFzBUFywHNGXp8Og_RpC1QSOUt0q2t8 +### testEnd +GET {{url}}/testEnd + ### Sign Up POST {{url}}/account/signUp Content-Type: application/json @@ -30,6 +33,10 @@ Content-Type: application/json "email": "user@brewery.com", "password": "secretBeer" } + +### Get Account Info +GET {{url}}/account +//Authorization: Bearer {{accessToken}} ### List all beers by brewery GET {{url}}/brewery/{{breweryId}}/beers @@ -76,7 +83,7 @@ Content-Type: application/json ### GET {{url}}/brewer/{{brewerId}} -### +### Add brewer POST {{url}}/brewer Content-Type: application/json @@ -91,13 +98,13 @@ GET {{url}}/brewery/{{breweryId}} ### GET {{url}}/brewery/{{breweryId}}/beers -//Add Brewery -### +### Add Brewery POST {{url}}/brewery Content-Type: application/json +Authorization: Bearer {{accessToken}} { - "name": "brewery 333" + "name": "brewery with Auth" } //Add wholesaler diff --git a/src/Brewery.Api/Controllers/AccountController.cs b/src/Brewery.Api/Controllers/AccountController.cs index 29d3585..7f4f3fd 100644 --- a/src/Brewery.Api/Controllers/AccountController.cs +++ b/src/Brewery.Api/Controllers/AccountController.cs @@ -13,17 +13,17 @@ namespace Brewery.Api.Controllers; public class AccountController : BaseController { private readonly ICommandDispatcher _commandDispatcher; - private readonly ICommandHandler _signInCommandHandler; + //private readonly ICommandHandler _signInCommandHandler; private readonly IQueryDispatcher _queryDispatcher; private readonly IContext _context; public AccountController(ICommandDispatcher commandDispatcher, - ICommandHandler signInCommandHandler, + //ICommandHandler signInCommandHandler, IQueryDispatcher queryDispatcher, IContext context) { _commandDispatcher = commandDispatcher; - _signInCommandHandler = signInCommandHandler; + //_signInCommandHandler = signInCommandHandler; _queryDispatcher = queryDispatcher; _context = context; } @@ -40,7 +40,15 @@ public async Task SignIn(CreateAccount command) return NoContent(); } + // [HttpPost("signIn")] + // public async Task> SignIn(SignIn command) + // => OkOrNotFound(await _signInCommandHandler.HandleAsync(command)); + [HttpPost("signIn")] - public async Task> SignIn(SignIn command) - => OkOrNotFound(await _signInCommandHandler.HandleAsync(command)); + public async Task SignIn(SignIn command) + { + await _commandDispatcher.DispatchAsync(command); + return Ok(); + } + } \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/BreweryController.cs b/src/Brewery.Api/Controllers/BreweryController.cs index 95eadd5..4d04030 100644 --- a/src/Brewery.Api/Controllers/BreweryController.cs +++ b/src/Brewery.Api/Controllers/BreweryController.cs @@ -3,6 +3,7 @@ using Brewery.Application.Commands; using Brewery.Application.DTO; using Brewery.Application.Queries; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Brewery.Api.Controllers; @@ -35,6 +36,7 @@ public async Task>> Browse(Guid breweryId) } [HttpPost] + [Authorize] public async Task Post(AddBrewery command) { await _commandDispatcher.DispatchAsync(command); diff --git a/src/Brewery.Api/appsettings.json b/src/Brewery.Api/appsettings.json index 9716366..caab767 100644 --- a/src/Brewery.Api/appsettings.json +++ b/src/Brewery.Api/appsettings.json @@ -17,5 +17,8 @@ "validateAudience": false, "validateLifetime": true, "expiry": "90:00:00" + }, + "rabbitMq": { + "hostName": "localhost" } } diff --git a/src/Brewery.Application/Commands/Handlers/SignInHandler.cs b/src/Brewery.Application/Commands/Handlers/SignInHandler.cs index f7a78ff..ff49b4c 100644 --- a/src/Brewery.Application/Commands/Handlers/SignInHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/SignInHandler.cs @@ -1,5 +1,6 @@ using Brewery.Abstractions.Auth; using Brewery.Abstractions.Commands; +using Brewery.Abstractions.Messaging; using Brewery.Domain.Entities; using Brewery.Domain.Exceptions; using Brewery.Domain.Repositories; @@ -7,39 +8,16 @@ namespace Brewery.Application.Commands.Handlers; -public class SignInHandler : ICommandHandler +public class SignInHandler : ICommandHandler { - private readonly IUserRepository _userRepository; - private readonly IPasswordHasher _passwordHasher; - private readonly IAuthManager _authManager; - - public SignInHandler(IUserRepository userRepository, - IPasswordHasher passwordHasher, - IAuthManager authManager) + private readonly IMessagePublisher _messagePublisher; + public SignInHandler(IMessagePublisher messagePublisher) { - _userRepository = userRepository; - _passwordHasher = passwordHasher; - _authManager = authManager; + _messagePublisher = messagePublisher; } - public async Task HandleAsync(SignIn command) + public async Task HandleAsync(SignIn command) { - var user = await _userRepository.GetUserByEmail(command.Email); - if (user is null) - { - throw new InvalidCredentialsException(); - } - - var isPasswordVerified = _passwordHasher. - VerifyHashedPassword(default, user.Password, command.Password) - == PasswordVerificationResult.Success; - if (isPasswordVerified is false) - { - throw new InvalidCredentialsException(); - } - - var jwt = _authManager.GenerateToken(user.Id.ToString(), user.Role, audience: null, user.Claims); - - return jwt; + await _messagePublisher.PublishAsync(command, "brewery-id-service-exchange"); } } \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Auth/Extensions.cs b/src/Brewery.Infrastructure/Auth/Extensions.cs index 119e958..bee1d08 100644 --- a/src/Brewery.Infrastructure/Auth/Extensions.cs +++ b/src/Brewery.Infrastructure/Auth/Extensions.cs @@ -22,7 +22,7 @@ public static IServiceCollection AddAuth(this IServiceCollection services) ValidateIssuer = authOptions.ValidateIssuer, ValidateAudience = authOptions.ValidateAudience, ValidateLifetime = authOptions.ValidateLifetime, - ClockSkew = TimeSpan.Zero + ClockSkew = TimeSpan.Zero }; if (string.IsNullOrWhiteSpace(authOptions.IssuerSigningKey)) diff --git a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj index a35ef6b..4d2ff60 100644 --- a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj +++ b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj @@ -13,7 +13,9 @@ + + diff --git a/src/Brewery.Infrastructure/Commands/Extensions.cs b/src/Brewery.Infrastructure/Commands/Extensions.cs index bc90f30..1e680a8 100644 --- a/src/Brewery.Infrastructure/Commands/Extensions.cs +++ b/src/Brewery.Infrastructure/Commands/Extensions.cs @@ -17,7 +17,7 @@ public static IServiceCollection AddCommands(this IServiceCollection services, I .AddClasses(c => c.AssignableTo(typeof(ICommandHandler<>))) .AsImplementedInterfaces() .WithScopedLifetime()); - services.AddScoped, SignInHandler>(); + //services.AddScoped, SignInHandler>(); return services; } diff --git a/src/Brewery.Infrastructure/Extensions.cs b/src/Brewery.Infrastructure/Extensions.cs index f20503c..6fb00ff 100644 --- a/src/Brewery.Infrastructure/Extensions.cs +++ b/src/Brewery.Infrastructure/Extensions.cs @@ -4,6 +4,7 @@ using Brewery.Infrastructure.Contexts; using Brewery.Infrastructure.EF; using Brewery.Infrastructure.Exceptions; +using Brewery.Infrastructure.Messaging; using Brewery.Infrastructure.Middleware; using Brewery.Infrastructure.Queries; using Brewery.Infrastructure.Services; @@ -22,12 +23,12 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi services.AddContexts(); services.AddControllers(); services.AddExceptionHanding(); - services.AddControllers(); services.AddHostedService(); services.AddSingleton(); services.AddEF(); services.AddCommands(assemblies); services.AddQueries(assemblies); + services.AddRabbitMq(); return services; } diff --git a/src/Brewery.Infrastructure/Messaging/Extensions.cs b/src/Brewery.Infrastructure/Messaging/Extensions.cs new file mode 100644 index 0000000..0632703 --- /dev/null +++ b/src/Brewery.Infrastructure/Messaging/Extensions.cs @@ -0,0 +1,16 @@ +using Brewery.Abstractions.Messaging; +using Microsoft.Extensions.DependencyInjection; + +namespace Brewery.Infrastructure.Messaging; + +public static class Extensions +{ + public static IServiceCollection AddRabbitMq(this IServiceCollection services) + { + var options = services.GetOptions("rabbitmq"); + services.AddSingleton(options); + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs new file mode 100644 index 0000000..ced040f --- /dev/null +++ b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs @@ -0,0 +1,44 @@ +using System.Text; +using System.Text.Json.Serialization; +using Brewery.Abstractions.Messaging; +using Humanizer; +using Newtonsoft.Json; +using RabbitMQ.Client; + +namespace Brewery.Infrastructure.Messaging; + +public class RabbitMessagePublisher : IMessagePublisher +{ + private readonly RabbitMqOptions _options; + private readonly IModel _channel; + + public RabbitMessagePublisher(RabbitMqOptions options) + { + _options = options; + var connectionFactory = new ConnectionFactory + { + HostName = _options.HostName, + }; + + var connection = connectionFactory.CreateConnection(); + _channel = connection.CreateModel(); + } + + public Task PublishAsync(TMessage message, string exchange) where TMessage : class, IMessage + { + var routingKey = message.GetType().Name.Underscore(); + var json = JsonConvert.SerializeObject(message); + var body = Encoding.UTF8.GetBytes(json); + //var properties = _channel.CreateBasicProperties(); + + _channel.ExchangeDeclare(exchange, ExchangeType.Direct, durable: true, autoDelete: false); + + _channel.BasicPublish( + exchange: exchange, + routingKey: routingKey, + //basicProperties: properties, + body: body); + + return Task.CompletedTask; + } +} \ No newline at end of file From 97a5ec9df031af7f2ccbea7047999d0992797689 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Thu, 21 Nov 2024 23:04:47 +0000 Subject: [PATCH 20/24] BackgroundConsumer --- .../Messaging/RabbitMessagePublisher.cs | 10 ++++++++-- .../Services/BackgroundConsumer.cs | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/Brewery.Infrastructure/Services/BackgroundConsumer.cs diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs index ced040f..b6d7a22 100644 --- a/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs +++ b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs @@ -26,17 +26,23 @@ public RabbitMessagePublisher(RabbitMqOptions options) public Task PublishAsync(TMessage message, string exchange) where TMessage : class, IMessage { + var correlationId = Guid.NewGuid().ToString(); + var callbackQueue = "brewery-id-service-queue-jwt"; + var routingKey = message.GetType().Name.Underscore(); var json = JsonConvert.SerializeObject(message); var body = Encoding.UTF8.GetBytes(json); - //var properties = _channel.CreateBasicProperties(); + + var properties = _channel.CreateBasicProperties(); + properties.CorrelationId = correlationId; + properties.ReplyTo = callbackQueue; _channel.ExchangeDeclare(exchange, ExchangeType.Direct, durable: true, autoDelete: false); _channel.BasicPublish( exchange: exchange, routingKey: routingKey, - //basicProperties: properties, + basicProperties: properties, body: body); return Task.CompletedTask; diff --git a/src/Brewery.Infrastructure/Services/BackgroundConsumer.cs b/src/Brewery.Infrastructure/Services/BackgroundConsumer.cs new file mode 100644 index 0000000..e4b7a85 --- /dev/null +++ b/src/Brewery.Infrastructure/Services/BackgroundConsumer.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Hosting; + +namespace Brewery.Infrastructure.Services; + +public class BackgroundConsumer : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + + public BackgroundConsumer(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + + throw new NotImplementedException(); + } +} \ No newline at end of file From b9407efcea6c799552c051a0157c60e43737e062 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Fri, 22 Nov 2024 23:27:37 +0000 Subject: [PATCH 21/24] edit AccountController --- .../Messaging/IMessagePublisher.cs | 7 +- .../Controllers/AccountController.cs | 24 +++--- .../Commands/Handlers/SignInHandler.cs | 16 ++-- .../Brewery.Infrastructure.csproj | 2 +- .../Commands/Extensions.cs | 2 +- .../Messaging/RabbitMessagePublisher.cs | 80 +++++++++++++++---- 6 files changed, 95 insertions(+), 36 deletions(-) diff --git a/src/Brewery.Abstractions/Messaging/IMessagePublisher.cs b/src/Brewery.Abstractions/Messaging/IMessagePublisher.cs index b356084..89c2545 100644 --- a/src/Brewery.Abstractions/Messaging/IMessagePublisher.cs +++ b/src/Brewery.Abstractions/Messaging/IMessagePublisher.cs @@ -1,7 +1,12 @@ -namespace Brewery.Abstractions.Messaging; +using Brewery.Abstractions.Auth; + +namespace Brewery.Abstractions.Messaging; public interface IMessagePublisher { Task PublishAsync(TMessage message, string exchange) where TMessage : class, IMessage; + + Task PublishAsync(TMessage message, string exchange) + where TMessage : class, IMessage where TResult : JsonWebToken; } \ No newline at end of file diff --git a/src/Brewery.Api/Controllers/AccountController.cs b/src/Brewery.Api/Controllers/AccountController.cs index 7f4f3fd..559dc5d 100644 --- a/src/Brewery.Api/Controllers/AccountController.cs +++ b/src/Brewery.Api/Controllers/AccountController.cs @@ -13,17 +13,17 @@ namespace Brewery.Api.Controllers; public class AccountController : BaseController { private readonly ICommandDispatcher _commandDispatcher; - //private readonly ICommandHandler _signInCommandHandler; + private readonly ICommandHandler _signInCommandHandler; private readonly IQueryDispatcher _queryDispatcher; private readonly IContext _context; public AccountController(ICommandDispatcher commandDispatcher, - //ICommandHandler signInCommandHandler, + ICommandHandler signInCommandHandler, IQueryDispatcher queryDispatcher, IContext context) { _commandDispatcher = commandDispatcher; - //_signInCommandHandler = signInCommandHandler; + _signInCommandHandler = signInCommandHandler; _queryDispatcher = queryDispatcher; _context = context; } @@ -40,15 +40,15 @@ public async Task SignIn(CreateAccount command) return NoContent(); } - // [HttpPost("signIn")] - // public async Task> SignIn(SignIn command) - // => OkOrNotFound(await _signInCommandHandler.HandleAsync(command)); - [HttpPost("signIn")] - public async Task SignIn(SignIn command) - { - await _commandDispatcher.DispatchAsync(command); - return Ok(); - } + public async Task> SignIn(SignIn command) + => OkOrNotFound(await _signInCommandHandler.HandleAsync(command)); + + // [HttpPost("signIn")] + // public async Task SignIn(SignIn command) + // { + // await _commandDispatcher.DispatchAsync(command); + // return Ok(); + // } } \ No newline at end of file diff --git a/src/Brewery.Application/Commands/Handlers/SignInHandler.cs b/src/Brewery.Application/Commands/Handlers/SignInHandler.cs index ff49b4c..049f73c 100644 --- a/src/Brewery.Application/Commands/Handlers/SignInHandler.cs +++ b/src/Brewery.Application/Commands/Handlers/SignInHandler.cs @@ -1,14 +1,10 @@ using Brewery.Abstractions.Auth; using Brewery.Abstractions.Commands; using Brewery.Abstractions.Messaging; -using Brewery.Domain.Entities; -using Brewery.Domain.Exceptions; -using Brewery.Domain.Repositories; -using Microsoft.AspNetCore.Identity; namespace Brewery.Application.Commands.Handlers; -public class SignInHandler : ICommandHandler +public class SignInHandler : ICommandHandler { private readonly IMessagePublisher _messagePublisher; public SignInHandler(IMessagePublisher messagePublisher) @@ -16,8 +12,14 @@ public SignInHandler(IMessagePublisher messagePublisher) _messagePublisher = messagePublisher; } - public async Task HandleAsync(SignIn command) + public async Task HandleAsync(SignIn command) { - await _messagePublisher.PublishAsync(command, "brewery-id-service-exchange"); + var jwt = await _messagePublisher + .PublishAsync(command, "brewery_id_service_exchange"); + + // var jwt = await _messagePublisher + // .PublishAsync(command, "brewery_id_service_exchange"); + + return jwt; } } \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj index 4d2ff60..0f40ace 100644 --- a/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj +++ b/src/Brewery.Infrastructure/Brewery.Infrastructure.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Brewery.Infrastructure/Commands/Extensions.cs b/src/Brewery.Infrastructure/Commands/Extensions.cs index 1e680a8..bc90f30 100644 --- a/src/Brewery.Infrastructure/Commands/Extensions.cs +++ b/src/Brewery.Infrastructure/Commands/Extensions.cs @@ -17,7 +17,7 @@ public static IServiceCollection AddCommands(this IServiceCollection services, I .AddClasses(c => c.AssignableTo(typeof(ICommandHandler<>))) .AsImplementedInterfaces() .WithScopedLifetime()); - //services.AddScoped, SignInHandler>(); + services.AddScoped, SignInHandler>(); return services; } diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs index b6d7a22..eeec8eb 100644 --- a/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs +++ b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs @@ -1,50 +1,102 @@ using System.Text; using System.Text.Json.Serialization; +using Brewery.Abstractions.Auth; using Brewery.Abstractions.Messaging; using Humanizer; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using RabbitMQ.Client; +using RabbitMQ.Client.Events; namespace Brewery.Infrastructure.Messaging; public class RabbitMessagePublisher : IMessagePublisher { private readonly RabbitMqOptions _options; - private readonly IModel _channel; + private readonly ConnectionFactory _connectionFactory; + private readonly ILogger _logger; - public RabbitMessagePublisher(RabbitMqOptions options) + public RabbitMessagePublisher(RabbitMqOptions options, + ILogger logger) { _options = options; - var connectionFactory = new ConnectionFactory - { - HostName = _options.HostName, - }; + _connectionFactory = new ConnectionFactory { HostName = _options.HostName }; + _logger = logger; + } - var connection = connectionFactory.CreateConnection(); - _channel = connection.CreateModel(); + public async Task PublishAsync(TMessage message, string exchange) where TMessage : class, IMessage + { + var connection = await _connectionFactory.CreateConnectionAsync(); + var channel = await connection.CreateChannelAsync(); + + var correlationId = Guid.NewGuid().ToString(); + var callbackQueue = "brewery_id_service_callback_queue"; + + var routingKey = message.GetType().Name.Underscore(); + var json = JsonConvert.SerializeObject(message); + var body = Encoding.UTF8.GetBytes(json); + + var properties = new BasicProperties(); + properties.CorrelationId = correlationId; + properties.ReplyTo = callbackQueue; + + await channel.ExchangeDeclareAsync(exchange, ExchangeType.Direct, durable: true, autoDelete: false); + + await channel.BasicPublishAsync( + exchange, + routingKey, + false, + properties, + body); } - public Task PublishAsync(TMessage message, string exchange) where TMessage : class, IMessage + public async Task PublishAsync(TMessage message, string exchange) + where TMessage : class, IMessage where TResult : JsonWebToken { + var connection = await _connectionFactory.CreateConnectionAsync(); + var channel = await connection.CreateChannelAsync(); + var correlationId = Guid.NewGuid().ToString(); - var callbackQueue = "brewery-id-service-queue-jwt"; + var callbackQueue = "brewery_id_service_callback_queue"; var routingKey = message.GetType().Name.Underscore(); var json = JsonConvert.SerializeObject(message); var body = Encoding.UTF8.GetBytes(json); - var properties = _channel.CreateBasicProperties(); + var properties = new BasicProperties(); properties.CorrelationId = correlationId; properties.ReplyTo = callbackQueue; - _channel.ExchangeDeclare(exchange, ExchangeType.Direct, durable: true, autoDelete: false); + await channel.ExchangeDeclareAsync(exchange, ExchangeType.Direct, durable: true, autoDelete: false); - _channel.BasicPublish( + await channel.BasicPublishAsync( exchange: exchange, routingKey: routingKey, + mandatory: false, basicProperties: properties, body: body); + + var tcs = new TaskCompletionSource(); + + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.ReceivedAsync += (model, ea) => + { + _logger.LogInformation("Start consuming on message received"); + if (ea.BasicProperties.CorrelationId == correlationId) + { + var responseJson = Encoding.UTF8.GetString(ea.Body.ToArray()); + var responseMessage = JsonConvert.DeserializeObject(responseJson); + + tcs.SetResult(responseMessage); + + channel.BasicAckAsync(deliveryTag: ea.DeliveryTag, false); + } + + return Task.CompletedTask; + }; - return Task.CompletedTask; + await channel.BasicConsumeAsync(queue: callbackQueue, autoAck: false, consumer: consumer); + + return await tcs.Task; } } \ No newline at end of file From 6cb32ece194b6ec063557e3940fec9fe2aaa3576 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sat, 23 Nov 2024 13:58:52 +0000 Subject: [PATCH 22/24] add IConnectionFactory --- src/Brewery.Api/BreweryApi.rest | 2 +- .../Messaging/RabbitMessagePublisher.cs | 9 ++++----- .../Messaging/RabbitMq/IConnectionFactory.cs | 6 ++++++ 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionFactory.cs diff --git a/src/Brewery.Api/BreweryApi.rest b/src/Brewery.Api/BreweryApi.rest index 99c566f..d35604b 100644 --- a/src/Brewery.Api/BreweryApi.rest +++ b/src/Brewery.Api/BreweryApi.rest @@ -31,7 +31,7 @@ Content-Type: application/json { "email": "user@brewery.com", - "password": "secretBeer" + "password": "secret" } ### Get Account Info diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs index eeec8eb..921d220 100644 --- a/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs +++ b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs @@ -79,7 +79,7 @@ await channel.BasicPublishAsync( var tcs = new TaskCompletionSource(); var consumer = new AsyncEventingBasicConsumer(channel); - consumer.ReceivedAsync += (model, ea) => + consumer.ReceivedAsync += async (model, ea) => { _logger.LogInformation("Start consuming on message received"); if (ea.BasicProperties.CorrelationId == correlationId) @@ -89,14 +89,13 @@ await channel.BasicPublishAsync( tcs.SetResult(responseMessage); - channel.BasicAckAsync(deliveryTag: ea.DeliveryTag, false); + await channel.BasicAckAsync(deliveryTag: ea.DeliveryTag, false); } - - return Task.CompletedTask; + }; await channel.BasicConsumeAsync(queue: callbackQueue, autoAck: false, consumer: consumer); - + return await tcs.Task; } } \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionFactory.cs b/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionFactory.cs new file mode 100644 index 0000000..3405fa0 --- /dev/null +++ b/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionFactory.cs @@ -0,0 +1,6 @@ +namespace Brewery.Infrastructure.Messaging.RabbitMq; + +public interface IConnectionFactory +{ + +} \ No newline at end of file From ee192216945668cf962cfbebe55d49c512837b46 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sat, 23 Nov 2024 14:47:45 +0000 Subject: [PATCH 23/24] add ConnectionManager --- .../Messaging/RabbitMq/ConnectionManager.cs | 24 +++++++++++++++++++ .../Messaging/RabbitMq/IConnectionFactory.cs | 6 ----- .../Messaging/RabbitMq/IConnectionManager.cs | 8 +++++++ 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs delete mode 100644 src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionFactory.cs create mode 100644 src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionManager.cs diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs b/src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs new file mode 100644 index 0000000..8bc7240 --- /dev/null +++ b/src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs @@ -0,0 +1,24 @@ +using Brewery.Abstractions.Messaging; +using RabbitMQ.Client; + +namespace Brewery.Infrastructure.Messaging.RabbitMq; + +public class ConnectionManager : IConnectionManager +{ + private readonly IConnectionFactory _factory; + private readonly RabbitMqOptions _options; + + public ConnectionManager(RabbitMqOptions options) + { + _options = options; + _factory = new ConnectionFactory { HostName = _options.HostName }; + } + + public async Task CreateChannel() + { + var connection = await _factory.CreateConnectionAsync(); + var channel = await connection.CreateChannelAsync(); + + return channel; + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionFactory.cs b/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionFactory.cs deleted file mode 100644 index 3405fa0..0000000 --- a/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionFactory.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Brewery.Infrastructure.Messaging.RabbitMq; - -public interface IConnectionFactory -{ - -} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionManager.cs b/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionManager.cs new file mode 100644 index 0000000..fb45807 --- /dev/null +++ b/src/Brewery.Infrastructure/Messaging/RabbitMq/IConnectionManager.cs @@ -0,0 +1,8 @@ +using RabbitMQ.Client; + +namespace Brewery.Infrastructure.Messaging.RabbitMq; + +public interface IConnectionManager +{ + Task CreateChannel(); +} \ No newline at end of file From 1f474a1026a210d617c420bb298ec7cb52f89e65 Mon Sep 17 00:00:00 2001 From: SingleUnitBox Date: Sun, 24 Nov 2024 20:13:51 +0000 Subject: [PATCH 24/24] add BreweryUnitOfWork --- .../Controllers/BrewerController.cs | 1 + .../Controllers/BreweryController.cs | 1 + src/Brewery.Api/appsettings.json | 6 ++-- .../Commands/Decorated/DecoratorAttribute.cs | 7 ++++ .../UnitOfWorkDecoratedAddBeerSaleHandler.cs | 24 +++++++++++++ .../Commands/Extensions.cs | 6 +++- src/Brewery.Infrastructure/EF/Extensions.cs | 3 ++ .../EF/UnitOfWork/BreweryUnitOfWork.cs | 28 +++++++++++++++ .../EF/UnitOfWork/IBreweryUnitOfWork.cs | 8 +++++ .../EF/UnitOfWork/IUnitOfWork.cs | 6 ++++ .../Messaging/Extensions.cs | 2 ++ .../Messaging/RabbitMessagePublisher.cs | 35 ++++++++++--------- .../Messaging/RabbitMq/ConnectionManager.cs | 6 ++-- 13 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 src/Brewery.Infrastructure/Commands/Decorated/DecoratorAttribute.cs create mode 100644 src/Brewery.Infrastructure/Commands/Decorated/UnitOfWorkDecoratedAddBeerSaleHandler.cs create mode 100644 src/Brewery.Infrastructure/EF/UnitOfWork/BreweryUnitOfWork.cs create mode 100644 src/Brewery.Infrastructure/EF/UnitOfWork/IBreweryUnitOfWork.cs create mode 100644 src/Brewery.Infrastructure/EF/UnitOfWork/IUnitOfWork.cs diff --git a/src/Brewery.Api/Controllers/BrewerController.cs b/src/Brewery.Api/Controllers/BrewerController.cs index 5e73261..40611a4 100644 --- a/src/Brewery.Api/Controllers/BrewerController.cs +++ b/src/Brewery.Api/Controllers/BrewerController.cs @@ -3,6 +3,7 @@ using Brewery.Application.Commands; using Brewery.Application.DTO; using Brewery.Application.Queries; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Brewery.Api.Controllers; diff --git a/src/Brewery.Api/Controllers/BreweryController.cs b/src/Brewery.Api/Controllers/BreweryController.cs index 4d04030..6799ff3 100644 --- a/src/Brewery.Api/Controllers/BreweryController.cs +++ b/src/Brewery.Api/Controllers/BreweryController.cs @@ -27,6 +27,7 @@ public async Task> Get(Guid breweryId) return OkOrNotFound(brewery); } + [Authorize] [HttpGet("{breweryId:guid}/beers")] public async Task>> Browse(Guid breweryId) { diff --git a/src/Brewery.Api/appsettings.json b/src/Brewery.Api/appsettings.json index caab767..99b2c3f 100644 --- a/src/Brewery.Api/appsettings.json +++ b/src/Brewery.Api/appsettings.json @@ -10,9 +10,9 @@ "connectionString": "Host=localhost;Database=Brewery;Username=postgres;Password=czcz" }, "auth" : { - "IssuerSigningKey": "jrgvjirgavijpotu89fdsfdsfjngdjghnghv3498c938", - "issuer": "brewery", - "validIssuer": "brewery", + "IssuerSigningKey": "reaaaureurewruhewurewebnf89432483hgviufdsg8wre8hg", + "issuer": "brewery.identity.service", + "validIssuer": "brewery.identity.service", "validateIssuer": true, "validateAudience": false, "validateLifetime": true, diff --git a/src/Brewery.Infrastructure/Commands/Decorated/DecoratorAttribute.cs b/src/Brewery.Infrastructure/Commands/Decorated/DecoratorAttribute.cs new file mode 100644 index 0000000..a0de504 --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/Decorated/DecoratorAttribute.cs @@ -0,0 +1,7 @@ +namespace Brewery.Infrastructure.Commands.Decorated; + +[AttributeUsage(AttributeTargets.Class)] +public class DecoratorAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/Decorated/UnitOfWorkDecoratedAddBeerSaleHandler.cs b/src/Brewery.Infrastructure/Commands/Decorated/UnitOfWorkDecoratedAddBeerSaleHandler.cs new file mode 100644 index 0000000..47bcf65 --- /dev/null +++ b/src/Brewery.Infrastructure/Commands/Decorated/UnitOfWorkDecoratedAddBeerSaleHandler.cs @@ -0,0 +1,24 @@ +using Brewery.Abstractions.Commands; +using Brewery.Application.Commands; +using Brewery.Infrastructure.EF.UnitOfWork; + +namespace Brewery.Infrastructure.Commands.Decorated; + +[Decorator] +public class UnitOfWorkDecoratedAddBeerSaleHandler : ICommandHandler +{ + private readonly ICommandHandler _commandHandler; + private readonly BreweryUnitOfWork _breweryUnitOfWork; + + public UnitOfWorkDecoratedAddBeerSaleHandler(ICommandHandler commandHandler, + BreweryUnitOfWork breweryUnitOfWork) + { + _commandHandler = commandHandler; + _breweryUnitOfWork = breweryUnitOfWork; + } + + public async Task HandleAsync(AddBeerSale command) + { + await _breweryUnitOfWork.ExecuteAsync(() => _commandHandler.HandleAsync(command)); + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Commands/Extensions.cs b/src/Brewery.Infrastructure/Commands/Extensions.cs index bc90f30..0f2494f 100644 --- a/src/Brewery.Infrastructure/Commands/Extensions.cs +++ b/src/Brewery.Infrastructure/Commands/Extensions.cs @@ -3,6 +3,8 @@ using Brewery.Abstractions.Commands; using Brewery.Application.Commands; using Brewery.Application.Commands.Handlers; +using Brewery.Infrastructure.Commands.Decorated; +using Brewery.Infrastructure.EF.UnitOfWork; using Microsoft.Extensions.DependencyInjection; namespace Brewery.Infrastructure.Commands; @@ -14,10 +16,12 @@ public static IServiceCollection AddCommands(this IServiceCollection services, I services.AddSingleton(); services.Scan(a => a.FromAssemblies(assemblies) - .AddClasses(c => c.AssignableTo(typeof(ICommandHandler<>))) + .AddClasses(c => c.AssignableTo(typeof(ICommandHandler<>)) + .WithoutAttribute()) .AsImplementedInterfaces() .WithScopedLifetime()); services.AddScoped, SignInHandler>(); + services.TryDecorate, UnitOfWorkDecoratedAddBeerSaleHandler>(); return services; } diff --git a/src/Brewery.Infrastructure/EF/Extensions.cs b/src/Brewery.Infrastructure/EF/Extensions.cs index aa36766..7b46897 100644 --- a/src/Brewery.Infrastructure/EF/Extensions.cs +++ b/src/Brewery.Infrastructure/EF/Extensions.cs @@ -1,6 +1,7 @@ using Brewery.Abstractions.Postgres; using Brewery.Domain.Repositories; using Brewery.Infrastructure.EF.Repositories; +using Brewery.Infrastructure.EF.UnitOfWork; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -19,6 +20,8 @@ internal static IServiceCollection AddEF(this IServiceCollection services) services.AddScoped(); services.AddScoped(); + services.AddScoped(); + var postgresOptions = services.GetOptions("postgres"); services.AddDbContext(options => { diff --git a/src/Brewery.Infrastructure/EF/UnitOfWork/BreweryUnitOfWork.cs b/src/Brewery.Infrastructure/EF/UnitOfWork/BreweryUnitOfWork.cs new file mode 100644 index 0000000..20e68e1 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/UnitOfWork/BreweryUnitOfWork.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.UnitOfWork; + +public class BreweryUnitOfWork : IUnitOfWork +{ + private readonly BreweryDbContext _dbContext; + + public BreweryUnitOfWork(BreweryDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task ExecuteAsync(Func action) + { + await using var transaction = await _dbContext.Database.BeginTransactionAsync(); + try + { + await action(); + await transaction.CommitAsync(); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + throw; + } + } +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/UnitOfWork/IBreweryUnitOfWork.cs b/src/Brewery.Infrastructure/EF/UnitOfWork/IBreweryUnitOfWork.cs new file mode 100644 index 0000000..c2468c5 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/UnitOfWork/IBreweryUnitOfWork.cs @@ -0,0 +1,8 @@ +using Microsoft.EntityFrameworkCore; + +namespace Brewery.Infrastructure.EF.UnitOfWork; + +public interface IBreweryUnitOfWork where TDbContext : DbContext +{ + +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/EF/UnitOfWork/IUnitOfWork.cs b/src/Brewery.Infrastructure/EF/UnitOfWork/IUnitOfWork.cs new file mode 100644 index 0000000..6a64366 --- /dev/null +++ b/src/Brewery.Infrastructure/EF/UnitOfWork/IUnitOfWork.cs @@ -0,0 +1,6 @@ +namespace Brewery.Infrastructure.EF.UnitOfWork; + +public interface IUnitOfWork +{ + Task ExecuteAsync(Func action); +} \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Messaging/Extensions.cs b/src/Brewery.Infrastructure/Messaging/Extensions.cs index 0632703..aa1ba51 100644 --- a/src/Brewery.Infrastructure/Messaging/Extensions.cs +++ b/src/Brewery.Infrastructure/Messaging/Extensions.cs @@ -1,4 +1,5 @@ using Brewery.Abstractions.Messaging; +using Brewery.Infrastructure.Messaging.RabbitMq; using Microsoft.Extensions.DependencyInjection; namespace Brewery.Infrastructure.Messaging; @@ -10,6 +11,7 @@ public static IServiceCollection AddRabbitMq(this IServiceCollection services) var options = services.GetOptions("rabbitmq"); services.AddSingleton(options); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs index 921d220..5140b1f 100644 --- a/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs +++ b/src/Brewery.Infrastructure/Messaging/RabbitMessagePublisher.cs @@ -1,7 +1,7 @@ using System.Text; -using System.Text.Json.Serialization; using Brewery.Abstractions.Auth; using Brewery.Abstractions.Messaging; +using Brewery.Infrastructure.Messaging.RabbitMq; using Humanizer; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -13,21 +13,21 @@ namespace Brewery.Infrastructure.Messaging; public class RabbitMessagePublisher : IMessagePublisher { private readonly RabbitMqOptions _options; - private readonly ConnectionFactory _connectionFactory; + private IConnectionManager _connectionManager; private readonly ILogger _logger; public RabbitMessagePublisher(RabbitMqOptions options, - ILogger logger) + ILogger logger, + IConnectionManager connectionManager) { _options = options; - _connectionFactory = new ConnectionFactory { HostName = _options.HostName }; + _connectionManager = connectionManager; _logger = logger; } public async Task PublishAsync(TMessage message, string exchange) where TMessage : class, IMessage { - var connection = await _connectionFactory.CreateConnectionAsync(); - var channel = await connection.CreateChannelAsync(); + var channel = await _connectionManager.CreateChannel(); var correlationId = Guid.NewGuid().ToString(); var callbackQueue = "brewery_id_service_callback_queue"; @@ -53,22 +53,21 @@ await channel.BasicPublishAsync( public async Task PublishAsync(TMessage message, string exchange) where TMessage : class, IMessage where TResult : JsonWebToken { - var connection = await _connectionFactory.CreateConnectionAsync(); - var channel = await connection.CreateChannelAsync(); + var channel = await _connectionManager.CreateChannel(); var correlationId = Guid.NewGuid().ToString(); var callbackQueue = "brewery_id_service_callback_queue"; - + var routingKey = message.GetType().Name.Underscore(); var json = JsonConvert.SerializeObject(message); var body = Encoding.UTF8.GetBytes(json); - + var properties = new BasicProperties(); properties.CorrelationId = correlationId; properties.ReplyTo = callbackQueue; - + await channel.ExchangeDeclareAsync(exchange, ExchangeType.Direct, durable: true, autoDelete: false); - + await channel.BasicPublishAsync( exchange: exchange, routingKey: routingKey, @@ -77,7 +76,7 @@ await channel.BasicPublishAsync( body: body); var tcs = new TaskCompletionSource(); - + var consumer = new AsyncEventingBasicConsumer(channel); consumer.ReceivedAsync += async (model, ea) => { @@ -91,11 +90,15 @@ await channel.BasicPublishAsync( await channel.BasicAckAsync(deliveryTag: ea.DeliveryTag, false); } - + }; - + await channel.BasicConsumeAsync(queue: callbackQueue, autoAck: false, consumer: consumer); + var result = await tcs.Task; + + await channel.DisposeAsync(); + + return result; - return await tcs.Task; } } \ No newline at end of file diff --git a/src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs b/src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs index 8bc7240..b6f605b 100644 --- a/src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs +++ b/src/Brewery.Infrastructure/Messaging/RabbitMq/ConnectionManager.cs @@ -7,16 +7,18 @@ public class ConnectionManager : IConnectionManager { private readonly IConnectionFactory _factory; private readonly RabbitMqOptions _options; - + private readonly Lazy> _lazyConnection; + public ConnectionManager(RabbitMqOptions options) { _options = options; _factory = new ConnectionFactory { HostName = _options.HostName }; + _lazyConnection = new Lazy>(() => _factory.CreateConnectionAsync()); } public async Task CreateChannel() { - var connection = await _factory.CreateConnectionAsync(); + var connection = await _lazyConnection.Value; var channel = await connection.CreateChannelAsync(); return channel;