├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .template.config
└── template.json
├── .vscode
├── launch.json
└── tasks.json
├── CleanArchTemplate.sln
├── Dockerfile
├── README.md
├── nuget.csproj
├── src
├── CleanArchTemplate.Api
│ ├── CleanArchTemplate.Api.csproj
│ ├── Controllers
│ │ └── PeopleController.cs
│ ├── Exceptions
│ │ └── MissingClaimsPrincipalException.cs
│ ├── Extensions
│ │ └── EndpointResultExtensions.cs
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Services
│ │ └── PrincipalService.cs
│ ├── Startup.cs
│ ├── appsettings.Development.json
│ └── appsettings.json
├── CleanArchTemplate.Application
│ ├── CleanArchTemplate.Application.csproj
│ ├── DependencyInjection.cs
│ ├── Endpoints
│ │ └── People
│ │ │ ├── Commands
│ │ │ ├── AddPersonCommand.cs
│ │ │ ├── AddPersonCommandHandler.cs
│ │ │ └── AddPersonCommandValidator.cs
│ │ │ ├── PersonViewModel.cs
│ │ │ └── Queries
│ │ │ ├── PeopleQuery.cs
│ │ │ └── PeopleQueryHandler.cs
│ ├── Interfaces
│ │ ├── IRequestValidator.cs
│ │ ├── Persistence
│ │ │ ├── DataServices
│ │ │ │ └── People
│ │ │ │ │ ├── Commands
│ │ │ │ │ └── IAddPersonDataService.cs
│ │ │ │ │ └── Queries
│ │ │ │ │ └── IGetPeopleDataService.cs
│ │ │ └── IApplicationDbContext.cs
│ │ └── Services
│ │ │ ├── IDateTimeService.cs
│ │ │ ├── ILoggerService.cs
│ │ │ └── IPrincipalService.cs
│ ├── Mapping
│ │ └── PeopleProfile.cs
│ ├── Models
│ │ ├── EndpointResult.cs
│ │ └── Enumerations
│ │ │ └── EndpointResultStatus.cs
│ ├── RequestValidator.cs
│ └── ViewModels
│ │ └── .gitkeep
├── CleanArchTemplate.Domain
│ ├── CleanArchTemplate.Domain.csproj
│ ├── Common
│ │ └── AuditableEntity.cs
│ └── Entities
│ │ └── Person.cs
└── CleanArchTemplate.Infrastructure
│ ├── CleanArchTemplate.Infrastructure.csproj
│ ├── DependencyInjection.cs
│ ├── Persistence
│ ├── ApplicationDbContext.cs
│ ├── Configuration
│ │ └── PersonConfiguration.cs
│ ├── DataServices
│ │ └── People
│ │ │ ├── Commands
│ │ │ └── AddPersonDataService.cs
│ │ │ └── Queries
│ │ │ └── GetPeopleDataService.cs
│ └── SeedData.cs
│ └── Services
│ ├── DateTimeService.cs
│ └── LoggerService.cs
└── tests
├── CleanArchTemplate.Api.Tests
├── CleanArchTemplate.Api.Tests.csproj
├── Extensions
│ └── EndpointResultExtensionsTests.cs
└── Services
│ └── PrincipalServiceTests.cs
├── CleanArchTemplate.Application.Tests
├── CleanArchTemplate.Application.Tests.csproj
└── Mapping
│ └── PeopleProfileTests.cs
└── CleanArchTemplate.Infrastructure.Tests
└── CleanArchTemplate.Infrastructure.Tests.csproj
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/azds.yaml
15 | **/bin
16 | **/charts
17 | **/docker-compose*
18 | **/compose*
19 | **/Dockerfile*
20 | **/node_modules
21 | **/npm-debug.log
22 | **/obj
23 | **/secrets.dev.yaml
24 | **/values.dev.yaml
25 | **/tests
26 | README.md
27 | CleanArchTemplate.sln
28 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
15 | [*.json]
16 | indent_size = 2
17 |
18 | ###############################
19 | # .NET Coding Conventions #
20 | ###############################
21 |
22 | [*.{cs,vb}]
23 | # Organize usings
24 | dotnet_sort_system_directives_first = true
25 | dotnet_separate_import_directive_groups = false
26 |
27 | # this. preferences
28 | dotnet_style_qualification_for_field = false:silent
29 | dotnet_style_qualification_for_property = false:silent
30 | dotnet_style_qualification_for_method = false:silent
31 | dotnet_style_qualification_for_event = false:silent
32 |
33 | # Language keywords vs BCL types preferences
34 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent
35 | dotnet_style_predefined_type_for_member_access = true:silent
36 |
37 | # Parentheses preferences
38 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
40 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
41 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
42 |
43 | # Modifier preferences
44 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
45 | dotnet_style_readonly_field = true:suggestion
46 |
47 | # Expression-level preferences
48 | dotnet_style_object_initializer = true:suggestion
49 | dotnet_style_collection_initializer = true:suggestion
50 | dotnet_style_explicit_tuple_names = true:suggestion
51 | dotnet_style_null_propagation = true:suggestion
52 | dotnet_style_coalesce_expression = true:suggestion
53 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
54 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
55 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
56 | dotnet_style_prefer_auto_properties = true:silent
57 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
58 | dotnet_style_prefer_conditional_expression_over_return = true:silent
59 |
60 | ###############################
61 | # Naming Conventions #
62 | ###############################
63 |
64 | # Style Definitions
65 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case
66 |
67 | # Use PascalCase for constant fields
68 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
69 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
70 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
71 | dotnet_naming_symbols.constant_fields.applicable_kinds = field
72 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
73 | dotnet_naming_symbols.constant_fields.required_modifiers = const
74 |
75 | ###############################
76 | # C# Code Style Rules #
77 | ###############################
78 |
79 | [*.cs]
80 | # var preferences
81 | csharp_style_var_for_built_in_types = true:silent
82 | csharp_style_var_when_type_is_apparent = true:silent
83 | csharp_style_var_elsewhere = true:silent
84 |
85 | # Expression-bodied members
86 | csharp_style_expression_bodied_methods = false:silent
87 | csharp_style_expression_bodied_constructors = false:silent
88 | csharp_style_expression_bodied_operators = false:silent
89 | csharp_style_expression_bodied_properties = true:silent
90 | csharp_style_expression_bodied_indexers = true:silent
91 | csharp_style_expression_bodied_accessors = true:silent
92 |
93 | # Pattern-matching preferences
94 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
95 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
96 |
97 | # Null-checking preferences
98 | csharp_style_throw_expression = true:suggestion
99 | csharp_style_conditional_delegate_call = true:suggestion
100 |
101 | # Modifier preferences
102 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
103 |
104 | # Expression-level preferences
105 | csharp_prefer_braces = true:silent
106 | csharp_style_deconstructed_variable_declaration = true:suggestion
107 | csharp_prefer_simple_default_expression = true:suggestion
108 | csharp_style_pattern_local_over_anonymous_function = true:suggestion
109 | csharp_style_inlined_variable_declaration = true:suggestion
110 |
111 | ###############################
112 | # C# Formatting Rules #
113 | ###############################
114 |
115 | # New line preferences
116 | csharp_new_line_before_open_brace = all
117 | csharp_new_line_before_else = true
118 | csharp_new_line_before_catch = true
119 | csharp_new_line_before_finally = true
120 | csharp_new_line_before_members_in_object_initializers = true
121 | csharp_new_line_before_members_in_anonymous_types = true
122 | csharp_new_line_between_query_expression_clauses = true
123 |
124 | # Indentation preferences
125 | csharp_indent_case_contents = true
126 | csharp_indent_switch_labels = true
127 | csharp_indent_labels = flush_left
128 |
129 | # Space preferences
130 | csharp_space_after_cast = false
131 | csharp_space_after_keywords_in_control_flow_statements = true
132 | csharp_space_between_method_call_parameter_list_parentheses = false
133 | csharp_space_between_method_declaration_parameter_list_parentheses = false
134 | csharp_space_between_parentheses = false
135 | csharp_space_before_colon_in_inheritance_clause = true
136 | csharp_space_after_colon_in_inheritance_clause = true
137 | csharp_space_around_binary_operators = before_and_after
138 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
139 | csharp_space_between_method_call_name_and_opening_parenthesis = false
140 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
141 | csharp_space_after_comma = true
142 | csharp_space_after_dot = false
143 |
144 | # Wrapping preferences
145 | csharp_preserve_single_line_statements = true
146 | csharp_preserve_single_line_blocks = true
147 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | build/
21 | bld/
22 | bin/
23 | Bin/
24 | obj/
25 | Obj/
26 | dist/
27 | .vscode/solution-explorer/
28 |
29 | # Visual Studio 2015 cache/options directory
30 | .vs/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | *_i.c
46 | *_p.c
47 | *_i.h
48 | *.ilk
49 | *.meta
50 | *.obj
51 | *.pch
52 | *.pdb
53 | *.pgc
54 | *.pgd
55 | *.rsp
56 | *.sbr
57 | *.tlb
58 | *.tli
59 | *.tlh
60 | *.tmp
61 | *.tmp_proj
62 | *.log
63 | *.vspscc
64 | *.vssscc
65 | .builds
66 | *.pidb
67 | *.svclog
68 | *.scc
69 |
70 | # Chutzpah Test files
71 | _Chutzpah*
72 |
73 | # Visual C++ cache files
74 | ipch/
75 | *.aps
76 | *.ncb
77 | *.opendb
78 | *.opensdf
79 | *.sdf
80 | *.cachefile
81 |
82 | # Visual Studio profiler
83 | *.psess
84 | *.vsp
85 | *.vspx
86 | *.sap
87 |
88 | # TFS 2012 Local Workspace
89 | $tf/
90 |
91 | # Guidance Automation Toolkit
92 | *.gpState
93 |
94 | # ReSharper is a .NET coding add-in
95 | _ReSharper*/
96 | *.[Rr]e[Ss]harper
97 | *.DotSettings.user
98 |
99 | # JustCode is a .NET coding add-in
100 | .JustCode
101 |
102 | # TeamCity is a build add-in
103 | _TeamCity*
104 |
105 | # DotCover is a Code Coverage Tool
106 | *.dotCover
107 |
108 | # NCrunch
109 | _NCrunch_*
110 | .*crunch*.local.xml
111 | nCrunchTemp_*
112 |
113 | # MightyMoose
114 | *.mm.*
115 | AutoTest.Net/
116 |
117 | # Web workbench (sass)
118 | .sass-cache/
119 |
120 | # Installshield output folder
121 | [Ee]xpress/
122 |
123 | # DocProject is a documentation generator add-in
124 | DocProject/buildhelp/
125 | DocProject/Help/*.HxT
126 | DocProject/Help/*.HxC
127 | DocProject/Help/*.hhc
128 | DocProject/Help/*.hhk
129 | DocProject/Help/*.hhp
130 | DocProject/Help/Html2
131 | DocProject/Help/html
132 |
133 | # Click-Once directory
134 | publish/
135 |
136 | # Publish Web Output
137 | *.[Pp]ublish.xml
138 | *.azurePubxml
139 | # TODO: Comment the next line if you want to checkin your web deploy settings
140 | # but database connection strings (with potential passwords) will be unencrypted
141 | *.pubxml
142 | *.publishproj
143 |
144 | # NuGet Packages
145 | *.nupkg
146 | # The packages folder can be ignored because of Package Restore
147 | # **/packages/*
148 | # except build/, which is used as an MSBuild target.
149 | # !**/packages/build/
150 | # !**/app/packages/
151 | # Uncomment if necessary however generally it will be regenerated when needed
152 | #!**/packages/repositories.config
153 |
154 | # Microsoft Azure Build Output
155 | csx/
156 | *.build.csdef
157 |
158 | # Microsoft Azure Emulator
159 | ecf/
160 | rcf/
161 |
162 | # Microsoft Azure ApplicationInsights config file
163 | ApplicationInsights.config
164 |
165 | # Windows Store app package directory
166 | AppPackages/
167 | BundleArtifacts/
168 |
169 | # Visual Studio cache files
170 | # files ending in .cache can be ignored
171 | *.[Cc]ache
172 | # but keep track of directories ending in .cache
173 | !*.[Cc]ache/
174 |
175 | # Others
176 | ClientBin/
177 | ~$*
178 | *~
179 | *.dbmdl
180 | *.dbproj.schemaview
181 | *.pfx
182 | *.publishsettings
183 | orleans.codegen.cs
184 |
185 | /node_modules
186 |
187 | # RIA/Silverlight projects
188 | Generated_Code/
189 |
190 | # Backup & report files from converting an old project file
191 | # to a newer Visual Studio version. Backup files are not needed,
192 | # because we have git ;-)
193 | _UpgradeReport_Files/
194 | Backup*/
195 | UpgradeLog*.XML
196 | UpgradeLog*.htm
197 |
198 | # SQL Server files
199 | *.mdf
200 | *.ldf
201 |
202 | # Business Intelligence projects
203 | *.rdl.data
204 | *.bim.layout
205 | *.bim_*.settings
206 |
207 | # Microsoft Fakes
208 | FakesAssemblies/
209 |
210 | # GhostDoc plugin setting file
211 | *.GhostDoc.xml
212 |
213 | # Node.js Tools for Visual Studio
214 | .ntvs_analysis.dat
215 |
216 | # Visual Studio 6 build log
217 | *.plg
218 |
219 | # Visual Studio 6 workspace options file
220 | *.opt
221 |
222 | # Visual Studio LightSwitch build output
223 | **/*.HTMLClient/GeneratedArtifacts
224 | **/*.DesktopClient/GeneratedArtifacts
225 | **/*.DesktopClient/ModelManifest.xml
226 | **/*.Server/GeneratedArtifacts
227 | **/*.Server/ModelManifest.xml
228 | _Pvt_Extensions
229 |
230 | # Paket dependency manager
231 | .paket/paket.exe
232 |
233 | # FAKE - F# Make
234 | .fake/
235 |
--------------------------------------------------------------------------------
/.template.config/template.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/template",
3 | "author": "Jamie Nordmeyer",
4 | "classifications": ["Web", "ASP.NET", "API"],
5 | "identity": "StaticSphere.CleanArchitecture.Api",
6 | "name": "Clean Architecture Solution",
7 | "tags": {
8 | "language": "C#",
9 | "type": "solution"
10 | },
11 | "shortName": "clean-arch",
12 | "sourceName": "CleanArchTemplate",
13 | "preferNameDirectory": true,
14 |
15 | "symbols": {
16 | "includeTests": {
17 | "type": "parameter",
18 | "datatype": "bool",
19 | "defaultValue": "true",
20 | "description": "Determines if the test projects should be included or not"
21 | },
22 | "skipRestore": {
23 | "type": "parameter",
24 | "datatype": "bool",
25 | "description": "If specified, skips the automatic restore of the project on create.",
26 | "defaultValue": "false"
27 | },
28 | "includeEF": {
29 | "type": "parameter",
30 | "datatype": "choice",
31 | "choices": [
32 | {
33 | "choice": "postgres",
34 | "description": "Adds Postgres Entity Framework congiguration"
35 | },
36 | {
37 | "choice": "sqlserver",
38 | "description": "Adds SQL Server Entity Framework configuration"
39 | }
40 | ]
41 | },
42 | "includePostgres": {
43 | "type": "computed",
44 | "value": "(includeEF == \"postgres\")"
45 | },
46 | "includeSqlServer": {
47 | "type": "computed",
48 | "value": "(includeEF == \"sqlserver\")"
49 | },
50 | "includeDB": {
51 | "type": "computed",
52 | "value": "(includePostgres || includeSqlServer)"
53 | },
54 | "useStartup": {
55 | "type": "parameter",
56 | "datatype": "bool",
57 | "description": "Determines if the API project should use Startup.cs instead of the newer Minimal API style Program.cs file",
58 | "defaultValue": "false"
59 | },
60 | "dbContextName": {
61 | "type": "parameter",
62 | "datatype": "text",
63 | "description": "If specified, sets the name of the EF DbContext when using EF. You MUST specify the `DbContext` suffix in this name if you want it in the class name generated.",
64 | "replaces": "ApplicationDbContext",
65 | "fileRename": "ApplicationDbContext"
66 | }
67 | },
68 |
69 | "sources": [
70 | {
71 | "exclude": [
72 | "**/[Bb]in/**",
73 | "**/[Oo]bj/**",
74 | ".template.config/**/*",
75 | "**/*.filelist",
76 | "**/*.user",
77 | "**/*.lock.json",
78 | ".vscode/**/*",
79 | "nuget.csproj"
80 | ],
81 | "modifiers": [
82 | {
83 | "condition": "(!includeTests)",
84 | "exclude": [
85 | "tests/CleanArchTemplate.Application.Tests/**/*",
86 | "tests/CleanArchTemplate.Infrastructure.Tests/**/*",
87 | "tests/CleanArchTemplate.Api.Tests/**/*"
88 | ]
89 | },
90 | {
91 | "condition": "(!includeDB)",
92 | "exclude": [
93 | "src/CleanArchTemplate.Application/Interfaces/Persistence/IApplicationDbContext.cs",
94 | "src/CleanArchTemplate.Infrastructure/Persistence/Configuration/**/*",
95 | "src/CleanArchTemplate.Infrastructure/Persistence/ApplicationDbContext.cs",
96 | "src/CleanArchTemplate.Infrastructure/Persistence/SeedData.cs"
97 | ]
98 | },
99 | {
100 | "condition": "(!useStartup)",
101 | "exclude": ["src/CleanArchTemplate.Api/Startup.cs"]
102 | }
103 | ]
104 | }
105 | ],
106 | "primaryOutputs": [
107 | {
108 | "path": "CleanArchTemplate.sln"
109 | }
110 | ],
111 | "postActions": [
112 | {
113 | "condition": "(!skipRestore)",
114 | "description": "Restore NuGet packages required by this project.",
115 | "manualInstructions": [{ "text": "Run 'dotnet restore'" }],
116 | "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025",
117 | "continueOnError": true
118 | }
119 | ],
120 |
121 | "SpecialCustomOperations": {
122 | "**/*.csproj": {
123 | "operations": [
124 | {
125 | "type": "conditional",
126 | "configuration": {
127 | "if": ["
12 |
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 | all
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Api/Controllers/PeopleController.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using CleanArchTemplate.Api.Extensions;
3 | using CleanArchTemplate.Application.Endpoints.People.Commands;
4 | using CleanArchTemplate.Application.Endpoints.People.Queries;
5 | using MediatR;
6 | using Microsoft.AspNetCore.Mvc;
7 |
8 | namespace CleanArchTemplate.Api.Controllers;
9 |
10 | [ExcludeFromCodeCoverage]
11 | [ApiController]
12 | [Route("api/v{version:apiVersion}/people")]
13 | [ApiVersion("1.0")]
14 | public class PeopleController : ControllerBase
15 | {
16 | private readonly IMediator _mediator;
17 |
18 | public PeopleController(IMediator mediator)
19 | {
20 | _mediator = mediator;
21 | }
22 |
23 | [HttpGet]
24 | public async Task GetPeopleAsync([FromQuery] PeopleQuery request) =>
25 | (await _mediator.Send(request)).ToActionResult();
26 |
27 | [HttpPost]
28 | public async Task AddPersonAsync([FromBody] AddPersonCommand command) =>
29 | (await _mediator.Send(command)).ToActionResult();
30 | }
31 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Api/Exceptions/MissingClaimsPrincipalException.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchTemplate.Api.Exceptions;
2 |
3 | public class MissingClaimsPrincipalException : Exception
4 | {
5 | }
6 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Api/Extensions/EndpointResultExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using Microsoft.AspNetCore.Mvc;
3 | using CleanArchTemplate.Application.Models;
4 | using CleanArchTemplate.Application.Models.Enumerations;
5 |
6 | namespace CleanArchTemplate.Api.Extensions;
7 |
8 | public static class EndpointResultExtensions
9 | {
10 | public static ActionResult ToActionResult(this EndpointResult endpointResult)
11 | {
12 | return endpointResult.Status switch
13 | {
14 | EndpointResultStatus.Success => new OkResult(),
15 | EndpointResultStatus.NotFound => new NotFoundResult(),
16 | EndpointResultStatus.Invalid when endpointResult.Messages.Count() > 0 =>
17 | new UnprocessableEntityObjectResult(endpointResult.Messages),
18 | EndpointResultStatus.Invalid => new UnprocessableEntityResult(),
19 | EndpointResultStatus.Duplicate => new ConflictResult(),
20 | EndpointResultStatus.Unauthorized => new UnauthorizedResult(),
21 | EndpointResultStatus.Gone => new StatusCodeResult((int)HttpStatusCode.Gone),
22 | _ => new StatusCodeResult((int)HttpStatusCode.InternalServerError)
23 | };
24 | }
25 |
26 | public static ActionResult ToActionResult(this EndpointResult endpointResult)
27 | {
28 | return endpointResult.Status switch
29 | {
30 | EndpointResultStatus.Success => new OkObjectResult(endpointResult.Data),
31 | _ => ((EndpointResult)endpointResult).ToActionResult()
32 | };
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Api/Program.cs:
--------------------------------------------------------------------------------
1 | #if (useStartup)
2 | using CleanArchTemplate.Api;
3 | #endif
4 | #if (!useStartup)
5 | using CleanArchTemplate.Api.Services;
6 | using CleanArchTemplate.Application;
7 | using CleanArchTemplate.Application.Interfaces.Services;
8 | using CleanArchTemplate.Infrastructure;
9 | using Microsoft.AspNetCore.Mvc;
10 | using Microsoft.Net.Http.Headers;
11 | using Microsoft.OpenApi.Models;
12 | #endif
13 |
14 | #if (useStartup)
15 | CreateHostBuilder(args).Build().Run();
16 | return;
17 |
18 | IHostBuilder CreateHostBuilder(string[] args) =>
19 | Host.CreateDefaultBuilder(args)
20 | .ConfigureWebHostDefaults(webBuilder =>
21 | {
22 | webBuilder
23 | // TODO: Remove this line if you want to return the Server header
24 | .ConfigureKestrel(config => config.AddServerHeader = false)
25 | .UseStartup();
26 | });
27 | #else
28 | // Configure Services
29 | var builder = WebApplication.CreateBuilder(args);
30 | // TODO: Remove this line if you want to return the Server header
31 | builder.WebHost.ConfigureKestrel(options => options.AddServerHeader = false);
32 |
33 | builder.Services.AddSingleton(builder.Configuration);
34 |
35 | // Adds in Application dependencies
36 | builder.Services.AddApplication(builder.Configuration);
37 | // Adds in Infrastructure dependencies
38 | builder.Services.AddInfrastructure(builder.Configuration);
39 |
40 | builder.Services.AddHttpContextAccessor();
41 | builder.Services.AddHealthChecks();
42 |
43 | builder.Services.AddApiVersioning(options =>
44 | {
45 | options.AssumeDefaultVersionWhenUnspecified = true;
46 | options.DefaultApiVersion = ApiVersion.Default;
47 | });
48 |
49 | builder.Services.AddScoped();
50 |
51 | builder.Services.AddControllers();
52 | builder.Services.AddSwaggerGen(c =>
53 | {
54 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "CleanArchTemplate.Api", Version = "v1" });
55 | });
56 |
57 | // Configure Application
58 | var app = builder.Build();
59 |
60 | if (app.Environment.IsDevelopment())
61 | {
62 | app.UseDeveloperExceptionPage();
63 | app.UseSwagger();
64 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "CleanArchTemplate.Api v1"));
65 | }
66 |
67 | app.UseHttpsRedirection();
68 |
69 | app.UseRouting();
70 |
71 | app.Use(async (httpContext, next) =>
72 | {
73 | var apiMode = httpContext.Request.Path.StartsWithSegments("/api");
74 | if (apiMode)
75 | {
76 | httpContext.Request.Headers[HeaderNames.XRequestedWith] = "XMLHttpRequest";
77 | }
78 | await next();
79 | });
80 |
81 | app.UseAuthorization();
82 |
83 | app.MapHealthChecks("/health");
84 | app.MapControllers();
85 |
86 | app.Run();
87 | #endif
88 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Api/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:26318",
8 | "sslPort": 44328
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "swagger",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "CleanArchTemplate.Api": {
21 | "commandName": "Project",
22 | "dotnetRunMessages": "true",
23 | "launchBrowser": true,
24 | "launchUrl": "swagger",
25 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
26 | "environmentVariables": {
27 | "ASPNETCORE_ENVIRONMENT": "Development"
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Api/Services/PrincipalService.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Security.Claims;
3 | using CleanArchTemplate.Api.Exceptions;
4 | using CleanArchTemplate.Application.Interfaces.Services;
5 |
6 | namespace CleanArchTemplate.Api.Services;
7 |
8 | public class PrincipalService : IPrincipalService
9 | {
10 | private readonly IHttpContextAccessor _httpContextAccessor;
11 |
12 | public PrincipalService(IHttpContextAccessor httpContextAccessor)
13 | {
14 | _httpContextAccessor = httpContextAccessor;
15 | }
16 |
17 | public int UserId
18 | {
19 | get
20 | {
21 | var claimsPrincipal = _httpContextAccessor.HttpContext?.User;
22 | if (claimsPrincipal == null)
23 | throw new MissingClaimsPrincipalException();
24 |
25 | return int.Parse(claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "0", CultureInfo.CurrentCulture);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Api/Startup.cs:
--------------------------------------------------------------------------------
1 | using CleanArchTemplate.Api.Services;
2 | using CleanArchTemplate.Application;
3 | using CleanArchTemplate.Application.Interfaces.Services;
4 | using CleanArchTemplate.Infrastructure;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.Net.Http.Headers;
7 | using Microsoft.OpenApi.Models;
8 |
9 | namespace CleanArchTemplate.Api;
10 |
11 | public class Startup
12 | {
13 | private IConfiguration Configuration { get; }
14 |
15 | public Startup(IConfiguration configuration)
16 | {
17 | Configuration = configuration;
18 | }
19 |
20 | public void ConfigureServices(IServiceCollection services)
21 | {
22 | services.AddSingleton(Configuration);
23 |
24 | // Adds in Application dependencies
25 | services.AddApplication(Configuration);
26 | // Adds in Infrastructure dependencies
27 | services.AddInfrastructure(Configuration);
28 |
29 | services.AddHttpContextAccessor();
30 | services.AddHealthChecks();
31 |
32 | services.AddApiVersioning(options =>
33 | {
34 | options.AssumeDefaultVersionWhenUnspecified = true;
35 | options.DefaultApiVersion = ApiVersion.Default;
36 | });
37 |
38 | services.AddScoped();
39 |
40 | services.AddControllers();
41 | services.AddSwaggerGen(c =>
42 | {
43 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "CleanArchTemplate.Api", Version = "v1" });
44 | });
45 | }
46 |
47 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
48 | {
49 | if (env.IsDevelopment())
50 | {
51 | app.UseDeveloperExceptionPage();
52 | app.UseSwagger();
53 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "CleanArchTemplate.Api v1"));
54 | }
55 |
56 | app.UseHttpsRedirection();
57 |
58 | app.UseRouting();
59 |
60 | app.Use(async (httpContext, next) =>
61 | {
62 | var apiMode = httpContext.Request.Path.StartsWithSegments("/api");
63 | if (apiMode)
64 | {
65 | httpContext.Request.Headers[HeaderNames.XRequestedWith] = "XMLHttpRequest";
66 | }
67 | await next();
68 | });
69 |
70 | app.UseAuthorization();
71 |
72 | app.UseEndpoints(endpoints =>
73 | {
74 | endpoints.MapHealthChecks("/health");
75 | endpoints.MapControllers();
76 | });
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Api/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Api/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/CleanArchTemplate.Application.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | CleanArchTemplate.Application
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/DependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using FluentValidation;
3 | using Testing.Application.Interfaces;
4 | using MediatR;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.DependencyInjection;
7 |
8 | namespace CleanArchTemplate.Application;
9 |
10 | public static class DependencyInjection
11 | {
12 | public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration)
13 | {
14 | services.AddTransient(typeof(IRequestValidator<>), typeof(RequestValidator<>));
15 |
16 | var thisAssembly = Assembly.GetExecutingAssembly();
17 | services.AddAutoMapper(thisAssembly);
18 | services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
19 | services.AddMediatR(thisAssembly);
20 |
21 | return services;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Endpoints/People/Commands/AddPersonCommand.cs:
--------------------------------------------------------------------------------
1 | using CleanArchTemplate.Application.Models;
2 | using MediatR;
3 |
4 | namespace CleanArchTemplate.Application.Endpoints.People.Commands;
5 |
6 | public class AddPersonCommand : IRequest>
7 | {
8 | public int? Id { get; init; }
9 | public string FirstName { get; init; } = "";
10 | public string LastName { get; init; } = "";
11 | }
12 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Endpoints/People/Commands/AddPersonCommandHandler.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchTemplate.Application.Interfaces;
3 | using CleanArchTemplate.Application.Interfaces.Persistence.DataServices.People.Commands;
4 | using CleanArchTemplate.Application.Models;
5 | using CleanArchTemplate.Application.Models.Enumerations;
6 | using CleanArchTemplate.Domain.Entities;
7 | using MediatR;
8 |
9 | namespace CleanArchTemplate.Application.Endpoints.People.Commands;
10 |
11 | public class AddPersonCommandHandler : IRequestHandler>
12 | {
13 | public readonly IRequestValidator _requestValidator;
14 | public readonly IAddPersonDataService _addPersonDataService;
15 | public readonly IMapper _mapper;
16 |
17 | public AddPersonCommandHandler(
18 | IRequestValidator requestValidator,
19 | IAddPersonDataService addPersonDataService,
20 | IMapper mapper
21 | )
22 | {
23 | _requestValidator = requestValidator;
24 | _addPersonDataService = addPersonDataService;
25 | _mapper = mapper;
26 | }
27 |
28 | public async Task> Handle(AddPersonCommand request, CancellationToken cancellationToken)
29 | {
30 | var validationErrors = _requestValidator.ValidateRequest(request);
31 | if (validationErrors.Count() > 0)
32 | return new EndpointResult(EndpointResultStatus.Invalid, validationErrors.ToArray());
33 |
34 | var person = await _addPersonDataService.ExecuteAsync(_mapper.Map(request));
35 | return new EndpointResult(_mapper.Map(person));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Endpoints/People/Commands/AddPersonCommandValidator.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation;
2 |
3 | namespace CleanArchTemplate.Application.Endpoints.People.Commands;
4 |
5 | public class AddPersonCommandValidator : AbstractValidator
6 | {
7 | public AddPersonCommandValidator()
8 | {
9 | RuleFor(x => x.Id)
10 | .Null();
11 |
12 | RuleFor(x => x.FirstName)
13 | .NotEmpty()
14 | .Length(1, 100);
15 |
16 | RuleFor(x => x.LastName)
17 | .NotEmpty()
18 | .Length(1, 100);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Endpoints/People/PersonViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchTemplate.Application.Endpoints.People;
2 |
3 | public record PersonViewModel
4 | {
5 | public int? Id { get; init; }
6 | public string FirstName { get; init; } = "";
7 | public string LastName { get; init; } = "";
8 | public bool Active { get; init; }
9 | public int CreatedBy { get; init; }
10 | public DateTime CreatedOn { get; init; }
11 | public int? ModifiedBy { get; init; }
12 | public DateTime? ModifiedOn { get; init; }
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Endpoints/People/Queries/PeopleQuery.cs:
--------------------------------------------------------------------------------
1 | using CleanArchTemplate.Application.Models;
2 | using MediatR;
3 |
4 | namespace CleanArchTemplate.Application.Endpoints.People.Queries;
5 |
6 | public class PeopleQuery : IRequest>>
7 | {
8 | public bool IncludeInactive { get; init; } = false;
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Endpoints/People/Queries/PeopleQueryHandler.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchTemplate.Application.Interfaces.Persistence.DataServices.People.Queries;
3 | using CleanArchTemplate.Application.Models;
4 | using MediatR;
5 |
6 | namespace CleanArchTemplate.Application.Endpoints.People.Queries;
7 |
8 | public class PeopleQueryHandler : IRequestHandler>>
9 | {
10 | public readonly IGetPeopleDataService _getPeopleDataService;
11 | public readonly IMapper _mapper;
12 |
13 | public PeopleQueryHandler(IGetPeopleDataService getPeopleDataService, IMapper mapper)
14 | {
15 | _getPeopleDataService = getPeopleDataService;
16 | _mapper = mapper;
17 | }
18 |
19 | public async Task>> Handle(PeopleQuery request, CancellationToken cancellationToken = default)
20 | {
21 | var people = await _getPeopleDataService.ExecuteAsync(request.IncludeInactive, cancellationToken);
22 |
23 | return new EndpointResult>(_mapper.Map(people));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Interfaces/IRequestValidator.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchTemplate.Application.Interfaces;
2 |
3 | public interface IRequestValidator
4 | {
5 | IEnumerable ValidateRequest(TRequest request);
6 | }
7 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Interfaces/Persistence/DataServices/People/Commands/IAddPersonDataService.cs:
--------------------------------------------------------------------------------
1 | using CleanArchTemplate.Domain.Entities;
2 |
3 | namespace CleanArchTemplate.Application.Interfaces.Persistence.DataServices.People.Commands;
4 |
5 | public interface IAddPersonDataService
6 | {
7 | Task ExecuteAsync(Person person, CancellationToken cancellationToken = default);
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Interfaces/Persistence/DataServices/People/Queries/IGetPeopleDataService.cs:
--------------------------------------------------------------------------------
1 | using CleanArchTemplate.Domain.Entities;
2 |
3 | namespace CleanArchTemplate.Application.Interfaces.Persistence.DataServices.People.Queries;
4 |
5 | public interface IGetPeopleDataService
6 | {
7 | Task> ExecuteAsync(bool includeInactive, CancellationToken cancellationToken = default);
8 | }
9 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Interfaces/Persistence/IApplicationDbContext.cs:
--------------------------------------------------------------------------------
1 | using CleanArchTemplate.Domain.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace CleanArchTemplate.Application.Interfaces.Persistence;
5 |
6 | public interface IApplicationDbContext
7 | {
8 | DbSet People { get; set; }
9 |
10 | Task SaveChangesAsync(CancellationToken cancellationToken);
11 | }
12 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Interfaces/Services/IDateTimeService.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchTemplate.Application.Interfaces.Services;
2 |
3 | public interface IDateTimeService
4 | {
5 | DateTime UtcNow { get; }
6 | }
7 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Interfaces/Services/ILoggerService.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchTemplate.Application.Interfaces.Services;
2 |
3 | // This interface exists to enable unit testing of log commands, since they are
4 | // extension methods on the ILogger interface.
5 | public interface ILoggerService
6 | {
7 | void LogInformation(string message);
8 | void LogError(Exception exception);
9 | void LogError(Exception exception, string message);
10 | }
11 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Interfaces/Services/IPrincipalService.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchTemplate.Application.Interfaces.Services;
2 |
3 | public interface IPrincipalService
4 | {
5 | int UserId { get; }
6 | }
7 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Mapping/PeopleProfile.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchTemplate.Application.Endpoints.People;
3 | using CleanArchTemplate.Application.Endpoints.People.Commands;
4 | using CleanArchTemplate.Domain.Entities;
5 |
6 | namespace CleanArchTemplate.Application.Mapping;
7 |
8 | public class PeopleProfile : Profile
9 | {
10 | public PeopleProfile()
11 | {
12 | CreateMap()
13 | .ReverseMap();
14 | CreateMap()
15 | .ForMember(dest => dest.Active, opt => opt.MapFrom(src => true))
16 | .ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
17 | .ForMember(dest => dest.CreatedOn, opt => opt.Ignore())
18 | .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore())
19 | .ForMember(dest => dest.ModifiedOn, opt => opt.Ignore());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Models/EndpointResult.cs:
--------------------------------------------------------------------------------
1 | using CleanArchTemplate.Application.Models.Enumerations;
2 |
3 | namespace CleanArchTemplate.Application.Models;
4 |
5 | public record EndpointResult
6 | {
7 | public EndpointResultStatus Status { get; init; } = EndpointResultStatus.Success;
8 | public IEnumerable Messages { get; init; } = new List();
9 |
10 | public EndpointResult()
11 | {
12 | }
13 |
14 | public EndpointResult(EndpointResultStatus status)
15 | {
16 | Status = status;
17 | }
18 |
19 | public EndpointResult(EndpointResultStatus status, params string[] messages)
20 | {
21 | Status = status;
22 | Messages = messages;
23 | }
24 | }
25 |
26 | public record EndpointResult : EndpointResult
27 | {
28 | public TResult? Data { get; init; }
29 |
30 | public EndpointResult(EndpointResultStatus status)
31 | : base(status)
32 | {
33 | }
34 |
35 | public EndpointResult(EndpointResultStatus status, params string[] messages)
36 | : base(status, messages)
37 | {
38 | }
39 |
40 | public EndpointResult(TResult data)
41 | {
42 | Data = data;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/Models/Enumerations/EndpointResultStatus.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchTemplate.Application.Models.Enumerations;
2 |
3 | public enum EndpointResultStatus
4 | {
5 | Success,
6 | NotFound,
7 | Invalid,
8 | Duplicate,
9 | Unauthorized,
10 | Gone,
11 | Error
12 | }
13 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/RequestValidator.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using CleanArchTemplate.Application.Interfaces;
3 | using FluentValidation;
4 |
5 | namespace CleanArchTemplate.Application;
6 |
7 | [ExcludeFromCodeCoverage]
8 | public class RequestValidator : IRequestValidator
9 | {
10 | private readonly IEnumerable> _validators;
11 |
12 | public RequestValidator(IEnumerable> validators)
13 | {
14 | _validators = validators;
15 | }
16 |
17 | public IEnumerable ValidateRequest(TRequest request)
18 | {
19 | var context = new ValidationContext(request);
20 |
21 | return _validators
22 | .Select(v => v.Validate(context))
23 | .SelectMany(result => result.Errors)
24 | .Where(vf => vf != null)
25 | .Select(vf => vf.ErrorMessage)
26 | .ToList();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Application/ViewModels/.gitkeep:
--------------------------------------------------------------------------------
1 | Put general view models here
2 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Domain/CleanArchTemplate.Domain.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | CleanArchTemplate.Domain
6 | enable
7 | enable
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Domain/Common/AuditableEntity.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchTemplate.Domain.Common;
2 |
3 | public abstract class AuditableEntity
4 | {
5 | public int CreatedBy { get; set; }
6 | public DateTime CreatedOn { get; set; }
7 | public int? ModifiedBy { get; set; }
8 | public DateTime? ModifiedOn { get; set; }
9 | }
10 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Domain/Entities/Person.cs:
--------------------------------------------------------------------------------
1 | using CleanArchTemplate.Domain.Common;
2 |
3 | namespace CleanArchTemplate.Domain.Entities;
4 |
5 | public class Person : AuditableEntity
6 | {
7 | public int? Id { get; set; }
8 | public string FirstName { get; set; } = "";
9 | public string LastName { get; set; } = "";
10 | public bool Active { get; set; }
11 | }
12 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Infrastructure/CleanArchTemplate.Infrastructure.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | CleanArchTemplate.Infrastructure
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Infrastructure/DependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 | #if (includeDB)
3 | using CleanArchTemplate.Application.Interfaces.Persistence;
4 | #endif
5 | using CleanArchTemplate.Application.Interfaces.Services;
6 | #if (includeDB)
7 | using CleanArchTemplate.Infrastructure.Persistence;
8 | #endif
9 | using CleanArchTemplate.Infrastructure.Services;
10 | using Microsoft.Extensions.Configuration;
11 | using Microsoft.Extensions.DependencyInjection;
12 |
13 | namespace CleanArchTemplate.Infrastructure;
14 |
15 | public static class DependencyInjection
16 | {
17 | private static readonly Regex InterfacePattern = new Regex("I(?:.+)DataService", RegexOptions.Compiled);
18 |
19 | public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
20 | {
21 | #if (includeDB)
22 | services
23 | .AddDbContext()
24 | .AddScoped();
25 | #endif
26 |
27 | (from c in typeof(Application.DependencyInjection).Assembly.GetTypes()
28 | where c.IsInterface && InterfacePattern.IsMatch(c.Name)
29 | from i in typeof(DependencyInjection).Assembly.GetTypes()
30 | where c.IsAssignableFrom(i)
31 | select new
32 | {
33 | Contract = c,
34 | Implementation = i
35 | }).ToList()
36 | .ForEach(x => services.AddScoped(x.Contract, x.Implementation));
37 |
38 | services.AddSingleton();
39 | services.AddScoped(typeof(ILoggerService<>), typeof(LoggerService<>));
40 |
41 | return services;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Infrastructure/Persistence/ApplicationDbContext.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using CleanArchTemplate.Application.Interfaces.Persistence;
3 | using CleanArchTemplate.Application.Interfaces.Services;
4 | using CleanArchTemplate.Domain.Common;
5 | using CleanArchTemplate.Domain.Entities;
6 | using Microsoft.EntityFrameworkCore;
7 | using Microsoft.Extensions.Configuration;
8 |
9 | namespace CleanArchTemplate.Infrastructure.Persistence;
10 |
11 | public class ApplicationDbContext : DbContext, IApplicationDbContext
12 | {
13 | private readonly IPrincipalService _principalService;
14 | private readonly IDateTimeService _dateTimeService;
15 | private readonly IConfiguration _configuration;
16 |
17 | public DbSet People { get; set; } = null!;
18 |
19 | public ApplicationDbContext(
20 | DbContextOptions options,
21 | IPrincipalService principalService,
22 | IDateTimeService dateTimeService,
23 | IConfiguration configuration) : base(options)
24 | {
25 | _principalService = principalService;
26 | _dateTimeService = dateTimeService;
27 | _configuration = configuration;
28 | }
29 |
30 | public override Task SaveChangesAsync(CancellationToken cancellationToken = default)
31 | {
32 | foreach (var entity in ChangeTracker.Entries())
33 | {
34 | switch (entity.State)
35 | {
36 | case EntityState.Added:
37 | entity.Entity.CreatedBy = _principalService.UserId;
38 | entity.Entity.CreatedOn = _dateTimeService.UtcNow;
39 | break;
40 | case EntityState.Modified:
41 | entity.Entity.ModifiedBy = _principalService.UserId;
42 | entity.Entity.ModifiedOn = _dateTimeService.UtcNow;
43 | break;
44 | }
45 | }
46 |
47 | return base.SaveChangesAsync(cancellationToken);
48 | }
49 |
50 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
51 | {
52 | base.OnConfiguring(optionsBuilder);
53 |
54 | #if(includePostgres)
55 | optionsBuilder.UseNpgsql(
56 | #else
57 | optionsBuilder.UseSqlServer(
58 | #endif
59 | _configuration.GetConnectionString("DefaultConnection")!,
60 | x => x.MigrationsHistoryTable("__EFMigrationsHistory")
61 | #if(includePostgres)
62 | ).UseSnakeCaseNamingConvention();
63 | #else
64 | );
65 | #endif
66 | }
67 |
68 | protected override void OnModelCreating(ModelBuilder builder)
69 | {
70 | base.OnModelCreating(builder);
71 |
72 | builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
73 | builder.SeedData();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Infrastructure/Persistence/Configuration/PersonConfiguration.cs:
--------------------------------------------------------------------------------
1 | using CleanArchTemplate.Domain.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
4 |
5 | namespace CleanArchTemplate.Infrastructure.Persistence.Configuration;
6 |
7 | public class PersonConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | builder.ToTable("person");
12 |
13 | builder.Property(t => t.FirstName)
14 | .IsRequired()
15 | .HasMaxLength(100);
16 |
17 | builder.Property(t => t.LastName)
18 | .IsRequired()
19 | .HasMaxLength(100);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Infrastructure/Persistence/DataServices/People/Commands/AddPersonDataService.cs:
--------------------------------------------------------------------------------
1 | #if (includeDB)
2 | using CleanArchTemplate.Application.Interfaces.Persistence;
3 | #endif
4 | using CleanArchTemplate.Application.Interfaces.Persistence.DataServices.People.Commands;
5 | using CleanArchTemplate.Domain.Entities;
6 |
7 | namespace CleanArchTemplate.Infrastructure.Persistence.DataServices.People.Commands;
8 |
9 | public class AddPersonDataService : IAddPersonDataService
10 | {
11 | #if (includeDB)
12 | private readonly IApplicationDbContext _dbContext;
13 |
14 | public AddPersonDataService(IApplicationDbContext dbContext)
15 | {
16 | _dbContext = dbContext;
17 | }
18 | #endif
19 |
20 | public async Task ExecuteAsync(Person person, CancellationToken cancellationToken = default)
21 | {
22 | #if (includeDB)
23 | _dbContext.People.Add(person);
24 |
25 | await _dbContext.SaveChangesAsync(cancellationToken);
26 | return person;
27 | #else
28 | // TODO: Create add code.
29 | return await Task.Run(() =>
30 | {
31 | person.Id = 1;
32 |
33 | return person;
34 | });
35 | #endif
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Infrastructure/Persistence/DataServices/People/Queries/GetPeopleDataService.cs:
--------------------------------------------------------------------------------
1 | #if (includeDB)
2 | using CleanArchTemplate.Application.Interfaces.Persistence;
3 | #endif
4 | using CleanArchTemplate.Application.Interfaces.Persistence.DataServices.People.Queries;
5 | using CleanArchTemplate.Domain.Entities;
6 | #if (includeDB)
7 | using Microsoft.EntityFrameworkCore;
8 | #endif
9 |
10 | namespace CleanArchTemplate.Infrastructure.Persistence.DataServices.People.Queries;
11 |
12 | public class GetPeopleDataService : IGetPeopleDataService
13 | {
14 | #if (includeDB)
15 | private readonly IApplicationDbContext _dbContext;
16 |
17 | public GetPeopleDataService(IApplicationDbContext dbContext)
18 | {
19 | _dbContext = dbContext;
20 | }
21 | #endif
22 |
23 | public async Task> ExecuteAsync(bool includeInactive, CancellationToken cancellationToken = default)
24 | {
25 | #if (includeDB)
26 | return await _dbContext.People.Where(p => includeInactive || p.Active).ToListAsync(cancellationToken);
27 | #else
28 | // TODO: Create get people code
29 | return await Task.Run(() => new List(new[] {
30 | new Person { Id = 1, FirstName = "Brandon", LastName = "Smith", Active = true },
31 | new Person { Id = 2, FirstName = "Allison", LastName = "Brown", Active = true },
32 | new Person { Id = 3, FirstName = "Patricia", LastName = "McDonald" }
33 | }).Where(p => includeInactive || p.Active));
34 | #endif
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Infrastructure/Persistence/SeedData.cs:
--------------------------------------------------------------------------------
1 | using CleanArchTemplate.Domain.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace CleanArchTemplate.Infrastructure.Persistence;
5 |
6 | public static class SeedDataExtension
7 | {
8 | public static void SeedData(this ModelBuilder builder)
9 | {
10 | // TODO: Use this file to seed the database with any initial data that
11 | // should exist the first time the application is run.
12 |
13 | builder.Entity().HasData(
14 | new Person { Id = 1, FirstName = "Brandon", LastName = "Smith", Active = true, CreatedBy = 1, CreatedOn = DateTime.UtcNow },
15 | new Person { Id = 2, FirstName = "Allison", LastName = "Brown", Active = true, CreatedBy = 1, CreatedOn = DateTime.UtcNow },
16 | new Person { Id = 3, FirstName = "Patricia", LastName = "McDonald", CreatedBy = 1, CreatedOn = DateTime.UtcNow }
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Infrastructure/Services/DateTimeService.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using CleanArchTemplate.Application.Interfaces.Services;
3 |
4 | namespace CleanArchTemplate.Infrastructure.Services;
5 |
6 | [ExcludeFromCodeCoverage]
7 | public class DateTimeService : IDateTimeService
8 | {
9 | public DateTime UtcNow => DateTime.UtcNow;
10 | }
11 |
--------------------------------------------------------------------------------
/src/CleanArchTemplate.Infrastructure/Services/LoggerService.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using Microsoft.Extensions.Logging;
3 | using CleanArchTemplate.Application.Interfaces.Services;
4 |
5 | namespace CleanArchTemplate.Infrastructure.Services;
6 |
7 | // This service exists to enable unit testing of log commands, since they are
8 | // extension methods on the ILogger interface.
9 | [ExcludeFromCodeCoverage]
10 | public class LoggerService : ILoggerService
11 | {
12 | private readonly ILogger _logger;
13 |
14 | public LoggerService(ILogger logger)
15 | {
16 | _logger = logger;
17 | }
18 |
19 | public void LogError(Exception exception)
20 | {
21 | _logger.LogError(exception, exception.Message);
22 | }
23 |
24 | public void LogError(Exception exception, string message)
25 | {
26 | _logger.LogError(exception, message);
27 | }
28 |
29 | public void LogInformation(string message)
30 | {
31 | _logger.LogInformation(message);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/CleanArchTemplate.Api.Tests/CleanArchTemplate.Api.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | CleanArchTemplate.Api.Tests
5 | enable
6 | false
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 | all
17 |
18 |
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 | all
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/tests/CleanArchTemplate.Api.Tests/Extensions/EndpointResultExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using FluentAssertions;
3 | using Microsoft.AspNetCore.Mvc;
4 | using CleanArchTemplate.Api.Extensions;
5 | using CleanArchTemplate.Application.Models;
6 | using CleanArchTemplate.Application.Models.Enumerations;
7 | using Xunit;
8 |
9 | namespace CleanArchTemplate.Api.Tests.Extensions;
10 |
11 | public class EndpointResultExtensionsTests
12 | {
13 | [Fact]
14 | public void ToActionResultReturnsOkResultOnSuccess()
15 | {
16 | var result = new EndpointResult(EndpointResultStatus.Success).ToActionResult();
17 |
18 | result.Should().NotBeNull().And.BeOfType();
19 | }
20 |
21 | [Fact]
22 | public void ToActionResultReturnsNotFoundResultOnNotFound()
23 | {
24 | var result = new EndpointResult(EndpointResultStatus.NotFound).ToActionResult();
25 |
26 | result.Should().NotBeNull().And.BeOfType();
27 | }
28 |
29 | [Fact]
30 | public void ToActionResultReturnsUnprocessableEntityResultOnInvalid()
31 | {
32 | var result = new EndpointResult(EndpointResultStatus.Invalid).ToActionResult();
33 |
34 | result.Should().NotBeNull().And.BeOfType();
35 | }
36 |
37 | [Fact]
38 | public void ToActionResultReturnsUnprocessableEntityObjectResultOnInvalidWithData()
39 | {
40 | var result = new EndpointResult(EndpointResultStatus.Invalid) { Messages = new[] { "Oops" } }.ToActionResult();
41 |
42 | result.Should().NotBeNull().And.BeOfType();
43 | }
44 |
45 | [Fact]
46 | public void ToActionResultReturnsInvalidDataDetailsOnInvalidWithData()
47 | {
48 | var result = new EndpointResult(EndpointResultStatus.Invalid) { Messages = new[] { "Oops" } }.ToActionResult();
49 |
50 | result.As().Value.Should().NotBeNull();
51 | result.As().Value.As>().Should().HaveCount(1);
52 | }
53 |
54 | [Fact]
55 | public void ToActionResultReturnsConflictResultOnDuplicate()
56 | {
57 | var result = new EndpointResult(EndpointResultStatus.Duplicate).ToActionResult();
58 |
59 | result.Should().NotBeNull().And.BeOfType();
60 | }
61 |
62 | [Fact]
63 | public void ToActionResultReturnsUnauthorizedEntityResultOnUnauthorized()
64 | {
65 | var result = new EndpointResult(EndpointResultStatus.Unauthorized).ToActionResult();
66 |
67 | result.Should().NotBeNull().And.BeOfType();
68 | }
69 |
70 | [Fact]
71 | public void ToActionResultReturnsGoneStatusCodeResultOnGone()
72 | {
73 | var result = new EndpointResult(EndpointResultStatus.Gone).ToActionResult();
74 |
75 | result.Should().NotBeNull().And.BeOfType();
76 | result.As().StatusCode.Should().Be((int)HttpStatusCode.Gone);
77 | }
78 |
79 | [Fact]
80 | public void ToActionResultReturnsServerErrorStatusCodeResultOnAnythingElse()
81 | {
82 | var result = new EndpointResult(EndpointResultStatus.Error).ToActionResult();
83 |
84 | result.Should().NotBeNull().And.BeOfType();
85 | result.As().StatusCode.Should().Be((int)HttpStatusCode.InternalServerError);
86 | }
87 |
88 | [Fact]
89 | public void ToActionResultReturnsOkObjectResultOnSuccessWithData()
90 | {
91 | var result = new EndpointResult("It works!").ToActionResult();
92 |
93 | result.Should().NotBeNull().And.BeOfType();
94 | }
95 |
96 | [Fact]
97 | public void ToActionResultReturnsDataOnSuccessWithData()
98 | {
99 | var result = new EndpointResult("It works!").ToActionResult();
100 |
101 | result.As().Value.Should().BeOfType().And.Be("It works!");
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tests/CleanArchTemplate.Api.Tests/Services/PrincipalServiceTests.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 | using FluentAssertions;
3 | using Microsoft.AspNetCore.Http;
4 | using Moq;
5 | using CleanArchTemplate.Api.Exceptions;
6 | using CleanArchTemplate.Api.Services;
7 | using Xunit;
8 |
9 | namespace CleanArchTemplate.Api.Tests.Services;
10 |
11 | public class PrincipalServiceTests
12 | {
13 | private readonly Mock _httpContextAccessor;
14 |
15 | public PrincipalServiceTests()
16 | {
17 | _httpContextAccessor = new Mock(MockBehavior.Strict);
18 | }
19 |
20 | [Fact]
21 | public void UserIdThrowsMissingClaimsPrincipleExceptionWhenClaimsPrincipalNotFound()
22 | {
23 | _httpContextAccessor.SetupGet(x => x.HttpContext).Returns((HttpContext)null!);
24 | var service = new PrincipalService(_httpContextAccessor.Object);
25 |
26 | var action = () => service.UserId;
27 |
28 | action.Should().Throw();
29 | }
30 |
31 | [Fact]
32 | public void UserIdReturnsIdWhenNameIdentifierClaimExists()
33 | {
34 | _httpContextAccessor.SetupGet(x => x.HttpContext!.User).Returns(new ClaimsPrincipal(new List(new[]{
35 | new ClaimsIdentity(new List(new []{ new Claim(ClaimTypes.NameIdentifier, "3")}))
36 | })));
37 | var service = new PrincipalService(_httpContextAccessor.Object);
38 |
39 | var result = service.UserId;
40 |
41 | result.Should().Be(3);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/CleanArchTemplate.Application.Tests/CleanArchTemplate.Application.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | CleanArchTemplate.Application.Tests
5 | enable
6 | false
7 | enable
8 |
9 |
10 |
11 |
12 |
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 | all
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/CleanArchTemplate.Application.Tests/Mapping/PeopleProfileTests.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchTemplate.Application.Mapping;
3 | using Xunit;
4 |
5 | namespace CleanArchTemplate.Application.Tests.Mapping;
6 |
7 | public class PeopleProfileTests
8 | {
9 | [Fact]
10 | public void VerifyConfiguration()
11 | {
12 | var configuration = new MapperConfiguration(cfg => cfg.AddProfile());
13 |
14 | configuration.AssertConfigurationIsValid();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/CleanArchTemplate.Infrastructure.Tests/CleanArchTemplate.Infrastructure.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | CleanArchTemplate.Infrastructure.Tests
5 | enable
6 | false
7 | enable
8 |
9 |
10 |
11 |
12 |
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 | all
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------