├── .github ├── arbems.jpg └── icon.png ├── .gitignore ├── CleanArchitectureSolution.sln ├── README.md ├── src ├── Application │ ├── Application.csproj │ ├── Common │ │ ├── Exceptions │ │ │ ├── ForbiddenAccessException.cs │ │ │ ├── NotFoundException.cs │ │ │ └── ValidationException.cs │ │ ├── Interfaces │ │ │ ├── IApplicationDbContext.cs │ │ │ ├── IDateTime.cs │ │ │ └── IDomainEventService.cs │ │ ├── Mappings │ │ │ ├── IMapFrom.cs │ │ │ ├── MappingExtensions.cs │ │ │ └── MappingProfile.cs │ │ └── Models │ │ │ ├── DomainEventNotification.cs │ │ │ ├── PaginatedList.cs │ │ │ └── Result.cs │ ├── DependencyInjection.cs │ ├── SuperHeroes │ │ ├── Commands │ │ │ ├── CreateHero │ │ │ │ ├── CreateHeroCommand.cs │ │ │ │ └── CreateHeroCommandValidator.cs │ │ │ ├── DeleteHero │ │ │ │ └── DeleteHeroCommand.cs │ │ │ └── UpdateHero │ │ │ │ ├── UpdateHeroCommand.cs │ │ │ │ └── UpdateHeroCommandValidator.cs │ │ └── Queries │ │ │ └── GetHeroesWithPagination │ │ │ ├── AttributeDto.cs │ │ │ ├── PowerDto.cs │ │ │ ├── PublisherDto.cs │ │ │ ├── RaceDto.cs │ │ │ └── SuperheroDto.cs │ └── Superheroes │ │ ├── EventHandlers │ │ ├── SuperheroCreatedEventHandler.cs │ │ └── SuperheroDeletedEventHandler.cs │ │ └── Queries │ │ └── GetHeroesWithPagination │ │ ├── GetSuperheroQuery.cs │ │ └── GetSuperheroesWithPaginationQuery.cs ├── Domain │ ├── Common │ │ ├── AuditableEntity.cs │ │ ├── DomainEvent.cs │ │ └── ValueObject.cs │ ├── Domain.csproj │ ├── Entities │ │ ├── Attribute.cs │ │ ├── Power.cs │ │ ├── Publisher.cs │ │ ├── Race.cs │ │ └── Superhero.cs │ ├── Events │ │ ├── SuperheroCreatedEvent.cs │ │ └── SuperheroDeletedEvent.cs │ ├── Exceptions │ │ ├── UnsupportedAlignmentException.cs │ │ ├── UnsupportedColourException.cs │ │ └── UnsupportedGenderException.cs │ └── ValueObjects │ │ ├── Alignment.cs │ │ ├── Colour.cs │ │ └── Gender.cs ├── Infrastructure │ ├── DependencyInjection.cs │ ├── Infrastructure.csproj │ ├── Persistence │ │ ├── ApplicationDbContext.cs │ │ ├── ApplicationDbContextSeed.cs │ │ └── Configurations │ │ │ ├── AttributeConfiguration.cs │ │ │ ├── PowerConfiguration.cs │ │ │ ├── PublisherConfiguration.cs │ │ │ ├── RaceConfiguration.cs │ │ │ └── SuperheroConfiguration.cs │ └── Services │ │ ├── DateTimeService.cs │ │ └── DomainEventService.cs └── PublicAPI │ ├── Controllers │ ├── ApiControllerBase.cs │ └── SuperheroesController.cs │ ├── Filters │ └── ApiExceptionFilterAttribute.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── PublicAPI.csproj │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ └── swagger-ui │ └── custom.css └── tests ├── Application.IntegrationTests └── Application.IntegrationTests.csproj ├── Application.UnitTests └── Application.UnitTests.csproj └── Domain.UnitTests └── Domain.UnitTests.csproj /.github/arbems.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arbems/Clean-Architecture-Solution/9a8abc79583d74d13d329ec3059e3ace16560716/.github/arbems.jpg -------------------------------------------------------------------------------- /.github/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arbems/Clean-Architecture-Solution/9a8abc79583d74d13d329ec3059e3ace16560716/.github/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /CleanArchitectureSolution.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 25.0.1700.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{88F2B662-375A-4EB8-941B-2ADCEBF96416}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9E289593-5AA7-471F-80C1-6ED91210E3B3}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "src\Application\Application.csproj", "{A59D91DA-92AE-4139-B9C9-C5F33F77AF43}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{9025F4D8-14E5-4B1B-A634-156EB0C41379}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "src\Domain\Domain.csproj", "{AFA15A39-AABC-40A8-B227-A6A5D8738789}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.IntegrationTests", "tests\Application.IntegrationTests\Application.IntegrationTests.csproj", "{14234959-DF2D-4EDF-A02C-F17C7AF684B1}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.UnitTests", "tests\Application.UnitTests\Application.UnitTests.csproj", "{004540C3-BD92-4593-814A-251D70D7F3FD}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain.UnitTests", "tests\Domain.UnitTests\Domain.UnitTests.csproj", "{A10C34B0-F784-4312-BCC2-75FF894BD803}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicAPI", "src\PublicAPI\PublicAPI.csproj", "{1C894B61-A86A-44DC-88D5-316318578EB6}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {A59D91DA-92AE-4139-B9C9-C5F33F77AF43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {A59D91DA-92AE-4139-B9C9-C5F33F77AF43}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {A59D91DA-92AE-4139-B9C9-C5F33F77AF43}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {A59D91DA-92AE-4139-B9C9-C5F33F77AF43}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {9025F4D8-14E5-4B1B-A634-156EB0C41379}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {9025F4D8-14E5-4B1B-A634-156EB0C41379}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {9025F4D8-14E5-4B1B-A634-156EB0C41379}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {9025F4D8-14E5-4B1B-A634-156EB0C41379}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {AFA15A39-AABC-40A8-B227-A6A5D8738789}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {AFA15A39-AABC-40A8-B227-A6A5D8738789}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {AFA15A39-AABC-40A8-B227-A6A5D8738789}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {AFA15A39-AABC-40A8-B227-A6A5D8738789}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {14234959-DF2D-4EDF-A02C-F17C7AF684B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {14234959-DF2D-4EDF-A02C-F17C7AF684B1}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {14234959-DF2D-4EDF-A02C-F17C7AF684B1}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {14234959-DF2D-4EDF-A02C-F17C7AF684B1}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {004540C3-BD92-4593-814A-251D70D7F3FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {004540C3-BD92-4593-814A-251D70D7F3FD}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {004540C3-BD92-4593-814A-251D70D7F3FD}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {004540C3-BD92-4593-814A-251D70D7F3FD}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {A10C34B0-F784-4312-BCC2-75FF894BD803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {A10C34B0-F784-4312-BCC2-75FF894BD803}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {A10C34B0-F784-4312-BCC2-75FF894BD803}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {A10C34B0-F784-4312-BCC2-75FF894BD803}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {1C894B61-A86A-44DC-88D5-316318578EB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {1C894B61-A86A-44DC-88D5-316318578EB6}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {1C894B61-A86A-44DC-88D5-316318578EB6}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {1C894B61-A86A-44DC-88D5-316318578EB6}.Release|Any CPU.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(ExtensibilityGlobals) = postSolution 63 | SolutionGuid = {F6808069-24F5-46D9-A472-197C900BCD7E} 64 | EndGlobalSection 65 | GlobalSection(NestedProjects) = preSolution 66 | {A59D91DA-92AE-4139-B9C9-C5F33F77AF43} = {88F2B662-375A-4EB8-941B-2ADCEBF96416} 67 | {9025F4D8-14E5-4B1B-A634-156EB0C41379} = {88F2B662-375A-4EB8-941B-2ADCEBF96416} 68 | {AFA15A39-AABC-40A8-B227-A6A5D8738789} = {88F2B662-375A-4EB8-941B-2ADCEBF96416} 69 | {14234959-DF2D-4EDF-A02C-F17C7AF684B1} = {9E289593-5AA7-471F-80C1-6ED91210E3B3} 70 | {004540C3-BD92-4593-814A-251D70D7F3FD} = {9E289593-5AA7-471F-80C1-6ED91210E3B3} 71 | {A10C34B0-F784-4312-BCC2-75FF894BD803} = {9E289593-5AA7-471F-80C1-6ED91210E3B3} 72 | {1C894B61-A86A-44DC-88D5-316318578EB6} = {88F2B662-375A-4EB8-941B-2ADCEBF96416} 73 | EndGlobalSection 74 | EndGlobal 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Clean Architecture Solution .NET 6 4 | Example of a solution following the principles of Clean Architecture using .NET 6 5 | 6 | ## Tecnologías 7 | * NET 6 / C# 8 | * ASP.NET Core 6 9 | * Entity Framework Core 6 10 | * Angular 12 11 | * MediatR 12 | * AutoMapper 13 | * FluentValidation 14 | * NUnit, FluentAssertions, Moq & Respawn 15 | * Docker 16 | -------------------------------------------------------------------------------- /src/Application/Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Application/Common/Exceptions/ForbiddenAccessException.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Exceptions; 2 | 3 | public class ForbiddenAccessException : Exception 4 | { 5 | public ForbiddenAccessException() : base() { } 6 | } 7 | -------------------------------------------------------------------------------- /src/Application/Common/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Exceptions; 2 | 3 | public class NotFoundException : Exception 4 | { 5 | public NotFoundException() 6 | : base() 7 | { 8 | } 9 | 10 | public NotFoundException(string message) 11 | : base(message) 12 | { 13 | } 14 | 15 | public NotFoundException(string message, Exception innerException) 16 | : base(message, innerException) 17 | { 18 | } 19 | 20 | public NotFoundException(string name, object key) 21 | : base($"Entity \"{name}\" ({key}) was not found.") 22 | { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Application/Common/Exceptions/ValidationException.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.Results; 2 | 3 | namespace Application.Common.Exceptions; 4 | 5 | public class ValidationException : Exception 6 | { 7 | public ValidationException() 8 | : base("One or more validation failures have occurred.") 9 | { 10 | Errors = new Dictionary(); 11 | } 12 | 13 | public ValidationException(IEnumerable failures) 14 | : this() 15 | { 16 | Errors = failures 17 | .GroupBy(e => e.PropertyName, e => e.ErrorMessage) 18 | .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()); 19 | } 20 | 21 | public IDictionary Errors { get; } 22 | } 23 | -------------------------------------------------------------------------------- /src/Application/Common/Interfaces/IApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace Application.Common.Interfaces; 5 | 6 | public interface IApplicationDbContext 7 | { 8 | public DbSet Attributes { get; } 9 | public DbSet Publishers { get; } 10 | public DbSet Races { get; } 11 | public DbSet Superheroes { get; } 12 | public DbSet Powers { get; } 13 | 14 | Task SaveChangesAsync(CancellationToken cancellationToken); 15 | } 16 | -------------------------------------------------------------------------------- /src/Application/Common/Interfaces/IDateTime.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Interfaces; 2 | 3 | public interface IDateTime 4 | { 5 | DateTime Now { get; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Application/Common/Interfaces/IDomainEventService.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | 3 | namespace Application.Common.Interfaces; 4 | 5 | public interface IDomainEventService 6 | { 7 | Task Publish(DomainEvent domainEvent); 8 | } 9 | -------------------------------------------------------------------------------- /src/Application/Common/Mappings/IMapFrom.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace Application.Common.Mappings; 4 | 5 | public interface IMapFrom 6 | { 7 | void Mapping(Profile profile) => profile.CreateMap(typeof(T), GetType()); 8 | } 9 | -------------------------------------------------------------------------------- /src/Application/Common/Mappings/MappingExtensions.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Models; 2 | using AutoMapper; 3 | using AutoMapper.QueryableExtensions; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Application.Common.Mappings; 7 | 8 | public static class MappingExtensions 9 | { 10 | public static Task> PaginatedListAsync(this IQueryable queryable, int pageNumber, int pageSize) 11 | => PaginatedList.CreateAsync(queryable, pageNumber, pageSize); 12 | 13 | public static Task> ProjectToListAsync(this IQueryable queryable, IConfigurationProvider configuration) 14 | => queryable.ProjectTo(configuration).ToListAsync(); 15 | } 16 | -------------------------------------------------------------------------------- /src/Application/Common/Mappings/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using AutoMapper; 3 | 4 | namespace Application.Common.Mappings; 5 | 6 | public class MappingProfile : Profile 7 | { 8 | public MappingProfile() 9 | { 10 | ApplyMappingsFromAssembly(Assembly.GetExecutingAssembly()); 11 | } 12 | 13 | private void ApplyMappingsFromAssembly(Assembly assembly) 14 | { 15 | var types = assembly.GetExportedTypes() 16 | .Where(t => t.GetInterfaces().Any(i => 17 | i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>))) 18 | .ToList(); 19 | 20 | foreach (var type in types) 21 | { 22 | var instance = Activator.CreateInstance(type); 23 | 24 | var methodInfo = type.GetMethod("Mapping") 25 | ?? type.GetInterface("IMapFrom`1")!.GetMethod("Mapping"); 26 | 27 | methodInfo?.Invoke(instance, new object[] { this }); 28 | 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Application/Common/Models/DomainEventNotification.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | using MediatR; 3 | 4 | namespace Application.Common.Models; 5 | 6 | public class DomainEventNotification : INotification where TDomainEvent : DomainEvent 7 | { 8 | public DomainEventNotification(TDomainEvent domainEvent) 9 | { 10 | DomainEvent = domainEvent; 11 | } 12 | 13 | public TDomainEvent DomainEvent { get; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Application/Common/Models/PaginatedList.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Application.Common.Models; 4 | 5 | public class PaginatedList 6 | { 7 | public List Items { get; } 8 | public int PageNumber { get; } 9 | public int TotalPages { get; } 10 | public int TotalCount { get; } 11 | 12 | public PaginatedList(List items, int count, int pageNumber, int pageSize) 13 | { 14 | PageNumber = pageNumber; 15 | TotalPages = (int)Math.Ceiling(count / (double)pageSize); 16 | TotalCount = count; 17 | Items = items; 18 | } 19 | 20 | public bool HasPreviousPage => PageNumber > 1; 21 | 22 | public bool HasNextPage => PageNumber < TotalPages; 23 | 24 | public static async Task> CreateAsync(IQueryable source, int pageNumber, int pageSize) 25 | { 26 | var count = await source.CountAsync(); 27 | var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(); 28 | 29 | return new PaginatedList(items, count, pageNumber, pageSize); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Application/Common/Models/Result.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Models; 2 | 3 | public class Result 4 | { 5 | internal Result(bool succeeded, IEnumerable errors) 6 | { 7 | Succeeded = succeeded; 8 | Errors = errors.ToArray(); 9 | } 10 | 11 | public bool Succeeded { get; set; } 12 | 13 | public string[] Errors { get; set; } 14 | 15 | public static Result Success() 16 | { 17 | return new Result(true, Array.Empty()); 18 | } 19 | 20 | public static Result Failure(IEnumerable errors) 21 | { 22 | return new Result(false, errors); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Application/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using MediatR; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System.Reflection; 5 | 6 | namespace Application; 7 | 8 | public static class DependencyInjection 9 | { 10 | public static IServiceCollection AddApplication(this IServiceCollection services) 11 | { 12 | services.AddAutoMapper(Assembly.GetExecutingAssembly()); 13 | services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); 14 | services.AddMediatR(Assembly.GetExecutingAssembly()); 15 | 16 | return services; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Application/SuperHeroes/Commands/CreateHero/CreateHeroCommand.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interfaces; 2 | using Domain.Entities; 3 | using Domain.Events; 4 | using Domain.ValueObjects; 5 | using MediatR; 6 | 7 | namespace Application.Superheroes.Commands.CreateHero; 8 | 9 | public class CreateHeroCommand : IRequest 10 | { 11 | public string SuperheroName { get; set; } = String.Empty; 12 | public string? FullName { get; set; } = String.Empty; 13 | public int? HeightCm { get; set; } 14 | public int? WeightKg { get; set; } 15 | 16 | public string? EyeColour { get; set; } 17 | public string? HairColour { get; set; } 18 | public string? SkinColour { get; set; } 19 | public string? Alignment { get; set; } 20 | public string? Gender { get; set; } 21 | 22 | public int? PublisherId { get; set; } 23 | 24 | public int? RaceId { get; set; } 25 | 26 | public List Attributes { get; set; } = new(); 27 | public List Powers { get; set; } = new(); 28 | } 29 | 30 | public class CreateHeroCommandHandler : IRequestHandler 31 | { 32 | private readonly IApplicationDbContext _context; 33 | 34 | public CreateHeroCommandHandler(IApplicationDbContext context) 35 | { 36 | _context = context; 37 | } 38 | 39 | public async Task Handle(CreateHeroCommand request, CancellationToken cancellationToken) 40 | { 41 | Superhero entity = new() 42 | { 43 | SuperheroName = request.SuperheroName, 44 | FullName = request.FullName, 45 | HeightCm = request.HeightCm, 46 | WeightKg = request.WeightKg, 47 | EyeColour = string.IsNullOrEmpty(request.EyeColour) ? Colour.NoColour : Colour.From(request.EyeColour), 48 | HairColour = string.IsNullOrEmpty(request.HairColour) ? Colour.NoColour : Colour.From(request.HairColour), 49 | SkinColour = string.IsNullOrEmpty(request.SkinColour) ? Colour.NoColour : Colour.From(request.SkinColour), 50 | Alignment = string.IsNullOrEmpty(request.Alignment) ? Alignment.NA : Alignment.From(request.Alignment), 51 | Gender = string.IsNullOrEmpty(request.Gender) ? Gender.NA : Gender.From(request.Gender), 52 | Publisher = _context.Publishers.FindAsync(request.PublisherId).Result, 53 | Race = _context.Races.FindAsync(request.RaceId).Result, 54 | Attributes = _context.Attributes.Where(a => request.Attributes.Contains(a.Id)).ToList(), 55 | Powers = _context.Powers.Where(a => request.Powers.Contains(a.Id)).ToList() 56 | }; 57 | 58 | entity.DomainEvents.Add(new SuperheroCreatedEvent(entity)); 59 | 60 | _context.Superheroes.Add(entity); 61 | 62 | await _context.SaveChangesAsync(cancellationToken); 63 | 64 | return entity.Id; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Application/SuperHeroes/Commands/CreateHero/CreateHeroCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace Application.Superheroes.Commands.CreateHero; 4 | 5 | public class CreateHeroCommandValidator : AbstractValidator 6 | { 7 | public CreateHeroCommandValidator() 8 | { 9 | RuleFor(v => v.SuperheroName) 10 | .MaximumLength(200) 11 | .NotEmpty(); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Application/SuperHeroes/Commands/DeleteHero/DeleteHeroCommand.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Exceptions; 2 | using Application.Common.Interfaces; 3 | using Domain.Entities; 4 | using Domain.Events; 5 | using MediatR; 6 | 7 | namespace Application.Superheroes.Commands.DeleteHero; 8 | 9 | public class DeleteSuperheroCommand : IRequest 10 | { 11 | public int Id { get; set; } 12 | } 13 | 14 | public class DeleteSuperheroCommandHandler : IRequestHandler 15 | { 16 | private readonly IApplicationDbContext _context; 17 | 18 | public DeleteSuperheroCommandHandler(IApplicationDbContext context) 19 | { 20 | _context = context; 21 | } 22 | 23 | public async Task Handle(DeleteSuperheroCommand request, CancellationToken cancellationToken) 24 | { 25 | var entity = await _context.Superheroes 26 | .FindAsync(new object[] { request.Id }, cancellationToken); 27 | 28 | if (entity == null) 29 | { 30 | throw new NotFoundException(nameof(Superhero), request.Id); 31 | } 32 | 33 | _context.Superheroes.Remove(entity); 34 | 35 | entity.DomainEvents.Add(new SuperheroDeletedEvent(entity)); 36 | 37 | await _context.SaveChangesAsync(cancellationToken); 38 | 39 | return Unit.Value; 40 | } 41 | } -------------------------------------------------------------------------------- /src/Application/SuperHeroes/Commands/UpdateHero/UpdateHeroCommand.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Exceptions; 2 | using Application.Common.Interfaces; 3 | using Domain.Entities; 4 | using Domain.ValueObjects; 5 | using MediatR; 6 | 7 | namespace Application.Superheroes.Commands.UpdateHero; 8 | 9 | public class UpdateHeroCommand : IRequest 10 | { 11 | public int Id { get; set; } 12 | public string SuperheroName { get; set; } = String.Empty; 13 | public string? FullName { get; set; } = String.Empty; 14 | public int? HeightCm { get; set; } 15 | public int? WeightKg { get; set; } 16 | 17 | public string? EyeColour { get; set; } 18 | public string? HairColour { get; set; } 19 | public string? SkinColour { get; set; } 20 | public string? Alignment { get; set; } 21 | public string? Gender { get; set; } 22 | 23 | public int? PublisherId { get; set; } 24 | 25 | public int? RaceId { get; set; } 26 | 27 | public List Attributes { get; set; } = new(); 28 | public List Powers { get; set; } = new(); 29 | } 30 | 31 | public class UpdateHeroCommandHandler : IRequestHandler 32 | { 33 | private readonly IApplicationDbContext _context; 34 | 35 | public UpdateHeroCommandHandler(IApplicationDbContext context) 36 | { 37 | _context = context; 38 | } 39 | 40 | public async Task Handle(UpdateHeroCommand request, CancellationToken cancellationToken) 41 | { 42 | var entity = await _context.Superheroes 43 | .FindAsync(new object[] { request.Id }, cancellationToken); 44 | 45 | if (entity == null) 46 | { 47 | throw new NotFoundException(nameof(Superhero), request.Id); 48 | } 49 | 50 | entity.SuperheroName = request.SuperheroName; 51 | entity.FullName = request.FullName; 52 | entity.HeightCm = request.HeightCm; 53 | entity.WeightKg = request.WeightKg; 54 | entity.EyeColour = string.IsNullOrEmpty(request.EyeColour) ? Colour.NoColour : Colour.From(request.EyeColour); 55 | entity.HairColour = string.IsNullOrEmpty(request.HairColour) ? Colour.NoColour : Colour.From(request.HairColour); 56 | entity.SkinColour = string.IsNullOrEmpty(request.SkinColour) ? Colour.NoColour : Colour.From(request.SkinColour); 57 | entity.Alignment = string.IsNullOrEmpty(request.Alignment) ? Alignment.NA : Alignment.From(request.Alignment); 58 | entity.Gender = string.IsNullOrEmpty(request.Gender) ? Gender.NA : Gender.From(request.Gender); 59 | entity.Publisher = _context.Publishers.FindAsync(request.PublisherId).Result; 60 | entity.Race = _context.Races.FindAsync(request.RaceId).Result; 61 | entity.Attributes = _context.Attributes.Where(a => request.Attributes.Contains(a.Id)).ToList(); 62 | entity.Powers = _context.Powers.Where(a => request.Powers.Contains(a.Id)).ToList(); 63 | 64 | await _context.SaveChangesAsync(cancellationToken); 65 | 66 | return Unit.Value; 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/Application/SuperHeroes/Commands/UpdateHero/UpdateHeroCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace Application.Superheroes.Commands.UpdateHero; 4 | 5 | public class UpdateHeroCommandValidator : AbstractValidator 6 | { 7 | public UpdateHeroCommandValidator() 8 | { 9 | RuleFor(v => v.SuperheroName) 10 | .MaximumLength(200) 11 | .NotEmpty(); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Application/SuperHeroes/Queries/GetHeroesWithPagination/AttributeDto.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Mappings; 2 | 3 | namespace Application.Superheroes.Queries.GetHeroesWithPagination; 4 | 5 | public class AttributeDto : IMapFrom 6 | { 7 | public int Id { get; set; } 8 | public string? AttributeName { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Application/SuperHeroes/Queries/GetHeroesWithPagination/PowerDto.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Mappings; 2 | using Domain.Entities; 3 | 4 | namespace Application.Superheroes.Queries.GetHeroesWithPagination; 5 | 6 | public class PowerDto : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | public string PowerName { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Application/SuperHeroes/Queries/GetHeroesWithPagination/PublisherDto.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Mappings; 2 | using Domain.Entities; 3 | 4 | namespace Application.Superheroes.Queries.GetHeroesWithPagination; 5 | 6 | public class PublisherDto : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | public string? PublisherName { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Application/SuperHeroes/Queries/GetHeroesWithPagination/RaceDto.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Mappings; 2 | using Domain.Entities; 3 | 4 | namespace Application.Superheroes.Queries.GetHeroesWithPagination; 5 | 6 | public class RaceDto : IMapFrom 7 | { 8 | public int Id { get; set; } 9 | public string? RaceName { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Application/SuperHeroes/Queries/GetHeroesWithPagination/SuperheroDto.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Mappings; 2 | using Domain.Entities; 3 | using Domain.ValueObjects; 4 | 5 | namespace Application.Superheroes.Queries.GetHeroesWithPagination; 6 | 7 | public class SuperheroDto : IMapFrom 8 | { 9 | public int Id { get; set; } 10 | public string SuperheroName { get; set; } = String.Empty; 11 | public string FullName { get; set; } = String.Empty; 12 | public int? HeightCm { get; set; } 13 | public int? WeightKg { get; set; } 14 | 15 | public Colour? EyeColour { get; set; } = Colour.NoColour; 16 | public Colour? HairColour { get; set; } = Colour.NoColour; 17 | public Colour? SkinColour { get; set; } = Colour.NoColour; 18 | public Alignment? Alignment { get; set; } = Alignment.NA; 19 | public Gender? Gender { get; set; } = Gender.NA; 20 | 21 | public PublisherDto? Publisher { get; set; } 22 | 23 | public RaceDto? Race { get; set; } 24 | 25 | public List Attributes { get; set; } = new(); 26 | public List Powers { get; set; } = new(); 27 | 28 | public DateTime Created { get; set; } 29 | public string? CreatedBy { get; set; } 30 | public DateTime? LastModified { get; set; } 31 | public string? LastModifiedBy { get; set; } 32 | public Guid RowVersion { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/Application/Superheroes/EventHandlers/SuperheroCreatedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Models; 2 | using Domain.Events; 3 | using MediatR; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Application.Superheroes.EventHandlers; 7 | 8 | public class SuperheroCreatedEventHandler : INotificationHandler> 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public SuperheroCreatedEventHandler(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | public Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) 18 | { 19 | var domainEvent = notification.DomainEvent; 20 | 21 | _logger.LogInformation("Domain Event: {DomainEvent}", domainEvent.GetType().Name); 22 | 23 | return Task.CompletedTask; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Application/Superheroes/EventHandlers/SuperheroDeletedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Models; 2 | using Domain.Events; 3 | using MediatR; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Application.Superheroes.EventHandlers; 7 | 8 | public class SuperheroDeletedEventHandler : INotificationHandler> 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public SuperheroDeletedEventHandler(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | public Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) 18 | { 19 | var domainEvent = notification.DomainEvent; 20 | 21 | _logger.LogInformation("Domain Event: {DomainEvent}", domainEvent.GetType().Name); 22 | 23 | return Task.CompletedTask; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Application/Superheroes/Queries/GetHeroesWithPagination/GetSuperheroQuery.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interfaces; 2 | using AutoMapper; 3 | using Domain.Entities; 4 | using MediatR; 5 | 6 | namespace Application.Superheroes.Queries.GetHeroesWithPagination; 7 | 8 | public class GetSuperheroQuery : IRequest 9 | { 10 | public int Id { get; set; } 11 | } 12 | 13 | public class GetSuperheroQueryHandler : IRequestHandler 14 | { 15 | private readonly IApplicationDbContext _context; 16 | private readonly IMapper _mapper; 17 | 18 | public GetSuperheroQueryHandler(IApplicationDbContext context, IMapper mapper) 19 | { 20 | _context = context; 21 | _mapper = mapper; 22 | } 23 | 24 | public async Task Handle(GetSuperheroQuery request, CancellationToken cancellationToken) 25 | { 26 | var entity = await _context.Superheroes.FindAsync(request.Id); 27 | 28 | return _mapper.Map(entity!); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/Application/Superheroes/Queries/GetHeroesWithPagination/GetSuperheroesWithPaginationQuery.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interfaces; 2 | using Application.Common.Mappings; 3 | using Application.Common.Models; 4 | using Application.Superheroes.Queries.GetHeroesWithPagination; 5 | using AutoMapper; 6 | using AutoMapper.QueryableExtensions; 7 | using MediatR; 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | namespace Application.Superheroes.Queries.GetHeroesWithPagination; 11 | 12 | public class GetSuperheroesWithPaginationQuery : IRequest> 13 | { 14 | public int PageNumber { get; set; } = 1; 15 | public int PageSize { get; set; } = 10; 16 | } 17 | 18 | public class GetSuperheroesWithPaginationQueryHandler : IRequestHandler> 19 | { 20 | private readonly IApplicationDbContext _context; 21 | private readonly IMapper _mapper; 22 | 23 | public GetSuperheroesWithPaginationQueryHandler(IApplicationDbContext context, IMapper mapper) 24 | { 25 | _context = context; 26 | _mapper = mapper; 27 | } 28 | 29 | public async Task> Handle(GetSuperheroesWithPaginationQuery request, CancellationToken cancellationToken) 30 | { 31 | return await _context.Superheroes.AsNoTracking() 32 | .OrderBy(x => x.SuperheroName) 33 | .ProjectTo(_mapper.ConfigurationProvider) 34 | .PaginatedListAsync(request.PageNumber, request.PageSize); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Domain/Common/AuditableEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Domain.Common 5 | { 6 | public abstract class AuditableEntity 7 | { 8 | public DateTime Created { get; set; } 9 | 10 | public string? CreatedBy { get; set; } 11 | 12 | public DateTime? LastModified { get; set; } 13 | 14 | public string? LastModifiedBy { get; set; } 15 | 16 | public Guid RowVersion { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Domain/Common/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Common; 2 | 3 | public interface IHasDomainEvent 4 | { 5 | public List DomainEvents { get; set; } 6 | } 7 | 8 | public abstract class DomainEvent 9 | { 10 | protected DomainEvent() 11 | { 12 | DateOccurred = DateTimeOffset.UtcNow; 13 | } 14 | public bool IsPublished { get; set; } 15 | public DateTimeOffset DateOccurred { get; protected set; } = DateTime.UtcNow; 16 | } 17 | -------------------------------------------------------------------------------- /src/Domain/Common/ValueObject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Domain.Common 5 | { 6 | public abstract class ValueObject 7 | { 8 | protected static bool EqualOperator(ValueObject left, ValueObject right) 9 | { 10 | if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null)) 11 | { 12 | return false; 13 | } 14 | return ReferenceEquals(left, null) || left.Equals(right); 15 | } 16 | 17 | protected static bool NotEqualOperator(ValueObject left, ValueObject right) 18 | { 19 | return !(EqualOperator(left, right)); 20 | } 21 | 22 | protected abstract IEnumerable GetEqualityComponents(); 23 | 24 | public override bool Equals(object obj) 25 | { 26 | if (obj == null || obj.GetType() != GetType()) 27 | { 28 | return false; 29 | } 30 | 31 | var other = (ValueObject)obj; 32 | 33 | return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); 34 | } 35 | 36 | public override int GetHashCode() 37 | { 38 | return GetEqualityComponents() 39 | .Select(x => x != null ? x.GetHashCode() : 0) 40 | .Aggregate((x, y) => x ^ y); 41 | } 42 | // Other utility methods 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Domain/Entities/Attribute.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Domain.Common; 3 | 4 | namespace Domain.Entities; 5 | 6 | public class Attribute : AuditableEntity 7 | { 8 | public int Id { get; set; } 9 | public string? AttributeName { get; set; } 10 | 11 | public IList Heroes { get; set; } = new List(); 12 | } 13 | -------------------------------------------------------------------------------- /src/Domain/Entities/Power.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | 3 | namespace Domain.Entities; 4 | 5 | public class Power : AuditableEntity 6 | { 7 | public int Id { get; set; } 8 | public string PowerName { get; set; } 9 | 10 | public IList Heroes { get; set; } = new List(); 11 | } -------------------------------------------------------------------------------- /src/Domain/Entities/Publisher.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Domain.Common; 3 | 4 | namespace Domain.Entities; 5 | 6 | public class Publisher : AuditableEntity 7 | { 8 | public int Id { get; set; } 9 | public string? PublisherName { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Domain/Entities/Race.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Domain.Common; 3 | 4 | namespace Domain.Entities; 5 | 6 | public class Race : AuditableEntity 7 | { 8 | public int Id { get; set; } 9 | public string? RaceName { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Domain/Entities/Superhero.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | using Domain.ValueObjects; 3 | 4 | namespace Domain.Entities; 5 | 6 | public class Superhero : AuditableEntity, IHasDomainEvent 7 | { 8 | public int Id { get; set; } 9 | public string SuperheroName { get; set; } = String.Empty; 10 | public string? FullName { get; set; } 11 | public int? HeightCm { get; set; } 12 | public int? WeightKg { get; set; } 13 | 14 | public Colour? EyeColour { get; set; } = Colour.NoColour; 15 | public Colour? HairColour { get; set; } = Colour.NoColour; 16 | public Colour? SkinColour { get; set; } = Colour.NoColour; 17 | public Alignment? Alignment { get; set; } = Alignment.NA; 18 | public Gender? Gender { get; set; } = Gender.NA; 19 | 20 | public int? PublisherId { get; set; } 21 | public Publisher? Publisher { get; set; } 22 | 23 | public int? RaceId { get; set; } 24 | public Race? Race { get; set; } 25 | 26 | public IList Attributes { get; set; } = new List(); 27 | public IList Powers { get; set; } = new List(); 28 | 29 | public List DomainEvents { get; set; } = new List(); 30 | } -------------------------------------------------------------------------------- /src/Domain/Events/SuperheroCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | using Domain.Entities; 3 | 4 | namespace Domain.Events; 5 | 6 | public class SuperheroCreatedEvent : DomainEvent 7 | { 8 | public SuperheroCreatedEvent(Superhero item) 9 | { 10 | Item = item; 11 | } 12 | 13 | public Superhero Item { get; } 14 | } -------------------------------------------------------------------------------- /src/Domain/Events/SuperheroDeletedEvent.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | using Domain.Entities; 3 | 4 | namespace Domain.Events; 5 | 6 | public class SuperheroDeletedEvent : DomainEvent 7 | { 8 | public SuperheroDeletedEvent(Superhero item) 9 | { 10 | Item = item; 11 | } 12 | 13 | public Superhero Item { get; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Domain/Exceptions/UnsupportedAlignmentException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Domain.Exceptions; 4 | 5 | public class UnsupportedAlignmentException : Exception 6 | { 7 | public UnsupportedAlignmentException(string gender) 8 | : base($"Alignment \"{gender}\" is unsupported.") 9 | { 10 | } 11 | } 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Domain/Exceptions/UnsupportedColourException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Domain.Exceptions; 4 | 5 | public class UnsupportedColourException : Exception 6 | { 7 | public UnsupportedColourException(string code) 8 | : base($"Colour \"{code}\" is unsupported.") 9 | { 10 | } 11 | } 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Domain/Exceptions/UnsupportedGenderException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Domain.Exceptions; 4 | 5 | public class UnsupportedGenderException : Exception 6 | { 7 | public UnsupportedGenderException(string gender) 8 | : base($"Gender \"{gender}\" is unsupported.") 9 | { 10 | } 11 | } 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Domain/ValueObjects/Alignment.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | using Domain.Exceptions; 3 | 4 | namespace Domain.ValueObjects; 5 | 6 | public class Alignment : ValueObject 7 | { 8 | static Alignment() 9 | { 10 | } 11 | 12 | private Alignment() 13 | { 14 | } 15 | 16 | private Alignment(string alignment) 17 | { 18 | Value = alignment; 19 | } 20 | 21 | public static Alignment From(string alignment) 22 | { 23 | var Value = new Alignment { Value = alignment }; 24 | 25 | if (!SupportedAlignments.Contains(Value)) 26 | { 27 | throw new UnsupportedAlignmentException(alignment); 28 | } 29 | 30 | return Value; 31 | } 32 | 33 | public static Alignment Good => new("Good"); 34 | public static Alignment Bad => new("Bad"); 35 | public static Alignment Neutral => new("Neutral"); 36 | public static Alignment NA => new("N/A"); 37 | 38 | public string Value { get; private set; } = "N/A"; 39 | 40 | public static implicit operator string(Alignment alignment) 41 | { 42 | return alignment; 43 | } 44 | 45 | public static explicit operator Alignment(string alignment) 46 | { 47 | return From(alignment); 48 | } 49 | 50 | public override string ToString() 51 | { 52 | return Value; 53 | } 54 | 55 | protected static IEnumerable SupportedAlignments 56 | { 57 | get 58 | { 59 | yield return Good; 60 | yield return Bad; 61 | yield return Neutral; 62 | } 63 | } 64 | 65 | protected override IEnumerable GetEqualityComponents() 66 | { 67 | yield return Value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Domain/ValueObjects/Colour.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | using Domain.Exceptions; 3 | 4 | namespace Domain.ValueObjects; 5 | 6 | public class Colour : ValueObject 7 | { 8 | static Colour() 9 | { 10 | } 11 | 12 | private Colour() 13 | { 14 | } 15 | 16 | private Colour(string code) 17 | { 18 | Code = code; 19 | } 20 | 21 | public static Colour From(string code) 22 | { 23 | var colour = new Colour { Code = code }; 24 | 25 | if (!SupportedColours.Contains(colour)) 26 | { 27 | throw new UnsupportedColourException(code); 28 | } 29 | 30 | return colour; 31 | } 32 | 33 | public static Colour NoColour => new("No Colour"); 34 | public static Colour Amber => new("Amber"); 35 | public static Colour Auburn => new("Auburn"); 36 | public static Colour Black => new("Black"); 37 | public static Colour BlackBlue => new ("Black/Blue"); 38 | public static Colour Blond => new("Blond"); 39 | public static Colour Blue => new("Blue"); 40 | public static Colour BlueWhite => new ("Blue/White"); 41 | public static Colour Brown => new("Brown"); 42 | public static Colour BrownBlack => new ("Brown/Black"); 43 | public static Colour BrownWhite => new ("Brown/White"); 44 | public static Colour Gold => new("Gold"); 45 | public static Colour Grey => new("Grey"); 46 | public static Colour Green => new("Green"); 47 | public static Colour GreenBlue => new ("Green/Blue"); 48 | public static Colour Hazel => new("Hazel"); 49 | public static Colour Indigo => new("Indigo"); 50 | public static Colour Magenta => new("Magenta"); 51 | public static Colour Orange => new("Orange"); 52 | public static Colour OrangeWhite => new ("Orange/White"); 53 | public static Colour Pink => new("Pink"); 54 | public static Colour Purple => new("Purple"); 55 | public static Colour Red => new("Red"); 56 | public static Colour RedBlack => new ("Red/Black"); 57 | public static Colour RedGrey => new ("Red/Grey"); 58 | public static Colour RedOrange => new ("Red/Orange"); 59 | public static Colour RedWhite => new ("Red/White"); 60 | public static Colour Silver => new("Silver"); 61 | public static Colour StrawberryBlond => new ("Strawberry/Blond"); 62 | public static Colour Violet => new("Violet"); 63 | public static Colour White => new("White"); 64 | public static Colour WhiteRed => new ("White/Red"); 65 | public static Colour Yellow => new("Yellow"); 66 | public static Colour YellowBlue => new ("Yellow/Blue"); 67 | public static Colour YellowRed => new ("Yellow/Red"); 68 | 69 | public string Code { get; private set; } = "No Colour"; 70 | 71 | public static implicit operator string(Colour colour) 72 | { 73 | return colour; 74 | } 75 | 76 | public static explicit operator Colour(string code) 77 | { 78 | return From(code); 79 | } 80 | 81 | public override string ToString() 82 | { 83 | return Code; 84 | } 85 | 86 | protected static IEnumerable SupportedColours 87 | { 88 | get 89 | { 90 | yield return Amber; 91 | yield return Auburn; 92 | yield return Black; 93 | yield return BlackBlue; 94 | yield return Blond; 95 | yield return Blue; 96 | yield return BlueWhite; 97 | yield return Brown; 98 | yield return BrownBlack; 99 | yield return BrownWhite; 100 | yield return Gold; 101 | yield return Grey; 102 | yield return Green; 103 | yield return GreenBlue; 104 | yield return Hazel; 105 | yield return Indigo; 106 | yield return Magenta; 107 | yield return Orange; 108 | yield return OrangeWhite; 109 | yield return Pink; 110 | yield return Purple; 111 | yield return Red; 112 | yield return RedBlack; 113 | yield return RedGrey; 114 | yield return RedOrange; 115 | yield return RedWhite; 116 | yield return Silver; 117 | yield return StrawberryBlond; 118 | yield return Violet; 119 | yield return White; 120 | yield return WhiteRed; 121 | yield return Yellow; 122 | yield return YellowBlue; 123 | yield return YellowRed; 124 | } 125 | } 126 | 127 | protected override IEnumerable GetEqualityComponents() 128 | { 129 | yield return Code; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Domain/ValueObjects/Gender.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | using Domain.Exceptions; 3 | 4 | namespace Domain.ValueObjects; 5 | 6 | public class Gender : ValueObject 7 | { 8 | static Gender() 9 | { 10 | } 11 | 12 | private Gender() 13 | { 14 | } 15 | 16 | private Gender(string gender) 17 | { 18 | Value = gender; 19 | } 20 | 21 | public static Gender From(string value) 22 | { 23 | var gender = new Gender { Value = value }; 24 | 25 | if (!SupportedGenders.Contains(gender)) 26 | { 27 | throw new UnsupportedGenderException(gender); 28 | } 29 | 30 | return gender; 31 | } 32 | 33 | public static Gender Male => new("Male"); 34 | public static Gender Female => new("Female"); 35 | public static Gender NA => new("N/A"); 36 | 37 | public string Value { get; private set; } = "N/A"; 38 | 39 | public static implicit operator string(Gender gender) 40 | { 41 | return gender; 42 | } 43 | 44 | public static explicit operator Gender(string gender) 45 | { 46 | return From(gender); 47 | } 48 | 49 | public override string ToString() 50 | { 51 | return Value.ToString(); 52 | } 53 | 54 | protected static IEnumerable SupportedGenders 55 | { 56 | get 57 | { 58 | yield return Male; 59 | yield return Female; 60 | } 61 | } 62 | 63 | protected override IEnumerable GetEqualityComponents() 64 | { 65 | yield return Value; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Infrastructure/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interfaces; 2 | using Infrastructure.Persistence; 3 | using Infrastructure.Services; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace Infrastructure; 9 | 10 | public static class DependencyInjection 11 | { 12 | public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) 13 | { 14 | if (configuration.GetValue("UseInMemoryDatabase")) 15 | { 16 | services.AddDbContext(options => 17 | options.UseInMemoryDatabase("CleanArchitectureDb")); 18 | } 19 | else 20 | { 21 | services.AddDbContext(options => 22 | options.UseSqlServer( 23 | configuration.GetConnectionString("DefaultConnection"), 24 | b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName))); 25 | } 26 | 27 | services.AddScoped(provider => provider.GetRequiredService()); 28 | 29 | services.AddScoped(); 30 | 31 | services.AddTransient(); 32 | 33 | return services; 34 | } 35 | } -------------------------------------------------------------------------------- /src/Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interfaces; 2 | using Domain.Common; 3 | using Domain.Entities; 4 | using Microsoft.EntityFrameworkCore; 5 | using System.Reflection; 6 | 7 | namespace Infrastructure.Persistence; 8 | 9 | public class ApplicationDbContext : DbContext, IApplicationDbContext 10 | { 11 | private readonly IDateTime _dateTime; 12 | private readonly IDomainEventService _domainEventService; 13 | 14 | public ApplicationDbContext( 15 | DbContextOptions options, 16 | IDateTime dateTime, 17 | IDomainEventService domainEventService) : base(options) 18 | { 19 | _dateTime = dateTime; 20 | _domainEventService = domainEventService; 21 | 22 | } 23 | 24 | public DbSet Attributes => Set(); 25 | public DbSet Publishers => Set(); 26 | public DbSet Races => Set(); 27 | public DbSet Superheroes => Set(); 28 | public DbSet Powers => Set(); 29 | 30 | public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) 31 | { 32 | foreach (var entry in ChangeTracker.Entries()) 33 | { 34 | switch (entry.State) 35 | { 36 | case EntityState.Added: 37 | entry.Entity.CreatedBy = "UserId"; 38 | entry.Entity.Created = _dateTime.Now; 39 | entry.Entity.RowVersion = Guid.NewGuid(); 40 | break; 41 | 42 | case EntityState.Modified: 43 | entry.Entity.LastModifiedBy = "UserId"; 44 | entry.Entity.LastModified = _dateTime.Now; 45 | entry.Entity.RowVersion = Guid.NewGuid(); 46 | break; 47 | } 48 | } 49 | 50 | var events = ChangeTracker.Entries() 51 | .Select(x => x.Entity.DomainEvents) 52 | .SelectMany(x => x) 53 | .Where(domainEvent => !domainEvent.IsPublished) 54 | .ToArray(); 55 | 56 | var result = 0; 57 | 58 | try 59 | { 60 | result = await base.SaveChangesAsync(cancellationToken); 61 | } 62 | catch (DbUpdateConcurrencyException ex) 63 | { 64 | // Update the values of the entity that failed to save from the store (https://docs.microsoft.com/es-es/ef/ef6/saving/concurrency) 65 | ex.Entries.Single().Reload(); 66 | } 67 | 68 | await DispatchEvents(events); 69 | 70 | return result; 71 | } 72 | 73 | protected override void OnModelCreating(ModelBuilder modelBuilder) 74 | { 75 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 76 | 77 | base.OnModelCreating(modelBuilder); 78 | } 79 | 80 | private async Task DispatchEvents(DomainEvent[] events) 81 | { 82 | foreach (var @event in events) 83 | { 84 | @event.IsPublished = true; 85 | await _domainEventService.Publish(@event); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/ApplicationDbContextSeed.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Domain.ValueObjects; 3 | 4 | namespace Infrastructure.Persistence; 5 | 6 | public class ApplicationDbContextSeed 7 | { 8 | public static async Task SeedSampleDataAsync(ApplicationDbContext context) 9 | { 10 | if (!context.Attributes.Any()) 11 | { 12 | context.Publishers.AddRange( 13 | new Publisher { Id = 1, PublisherName = "ABC Studios" }, 14 | new Publisher { Id = 2, PublisherName = "DC Comics" }, 15 | new Publisher { Id = 3, PublisherName = "George Lucas" }, 16 | new Publisher { Id = 4, PublisherName = "Icon Comics" }, 17 | new Publisher { Id = 5, PublisherName = "J. K. Rowling" }, 18 | new Publisher { Id = 6, PublisherName = "Marvel Comics" } 19 | ); 20 | 21 | context.Races.AddRange( 22 | new Race { Id = 1, RaceName = "Alien" }, 23 | new Race { Id = 2, RaceName = "Android" }, 24 | new Race { Id = 3, RaceName = "Animal" }, 25 | new Race { Id = 4, RaceName = "Demon" }, 26 | new Race { Id = 5, RaceName = "Eternal" }, 27 | new Race { Id = 6, RaceName = "Human" } 28 | ); 29 | 30 | context.Attributes.AddRange( 31 | new Domain.Entities.Attribute { Id = 1, AttributeName = "Intelligence" }, 32 | new Domain.Entities.Attribute { Id = 2, AttributeName = "Strength" }, 33 | new Domain.Entities.Attribute { Id = 3, AttributeName = "Speed" }, 34 | new Domain.Entities.Attribute { Id = 4, AttributeName = "Durability" }, 35 | new Domain.Entities.Attribute { Id = 5, AttributeName = "Power" }, 36 | new Domain.Entities.Attribute { Id = 6, AttributeName = "Combat" } 37 | ); 38 | 39 | context.Powers.AddRange( 40 | new Power { Id = 1, PowerName = "Agility" }, 41 | new Power { Id = 2, PowerName = "Accelerated Healing" }, 42 | new Power { Id = 3, PowerName = "Lantern Power Ring" }, 43 | new Power { Id = 4, PowerName = "Cold Resistance" }, 44 | new Power { Id = 5, PowerName = "Marksmanship" }, 45 | new Power { Id = 6, PowerName = "Power Augmentation" } 46 | ); 47 | 48 | await context.SaveChangesAsync(); 49 | 50 | context.Superheroes.AddRange( 51 | new Superhero 52 | { 53 | Id = 1, 54 | Alignment = Alignment.Good, 55 | EyeColour = Colour.Amber, 56 | Gender = Gender.Male, 57 | HairColour = Colour.Auburn, 58 | HeightCm = 183, 59 | WeightKg = 83, 60 | PublisherId = 1, 61 | RaceId = 1, 62 | SkinColour = Colour.Black, 63 | SuperheroName = "Animal Man", 64 | FullName = "Bernhard Baker", 65 | Attributes = 66 | { 67 | context.Attributes.FindAsync(1).Result!, 68 | context.Attributes.FindAsync(2).Result! 69 | }, 70 | Powers = 71 | { 72 | context.Powers.FindAsync(3).Result!, 73 | context.Powers.FindAsync(4).Result! 74 | } 75 | }, 76 | new Superhero 77 | { 78 | Id = 2, 79 | Alignment = Alignment.Good, 80 | EyeColour = Colour.Amber, 81 | Gender = Gender.Male, 82 | HairColour = Colour.Auburn, 83 | HeightCm = 180, 84 | WeightKg = 85, 85 | PublisherId = 1, 86 | RaceId = 1, 87 | SkinColour = Colour.Black, 88 | SuperheroName = "Ant-Man", 89 | FullName = "Henry Jonathan Pym", 90 | Attributes = 91 | { 92 | context.Attributes.FindAsync(1).Result!, 93 | context.Attributes.FindAsync(2).Result! 94 | }, 95 | Powers = 96 | { 97 | context.Powers.FindAsync(3).Result!, 98 | context.Powers.FindAsync(4).Result! 99 | } 100 | }, 101 | new Superhero 102 | { 103 | Id = 3, 104 | Alignment = Alignment.Good, 105 | EyeColour = Colour.Amber, 106 | Gender = Gender.Male, 107 | HairColour = Colour.Auburn, 108 | HeightCm = 188, 109 | WeightKg = 95, 110 | PublisherId = 1, 111 | RaceId = 1, 112 | SkinColour = Colour.Black, 113 | SuperheroName = "Batman", 114 | FullName = "Bruce Wayne", 115 | Attributes = 116 | { 117 | context.Attributes.FindAsync(1).Result!, 118 | context.Attributes.FindAsync(2).Result! 119 | }, 120 | Powers = 121 | { 122 | context.Powers.FindAsync(3).Result!, 123 | context.Powers.FindAsync(4).Result! 124 | } 125 | }, 126 | new Superhero 127 | { 128 | Id = 4, 129 | Alignment = Alignment.Good, 130 | EyeColour = Colour.Amber, 131 | Gender = Gender.Male, 132 | HairColour = Colour.Auburn, 133 | HeightCm = 160, 134 | WeightKg = 90, 135 | PublisherId = 1, 136 | RaceId = 1, 137 | SkinColour = Colour.Black, 138 | SuperheroName = "Bionic Woman", 139 | FullName = "Jamie Wells Sommers", 140 | Attributes = 141 | { 142 | context.Attributes.FindAsync(1).Result!, 143 | context.Attributes.FindAsync(2).Result! 144 | }, 145 | Powers = 146 | { 147 | context.Powers.FindAsync(3).Result!, 148 | context.Powers.FindAsync(4).Result! 149 | } 150 | }, 151 | new Superhero 152 | { 153 | Id = 5, 154 | Alignment = Alignment.Good, 155 | EyeColour = Colour.Amber, 156 | Gender = Gender.Male, 157 | HairColour = Colour.Auburn, 158 | HeightCm = 170, 159 | WeightKg = 87, 160 | PublisherId = 1, 161 | RaceId = 1, 162 | SkinColour = Colour.Black, 163 | SuperheroName = "Black Bolt", 164 | FullName = "Blackagar Boltagon", 165 | Attributes = 166 | { 167 | context.Attributes.FindAsync(1).Result!, 168 | context.Attributes.FindAsync(2).Result! 169 | }, 170 | Powers = 171 | { 172 | context.Powers.FindAsync(3).Result!, 173 | context.Powers.FindAsync(4).Result! 174 | } 175 | } 176 | ); 177 | 178 | await context.SaveChangesAsync(); 179 | } 180 | } 181 | } 182 | 183 | 184 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/AttributeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Persistence.Configurations; 6 | 7 | public class AttributeConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("Attribute"); 12 | 13 | builder.Property(e => e.Id) 14 | .ValueGeneratedOnAdd(); 15 | 16 | builder.Property(p => p.RowVersion) 17 | .IsConcurrencyToken() 18 | .ValueGeneratedOnAddOrUpdate(); 19 | 20 | builder.Property(e => e.AttributeName) 21 | .HasMaxLength(200) 22 | .IsUnicode(false); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/PowerConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Persistence.Configurations; 6 | 7 | public class PowerConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("Power"); 12 | 13 | builder.Property(e => e.Id) 14 | .ValueGeneratedOnAdd(); 15 | 16 | builder.Property(p => p.RowVersion) 17 | .IsConcurrencyToken() 18 | .ValueGeneratedOnAddOrUpdate(); 19 | 20 | builder.Property(e => e.PowerName) 21 | .HasMaxLength(200) 22 | .IsUnicode(false); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/PublisherConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Persistence.Configurations; 6 | 7 | public class PublisherConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("Publisher"); 12 | 13 | builder.Property(e => e.Id) 14 | .ValueGeneratedOnAdd(); 15 | 16 | builder.Property(p => p.RowVersion) 17 | .IsConcurrencyToken() 18 | .ValueGeneratedOnAddOrUpdate(); 19 | 20 | builder.Property(e => e.PublisherName) 21 | .HasMaxLength(50) 22 | .IsUnicode(false); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/RaceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Persistence.Configurations; 6 | 7 | public class RaceConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("Race"); 12 | 13 | builder.Property(e => e.Id) 14 | .ValueGeneratedOnAdd(); 15 | 16 | builder.Property(p => p.RowVersion) 17 | .IsConcurrencyToken() 18 | .ValueGeneratedOnAddOrUpdate(); 19 | 20 | builder.Property(e => e.RaceName) 21 | .HasMaxLength(100) 22 | .IsUnicode(false); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Configurations/SuperheroConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Persistence.Configurations; 6 | 7 | public class SuperheroConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("SuperHero"); 12 | 13 | builder.Ignore(e => e.DomainEvents); 14 | 15 | builder.Property(e => e.Id) 16 | .ValueGeneratedOnAdd(); 17 | 18 | builder.Property(p => p.RowVersion) 19 | .IsConcurrencyToken() 20 | .ValueGeneratedOnAddOrUpdate(); 21 | 22 | builder.Property(e => e.FullName) 23 | .HasMaxLength(200) 24 | .IsUnicode(false); 25 | 26 | builder.Property(e => e.SuperheroName) 27 | .HasMaxLength(200) 28 | .IsUnicode(false); 29 | 30 | builder 31 | .OwnsOne(b => b.EyeColour) 32 | .Property(e => e.Code); 33 | 34 | builder 35 | .OwnsOne(b => b.HairColour) 36 | .Property(e => e.Code); 37 | 38 | builder 39 | .OwnsOne(b => b.SkinColour) 40 | .Property(e => e.Code); 41 | 42 | builder 43 | .OwnsOne(b => b.Gender) 44 | .Property(e => e.Value); 45 | 46 | builder 47 | .OwnsOne(b => b.Alignment) 48 | .Property(e => e.Value); 49 | 50 | builder 51 | .HasMany(p => p.Attributes) 52 | .WithMany(p => p.Heroes) 53 | .UsingEntity>( 54 | "HeroAttribute", 55 | j => j 56 | .HasOne() 57 | .WithMany() 58 | .HasForeignKey("AttributeId") 59 | .HasConstraintName("FK_HeroAttribute_Attributes_AttributeId") 60 | .OnDelete(DeleteBehavior.Cascade), 61 | j => j 62 | .HasOne() 63 | .WithMany() 64 | .HasForeignKey("HeroId") 65 | .HasConstraintName("FK_HeroAttribute_Heroes_HeroId") 66 | .OnDelete(DeleteBehavior.ClientCascade)); 67 | 68 | builder 69 | .HasMany(p => p.Powers) 70 | .WithMany(p => p.Heroes) 71 | .UsingEntity>( 72 | "HeroPower", 73 | j => j 74 | .HasOne() 75 | .WithMany() 76 | .HasForeignKey("PowerId") 77 | .HasConstraintName("FK_HeroPower_Attributes_PowerId") 78 | .OnDelete(DeleteBehavior.Cascade), 79 | j => j 80 | .HasOne() 81 | .WithMany() 82 | .HasForeignKey("HeroId") 83 | .HasConstraintName("FK_HeroPower_Heroes_HeroId") 84 | .OnDelete(DeleteBehavior.ClientCascade)); 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Infrastructure/Services/DateTimeService.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interfaces; 2 | 3 | namespace Infrastructure.Services; 4 | 5 | public class DateTimeService : IDateTime 6 | { 7 | public DateTime Now => DateTime.Now; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/Infrastructure/Services/DomainEventService.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interfaces; 2 | using Domain.Common; 3 | using MediatR; 4 | using Microsoft.Extensions.Logging; 5 | using Application.Common.Models; 6 | 7 | namespace Infrastructure.Services; 8 | 9 | public class DomainEventService : IDomainEventService 10 | { 11 | private readonly ILogger _logger; 12 | private readonly IPublisher _mediator; 13 | 14 | public DomainEventService(ILogger logger, IPublisher mediator) 15 | { 16 | _logger = logger; 17 | _mediator = mediator; 18 | } 19 | 20 | public async Task Publish(DomainEvent domainEvent) 21 | { 22 | _logger.LogInformation("Publishing domain event. Event - {event}", domainEvent.GetType().Name); 23 | await _mediator.Publish(GetNotificationCorrespondingToDomainEvent(domainEvent)); 24 | } 25 | 26 | private INotification GetNotificationCorrespondingToDomainEvent(DomainEvent domainEvent) 27 | { 28 | return (INotification)Activator.CreateInstance( 29 | typeof(DomainEventNotification<>).MakeGenericType(domainEvent.GetType()), domainEvent)!; 30 | } 31 | } -------------------------------------------------------------------------------- /src/PublicAPI/Controllers/ApiControllerBase.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace PublicAPI.Controllers; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public abstract class ApiControllerBase : ControllerBase 9 | { 10 | private ISender _mediator = null!; 11 | 12 | protected ISender Mediator => _mediator ??= HttpContext.RequestServices.GetRequiredService(); 13 | } -------------------------------------------------------------------------------- /src/PublicAPI/Controllers/SuperheroesController.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Models; 2 | using Application.Superheroes.Commands.CreateHero; 3 | using Application.Superheroes.Commands.DeleteHero; 4 | using Application.Superheroes.Commands.UpdateHero; 5 | using Application.Superheroes.Queries.GetHeroesWithPagination; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace PublicAPI.Controllers; 9 | 10 | public class SuperheroesController : ApiControllerBase 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public SuperheroesController(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | [HttpGet] 20 | public async Task>> GetSuperheroesWithPagination([FromQuery] GetSuperheroesWithPaginationQuery query) 21 | { 22 | return await Mediator.Send(query); 23 | } 24 | 25 | [HttpGet("{id:int}")] 26 | public async Task> GetSuperhero(int id) 27 | { 28 | return await Mediator.Send(new GetSuperheroQuery { Id = id }); 29 | } 30 | 31 | [HttpPost] 32 | public async Task> Create([FromForm] CreateHeroCommand command) 33 | { 34 | return await Mediator.Send(command); 35 | } 36 | 37 | [HttpPut("{id}")] 38 | public async Task Update(int id, UpdateHeroCommand command) 39 | { 40 | if (id != command.Id) 41 | { 42 | return BadRequest(); 43 | } 44 | 45 | await Mediator.Send(command); 46 | 47 | return NoContent(); 48 | } 49 | 50 | [HttpDelete("{id}")] 51 | public async Task Delete(int id) 52 | { 53 | await Mediator.Send(new DeleteSuperheroCommand { Id = id }); 54 | 55 | return NoContent(); 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/PublicAPI/Filters/ApiExceptionFilterAttribute.cs: -------------------------------------------------------------------------------- 1 |  2 | using Application.Common.Exceptions; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | 6 | namespace PublicAPI.Filters; 7 | 8 | public class ApiExceptionFilterAttribute : ExceptionFilterAttribute 9 | { 10 | 11 | private readonly IDictionary> _exceptionHandlers; 12 | 13 | public ApiExceptionFilterAttribute() 14 | { 15 | // Register known exception types and handlers. 16 | _exceptionHandlers = new Dictionary> 17 | { 18 | { typeof(ValidationException), HandleValidationException }, 19 | { typeof(NotFoundException), HandleNotFoundException }, 20 | { typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException }, 21 | { typeof(ForbiddenAccessException), HandleForbiddenAccessException }, 22 | }; 23 | } 24 | 25 | public override void OnException(ExceptionContext context) 26 | { 27 | HandleException(context); 28 | 29 | base.OnException(context); 30 | } 31 | 32 | private void HandleException(ExceptionContext context) 33 | { 34 | Type type = context.Exception.GetType(); 35 | if (_exceptionHandlers.ContainsKey(type)) 36 | { 37 | _exceptionHandlers[type].Invoke(context); 38 | return; 39 | } 40 | 41 | if (!context.ModelState.IsValid) 42 | { 43 | HandleInvalidModelStateException(context); 44 | return; 45 | } 46 | 47 | HandleUnknownException(context); 48 | } 49 | 50 | private void HandleValidationException(ExceptionContext context) 51 | { 52 | var exception = (ValidationException)context.Exception; 53 | 54 | var details = new ValidationProblemDetails(exception.Errors) 55 | { 56 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1" 57 | }; 58 | 59 | context.Result = new BadRequestObjectResult(details); 60 | 61 | context.ExceptionHandled = true; 62 | } 63 | 64 | private void HandleInvalidModelStateException(ExceptionContext context) 65 | { 66 | var details = new ValidationProblemDetails(context.ModelState) 67 | { 68 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1" 69 | }; 70 | 71 | context.Result = new BadRequestObjectResult(details); 72 | 73 | context.ExceptionHandled = true; 74 | } 75 | 76 | private void HandleNotFoundException(ExceptionContext context) 77 | { 78 | var exception = (NotFoundException)context.Exception; 79 | 80 | var details = new ProblemDetails() 81 | { 82 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4", 83 | Title = "The specified resource was not found.", 84 | Detail = exception.Message 85 | }; 86 | 87 | context.Result = new NotFoundObjectResult(details); 88 | 89 | context.ExceptionHandled = true; 90 | } 91 | 92 | private void HandleUnauthorizedAccessException(ExceptionContext context) 93 | { 94 | var details = new ProblemDetails 95 | { 96 | Status = StatusCodes.Status401Unauthorized, 97 | Title = "Unauthorized", 98 | Type = "https://tools.ietf.org/html/rfc7235#section-3.1" 99 | }; 100 | 101 | context.Result = new ObjectResult(details) 102 | { 103 | StatusCode = StatusCodes.Status401Unauthorized 104 | }; 105 | 106 | context.ExceptionHandled = true; 107 | } 108 | 109 | private void HandleForbiddenAccessException(ExceptionContext context) 110 | { 111 | var details = new ProblemDetails 112 | { 113 | Status = StatusCodes.Status403Forbidden, 114 | Title = "Forbidden", 115 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3" 116 | }; 117 | 118 | context.Result = new ObjectResult(details) 119 | { 120 | StatusCode = StatusCodes.Status403Forbidden 121 | }; 122 | 123 | context.ExceptionHandled = true; 124 | } 125 | 126 | private void HandleUnknownException(ExceptionContext context) 127 | { 128 | var details = new ProblemDetails 129 | { 130 | Status = StatusCodes.Status500InternalServerError, 131 | Title = "An error occurred while processing your request.", 132 | Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1" 133 | }; 134 | 135 | context.Result = new ObjectResult(details) 136 | { 137 | StatusCode = StatusCodes.Status500InternalServerError 138 | }; 139 | 140 | context.ExceptionHandled = true; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/PublicAPI/Program.cs: -------------------------------------------------------------------------------- 1 | using Application; 2 | using FluentValidation.AspNetCore; 3 | using Infrastructure; 4 | using Infrastructure.Persistence; 5 | using Microsoft.OpenApi.Models; 6 | using PublicAPI.Filters; 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | 10 | // Add services to the container. 11 | builder.Services.AddApplication(); 12 | builder.Services.AddInfrastructure(builder.Configuration); 13 | 14 | builder.Services.AddControllers(); 15 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 16 | builder.Services.AddEndpointsApiExplorer(); 17 | builder.Services.AddSwaggerGen(options => 18 | { 19 | options.SwaggerDoc("v1", new OpenApiInfo 20 | { 21 | Version = "v1", 22 | Title = "Superheroes", 23 | Description = "Demo API - Clean Architecture Solution in .NET 6", 24 | }); 25 | }); 26 | 27 | builder.Services.AddControllersWithViews(options => 28 | options.Filters.Add()) 29 | .AddFluentValidation(x => x.AutomaticValidationEnabled = false); 30 | 31 | var app = builder.Build(); 32 | 33 | // Seed Data 34 | using (var scope = app.Services.CreateScope()) 35 | { 36 | var scopeProvider = scope.ServiceProvider; 37 | var dbContext = scope.ServiceProvider.GetRequiredService(); 38 | 39 | await ApplicationDbContextSeed.SeedSampleDataAsync(dbContext); 40 | } 41 | 42 | // Configure the HTTP request pipeline. 43 | if (app.Environment.IsDevelopment()) 44 | { 45 | app.UseSwagger(); 46 | app.UseSwaggerUI(options => 47 | { 48 | options.InjectStylesheet("/swagger-ui/custom.css"); 49 | }); 50 | } 51 | 52 | app.UseStaticFiles(); 53 | 54 | app.UseHttpsRedirection(); 55 | 56 | app.UseAuthorization(); 57 | 58 | app.MapControllers(); 59 | 60 | app.Run(); 61 | 62 | -------------------------------------------------------------------------------- /src/PublicAPI/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:46083", 8 | "sslPort": 44303 9 | } 10 | }, 11 | "profiles": { 12 | "PublicAPI": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "https://localhost:7054;http://localhost:5268", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "swagger", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/PublicAPI/PublicAPI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/PublicAPI/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "UseInMemoryDatabase": true, 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Server=localhost;Database=superhero2;User Id=sa;Password=@12051983SQLServer;MultipleActiveResultSets=true;" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Information", 9 | "Microsoft.AspNetCore": "Warning" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/PublicAPI/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "UseInMemoryDatabase": false, 3 | "ConnectionStrings": { 4 | "DefaultConnection": "" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Information", 9 | "Microsoft.AspNetCore": "Warning" 10 | } 11 | }, 12 | "AllowedHosts": "*" 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/PublicAPI/wwwroot/swagger-ui/custom.css: -------------------------------------------------------------------------------- 1 | .swagger-ui img { 2 | content: url("https://raw.githubusercontent.com/arbems/Clean-Architecture-Solution/main/.github/arbems.jpg"); 3 | height: 70px; 4 | } 5 | .swagger-ui .topbar { 6 | background-color: #FFFFFF; 7 | padding: 10px 0; 8 | color: #000000; 9 | } 10 | .swagger-ui .topbar .download-url-wrapper .select-label { 11 | color: #000000; 12 | } -------------------------------------------------------------------------------- /tests/Application.IntegrationTests/Application.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/Application.UnitTests/Application.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/Domain.UnitTests/Domain.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------