├── .gitignore ├── CleanMinimalApi.sln ├── Customers.Api ├── Contracts │ ├── Data │ │ └── CustomerDto.cs │ ├── Requests │ │ ├── CreateCustomerRequest.cs │ │ ├── DeleteCustomerRequest.cs │ │ ├── GetCustomerRequest.cs │ │ └── UpdateCustomerRequest.cs │ └── Responses │ │ ├── CustomerResponse.cs │ │ ├── GetAllCustomersResponse.cs │ │ └── ValidationFailureResponse.cs ├── Customers.Api.csproj ├── Database │ ├── DatabaseInitializer.cs │ └── DbConnectionFactory.cs ├── Domain │ ├── Common │ │ ├── CustomerId.cs │ │ ├── DateOfBirth.cs │ │ ├── EmailAddress.cs │ │ ├── FullName.cs │ │ └── Username.cs │ └── Customer.cs ├── Endpoints │ ├── CreateCustomerEndpoint.cs │ ├── DeleteCustomerEndpoint.cs │ ├── GetAllCustomersEndpoint.cs │ ├── GetCustomerEndpoint.cs │ └── UpdateCustomerEndpoint.cs ├── Mapping │ ├── ApiContractToDomainMapper.cs │ ├── DomainToApiContractMapper.cs │ ├── DomainToDtoMapper.cs │ └── DtoToDomainMapper.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Repositories │ ├── CustomerRepository.cs │ └── ICustomerRepository.cs ├── Services │ ├── CustomerService.cs │ └── ICustomerService.cs ├── Summaries │ ├── CreateCustomerSummary.cs │ ├── DeleteCustomerSummary.cs │ ├── GetAllCustomersSummary.cs │ ├── GetCustomerSummary.cs │ └── UpdateCustomerSummary.cs ├── Validation │ ├── CreateCustomerRequestValidator.cs │ ├── UpdateCustomerRequestValidator.cs │ └── ValidationExceptionMiddleware.cs ├── appsettings.Development.json ├── appsettings.json └── structured.http ├── LICENSE └── README.md /.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 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | /obj/ 263 | *.db 264 | .DS_Store 265 | -------------------------------------------------------------------------------- /CleanMinimalApi.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Customers.Api", "Customers.Api\Customers.Api.csproj", "{3EF2E620-2D05-4D38-A0D0-4779B29C51F2}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {3EF2E620-2D05-4D38-A0D0-4779B29C51F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {3EF2E620-2D05-4D38-A0D0-4779B29C51F2}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {3EF2E620-2D05-4D38-A0D0-4779B29C51F2}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {3EF2E620-2D05-4D38-A0D0-4779B29C51F2}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /Customers.Api/Contracts/Data/CustomerDto.cs: -------------------------------------------------------------------------------- 1 | namespace Customers.Api.Contracts.Data; 2 | 3 | public class CustomerDto 4 | { 5 | public string Id { get; init; } = default!; 6 | 7 | public string Username { get; init; } = default!; 8 | 9 | public string FullName { get; init; } = default!; 10 | 11 | public string Email { get; init; } = default!; 12 | 13 | public DateTime DateOfBirth { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /Customers.Api/Contracts/Requests/CreateCustomerRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Customers.Api.Contracts.Requests; 2 | 3 | public class CreateCustomerRequest 4 | { 5 | public string Username { get; init; } = default!; 6 | 7 | public string FullName { get; init; } = default!; 8 | 9 | public string Email { get; init; } = default!; 10 | 11 | public DateTime DateOfBirth { get; init; } 12 | } 13 | -------------------------------------------------------------------------------- /Customers.Api/Contracts/Requests/DeleteCustomerRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Customers.Api.Contracts.Requests; 2 | 3 | public class DeleteCustomerRequest 4 | { 5 | public Guid Id { get; init; } 6 | } 7 | -------------------------------------------------------------------------------- /Customers.Api/Contracts/Requests/GetCustomerRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Customers.Api.Contracts.Requests; 2 | 3 | public class GetCustomerRequest 4 | { 5 | public Guid Id { get; init; } 6 | } 7 | -------------------------------------------------------------------------------- /Customers.Api/Contracts/Requests/UpdateCustomerRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Customers.Api.Contracts.Requests; 2 | 3 | public class UpdateCustomerRequest 4 | { 5 | public Guid Id { get; init; } 6 | 7 | public string Username { get; init; } = default!; 8 | 9 | public string FullName { get; init; } = default!; 10 | 11 | public string Email { get; init; } = default!; 12 | 13 | public DateTime DateOfBirth { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /Customers.Api/Contracts/Responses/CustomerResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Customers.Api.Contracts.Responses; 2 | 3 | public class CustomerResponse 4 | { 5 | public Guid Id { get; init; } 6 | 7 | public string Username { get; init; } = default!; 8 | 9 | public string FullName { get; init; } = default!; 10 | 11 | public string Email { get; init; } = default!; 12 | 13 | public DateTime DateOfBirth { get; init; } 14 | } 15 | -------------------------------------------------------------------------------- /Customers.Api/Contracts/Responses/GetAllCustomersResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Customers.Api.Contracts.Responses; 2 | 3 | public class GetAllCustomersResponse 4 | { 5 | public IEnumerable Customers { get; init; } = Enumerable.Empty(); 6 | } 7 | -------------------------------------------------------------------------------- /Customers.Api/Contracts/Responses/ValidationFailureResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Customers.Api.Contracts.Responses; 2 | 3 | public class ValidationFailureResponse 4 | { 5 | public List Errors { get; init; } = new(); 6 | } 7 | -------------------------------------------------------------------------------- /Customers.Api/Customers.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Customers.Api/Database/DatabaseInitializer.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | 3 | namespace Customers.Api.Database; 4 | 5 | public class DatabaseInitializer 6 | { 7 | private readonly IDbConnectionFactory _connectionFactory; 8 | 9 | public DatabaseInitializer(IDbConnectionFactory connectionFactory) 10 | { 11 | _connectionFactory = connectionFactory; 12 | } 13 | 14 | public async Task InitializeAsync() 15 | { 16 | using var connection = await _connectionFactory.CreateConnectionAsync(); 17 | await connection.ExecuteAsync(@"CREATE TABLE IF NOT EXISTS Customers ( 18 | Id CHAR(36) PRIMARY KEY, 19 | Username TEXT NOT NULL, 20 | FullName TEXT NOT NULL, 21 | Email TEXT NOT NULL, 22 | DateOfBirth TEXT NOT NULL)"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Customers.Api/Database/DbConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using Microsoft.Data.Sqlite; 3 | 4 | namespace Customers.Api.Database; 5 | 6 | public interface IDbConnectionFactory 7 | { 8 | public Task CreateConnectionAsync(); 9 | } 10 | 11 | public class SqliteConnectionFactory : IDbConnectionFactory 12 | { 13 | private readonly string _connectionString; 14 | 15 | public SqliteConnectionFactory(string connectionString) 16 | { 17 | _connectionString = connectionString; 18 | } 19 | 20 | public async Task CreateConnectionAsync() 21 | { 22 | var connection = new SqliteConnection(_connectionString); 23 | await connection.OpenAsync(); 24 | return connection; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Customers.Api/Domain/Common/CustomerId.cs: -------------------------------------------------------------------------------- 1 | using ValueOf; 2 | 3 | namespace Customers.Api.Domain.Common; 4 | 5 | public class CustomerId : ValueOf 6 | { 7 | protected override void Validate() 8 | { 9 | if (Value == Guid.Empty) 10 | { 11 | throw new ArgumentException("Customer Id cannot be empty", nameof(CustomerId)); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Customers.Api/Domain/Common/DateOfBirth.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FluentValidation.Results; 3 | using ValueOf; 4 | 5 | namespace Customers.Api.Domain.Common; 6 | 7 | public class DateOfBirth : ValueOf 8 | { 9 | protected override void Validate() 10 | { 11 | if (Value > DateOnly.FromDateTime(DateTime.Now)) 12 | { 13 | const string message = "Your date of birth cannot be in the future"; 14 | throw new ValidationException(message, new [] 15 | { 16 | new ValidationFailure(nameof(DateOfBirth), message) 17 | }); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Customers.Api/Domain/Common/EmailAddress.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using FluentValidation; 3 | using FluentValidation.Results; 4 | using ValueOf; 5 | 6 | namespace Customers.Api.Domain.Common; 7 | 8 | public class EmailAddress : ValueOf 9 | { 10 | private static readonly Regex EmailRegex = 11 | new("^[\\w!#$%&’*+/=?`{|}~^-]+(?:\\.[\\w!#$%&’*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", 12 | RegexOptions.Compiled | RegexOptions.IgnoreCase); 13 | 14 | protected override void Validate() 15 | { 16 | if (!EmailRegex.IsMatch(Value)) 17 | { 18 | var message = $"{Value} is not a valid email address"; 19 | throw new ValidationException(message, new [] 20 | { 21 | new ValidationFailure(nameof(EmailAddress), message) 22 | }); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Customers.Api/Domain/Common/FullName.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using FluentValidation; 3 | using FluentValidation.Results; 4 | using ValueOf; 5 | 6 | namespace Customers.Api.Domain.Common; 7 | 8 | public class FullName : ValueOf 9 | { 10 | private static readonly Regex FullNameRegex = 11 | new("^[a-z ,.'-]+$", RegexOptions.Compiled | RegexOptions.IgnoreCase); 12 | 13 | protected override void Validate() 14 | { 15 | if (!FullNameRegex.IsMatch(Value)) 16 | { 17 | var message = $"{Value} is not a valid full name"; 18 | throw new ValidationException(message, new [] 19 | { 20 | new ValidationFailure(nameof(FullName), message) 21 | }); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Customers.Api/Domain/Common/Username.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using FluentValidation; 3 | using FluentValidation.Results; 4 | using ValueOf; 5 | 6 | namespace Customers.Api.Domain.Common; 7 | 8 | public class Username : ValueOf 9 | { 10 | private static readonly Regex UsernameRegex = 11 | new("^[a-z\\d](?:[a-z\\d]|-(?=[a-z\\d])){0,38}$", RegexOptions.Compiled | RegexOptions.IgnoreCase); 12 | 13 | protected override void Validate() 14 | { 15 | if (!UsernameRegex.IsMatch(Value)) 16 | { 17 | var message = $"{Value} is not a valid username"; 18 | throw new ValidationException(message, new [] 19 | { 20 | new ValidationFailure(nameof(Username), message) 21 | }); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Customers.Api/Domain/Customer.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Domain.Common; 2 | 3 | namespace Customers.Api.Domain; 4 | 5 | public class Customer 6 | { 7 | public CustomerId Id { get; init; } = CustomerId.From(Guid.NewGuid()); 8 | 9 | public Username Username { get; init; } = default!; 10 | 11 | public FullName FullName { get; init; } = default!; 12 | 13 | public EmailAddress Email { get; init; } = default!; 14 | 15 | public DateOfBirth DateOfBirth { get; init; } = default!; 16 | } 17 | -------------------------------------------------------------------------------- /Customers.Api/Endpoints/CreateCustomerEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Requests; 2 | using Customers.Api.Contracts.Responses; 3 | using Customers.Api.Mapping; 4 | using Customers.Api.Services; 5 | using FastEndpoints; 6 | using Microsoft.AspNetCore.Authorization; 7 | 8 | namespace Customers.Api.Endpoints; 9 | 10 | [HttpPost("customers"), AllowAnonymous] 11 | public class CreateCustomerEndpoint : Endpoint 12 | { 13 | private readonly ICustomerService _customerService; 14 | 15 | public CreateCustomerEndpoint(ICustomerService customerService) 16 | { 17 | _customerService = customerService; 18 | } 19 | 20 | public override async Task HandleAsync(CreateCustomerRequest req, CancellationToken ct) 21 | { 22 | var customer = req.ToCustomer(); 23 | 24 | await _customerService.CreateAsync(customer); 25 | 26 | var customerResponse = customer.ToCustomerResponse(); 27 | await SendCreatedAtAsync( 28 | new { Id = customer.Id.Value }, customerResponse, generateAbsoluteUrl: true, cancellation: ct); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Customers.Api/Endpoints/DeleteCustomerEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Requests; 2 | using Customers.Api.Services; 3 | using FastEndpoints; 4 | using Microsoft.AspNetCore.Authorization; 5 | 6 | namespace Customers.Api.Endpoints; 7 | 8 | [HttpDelete("customers/{id:guid}"), AllowAnonymous] 9 | public class DeleteCustomerEndpoint : Endpoint 10 | { 11 | private readonly ICustomerService _customerService; 12 | 13 | public DeleteCustomerEndpoint(ICustomerService customerService) 14 | { 15 | _customerService = customerService; 16 | } 17 | 18 | public override async Task HandleAsync(DeleteCustomerRequest req, CancellationToken ct) 19 | { 20 | var deleted = await _customerService.DeleteAsync(req.Id); 21 | if (!deleted) 22 | { 23 | await SendNotFoundAsync(ct); 24 | return; 25 | } 26 | 27 | await SendNoContentAsync(ct); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Customers.Api/Endpoints/GetAllCustomersEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Responses; 2 | using Customers.Api.Mapping; 3 | using Customers.Api.Services; 4 | using FastEndpoints; 5 | using Microsoft.AspNetCore.Authorization; 6 | 7 | namespace Customers.Api.Endpoints; 8 | 9 | [HttpGet("customers"), AllowAnonymous] 10 | public class GetAllCustomersEndpoint : EndpointWithoutRequest 11 | { 12 | private readonly ICustomerService _customerService; 13 | 14 | public GetAllCustomersEndpoint(ICustomerService customerService) 15 | { 16 | _customerService = customerService; 17 | } 18 | 19 | public override async Task HandleAsync(CancellationToken ct) 20 | { 21 | var customers = await _customerService.GetAllAsync(); 22 | var customersResponse = customers.ToCustomersResponse(); 23 | await SendOkAsync(customersResponse, ct); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Customers.Api/Endpoints/GetCustomerEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Requests; 2 | using Customers.Api.Contracts.Responses; 3 | using Customers.Api.Mapping; 4 | using Customers.Api.Services; 5 | using FastEndpoints; 6 | using Microsoft.AspNetCore.Authorization; 7 | 8 | namespace Customers.Api.Endpoints; 9 | 10 | [HttpGet("customers/{id:guid}"), AllowAnonymous] 11 | public class GetCustomerEndpoint : Endpoint 12 | { 13 | private readonly ICustomerService _customerService; 14 | 15 | public GetCustomerEndpoint(ICustomerService customerService) 16 | { 17 | _customerService = customerService; 18 | } 19 | 20 | public override async Task HandleAsync(GetCustomerRequest req, CancellationToken ct) 21 | { 22 | var customer = await _customerService.GetAsync(req.Id); 23 | 24 | if (customer is null) 25 | { 26 | await SendNotFoundAsync(ct); 27 | return; 28 | } 29 | 30 | var customerResponse = customer.ToCustomerResponse(); 31 | await SendOkAsync(customerResponse, ct); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Customers.Api/Endpoints/UpdateCustomerEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Requests; 2 | using Customers.Api.Contracts.Responses; 3 | using Customers.Api.Mapping; 4 | using Customers.Api.Services; 5 | using FastEndpoints; 6 | using Microsoft.AspNetCore.Authorization; 7 | 8 | namespace Customers.Api.Endpoints; 9 | 10 | [HttpPut("customers/{id:guid}"), AllowAnonymous] 11 | public class UpdateCustomerEndpoint : Endpoint 12 | { 13 | private readonly ICustomerService _customerService; 14 | 15 | public UpdateCustomerEndpoint(ICustomerService customerService) 16 | { 17 | _customerService = customerService; 18 | } 19 | 20 | public override async Task HandleAsync(UpdateCustomerRequest req, CancellationToken ct) 21 | { 22 | var existingCustomer = await _customerService.GetAsync(req.Id); 23 | 24 | if (existingCustomer is null) 25 | { 26 | await SendNotFoundAsync(ct); 27 | return; 28 | } 29 | 30 | var customer = req.ToCustomer(); 31 | await _customerService.UpdateAsync(customer); 32 | 33 | var customerResponse = customer.ToCustomerResponse(); 34 | await SendOkAsync(customerResponse, ct); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Customers.Api/Mapping/ApiContractToDomainMapper.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Requests; 2 | using Customers.Api.Domain; 3 | using Customers.Api.Domain.Common; 4 | 5 | namespace Customers.Api.Mapping; 6 | 7 | public static class ApiContractToDomainMapper 8 | { 9 | public static Customer ToCustomer(this CreateCustomerRequest request) 10 | { 11 | return new Customer 12 | { 13 | Id = CustomerId.From(Guid.NewGuid()), 14 | Email = EmailAddress.From(request.Email), 15 | Username = Username.From(request.Username), 16 | FullName = FullName.From(request.FullName), 17 | DateOfBirth = DateOfBirth.From(DateOnly.FromDateTime(request.DateOfBirth)) 18 | }; 19 | } 20 | 21 | public static Customer ToCustomer(this UpdateCustomerRequest request) 22 | { 23 | return new Customer 24 | { 25 | Id = CustomerId.From(request.Id), 26 | Email = EmailAddress.From(request.Email), 27 | Username = Username.From(request.Username), 28 | FullName = FullName.From(request.FullName), 29 | DateOfBirth = DateOfBirth.From(DateOnly.FromDateTime(request.DateOfBirth)) 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Customers.Api/Mapping/DomainToApiContractMapper.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Responses; 2 | using Customers.Api.Domain; 3 | 4 | namespace Customers.Api.Mapping; 5 | 6 | public static class DomainToApiContractMapper 7 | { 8 | public static CustomerResponse ToCustomerResponse(this Customer customer) 9 | { 10 | return new CustomerResponse 11 | { 12 | Id = customer.Id.Value, 13 | Email = customer.Email.Value, 14 | Username = customer.Username.Value, 15 | FullName = customer.FullName.Value, 16 | DateOfBirth = customer.DateOfBirth.Value.ToDateTime(TimeOnly.MinValue) 17 | }; 18 | } 19 | 20 | public static GetAllCustomersResponse ToCustomersResponse(this IEnumerable customers) 21 | { 22 | return new GetAllCustomersResponse 23 | { 24 | Customers = customers.Select(x => new CustomerResponse 25 | { 26 | Id = x.Id.Value, 27 | Email = x.Email.Value, 28 | Username = x.Username.Value, 29 | FullName = x.FullName.Value, 30 | DateOfBirth = x.DateOfBirth.Value.ToDateTime(TimeOnly.MinValue) 31 | }) 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Customers.Api/Mapping/DomainToDtoMapper.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Data; 2 | using Customers.Api.Domain; 3 | 4 | namespace Customers.Api.Mapping; 5 | 6 | public static class DomainToDtoMapper 7 | { 8 | public static CustomerDto ToCustomerDto(this Customer customer) 9 | { 10 | return new CustomerDto 11 | { 12 | Id = customer.Id.Value.ToString(), 13 | Email = customer.Email.Value, 14 | Username = customer.Username.Value, 15 | FullName = customer.FullName.Value, 16 | DateOfBirth = customer.DateOfBirth.Value.ToDateTime(TimeOnly.MinValue) 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Customers.Api/Mapping/DtoToDomainMapper.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Data; 2 | using Customers.Api.Domain; 3 | using Customers.Api.Domain.Common; 4 | 5 | namespace Customers.Api.Mapping; 6 | 7 | public static class DtoToDomainMapper 8 | { 9 | public static Customer ToCustomer(this CustomerDto customerDto) 10 | { 11 | return new Customer 12 | { 13 | Id = CustomerId.From(Guid.Parse(customerDto.Id)), 14 | Email = EmailAddress.From(customerDto.Email), 15 | Username = Username.From(customerDto.Username), 16 | FullName = FullName.From(customerDto.FullName), 17 | DateOfBirth = DateOfBirth.From(DateOnly.FromDateTime(customerDto.DateOfBirth)) 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Customers.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Responses; 2 | using Customers.Api.Database; 3 | using Customers.Api.Repositories; 4 | using Customers.Api.Services; 5 | using Customers.Api.Validation; 6 | using FastEndpoints; 7 | using FastEndpoints.Swagger; 8 | 9 | var builder = WebApplication.CreateBuilder(args); 10 | var config = builder.Configuration; 11 | 12 | builder.Services.AddFastEndpoints(); 13 | builder.Services.AddSwaggerDoc(); 14 | 15 | builder.Services.AddSingleton(_ => 16 | new SqliteConnectionFactory(config.GetValue("Database:ConnectionString"))); 17 | builder.Services.AddSingleton(); 18 | builder.Services.AddSingleton(); 19 | builder.Services.AddSingleton(); 20 | 21 | var app = builder.Build(); 22 | 23 | app.UseMiddleware(); 24 | app.UseFastEndpoints(x => 25 | { 26 | x.ErrorResponseBuilder = (failures, _) => 27 | { 28 | return new ValidationFailureResponse 29 | { 30 | Errors = failures.Select(y => y.ErrorMessage).ToList() 31 | }; 32 | }; 33 | }); 34 | 35 | app.UseOpenApi(); 36 | app.UseSwaggerUi3(s => s.ConfigureDefaults()); 37 | 38 | var databaseInitializer = app.Services.GetRequiredService(); 39 | await databaseInitializer.InitializeAsync(); 40 | 41 | app.Run(); 42 | -------------------------------------------------------------------------------- /Customers.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Customer.Api": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Customers.Api/Repositories/CustomerRepository.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Data; 2 | using Customers.Api.Database; 3 | using Dapper; 4 | 5 | namespace Customers.Api.Repositories; 6 | 7 | public class CustomerRepository : ICustomerRepository 8 | { 9 | private readonly IDbConnectionFactory _connectionFactory; 10 | 11 | public CustomerRepository(IDbConnectionFactory connectionFactory) 12 | { 13 | _connectionFactory = connectionFactory; 14 | } 15 | 16 | public async Task CreateAsync(CustomerDto customer) 17 | { 18 | using var connection = await _connectionFactory.CreateConnectionAsync(); 19 | var result = await connection.ExecuteAsync( 20 | @"INSERT INTO Customers (Id, Username, FullName, Email, DateOfBirth) 21 | VALUES (@Id, @Username, @FullName, @Email, @DateOfBirth)", 22 | customer); 23 | return result > 0; 24 | } 25 | 26 | public async Task GetAsync(Guid id) 27 | { 28 | using var connection = await _connectionFactory.CreateConnectionAsync(); 29 | return await connection.QuerySingleOrDefaultAsync( 30 | "SELECT * FROM Customers WHERE Id = @Id LIMIT 1", new { Id = id.ToString() }); 31 | } 32 | 33 | public async Task> GetAllAsync() 34 | { 35 | using var connection = await _connectionFactory.CreateConnectionAsync(); 36 | return await connection.QueryAsync("SELECT * FROM Customers"); 37 | } 38 | 39 | public async Task UpdateAsync(CustomerDto customer) 40 | { 41 | using var connection = await _connectionFactory.CreateConnectionAsync(); 42 | var result = await connection.ExecuteAsync( 43 | @"UPDATE Customers SET Username = @Username, FullName = @FullName, Email = @Email, 44 | DateOfBirth = @DateOfBirth WHERE Id = @Id", 45 | customer); 46 | return result > 0; 47 | } 48 | 49 | public async Task DeleteAsync(Guid id) 50 | { 51 | using var connection = await _connectionFactory.CreateConnectionAsync(); 52 | var result = await connection.ExecuteAsync(@"DELETE FROM Customers WHERE Id = @Id", 53 | new {Id = id.ToString()}); 54 | return result > 0; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Customers.Api/Repositories/ICustomerRepository.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Data; 2 | 3 | namespace Customers.Api.Repositories; 4 | 5 | public interface ICustomerRepository 6 | { 7 | Task CreateAsync(CustomerDto customer); 8 | 9 | Task GetAsync(Guid id); 10 | 11 | Task> GetAllAsync(); 12 | 13 | Task UpdateAsync(CustomerDto customer); 14 | 15 | Task DeleteAsync(Guid id); 16 | } 17 | -------------------------------------------------------------------------------- /Customers.Api/Services/CustomerService.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Domain; 2 | using Customers.Api.Mapping; 3 | using Customers.Api.Repositories; 4 | using FluentValidation; 5 | using FluentValidation.Results; 6 | 7 | namespace Customers.Api.Services; 8 | 9 | public class CustomerService : ICustomerService 10 | { 11 | private readonly ICustomerRepository _customerRepository; 12 | 13 | public CustomerService(ICustomerRepository customerRepository) 14 | { 15 | _customerRepository = customerRepository; 16 | } 17 | 18 | public async Task CreateAsync(Customer customer) 19 | { 20 | var existingUser = await _customerRepository.GetAsync(customer.Id.Value); 21 | if (existingUser is not null) 22 | { 23 | var message = $"A user with id {customer.Id} already exists"; 24 | throw new ValidationException(message, new [] 25 | { 26 | new ValidationFailure(nameof(Customer), message) 27 | }); 28 | } 29 | 30 | var customerDto = customer.ToCustomerDto(); 31 | return await _customerRepository.CreateAsync(customerDto); 32 | } 33 | 34 | public async Task GetAsync(Guid id) 35 | { 36 | var customerDto = await _customerRepository.GetAsync(id); 37 | return customerDto?.ToCustomer(); 38 | } 39 | 40 | public async Task> GetAllAsync() 41 | { 42 | var customerDtos = await _customerRepository.GetAllAsync(); 43 | return customerDtos.Select(x => x.ToCustomer()); 44 | } 45 | 46 | public async Task UpdateAsync(Customer customer) 47 | { 48 | var customerDto = customer.ToCustomerDto(); 49 | return await _customerRepository.UpdateAsync(customerDto); 50 | } 51 | 52 | public async Task DeleteAsync(Guid id) 53 | { 54 | return await _customerRepository.DeleteAsync(id); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Customers.Api/Services/ICustomerService.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Domain; 2 | 3 | namespace Customers.Api.Services; 4 | 5 | public interface ICustomerService 6 | { 7 | Task CreateAsync(Customer customer); 8 | 9 | Task GetAsync(Guid id); 10 | 11 | Task> GetAllAsync(); 12 | 13 | Task UpdateAsync(Customer customer); 14 | 15 | Task DeleteAsync(Guid id); 16 | } 17 | -------------------------------------------------------------------------------- /Customers.Api/Summaries/CreateCustomerSummary.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Responses; 2 | using Customers.Api.Endpoints; 3 | using FastEndpoints; 4 | 5 | namespace Customers.Api.Summaries; 6 | 7 | public class CreateCustomerSummary : Summary 8 | { 9 | public CreateCustomerSummary() 10 | { 11 | Summary = "Creates a new customer in the system"; 12 | Description = "Creates a new customer in the system"; 13 | Response(201, "Customer was successfully created"); 14 | Response(400, "The request did not pass validation checks"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Customers.Api/Summaries/DeleteCustomerSummary.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Endpoints; 2 | using FastEndpoints; 3 | 4 | namespace Customers.Api.Summaries; 5 | 6 | public class DeleteCustomerSummary : Summary 7 | { 8 | public DeleteCustomerSummary() 9 | { 10 | Summary = "Deleted a customer the system"; 11 | Description = "Deleted a customer the system"; 12 | Response(204, "The customer was deleted successfully"); 13 | Response(404, "The customer was not found in the system"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Customers.Api/Summaries/GetAllCustomersSummary.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Responses; 2 | using Customers.Api.Endpoints; 3 | using FastEndpoints; 4 | 5 | namespace Customers.Api.Summaries; 6 | 7 | public class GetAllCustomersSummary : Summary 8 | { 9 | public GetAllCustomersSummary() 10 | { 11 | Summary = "Returns all the customers in the system"; 12 | Description = "Returns all the customers in the system"; 13 | Response(200, "All customers in the system are returned"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Customers.Api/Summaries/GetCustomerSummary.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Responses; 2 | using Customers.Api.Endpoints; 3 | using FastEndpoints; 4 | 5 | namespace Customers.Api.Summaries; 6 | 7 | public class GetCustomerSummary : Summary 8 | { 9 | public GetCustomerSummary() 10 | { 11 | Summary = "Returns a single customer by id"; 12 | Description = "Returns a single customer by id"; 13 | Response(200, "Successfully found and returned the customer"); 14 | Response(404, "The customer does not exist in the system"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Customers.Api/Summaries/UpdateCustomerSummary.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Responses; 2 | using Customers.Api.Endpoints; 3 | using FastEndpoints; 4 | 5 | namespace Customers.Api.Summaries; 6 | 7 | public class UpdateCustomerSummary : Summary 8 | { 9 | public UpdateCustomerSummary() 10 | { 11 | Summary = "Updates an existing customer in the system"; 12 | Description = "Updates an existing customer in the system"; 13 | Response(201, "Customer was successfully updated"); 14 | Response(400, "The request did not pass validation checks"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Customers.Api/Validation/CreateCustomerRequestValidator.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Requests; 2 | using FluentValidation; 3 | 4 | namespace Customers.Api.Validation; 5 | 6 | public class CreateCustomerRequestValidator : AbstractValidator 7 | { 8 | public CreateCustomerRequestValidator() 9 | { 10 | RuleFor(x => x.FullName).NotEmpty(); 11 | RuleFor(x => x.Email).NotEmpty(); 12 | RuleFor(x => x.Username).NotEmpty(); 13 | RuleFor(x => x.DateOfBirth).NotEmpty(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Customers.Api/Validation/UpdateCustomerRequestValidator.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Requests; 2 | using FluentValidation; 3 | 4 | namespace Customers.Api.Validation; 5 | 6 | public class UpdateCustomerRequestValidator : AbstractValidator 7 | { 8 | public UpdateCustomerRequestValidator() 9 | { 10 | RuleFor(x => x.FullName).NotEmpty(); 11 | RuleFor(x => x.Email).NotEmpty(); 12 | RuleFor(x => x.Username).NotEmpty(); 13 | RuleFor(x => x.DateOfBirth).NotEmpty(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Customers.Api/Validation/ValidationExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Customers.Api.Contracts.Responses; 2 | using FluentValidation; 3 | 4 | namespace Customers.Api.Validation; 5 | 6 | public class ValidationExceptionMiddleware 7 | { 8 | private readonly RequestDelegate _request; 9 | 10 | public ValidationExceptionMiddleware(RequestDelegate request) 11 | { 12 | _request = request; 13 | } 14 | 15 | public async Task InvokeAsync(HttpContext context) 16 | { 17 | try 18 | { 19 | await _request(context); 20 | } 21 | catch (ValidationException exception) 22 | { 23 | context.Response.StatusCode = 400; 24 | var messages = exception.Errors.Select(x => x.ErrorMessage).ToList(); 25 | var validationFailureResponse = new ValidationFailureResponse 26 | { 27 | Errors = messages 28 | }; 29 | await context.Response.WriteAsJsonAsync(validationFailureResponse); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Customers.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Customers.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Database": { 3 | "ConnectionString": "Data Source=./customer.db" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft.AspNetCore": "Warning" 9 | } 10 | }, 11 | "AllowedHosts": "*" 12 | } 13 | -------------------------------------------------------------------------------- /Customers.Api/structured.http: -------------------------------------------------------------------------------- 1 | ### Get all users 2 | GET https://localhost:5001/customers 3 | Accept: application/json 4 | 5 | ### Get user by id 6 | GET https://localhost:5001/customers/ 7 | Accept: application/json 8 | 9 | ### Create customer 10 | POST https://localhost:5001/customers 11 | Content-Type: application/json 12 | 13 | { 14 | "username": "nickchapsas", 15 | "fullName": "Nick Chapsas", 16 | "email": "nick@nickchapsas.com", 17 | "dateOfBirth": "1993-04-20" 18 | } 19 | 20 | ### Update customer 21 | PUT https://localhost:5001/customers/ 22 | Content-Type: application/json 23 | 24 | { 25 | "username": "chapsas", 26 | "fullName": "Nick Chapsas", 27 | "email": "nick@nickchapsas.com", 28 | "dateOfBirth": "1993-04-20" 29 | } 30 | 31 | ### Delete user 32 | DELETE https://localhost:5001/customers/ 33 | Accept: application/json 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nick Chapsas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean Minimal API 2 | A project showcasing how you can build a clean Minimal API using FastEndpoints 3 | 4 | # TODO: Write the rest of the README 5 | --------------------------------------------------------------------------------