├── .gitignore ├── CleanArchitectureCosmosDB.sln ├── LICENSE ├── README.md ├── SolutionItems └── SystemDesign.jpg ├── src ├── CleanArchitectureCosmosDB.AzureFunctions │ ├── .gitignore │ ├── CleanArchitectureCosmosDB.AzureFunctions.csproj │ ├── Properties │ │ ├── serviceDependencies.json │ │ └── serviceDependencies.local.json │ ├── README.md │ ├── StarterFunction.cs │ ├── Startup.cs │ ├── host.json │ └── local.settings.json ├── CleanArchitectureCosmosDB.ClientApp │ ├── .gitignore │ ├── CleanArchitectureCosmosDB.API.nswag │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── components │ │ │ ├── Alert.tsx │ │ │ ├── LoadingProgress.tsx │ │ │ ├── TextFieldWithFormikValidation.tsx │ │ │ └── ToDo │ │ │ │ └── ToDoDataTable.tsx │ │ ├── helpers │ │ │ └── api │ │ │ │ ├── ApiClientFactory.ts │ │ │ │ └── Resources.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── layouts │ │ │ ├── DashboardLayout.tsx │ │ │ └── shared │ │ │ │ └── Sidebar.tsx │ │ ├── logo.svg │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ ├── setupTests.ts │ │ └── views │ │ │ ├── Dashboard.tsx │ │ │ ├── ToDoCreate.tsx │ │ │ └── TodoList.tsx │ ├── tsconfig.json │ └── yarn.lock ├── CleanArchitectureCosmosDB.Core │ ├── CleanArchitectureCosmosDB.Core.csproj │ ├── Constants │ │ └── ApplicationIdentityConstants.cs │ ├── Entities │ │ ├── Audit │ │ │ └── Audit.cs │ │ ├── Base │ │ │ └── BaseEntity.cs │ │ └── ToDoItem.cs │ ├── Exceptions │ │ ├── EntityAlreadyExistsException.cs │ │ ├── EntityNotFoundException.cs │ │ └── InvalidCredentialsException.cs │ ├── Interfaces │ │ ├── Cache │ │ │ └── ICachedToDoItemsService.cs │ │ ├── Email │ │ │ └── IEmailService.cs │ │ ├── Persistence │ │ │ ├── IAuditRepository.cs │ │ │ ├── IRepository.cs │ │ │ └── IToDoItemRepository.cs │ │ └── Storage │ │ │ └── IStorageService.cs │ ├── README.md │ └── Specifications │ │ ├── AuditFilterSpecification.cs │ │ ├── Base │ │ └── CosmosDbSpecificationEvaluator.cs │ │ ├── Interfaces │ │ └── ISearchQuery.cs │ │ ├── ToDoItemGetAllSpecification.cs │ │ ├── ToDoItemSearchAggregationSpecification.cs │ │ └── ToDoItemSearchSpecification.cs ├── CleanArchitectureCosmosDB.Infrastructure │ ├── AppSettings │ │ ├── CosmosDbSettings.cs │ │ └── SendGridEmailSettings.cs │ ├── CleanArchitectureCosmosDB.Infrastructure.csproj │ ├── CosmosDbData │ │ ├── Constants │ │ │ └── CosmosDbConfigConstants.cs │ │ ├── CosmosDbContainer.cs │ │ ├── CosmosDbContainerFactory.cs │ │ ├── Interfaces │ │ │ ├── IContainerContext.cs │ │ │ ├── ICosmosDbContainer.cs │ │ │ └── ICosmosDbContainerFactory.cs │ │ └── Repository │ │ │ ├── AuditRepository.cs │ │ │ ├── CosmosDbRepository.cs │ │ │ └── ToDoItemRepository.cs │ ├── Extensions │ │ ├── CacheHelpers.cs │ │ ├── IApplicationBuilderExtensions.cs │ │ └── IServiceCollectionExtensions.cs │ ├── Identity │ │ ├── DesignTime │ │ │ ├── ApplicationDbContext.cs │ │ │ └── DesignTimeDbContextFactory.cs │ │ ├── Migrations │ │ │ ├── 20210108033404_InitialIdentityDbCreation.Designer.cs │ │ │ ├── 20210108033404_InitialIdentityDbCreation.cs │ │ │ └── ApplicationDbContextModelSnapshot.cs │ │ ├── Models │ │ │ ├── ApplicationDbContext.cs │ │ │ ├── ApplicationUser.cs │ │ │ ├── Authentication │ │ │ │ ├── Token.cs │ │ │ │ ├── TokenRequest.cs │ │ │ │ └── TokenResponse.cs │ │ │ └── RefreshToken.cs │ │ ├── README_Identity.md │ │ ├── Seed │ │ │ └── ApplicationDbContextDataSeed.cs │ │ ├── Services │ │ │ ├── ITokenService.cs │ │ │ └── TokenService.cs │ │ └── TokenServiceProvider.cs │ ├── README.md │ ├── Services │ │ ├── AzureBlobStorageService.cs │ │ └── SendGridEmailService.cs │ └── appsettings.json └── CleanArchitectureCosmosDB.WebAPI │ ├── CleanArchitectureCosmosDB.WebAPI.csproj │ ├── Config │ ├── AuthenticationConfig.cs │ ├── AuthorizationConfig.cs │ ├── CachingConfig.cs │ ├── DatabaseConfig.cs │ ├── MediatrConfig.cs │ ├── MvcConfig.cs │ ├── ODataConfig.cs │ └── SwaggerConfig.cs │ ├── Controllers │ ├── AttachmentController.cs │ ├── ToDoItemController.cs │ └── TokenController.cs │ ├── Infrastructure │ ├── ApiExceptions │ │ └── ApiModelValidationException.cs │ ├── Behaviours │ │ ├── UnhandledExceptionBehaviour.cs │ │ └── ValidationBehaviour.cs │ ├── Filters │ │ └── ApiExceptionFilterAttribute.cs │ └── Services │ │ └── InMemoryCachedToDoItemsService.cs │ ├── Models │ ├── Attachment │ │ ├── AttachmentModel.cs │ │ ├── Download.cs │ │ ├── Upload.cs │ │ └── UploadMultiple.cs │ ├── Shared │ │ └── DataTablesResponse.cs │ ├── ToDoItem │ │ ├── Create.cs │ │ ├── Delete.cs │ │ ├── Get.cs │ │ ├── GetAll.cs │ │ ├── GetAuditHistory.cs │ │ ├── MappingProfile.cs │ │ ├── Search.cs │ │ ├── ToDoItemAuditModel.cs │ │ ├── ToDoItemModel.cs │ │ └── Update.cs │ └── Token │ │ └── Authenticate.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── README.md │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.Production.json │ └── appsettings.json └── tests └── CleanArchitectureCosmosDB.UnitTests ├── CleanArchitectureCosmosDB.UnitTests.csproj └── Core └── Entities └── ToDoItemTest.cs /CleanArchitectureCosmosDB.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30104.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitectureCosmosDB.Core", "src\CleanArchitectureCosmosDB.Core\CleanArchitectureCosmosDB.Core.csproj", "{20DB98D4-BDEF-41C7-A4B6-8CE039F17CD6}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitectureCosmosDB.Infrastructure", "src\CleanArchitectureCosmosDB.Infrastructure\CleanArchitectureCosmosDB.Infrastructure.csproj", "{E6602277-4A75-47CB-9C11-351C1913964C}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitectureCosmosDB.WebAPI", "src\CleanArchitectureCosmosDB.WebAPI\CleanArchitectureCosmosDB.WebAPI.csproj", "{5592E51D-BD9E-4467-AB00-ADAD174F7BEE}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitectureCosmosDB.AzureFunctions", "src\CleanArchitectureCosmosDB.AzureFunctions\CleanArchitectureCosmosDB.AzureFunctions.csproj", "{3722E536-1041-4CAB-8449-F343455C4F5A}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{6BDAAFF8-8055-41CE-A72C-B37E01E7A84A}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitectureCosmosDB.UnitTests", "tests\CleanArchitectureCosmosDB.UnitTests\CleanArchitectureCosmosDB.UnitTests.csproj", "{910599EC-D9FB-40DD-A9B7-55AB3264D227}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{76C55982-43BD-4A94-B574-1232C4DBD410}" 19 | ProjectSection(SolutionItems) = preProject 20 | SolutionItems\System Design.jpg = SolutionItems\System Design.jpg 21 | EndProjectSection 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {20DB98D4-BDEF-41C7-A4B6-8CE039F17CD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {20DB98D4-BDEF-41C7-A4B6-8CE039F17CD6}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {20DB98D4-BDEF-41C7-A4B6-8CE039F17CD6}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {20DB98D4-BDEF-41C7-A4B6-8CE039F17CD6}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {E6602277-4A75-47CB-9C11-351C1913964C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {E6602277-4A75-47CB-9C11-351C1913964C}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {E6602277-4A75-47CB-9C11-351C1913964C}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {E6602277-4A75-47CB-9C11-351C1913964C}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {5592E51D-BD9E-4467-AB00-ADAD174F7BEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {5592E51D-BD9E-4467-AB00-ADAD174F7BEE}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {5592E51D-BD9E-4467-AB00-ADAD174F7BEE}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {5592E51D-BD9E-4467-AB00-ADAD174F7BEE}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {3722E536-1041-4CAB-8449-F343455C4F5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {3722E536-1041-4CAB-8449-F343455C4F5A}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {3722E536-1041-4CAB-8449-F343455C4F5A}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {3722E536-1041-4CAB-8449-F343455C4F5A}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {910599EC-D9FB-40DD-A9B7-55AB3264D227}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {910599EC-D9FB-40DD-A9B7-55AB3264D227}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {910599EC-D9FB-40DD-A9B7-55AB3264D227}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {910599EC-D9FB-40DD-A9B7-55AB3264D227}.Release|Any CPU.Build.0 = Release|Any CPU 49 | EndGlobalSection 50 | GlobalSection(SolutionProperties) = preSolution 51 | HideSolutionNode = FALSE 52 | EndGlobalSection 53 | GlobalSection(NestedProjects) = preSolution 54 | {910599EC-D9FB-40DD-A9B7-55AB3264D227} = {6BDAAFF8-8055-41CE-A72C-B37E01E7A84A} 55 | EndGlobalSection 56 | GlobalSection(ExtensibilityGlobals) = postSolution 57 | SolutionGuid = {B7EE11AF-6FED-48CD-A304-7C7B160285AA} 58 | EndGlobalSection 59 | EndGlobal 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Shawn Shi 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 | -------------------------------------------------------------------------------- /SolutionItems/SystemDesign.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnShiSS/clean-architecture-azure-cosmos-db/f8a0105c9ce46c25a980b09ad6eb3649f1f44416/SolutionItems/SystemDesign.jpg -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.AzureFunctions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.AzureFunctions/CleanArchitectureCosmosDB.AzureFunctions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | v4 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | PreserveNewest 24 | Never 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.AzureFunctions/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "storage1": { 4 | "type": "storage", 5 | "connectionId": "AzureWebJobsStorage" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.AzureFunctions/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "storage1": { 4 | "type": "storage.emulator", 5 | "connectionId": "AzureWebJobsStorage" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.AzureFunctions/README.md: -------------------------------------------------------------------------------- 1 | # Azure Functions project 2 | 3 | This project hosts Azure Functions: 4 | * StarterFunction, which is a time-triggered function. 5 | 6 | # Getting Started - Development 7 | 1. Update the RunOnStartup=true 8 | 1. Run project in Visual Studio 9 | 10 | # Deployment Notes 11 | 1. Make sure your Application settings in Azure Portal are property setup to match what you have in local.settings.json with production values. 12 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.AzureFunctions/StarterFunction.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Interfaces; 2 | using CleanArchitectureCosmosDB.Core.Specifications; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | 9 | namespace CleanArchitectureCosmosDB.AzureFunctions 10 | { 11 | public class StarterFunction 12 | { 13 | private readonly ILogger _log; 14 | private readonly IEmailService _emailService; 15 | private readonly IToDoItemRepository _repo; 16 | 17 | 18 | public StarterFunction(ILogger log, 19 | IEmailService emailService, 20 | IToDoItemRepository repo) 21 | { 22 | this._log = log ?? throw new ArgumentNullException(nameof(log)); 23 | this._emailService = emailService ?? throw new ArgumentNullException(nameof(emailService)); 24 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo)); 25 | 26 | } 27 | 28 | [FunctionName("StarterFunction")] 29 | public async Task Run([TimerTrigger("0 */5 * * * *", RunOnStartup = true)] TimerInfo myTimer) 30 | { 31 | _log.LogInformation($"StarterFunction Timer trigger function executed at: {DateTime.Now}"); 32 | 33 | // Example code to retrieve all ToDoItems and email "noreply@noreply.com" when time trigger is hit. 34 | ToDoItemGetAllSpecification specification = new ToDoItemGetAllSpecification(false); 35 | System.Collections.Generic.IEnumerable entities = await _repo.GetItemsAsync(specification); 36 | 37 | string messageBody = JsonSerializer.Serialize(entities); 38 | 39 | await _emailService.SendEmailAsync("noreply@noreply.com", "No Reply", "Todo Item Summary", messageBody); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.AzureFunctions/Startup.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Interfaces; 2 | using CleanArchitectureCosmosDB.Infrastructure.AppSettings; 3 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Repository; 4 | using CleanArchitectureCosmosDB.Infrastructure.Extensions; 5 | using CleanArchitectureCosmosDB.Infrastructure.Services; 6 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Serilog; 10 | using System.IO; 11 | 12 | // add the FunctionsStartup assembly attribute that specifies the type name used during startup 13 | [assembly: FunctionsStartup(typeof(CleanArchitectureCosmosDB.AzureFunctions.Startup))] 14 | 15 | namespace CleanArchitectureCosmosDB.AzureFunctions 16 | { 17 | public class Startup : FunctionsStartup 18 | { 19 | public override void Configure(IFunctionsHostBuilder builder) 20 | { 21 | ConfigureServices(builder.Services); 22 | } 23 | 24 | public void ConfigureServices(IServiceCollection services) 25 | { 26 | // Configurations 27 | IConfigurationRoot configuration = new ConfigurationBuilder() 28 | .SetBasePath(Directory.GetCurrentDirectory()) 29 | .AddJsonFile($"local.settings.json", optional: true, reloadOnChange: true) 30 | .AddEnvironmentVariables() 31 | .Build(); 32 | 33 | // Use a singleton Configuration throughout the application 34 | services.AddSingleton(configuration); 35 | 36 | // Singleton instance. See example usage in SendGridEmailService: inject IOptions in SendGridEmailService constructor 37 | services.Configure(configuration.GetSection("SendGridEmailSettings")); 38 | 39 | // if default ILogger is desired instead of Serilog 40 | //services.AddLogging(); 41 | 42 | // configure serilog 43 | Serilog.Core.Logger logger = new LoggerConfiguration() 44 | .Enrich.FromLogContext() 45 | .WriteTo.Console() 46 | .WriteTo.File("C:\\Logs\\AzureFunctions.Starter\\log-StaterFunction.txt", rollingInterval: RollingInterval.Day) 47 | .CreateLogger(); 48 | services.AddLogging(lb => lb.AddSerilog(logger)); 49 | 50 | //Register SendGrid Email 51 | services.AddScoped(); 52 | 53 | // Bind database-related bindings 54 | CosmosDbSettings cosmosDbConfig = configuration.GetSection("ConnectionStrings:CleanArchitectureCosmosDB").Get(); 55 | // register CosmosDB client and data repositories 56 | services.AddCosmosDb(cosmosDbConfig.EndpointUrl, 57 | cosmosDbConfig.PrimaryKey, 58 | cosmosDbConfig.DatabaseName, 59 | cosmosDbConfig.Containers); 60 | services.AddScoped(); 61 | 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.AzureFunctions/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "functionTimeout": "00:10:00", 4 | "logging": { 5 | "applicationInsights": { 6 | "samplingExcludedTypes": "Request", 7 | "samplingSettings": { 8 | "isEnabled": true 9 | } 10 | }, 11 | "logLevel": { 12 | "CleanArchitectureCosmosDB.AzureFunctions.StarterFunction": "Information" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.AzureFunctions/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet" 6 | }, 7 | "SendGridEmailSettings": { 8 | "FromEmail": "noreply@noreply.com", 9 | "FromName": "NoReply", 10 | "SendGridApiKey": "YourSendGridKeyXXXXXXXXXXYYYYYYYYZZZZZZZZZZZZZZS" 11 | }, 12 | "ConnectionStrings": { 13 | "CleanArchitectureCosmosDB": { 14 | "EndpointUrl": "https://localhost:8081", 15 | // default primary key used by CosmosDB emulator 16 | "PrimaryKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", 17 | "DatabaseName": "CleanArchitectureCosmosDB", 18 | "Containers": [ 19 | { 20 | "Name": "Todo", 21 | "PartitionKey": "/Category" 22 | } 23 | ] 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clientapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.0", 7 | "@material-ui/icons": "^4.9.1", 8 | "@material-ui/lab": "^4.0.0-alpha.56", 9 | "@testing-library/jest-dom": "^5.11.4", 10 | "@testing-library/react": "^11.1.0", 11 | "@testing-library/user-event": "^12.1.10", 12 | "@types/jest": "^26.0.15", 13 | "@types/node": "^12.0.0", 14 | "@types/react": "^16.9.53", 15 | "@types/react-dom": "^16.9.8", 16 | "@types/react-router-dom": "^5.1.6", 17 | "@types/yup": "^0.29.9", 18 | "camelcase-keys": "^6.2.2", 19 | "clsx": "^1.1.1", 20 | "formik": "^2.2.5", 21 | "material-table": "^1.69.2", 22 | "react": "^17.0.1", 23 | "react-dom": "^17.0.1", 24 | "react-router-dom": "^5.2.0", 25 | "react-scripts": "4.0.0", 26 | "recharts": "^1.8.5", 27 | "typescript": "^4.0.3", 28 | "web-vitals": "^0.2.4", 29 | "yup": "^0.31.0" 30 | }, 31 | "scripts": { 32 | "start": "react-scripts start", 33 | "build": "react-scripts build", 34 | "test": "react-scripts test", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": [ 39 | "react-app", 40 | "react-app/jest" 41 | ] 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnShiSS/clean-architecture-azure-cosmos-db/f8a0105c9ce46c25a980b09ad6eb3649f1f44416/src/CleanArchitectureCosmosDB.ClientApp/public/favicon.ico -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnShiSS/clean-architecture-azure-cosmos-db/f8a0105c9ce46c25a980b09ad6eb3649f1f44416/src/CleanArchitectureCosmosDB.ClientApp/public/logo192.png -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShawnShiSS/clean-architecture-azure-cosmos-db/f8a0105c9ce46c25a980b09ad6eb3649f1f44416/src/CleanArchitectureCosmosDB.ClientApp/public/logo512.png -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link as RouterLink, Switch, Route, Redirect } from 'react-router-dom'; 3 | import './App.css'; 4 | import DashboardLayout from './layouts/DashboardLayout' 5 | import Dashboard from './views/Dashboard' 6 | import TodoList from './views/TodoList' 7 | import ToDoCreate from './views/ToDoCreate' 8 | 9 | function App() { 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/components/Alert.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MuiAlert, { AlertProps } from '@material-ui/lab/Alert'; 3 | 4 | const Alert : React.FC= (props) => { 5 | return ( 6 | 7 | ); 8 | } 9 | 10 | export default Alert; -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/components/LoadingProgress.tsx: -------------------------------------------------------------------------------- 1 | import { Box, CircularProgress } from "@material-ui/core"; 2 | import React from "react"; 3 | 4 | type LoadingProgressProps = { 5 | isLoading: boolean 6 | } 7 | 8 | const LoadingProgress: React.FC = (props) => 9 | props.isLoading ? 10 | 11 | 12 | 13 | : <>{props.children} 14 | 15 | export default LoadingProgress; -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/components/TextFieldWithFormikValidation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Formik, Form, Field, FieldArray, useField, FieldAttributes } from 'formik'; 3 | import TextField from '@material-ui/core/TextField'; 4 | 5 | type TextFieldWithFormikValidationProps = {label: string, fullWidth: boolean} & FieldAttributes<{}>; 6 | 7 | const TextFieldWithFormikValidation : React.FC = ({placeholder, label, fullWidth, required, ...props }) => { 8 | const [field, meta] = useField<{}>(props); 9 | const errorText = meta.error && meta.touched ? meta.error : ''; 10 | 11 | return ( 12 | <> 13 | 14 | 15 | ); 16 | 17 | } 18 | 19 | export default TextFieldWithFormikValidation; -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/components/ToDo/ToDoDataTable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {ApiClientFactory} from '../../helpers/api/ApiClientFactory'; 3 | 4 | // required by Material Table, see documentation: https://material-table.com/#/docs/install 5 | import MaterialTable, { Icons, Query, QueryResult } from 'material-table'; 6 | import { forwardRef } from 'react'; 7 | import AddBox from '@material-ui/icons/AddBox'; 8 | import ArrowUpward from '@material-ui/icons/ArrowUpward'; 9 | import Check from '@material-ui/icons/Check'; 10 | import ChevronLeft from '@material-ui/icons/ChevronLeft'; 11 | import ChevronRight from '@material-ui/icons/ChevronRight'; 12 | import Clear from '@material-ui/icons/Clear'; 13 | import DeleteOutline from '@material-ui/icons/DeleteOutline'; 14 | import Edit from '@material-ui/icons/Edit'; 15 | import FilterList from '@material-ui/icons/FilterList'; 16 | import FirstPage from '@material-ui/icons/FirstPage'; 17 | import LastPage from '@material-ui/icons/LastPage'; 18 | import Remove from '@material-ui/icons/Remove'; 19 | import SaveAlt from '@material-ui/icons/SaveAlt'; 20 | import Search from '@material-ui/icons/Search'; 21 | import ViewColumn from '@material-ui/icons/ViewColumn'; 22 | 23 | const tableIcons : Icons = { 24 | Add: forwardRef((props, ref) => ), 25 | Check: forwardRef((props, ref) => ), 26 | Clear: forwardRef((props, ref) => ), 27 | Delete: forwardRef((props, ref) => ), 28 | DetailPanel: forwardRef((props, ref) => ), 29 | Edit: forwardRef((props, ref) => ), 30 | Export: forwardRef((props, ref) => ), 31 | Filter: forwardRef((props, ref) => ), 32 | FirstPage: forwardRef((props, ref) => ), 33 | LastPage: forwardRef((props, ref) => ), 34 | NextPage: forwardRef((props, ref) => ), 35 | PreviousPage: forwardRef((props, ref) => ), 36 | ResetSearch: forwardRef((props, ref) => ), 37 | Search: forwardRef((props, ref) => ), 38 | SortArrow: forwardRef((props, ref) => ), 39 | ThirdStateCheck: forwardRef((props, ref) => ), 40 | ViewColumn: forwardRef((props, ref) => ) 41 | }; 42 | 43 | const ToDoDataTable : React.FC = () => { 44 | 45 | const client = ApiClientFactory.GetToDoItemClient(); 46 | const columns = [ 47 | { title: 'Category', field: 'category' }, 48 | { title: 'Title', field: 'title' } 49 | ]; 50 | 51 | const loadRemoateData = (query: Query) : Promise> => 52 | { 53 | return ( 54 | new Promise>((resolve, reject) => { 55 | const sortColumn : string = query.orderBy !== undefined ? String(query.orderBy.field) : ""; 56 | const sortDirection = String(query.orderDirection) === "" || String(query.orderDirection) === "asc" ? "Ascending" : "Descending"; 57 | client.search({ 58 | "start": query.page*query.pageSize, 59 | "pageSize": query.pageSize, 60 | "sortColumn": sortColumn, 61 | "sortDirection": sortDirection, 62 | "titleFilter": query.search 63 | }) 64 | .then(response => { 65 | resolve({ 66 | data: response.result.data, 67 | page: response.result.page, // current page 68 | totalCount: response.result.totalRecords! 69 | }); 70 | }) 71 | .catch(() => { 72 | reject("Error occurred while retrieving date."); 73 | }); 74 | }) 75 | 76 | ); 77 | } 78 | 79 | return ( 80 | , 88 | tooltip: 'Edit', 89 | onClick: (event, data) => console.log(data) 90 | } 91 | ]} 92 | options={{ 93 | actionsColumnIndex: -1, 94 | exportButton: true, 95 | headerStyle: { 96 | fontWeight: "bold" 97 | } 98 | }} 99 | /> 100 | ) 101 | } 102 | 103 | export default ToDoDataTable; -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/helpers/api/ApiClientFactory.ts: -------------------------------------------------------------------------------- 1 | import { ToDoItemClient } from './Resources'; 2 | 3 | export class ApiClientFactory { 4 | static GetToDoItemClient(): ToDoItemClient { 5 | const baseUrl : string = "https://localhost:5001"; 6 | const client : ToDoItemClient = new ToDoItemClient(baseUrl); 7 | 8 | return client; 9 | } 10 | } -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import './index.css'; 5 | import App from './App'; 6 | import reportWebVitals from './reportWebVitals'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/layouts/shared/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ListItem from '@material-ui/core/ListItem'; 3 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 4 | import ListItemText from '@material-ui/core/ListItemText'; 5 | import ListSubheader from '@material-ui/core/ListSubheader'; 6 | import DashboardIcon from '@material-ui/icons/Dashboard'; 7 | import ShoppingCartIcon from '@material-ui/icons/ShoppingCart'; 8 | import PeopleIcon from '@material-ui/icons/People'; 9 | import BarChartIcon from '@material-ui/icons/BarChart'; 10 | import LayersIcon from '@material-ui/icons/Layers'; 11 | import AssignmentIcon from '@material-ui/icons/Assignment'; 12 | import Divider from '@material-ui/core/Divider'; 13 | import ListAltIcon from '@material-ui/icons/ListAlt'; 14 | 15 | const Sidebar: React.FC = () => ( 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {/* 30 | 31 | 32 | 33 | 34 | */} 35 | 36 | 37 | 38 | 39 | 40 | 41 | {/* 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Saved reports 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | */} 69 |
70 | ); 71 | 72 | 73 | export default Sidebar; 74 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | } 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/views/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Typography from '@material-ui/core/Typography'; 3 | 4 | const Dashboard : React.FC = () => { 5 | return ( 6 | Dashboard 7 | ) 8 | 9 | } 10 | 11 | export default Dashboard -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/src/views/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; 3 | import { Link as RouterLink } from 'react-router-dom'; 4 | import AddIcon from '@material-ui/icons/Add'; 5 | import ToDoDataTable from '../components/ToDo/ToDoDataTable'; 6 | 7 | // API 8 | import { Box, Button } from '@material-ui/core'; 9 | 10 | const useStyles = makeStyles((theme: Theme) => 11 | createStyles({ 12 | boxroot: { 13 | width: '100%', 14 | backgroundColor: theme.palette.background.paper, 15 | padding: theme.spacing(2) 16 | }, 17 | box: { 18 | display: "flex", 19 | justifyContent: "flex-end" 20 | }, 21 | datatableRoot: { 22 | display: 'flex', 23 | height: '100%' 24 | }, 25 | datatable: { 26 | flexGrow: 1 27 | } 28 | }), 29 | ); 30 | 31 | const TodoList : React.FC = () => { 32 | const classes = useStyles(); 33 | 34 | return ( 35 | <> 36 |
37 | 38 | 47 | 48 | 49 |
50 |
51 |
52 | 53 |
54 |
55 | 56 | 57 | ); 58 | } 59 | 60 | export default TodoList -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.ClientApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/CleanArchitectureCosmosDB.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Constants/ApplicationIdentityConstants.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitectureCosmosDB.Core.Constants 2 | { 3 | public static class ApplicationIdentityConstants 4 | { 5 | public static class Roles 6 | { 7 | public static readonly string Administrator = "Administrator"; 8 | public static readonly string Member = "Member"; 9 | 10 | public static readonly string[] RolesSupported = { Administrator, Member }; 11 | } 12 | 13 | public static readonly string DefaultPassword = "Password@1"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Entities/Audit/Audit.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities.Base; 2 | using System; 3 | 4 | namespace CleanArchitectureCosmosDB.Core.Entities 5 | { 6 | public class Audit : BaseEntity 7 | { 8 | public Audit(string entityType, 9 | string entityId, 10 | string entity) 11 | { 12 | this.EntityType = entityType; 13 | this.EntityId = entityId; 14 | this.Entity = entity; 15 | this.DateCreatedUTC = DateTime.UtcNow; 16 | } 17 | 18 | /// 19 | /// Type of the entity, e.g., ToDoItem 20 | /// 21 | public string EntityType { get; set; } 22 | 23 | /// 24 | /// Entity Id. 25 | /// Use this as the Partition Key, so that all the auditing records for the same entity are stored in the same logical partition. 26 | /// 27 | public string EntityId { get; set; } 28 | 29 | /// 30 | /// Entity itself 31 | /// 32 | public string Entity { get; set; } 33 | 34 | /// 35 | /// Date audit record created 36 | /// 37 | public DateTime DateCreatedUTC { get; set; } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Entities/Base/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace CleanArchitectureCosmosDB.Core.Entities.Base 4 | { 5 | public abstract class BaseEntity 6 | { 7 | [JsonProperty(PropertyName = "id")] 8 | public virtual string Id { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Entities/ToDoItem.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities.Base; 2 | 3 | namespace CleanArchitectureCosmosDB.Core.Entities 4 | { 5 | public class ToDoItem : BaseEntity 6 | { 7 | /// 8 | /// Category which the To-Do-Item belongs to 9 | /// 10 | public string Category { get; set; } 11 | /// 12 | /// Title of the To-Do-Item 13 | /// 14 | public string Title { get; set; } 15 | 16 | /// 17 | /// Whether the To-Do-Item is done 18 | /// 19 | public bool IsCompleted { get; private set; } 20 | 21 | public void MarkComplete() 22 | { 23 | IsCompleted = true; 24 | } 25 | 26 | public override string ToString() 27 | { 28 | string status = IsCompleted ? "Done!" : "Not done."; 29 | return $"{Id}: Status: {status} - {Title}"; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Exceptions/EntityAlreadyExistsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace CleanArchitectureCosmosDB.Core.Exceptions 6 | { 7 | public class EntityAlreadyExistsException : Exception 8 | { 9 | public EntityAlreadyExistsException() { } 10 | 11 | public EntityAlreadyExistsException(string message) : base(message) { } 12 | 13 | public EntityAlreadyExistsException(string message, Exception inner) : base(message, inner) 14 | { } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Exceptions/EntityNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanArchitectureCosmosDB.Core.Exceptions 4 | { 5 | public class EntityNotFoundException : Exception 6 | { 7 | public EntityNotFoundException() { } 8 | public EntityNotFoundException(string message) : base(message) { } 9 | public EntityNotFoundException(string message, Exception innerException) : base(message, innerException) 10 | { } 11 | 12 | public EntityNotFoundException(string name, object key) 13 | : base($"Entity \"{name}\" ({key}) was not found.") 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Exceptions/InvalidCredentialsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace CleanArchitectureCosmosDB.Core.Exceptions 6 | { 7 | public class InvalidCredentialsException : Exception 8 | { 9 | public InvalidCredentialsException() : base("Invalid Username and/or Password. Please try again.") 10 | { } 11 | public InvalidCredentialsException(string message) : base(message) { } 12 | public InvalidCredentialsException(string message, Exception innerException) : base(message, innerException) 13 | { } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Interfaces/Cache/ICachedToDoItemsService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities; 2 | using System.Collections.Generic; 3 | 4 | namespace CleanArchitectureCosmosDB.Core.Interfaces 5 | { 6 | public interface ICachedToDoItemsService 7 | { 8 | IEnumerable GetCachedToDoItems(); 9 | void DeleteCachedToDoItems(); 10 | void SetCachedToDoItems(IEnumerable entry); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Interfaces/Email/IEmailService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CleanArchitectureCosmosDB.Core.Interfaces 4 | { 5 | public interface IEmailService 6 | { 7 | Task SendEmailAsync(string toEmail, string toName, string subject, string message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Interfaces/Persistence/IAuditRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities; 2 | 3 | namespace CleanArchitectureCosmosDB.Core.Interfaces.Persistence 4 | { 5 | public interface IAuditRepository : IRepository 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Interfaces/Persistence/IRepository.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | using CleanArchitectureCosmosDB.Core.Entities.Base; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace CleanArchitectureCosmosDB.Core.Interfaces 7 | { 8 | public interface IRepository where T : BaseEntity 9 | { 10 | /// 11 | /// Get items given a string SQL query directly. 12 | /// Likely in production, you may want to use alternatives like Parameterized Query or LINQ to avoid SQL Injection and avoid having to work with strings directly. 13 | /// This is kept here for demonstration purpose. 14 | /// 15 | /// 16 | /// 17 | Task> GetItemsAsync(string query); 18 | /// 19 | /// Get items given a specification. 20 | /// 21 | /// 22 | /// 23 | Task> GetItemsAsync(ISpecification specification); 24 | 25 | /// 26 | /// Get the count on items that match the specification 27 | /// 28 | /// 29 | /// 30 | Task GetItemsCountAsync(ISpecification specification); 31 | 32 | /// 33 | /// Get one item by Id 34 | /// 35 | /// 36 | /// 37 | Task GetItemAsync(string id); 38 | Task AddItemAsync(T item); 39 | Task UpdateItemAsync(string id, T item); 40 | Task DeleteItemAsync(string id); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Interfaces/Persistence/IToDoItemRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities; 2 | 3 | namespace CleanArchitectureCosmosDB.Core.Interfaces 4 | { 5 | public interface IToDoItemRepository : IRepository 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Interfaces/Storage/IStorageService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace CleanArchitectureCosmosDB.Core.Interfaces.Storage 6 | { 7 | public interface IStorageService 8 | { 9 | /// 10 | /// Upload a file and returns the full path to retrieve the file 11 | /// 12 | /// 13 | /// 14 | /// 15 | Task UploadFile(IFormFile file, string fullPath); 16 | 17 | /// 18 | /// Get the file stream by full path 19 | /// 20 | /// 21 | /// 22 | Task GetFileStream(string filePath); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/README.md: -------------------------------------------------------------------------------- 1 | # Core Project 2 | 3 | This project includes business entities and business logics in abstraction. 4 | 5 | 6 | * Application core defines the domain models and abstractions, such as interfaces, entities, domain services that do not belong to any specific entity, exceptions, domain events and handlers, specifications, etc. 7 | * Core project should have minimal dependencies, see dependencies in the project, 8 | particularly, application core should NOT depend on things like EF Core, SQL client, etc., since application core is only about high level business level logic, and should NOT care how things are implemented. 9 | * Infrastructure has dependency on application core, but not vice versa! -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Specifications/AuditFilterSpecification.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | 3 | namespace CleanArchitectureCosmosDB.Core.Specifications 4 | { 5 | public class AuditFilterSpecification : Specification 6 | { 7 | /// 8 | /// Search by a matching entity Id 9 | /// 10 | /// 11 | public AuditFilterSpecification(string entityId) 12 | { 13 | Query.Where(audit => 14 | // Must include EntityId, because it is part of the Partition Key 15 | audit.EntityId == entityId) 16 | .OrderByDescending(audit => audit.DateCreatedUTC); 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Specifications/Base/CosmosDbSpecificationEvaluator.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | 3 | namespace CleanArchitectureCosmosDB.Core.Specifications.Base 4 | { 5 | /// 6 | /// Specification Evaluator for Cosmos DB. 7 | /// The evaluator implements methods to translate specifications into Cosmos DB IQueryables, which then allows us to build queryables with filters, predicates etc. to query data. 8 | /// 9 | /// 10 | public class CosmosDbSpecificationEvaluator : SpecificationEvaluatorBase where T : class 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Specifications/Interfaces/ISearchQuery.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitectureCosmosDB.Core.Specifications.Interfaces 2 | { 3 | public interface ISearchQuery 4 | { 5 | int Start { get; set; } 6 | int PageSize { get; set; } 7 | string SortColumn { get; set; } 8 | SortDirection? SortDirection { get; set; } 9 | } 10 | 11 | public enum SortDirection 12 | { 13 | Ascending = 0, 14 | Descending = 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Specifications/ToDoItemGetAllSpecification.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | 3 | namespace CleanArchitectureCosmosDB.Core.Specifications 4 | { 5 | public class ToDoItemGetAllSpecification : Specification 6 | { 7 | public ToDoItemGetAllSpecification(bool isCompleted) 8 | { 9 | // Use Specification Builder 10 | Query.Where(item => 11 | item.IsCompleted == isCompleted 12 | ); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Specifications/ToDoItemSearchAggregationSpecification.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | using CleanArchitectureCosmosDB.Core.Specifications.Interfaces; 3 | 4 | namespace CleanArchitectureCosmosDB.Core.Specifications 5 | { 6 | /// 7 | /// Specification for searching and returning aggregated value. E.g. Count, Sum, etc.. 8 | /// This is similar to a search specification, minus the sorting. 9 | /// 10 | public class ToDoItemSearchAggregationSpecification : Specification 11 | { 12 | public ToDoItemSearchAggregationSpecification(string title = "", 13 | int pageStart = 0, 14 | int pageSize = 50, 15 | string sortColumn = "title", 16 | SortDirection sortDirection = SortDirection.Ascending, 17 | bool exactSearch = false 18 | ) 19 | { 20 | if (!string.IsNullOrWhiteSpace(title)) 21 | { 22 | if (exactSearch) 23 | { 24 | Query.Where(item => item.Title.ToLower() == title.ToLower()); 25 | } 26 | else 27 | { 28 | Query.Where(item => item.Title.ToLower().Contains(title.ToLower())); 29 | } 30 | } 31 | 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Core/Specifications/ToDoItemSearchSpecification.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.Specification; 2 | using CleanArchitectureCosmosDB.Core.Specifications.Interfaces; 3 | 4 | namespace CleanArchitectureCosmosDB.Core.Specifications 5 | { 6 | public class ToDoItemSearchSpecification : Specification 7 | { 8 | public ToDoItemSearchSpecification(string title = "", 9 | int pageStart = 0, 10 | int pageSize = 50, 11 | string sortColumn = "title", 12 | SortDirection sortDirection = SortDirection.Ascending, 13 | bool exactSearch = false 14 | ) 15 | { 16 | if (!string.IsNullOrWhiteSpace(title)) 17 | { 18 | if (exactSearch) 19 | { 20 | Query.Where(item => item.Title.ToLower() == title.ToLower()); 21 | } 22 | else 23 | { 24 | Query.Where(item => item.Title.ToLower().Contains(title.ToLower())); 25 | } 26 | } 27 | 28 | // Pagination 29 | if (pageSize != -1) //Display all entries and disable pagination 30 | { 31 | Query.Skip(pageStart).Take(pageSize); 32 | } 33 | 34 | // Sort 35 | switch (sortColumn.ToLower()) 36 | { 37 | case ("title"): 38 | { 39 | if (sortDirection == SortDirection.Ascending) 40 | { 41 | Query.OrderBy(x => x.Title); 42 | } 43 | else 44 | { 45 | Query.OrderByDescending(x => x.Title); 46 | } 47 | } 48 | break; 49 | default: 50 | break; 51 | } 52 | 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/AppSettings/CosmosDbSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace CleanArchitectureCosmosDB.Infrastructure.AppSettings 4 | { 5 | public class CosmosDbSettings 6 | { 7 | /// 8 | /// CosmosDb Account - The Azure Cosmos DB endpoint 9 | /// 10 | public string EndpointUrl { get; set; } 11 | /// 12 | /// Key - The primary key for the Azure DocumentDB account. 13 | /// 14 | public string PrimaryKey { get; set; } 15 | /// 16 | /// Database name 17 | /// 18 | public string DatabaseName { get; set; } 19 | 20 | /// 21 | /// List of containers in the database 22 | /// 23 | public List Containers { get; set; } 24 | 25 | } 26 | public class ContainerInfo 27 | { 28 | /// 29 | /// Container Name 30 | /// 31 | public string Name { get; set; } 32 | /// 33 | /// Container partition Key 34 | /// 35 | public string PartitionKey { get; set; } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/AppSettings/SendGridEmailSettings.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitectureCosmosDB.Infrastructure.AppSettings 2 | { 3 | /// 4 | /// SendGrid email settings 5 | /// 6 | public class SendGridEmailSettings 7 | { 8 | /// 9 | /// API Key 10 | /// 11 | public string SendGridApiKey { get; set; } 12 | /// 13 | /// From Email 14 | /// 15 | public string FromEmail { get; set; } 16 | /// 17 | /// From Name 18 | /// 19 | public string FromName { get; set; } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/CleanArchitectureCosmosDB.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Constants/CosmosDbConfigConstants.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Constants 2 | { 3 | public class CosmosDbContainerConstants 4 | { 5 | // TODO : consider retrieving this from appsettings using IOptions, instead of defining it as a constant 6 | public const string CONTAINER_NAME_TODO = "Todo"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/CosmosDbContainer.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces; 2 | using Microsoft.Azure.Cosmos; 3 | 4 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData 5 | { 6 | public class CosmosDbContainer : ICosmosDbContainer 7 | { 8 | public Container _container { get; } 9 | 10 | public CosmosDbContainer(CosmosClient cosmosClient, 11 | string databaseName, 12 | string containerName) 13 | { 14 | this._container = cosmosClient.GetContainer(databaseName, containerName); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/CosmosDbContainerFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Infrastructure.AppSettings; 2 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces; 3 | using Microsoft.Azure.Cosmos; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData 10 | { 11 | public class CosmosDbContainerFactory : ICosmosDbContainerFactory 12 | { 13 | /// 14 | /// Azure Cosmos DB Client 15 | /// 16 | private readonly CosmosClient _cosmosClient; 17 | private readonly string _databaseName; 18 | private readonly List _containers; 19 | 20 | /// 21 | /// Ctor 22 | /// 23 | /// 24 | /// 25 | /// 26 | public CosmosDbContainerFactory(CosmosClient cosmosClient, 27 | string databaseName, 28 | List containers) 29 | { 30 | _databaseName = databaseName ?? throw new ArgumentNullException(nameof(databaseName)); 31 | _containers = containers ?? throw new ArgumentNullException(nameof(containers)); 32 | _cosmosClient = cosmosClient ?? throw new ArgumentNullException(nameof(cosmosClient)); 33 | } 34 | 35 | public ICosmosDbContainer GetContainer(string containerName) 36 | { 37 | if (_containers.Where(x => x.Name == containerName) == null) 38 | { 39 | throw new ArgumentException($"Unable to find container: {containerName}"); 40 | } 41 | 42 | return new CosmosDbContainer(_cosmosClient, _databaseName, containerName); 43 | } 44 | 45 | public async Task EnsureDbSetupAsync() 46 | { 47 | Microsoft.Azure.Cosmos.DatabaseResponse database = await _cosmosClient.CreateDatabaseIfNotExistsAsync(_databaseName); 48 | 49 | foreach (ContainerInfo container in _containers) 50 | { 51 | await database.Database.CreateContainerIfNotExistsAsync(container.Name, $"{container.PartitionKey}"); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Interfaces/IContainerContext.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities.Base; 2 | using Microsoft.Azure.Cosmos; 3 | 4 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces 5 | { 6 | /// 7 | /// Defines the container level context 8 | /// 9 | /// 10 | public interface IContainerContext where T : BaseEntity 11 | { 12 | string ContainerName { get; } 13 | string GenerateId(T entity); 14 | PartitionKey ResolvePartitionKey(string entityId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Interfaces/ICosmosDbContainer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Cosmos; 2 | 3 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces 4 | { 5 | public interface ICosmosDbContainer 6 | { 7 | /// 8 | /// Instance of Azure Cosmos DB Container class 9 | /// 10 | Container _container { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Interfaces/ICosmosDbContainerFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces 4 | { 5 | public interface ICosmosDbContainerFactory 6 | { 7 | /// 8 | /// Returns a CosmosDbContainer wrapper 9 | /// 10 | /// 11 | /// 12 | ICosmosDbContainer GetContainer(string containerName); 13 | 14 | /// 15 | /// Ensure the database is created 16 | /// 17 | /// 18 | Task EnsureDbSetupAsync(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Repository/AuditRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities; 2 | using CleanArchitectureCosmosDB.Core.Interfaces.Persistence; 3 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces; 4 | using Microsoft.Azure.Cosmos; 5 | 6 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Repository 7 | { 8 | /// 9 | /// Audit repository 10 | /// 11 | public class AuditRepository : CosmosDbRepository, IAuditRepository 12 | { 13 | /// 14 | /// Name of the cosmosDb container where entity records will reside. 15 | /// 16 | public override string ContainerName { get; } = "Audit"; 17 | public override string GenerateId(Audit entity) => GenerateAuditId(entity); 18 | public override PartitionKey ResolvePartitionKey(string entityId) => ResolveAuditPartitionKey(entityId); 19 | 20 | public AuditRepository(ICosmosDbContainerFactory factory) : base(factory) 21 | { } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Repository/ToDoItemRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities; 2 | using CleanArchitectureCosmosDB.Core.Interfaces; 3 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces; 4 | using Microsoft.Azure.Cosmos; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | 9 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Repository 10 | { 11 | public class ToDoItemRepository : CosmosDbRepository, IToDoItemRepository 12 | { 13 | /// 14 | /// CosmosDB container name 15 | /// 16 | public override string ContainerName { get; } = "Todo"; 17 | 18 | /// 19 | /// Generate Id. 20 | /// e.g. "shoppinglist:783dfe25-7ece-4f0b-885e-c0ea72135942" 21 | /// 22 | /// 23 | /// 24 | public override string GenerateId(ToDoItem entity) => $"{entity.Category}:{Guid.NewGuid()}"; 25 | 26 | /// 27 | /// Returns the value of the partition key 28 | /// 29 | /// 30 | /// 31 | public override PartitionKey ResolvePartitionKey(string entityId) => new PartitionKey(entityId.Split(':')[0]); 32 | 33 | public ToDoItemRepository(ICosmosDbContainerFactory factory) : base(factory) 34 | { } 35 | 36 | // Use Cosmos DB Parameterized Query to avoid SQL Injection. 37 | // Get by Category is also an example of single partition read, where get by title will be a cross partition read 38 | public async Task> GetItemsAsyncByCategory(string category) 39 | { 40 | List results = new List(); 41 | string query = @$"SELECT c.Name FROM c WHERE c.Category = @Category"; 42 | 43 | QueryDefinition queryDefinition = new QueryDefinition(query) 44 | .WithParameter("@Category", category); 45 | string queryString = queryDefinition.QueryText; 46 | 47 | IEnumerable entities = await this.GetItemsAsync(queryString); 48 | 49 | return results; 50 | } 51 | 52 | // Use Cosmos DB Parameterized Query to avoid SQL Injection. 53 | // Get by Title is also an example of cross partition read, where Get by Category will be single partition read 54 | public async Task> GetItemsAsyncByTitle(string title) 55 | { 56 | List results = new List(); 57 | string query = @$"SELECT c.Name FROM c WHERE c.Title = @Title"; 58 | 59 | QueryDefinition queryDefinition = new QueryDefinition(query) 60 | .WithParameter("@Title", title); 61 | string queryString = queryDefinition.QueryText; 62 | 63 | IEnumerable entities = await this.GetItemsAsync(queryString); 64 | 65 | return results; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Extensions/CacheHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitectureCosmosDB.Infrastructure.Extensions 2 | { 3 | public static class CacheHelpers 4 | { 5 | public static string GenerateToDoItemsCacheKey() 6 | { 7 | return "todoitems"; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Extensions/IApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities; 2 | using CleanArchitectureCosmosDB.Core.Interfaces; 3 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces; 4 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace CleanArchitectureCosmosDB.Infrastructure.Extensions 13 | { 14 | /// 15 | /// Extension methods for IApplicationBuilder 16 | /// 17 | public static class IApplicationBuilderExtensions 18 | { 19 | /// 20 | /// Ensure Cosmos DB is created 21 | /// 22 | /// 23 | public static void EnsureCosmosDbIsCreated(this IApplicationBuilder builder) 24 | { 25 | using (IServiceScope serviceScope = builder.ApplicationServices.GetRequiredService().CreateScope()) 26 | { 27 | ICosmosDbContainerFactory factory = serviceScope.ServiceProvider.GetService(); 28 | 29 | factory.EnsureDbSetupAsync().Wait(); 30 | } 31 | } 32 | 33 | /// 34 | /// Seed sample data in the Todo container 35 | /// 36 | /// 37 | /// 38 | public static async Task SeedToDoContainerIfEmptyAsync(this IApplicationBuilder builder) 39 | { 40 | using (IServiceScope serviceScope = builder.ApplicationServices.GetRequiredService().CreateScope()) 41 | { 42 | IToDoItemRepository _repo = serviceScope.ServiceProvider.GetService(); 43 | 44 | // Check if empty 45 | string sqlQueryText = "SELECT * FROM c"; 46 | IEnumerable todos = await _repo.GetItemsAsync(sqlQueryText); 47 | 48 | if (todos.Count() == 0) 49 | { 50 | for (int i = 0; i < 100; i++) 51 | { 52 | ToDoItem beer = new ToDoItem() 53 | { 54 | Category = "Grocery", 55 | Title = $"Get {i} beers" 56 | }; 57 | 58 | await _repo.AddItemAsync(beer); 59 | } 60 | } 61 | } 62 | } 63 | 64 | /// 65 | /// Create Identity DB if not exist 66 | /// 67 | /// 68 | public static void EnsureIdentityDbIsCreated(this IApplicationBuilder builder) 69 | { 70 | using (var serviceScope = builder.ApplicationServices.GetRequiredService().CreateScope()) 71 | { 72 | var services = serviceScope.ServiceProvider; 73 | 74 | var dbContext = services.GetRequiredService(); 75 | 76 | // Ensure the database is created. 77 | // Note this does not use migrations. If database may be updated using migrations, use DbContext.Database.Migrate() instead. 78 | dbContext.Database.EnsureCreated(); 79 | } 80 | } 81 | 82 | /// 83 | /// Seed Identity data 84 | /// 85 | /// 86 | public static async Task SeedIdentityDataAsync(this IApplicationBuilder builder) 87 | { 88 | using (var serviceScope = builder.ApplicationServices.GetRequiredService().CreateScope()) 89 | { 90 | var services = serviceScope.ServiceProvider; 91 | 92 | var userManager = services.GetRequiredService>(); 93 | var roleManager = services.GetRequiredService>(); 94 | 95 | await Infrastructure.Identity.Seed.ApplicationDbContextDataSeed.SeedAsync(userManager, roleManager); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Extensions/IServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Interfaces.Storage; 2 | using CleanArchitectureCosmosDB.Infrastructure.AppSettings; 3 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData; 4 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces; 5 | using CleanArchitectureCosmosDB.Infrastructure.Services; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Storage.Net; 9 | using System.Collections.Generic; 10 | 11 | namespace CleanArchitectureCosmosDB.Infrastructure.Extensions 12 | { 13 | public static class IServiceCollectionExtensions 14 | { 15 | /// 16 | /// Register a singleton instance of Cosmos Db Container Factory, which is a wrapper for the CosmosClient. 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | public static IServiceCollection AddCosmosDb(this IServiceCollection services, 25 | string endpointUrl, 26 | string primaryKey, 27 | string databaseName, 28 | List containers) 29 | { 30 | Microsoft.Azure.Cosmos.CosmosClient client = new Microsoft.Azure.Cosmos.CosmosClient(endpointUrl, primaryKey); 31 | CosmosDbContainerFactory cosmosDbClientFactory = new CosmosDbContainerFactory(client, databaseName, containers); 32 | 33 | // Microsoft recommends a singleton client instance to be used throughout the application 34 | // https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.cosmos.cosmosclient?view=azure-dotnet#definition 35 | // "CosmosClient is thread-safe. Its recommended to maintain a single instance of CosmosClient per lifetime of the application which enables efficient connection management and performance" 36 | services.AddSingleton(cosmosDbClientFactory); 37 | 38 | return services; 39 | } 40 | 41 | /// 42 | /// Setup Azure Blob storage 43 | /// 44 | /// 45 | /// 46 | public static void SetupStorage(this IServiceCollection services, IConfiguration configuration) 47 | { 48 | StorageFactory.Modules.UseAzureBlobStorage(); 49 | 50 | // Register IBlobStorage, which is used in AzureBlobStorageService 51 | // Avoid using IBlobStorage directly outside of AzureBlobStorageService. 52 | services.AddScoped( 53 | factory => StorageFactory.Blobs.FromConnectionString(configuration.GetConnectionString("StorageConnectionString"))); 54 | 55 | services.AddScoped(); 56 | } 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/DesignTime/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | 4 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models 5 | { 6 | public partial class ApplicationDbContext 7 | { 8 | // for checking that DI is getting a different instance each time when the dbcontext is injected in the context of a web request 9 | private Guid _instanceId = Guid.NewGuid(); 10 | 11 | public static void AddBaseOptions(DbContextOptionsBuilder builder, string connectionString) 12 | { 13 | if (builder == null) 14 | throw new ArgumentNullException(nameof(builder)); 15 | 16 | if (string.IsNullOrWhiteSpace(connectionString)) 17 | throw new ArgumentException("Connection string must be provided", nameof(connectionString)); 18 | 19 | builder.UseSqlServer(connectionString, x => 20 | { 21 | x.EnableRetryOnFailure(); 22 | }); 23 | } 24 | 25 | public static void AddBaseOptions(DbContextOptionsBuilder builder, string connectionString) 26 | { 27 | if (builder == null) 28 | throw new ArgumentNullException(nameof(builder)); 29 | 30 | if (string.IsNullOrWhiteSpace(connectionString)) 31 | throw new ArgumentException("Connection string must be provided", nameof(connectionString)); 32 | 33 | builder.UseSqlServer(connectionString, x => 34 | { 35 | x.EnableRetryOnFailure(); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/DesignTime/DesignTimeDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Design; 4 | using Microsoft.Extensions.Configuration; 5 | using System; 6 | using System.IO; 7 | 8 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.DesignTime 9 | { 10 | /// 11 | /// Used for design time migrations. Will look to the appsettings.json file in this project 12 | /// for the connection string. 13 | /// EF Core tools scans the assembly containing the dbcontext for an implementation 14 | /// of IDesignTimeDbContextFactory. 15 | /// 16 | public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory 17 | { 18 | public ApplicationDbContext CreateDbContext(string[] args) 19 | { 20 | string path = Directory.GetCurrentDirectory(); 21 | 22 | IConfigurationBuilder builder = 23 | new ConfigurationBuilder() 24 | .SetBasePath(path) 25 | .AddJsonFile("appsettings.json"); 26 | 27 | IConfigurationRoot config = builder.Build(); 28 | 29 | string connectionString = config.GetConnectionString("CleanArchitectureIdentity"); 30 | 31 | Console.WriteLine($"DesignTimeDbContextFactory: using base path = {path}"); 32 | Console.WriteLine($"DesignTimeDbContextFactory: using connection string = {connectionString}"); 33 | 34 | if (string.IsNullOrWhiteSpace(connectionString)) 35 | { 36 | throw new InvalidOperationException("Could not find connection string named 'CleanArchitectureIdentity'"); 37 | } 38 | 39 | DbContextOptionsBuilder dbContextOptionsBuilder = 40 | new DbContextOptionsBuilder(); 41 | 42 | ApplicationDbContext.AddBaseOptions(dbContextOptionsBuilder, connectionString); 43 | 44 | return new ApplicationDbContext(dbContextOptionsBuilder.Options); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models 5 | { 6 | public partial class ApplicationDbContext : IdentityDbContext 7 | { 8 | public ApplicationDbContext(DbContextOptions options) 9 | : base(options) 10 | { } 11 | 12 | protected override void OnModelCreating(ModelBuilder builder) 13 | { 14 | base.OnModelCreating(builder); 15 | 16 | // Customize the ASP.NET Identity model and override the defaults if needed. 17 | // For example, you can rename the ASP.NET Identity table names and more. 18 | // Add your customizations after calling base.OnModelCreating(builder); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using System.Collections.Generic; 3 | using System.Runtime.Serialization; 4 | 5 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models 6 | { 7 | public class ApplicationUser : IdentityUser 8 | { 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | public bool IsEnabled { get; set; } 12 | 13 | [IgnoreDataMember] 14 | public string FullName 15 | { 16 | get 17 | { 18 | return $"{FirstName} {LastName}"; 19 | } 20 | } 21 | 22 | //[JsonIgnore] 23 | public List RefreshTokens { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/Authentication/Token.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication 4 | { 5 | [JsonObject("token")] 6 | public class Token 7 | { 8 | [JsonProperty("secret")] 9 | public string Secret { get; set; } 10 | 11 | [JsonProperty("issuer")] 12 | public string Issuer { get; set; } 13 | 14 | [JsonProperty("audience")] 15 | public string Audience { get; set; } 16 | 17 | [JsonProperty("expiry")] 18 | public int Expiry { get; set; } 19 | 20 | [JsonProperty("refreshExpiry")] 21 | public int RefreshExpiry { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/Authentication/TokenRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication 5 | { 6 | public class TokenRequest 7 | { 8 | /// 9 | /// The username of the user logging in. 10 | /// 11 | [Required] 12 | [JsonProperty("username")] 13 | public string Username { get; set; } 14 | 15 | /// 16 | /// The password for the user logging in. 17 | /// 18 | [Required] 19 | [JsonProperty("password")] 20 | public string Password { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/Authentication/TokenResponse.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication 2 | { 3 | public class TokenResponse 4 | { 5 | public TokenResponse(ApplicationUser user, 6 | string role, 7 | string token 8 | //string refreshToken 9 | ) 10 | { 11 | Id = user.Id; 12 | FullName = user.FullName; 13 | EmailAddress = user.Email; 14 | Token = token; 15 | Role = role; 16 | //RefreshToken = refreshToken; 17 | } 18 | 19 | public string Id { get; set; } 20 | public string FullName { get; set; } 21 | public string EmailAddress { get; set; } 22 | public string Token { get; set; } 23 | public string Role { get; set; } 24 | 25 | //[JsonIgnore] 26 | //public string RefreshToken { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/RefreshToken.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.Text; 7 | 8 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models 9 | { 10 | [Owned] 11 | public class RefreshToken 12 | { 13 | [Key] 14 | [JsonIgnore] 15 | public int Id { get; set; } 16 | 17 | public string Token { get; set; } 18 | public DateTime Expiry { get; set; } 19 | public bool IsExpired 20 | { 21 | get { return DateTime.UtcNow >= Expiry; } 22 | } 23 | public DateTime Created { get; set; } 24 | public string CreatedByIp { get; set; } 25 | public DateTime? Revoked { get; set; } 26 | public string RevokedByIp { get; set; } 27 | public string ReplacedByToken { get; set; } 28 | 29 | public bool IsActive 30 | { 31 | get { return Revoked == null && !IsExpired; } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/README_Identity.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core Identity is used to provide token service and user management service. 2 | 3 | # Entity Framework Core 4 | ## NOTES: 5 | 1. migrations will use the connection string defined in CleanArchitectureCosmosDB.Infrastructure/appsettings.json 6 | as this is the connection string referenced by DesignTimeDbContextFactory 7 | 1. Consult Microsoft documentation on Entity Framework Core Code First migrations for more information on migrations. 8 | 9 | ## To create migrations for the first time: 10 | * Add-Migration -Name "InitialIdentityDbCreation" -OutputDir "Identity\Migrations" -Context "CleanArchitectureCosmosDB.Infrastructure.Identity.Models.ApplicationDbContext" -Project "CleanArchitectureCosmosDB.Infrastructure" 11 | 12 | ## To run the migrations: 13 | * Update-Database -Context "CleanArchitectureCosmosDB.Infrastructure.Identity.Models.ApplicationDbContext" -Project "CleanArchitectureCosmosDB.Infrastructure" -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/Seed/ApplicationDbContextDataSeed.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Constants; 2 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models; 3 | using Microsoft.AspNetCore.Identity; 4 | using System.Threading.Tasks; 5 | 6 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Seed 7 | { 8 | public class ApplicationDbContextDataSeed 9 | { 10 | /// 11 | /// Seed users and roles in the Identity database. 12 | /// 13 | /// ASP.NET Core Identity User Manager 14 | /// ASP.NET Core Identity Role Manager 15 | /// 16 | public static async Task SeedAsync(UserManager userManager, RoleManager roleManager) 17 | { 18 | // Add roles supported 19 | await roleManager.CreateAsync(new IdentityRole(ApplicationIdentityConstants.Roles.Administrator)); 20 | await roleManager.CreateAsync(new IdentityRole(ApplicationIdentityConstants.Roles.Member)); 21 | 22 | // New admin user 23 | string adminUserName = "shawn@test.com"; 24 | var adminUser = new ApplicationUser { 25 | UserName = adminUserName, 26 | Email = adminUserName, 27 | IsEnabled = true, 28 | EmailConfirmed = true, 29 | FirstName = "Shawn", 30 | LastName = "Administrator" 31 | }; 32 | 33 | // Add new user and their role 34 | await userManager.CreateAsync(adminUser, ApplicationIdentityConstants.DefaultPassword); 35 | adminUser = await userManager.FindByNameAsync(adminUserName); 36 | await userManager.AddToRoleAsync(adminUser, ApplicationIdentityConstants.Roles.Administrator); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/Services/ITokenService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models; 2 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication; 3 | using System.Threading.Tasks; 4 | 5 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Services 6 | { 7 | /// 8 | /// A collection of token related services 9 | /// 10 | public interface ITokenService 11 | { 12 | /// 13 | /// Validate the credentials entered when logging in. 14 | /// 15 | /// 16 | /// 17 | /// 18 | Task Authenticate(TokenRequest request, string ipAddress); 19 | 20 | /// 21 | /// If the refresh token is valid, a new JWT token will be issued containing the user details. 22 | /// 23 | /// An existing refresh token. 24 | /// The users current ip 25 | /// 26 | /// 27 | /// 28 | Task RefreshToken(string refreshToken, string ipAddress); 29 | 30 | 31 | /// 32 | /// Check if the credentials passed in are valid. 33 | /// 34 | /// The username to check. 35 | /// The matching password to verify. 36 | /// If the credentials are valid or not. 37 | Task IsValidUser(string username, string password); 38 | 39 | /// 40 | /// Find an by their email. 41 | /// 42 | /// 43 | /// 44 | /// 45 | /// 46 | /// 47 | /// 48 | Task GetUserByEmail(string email); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/Services/TokenService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models; 2 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.Extensions.Options; 6 | using Microsoft.IdentityModel.Tokens; 7 | using System; 8 | using System.IdentityModel.Tokens.Jwt; 9 | using System.Security.Claims; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Services 14 | { 15 | /// 16 | public class TokenService : ITokenService 17 | { 18 | private readonly SignInManager _signInManager; 19 | private readonly UserManager _userManager; 20 | private readonly Token _token; 21 | private readonly HttpContext _httpContext; 22 | 23 | /// 24 | public TokenService( 25 | UserManager userManager, 26 | SignInManager signInManager, 27 | IOptions tokenOptions, 28 | IHttpContextAccessor httpContextAccessor) 29 | { 30 | _userManager = userManager; 31 | _signInManager = signInManager; 32 | _token = tokenOptions.Value; 33 | _httpContext = httpContextAccessor.HttpContext; 34 | } 35 | 36 | /// 37 | public async Task Authenticate(TokenRequest request, string ipAddress) 38 | { 39 | if (await IsValidUser(request.Username, request.Password)) 40 | { 41 | ApplicationUser user = await GetUserByEmail(request.Username); 42 | 43 | if (user != null && user.IsEnabled) 44 | { 45 | string role = (await _userManager.GetRolesAsync(user))[0]; 46 | string jwtToken = await GenerateJwtToken(user); 47 | 48 | //RefreshToken refreshToken = GenerateRefreshToken(ipAddress); 49 | 50 | //user.RefreshTokens.Add(refreshToken); 51 | await _userManager.UpdateAsync(user); 52 | 53 | return new TokenResponse(user, 54 | role, 55 | jwtToken 56 | //""//refreshToken.Token 57 | ); 58 | } 59 | } 60 | 61 | return null; 62 | } 63 | 64 | public Task RefreshToken(string refreshToken, string ipAddress) 65 | { 66 | throw new NotImplementedException(); 67 | } 68 | 69 | /// 70 | public async Task IsValidUser(string username, string password) 71 | { 72 | ApplicationUser user = await GetUserByEmail(username); 73 | 74 | if (user == null) 75 | { 76 | // Username or password was incorrect. 77 | return false; 78 | } 79 | 80 | SignInResult signInResult = await _signInManager.PasswordSignInAsync(user, password, true, false); 81 | 82 | return signInResult.Succeeded; 83 | } 84 | 85 | /// 86 | public async Task GetUserByEmail(string email) 87 | { 88 | return await _userManager.FindByEmailAsync(email); 89 | } 90 | 91 | /// 92 | /// Issue JWT token 93 | /// 94 | /// 95 | /// 96 | private async Task GenerateJwtToken(ApplicationUser user) 97 | { 98 | string role = (await _userManager.GetRolesAsync(user))[0]; 99 | byte[] secret = Encoding.ASCII.GetBytes(_token.Secret); 100 | 101 | JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); 102 | SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor 103 | { 104 | Issuer = _token.Issuer, 105 | Audience = _token.Audience, 106 | Subject = new ClaimsIdentity(new Claim[] 107 | { 108 | new Claim("UserId", user.Id), 109 | new Claim("FullName", $"{user.FirstName} {user.LastName}"), 110 | new Claim(ClaimTypes.Name, user.Email), 111 | new Claim(ClaimTypes.NameIdentifier, user.Email), 112 | new Claim(ClaimTypes.Role, role) 113 | }), 114 | Expires = DateTime.UtcNow.AddMinutes(_token.Expiry), 115 | SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(secret), SecurityAlgorithms.HmacSha256Signature) 116 | }; 117 | 118 | SecurityToken token = handler.CreateToken(descriptor); 119 | return handler.WriteToken(token); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Identity/TokenServiceProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | 6 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity 7 | { 8 | /// 9 | /// Config settings for token service provider. 10 | /// E.g., application using ASP.NET Core Identity, Identity Server, etc.. 11 | /// 12 | public class TokenServiceProvider 13 | { 14 | public string Authority { get; set; } 15 | public string SetPasswordPath { get; set; } 16 | public string ResetPasswordPath { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/README.md: -------------------------------------------------------------------------------- 1 | # Infrastructure 2 | 3 | This project has plumbing code that implements the abstractions defined in Core project. 4 | 5 | * Infrastructure has plumbing code, such as repositories, EF Core DbContext if used, Cached repositories, third party APIs, file systems, email service implementations, logging adapters, third party SDKs like S3 or Azure Blob Storage SDKs. 6 | * Infrastructure has dependency on application core, but not vice versa! -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Services/AzureBlobStorageService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Interfaces.Storage; 2 | using Microsoft.AspNetCore.Http; 3 | using Storage.Net.Blobs; 4 | using System; 5 | using System.IO; 6 | using System.Threading.Tasks; 7 | 8 | namespace CleanArchitectureCosmosDB.Infrastructure.Services 9 | { 10 | /// 11 | /// Azure Blob Storage 12 | /// 13 | public class AzureBlobStorageService : IStorageService 14 | { 15 | private readonly IBlobStorage _blobStorage; 16 | 17 | public AzureBlobStorageService(IBlobStorage blobStorage) 18 | { 19 | _blobStorage = blobStorage ?? throw new ArgumentNullException(nameof(blobStorage)); 20 | } 21 | 22 | public async Task UploadFile(IFormFile file, string fullPath) 23 | { 24 | using (Stream str = file.OpenReadStream()) 25 | { 26 | await _blobStorage.WriteAsync(fullPath, str, false); 27 | } 28 | 29 | return fullPath; 30 | } 31 | 32 | public async Task GetFileStream(string filePath) 33 | { 34 | MemoryStream ms = new MemoryStream(); 35 | await _blobStorage.ReadToStreamAsync(filePath, ms); 36 | return ms; 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/Services/SendGridEmailService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Interfaces; 2 | using CleanArchitectureCosmosDB.Infrastructure.AppSettings; 3 | using Microsoft.Extensions.Options; 4 | using SendGrid; 5 | using SendGrid.Helpers.Mail; 6 | using System; 7 | using System.Threading.Tasks; 8 | 9 | namespace CleanArchitectureCosmosDB.Infrastructure.Services 10 | { 11 | public class SendGridEmailService : IEmailService 12 | { 13 | /// 14 | /// Settings 15 | /// 16 | private readonly SendGridEmailSettings _sendGridEmailSettings; 17 | 18 | /// 19 | /// Send Grid wrapper 20 | /// 21 | private readonly SendGridClient _sendGridClient; 22 | 23 | /// 24 | /// FromEmail from the settings 25 | /// 26 | private string FromEmail => _sendGridEmailSettings.FromEmail; 27 | 28 | /// 29 | /// FromName from the settings 30 | /// 31 | private string FromName => _sendGridEmailSettings.FromName; 32 | 33 | /// 34 | /// ctor 35 | /// 36 | /// 37 | public SendGridEmailService(IOptions sendGridEmailSettings) 38 | { 39 | _sendGridEmailSettings = sendGridEmailSettings.Value ?? throw new ArgumentNullException(nameof(sendGridEmailSettings)); 40 | _sendGridClient = new SendGridClient(_sendGridEmailSettings.SendGridApiKey); 41 | 42 | } 43 | 44 | // TODO : consider adding support for HTML content 45 | /// 46 | /// Send message 47 | /// 48 | /// 49 | /// 50 | /// 51 | /// 52 | /// 53 | public async Task SendEmailAsync(string toEmail, string toName, string subject, string message) 54 | { 55 | SendGridMessage sendGridMessage = MailHelper.CreateSingleEmail( 56 | new EmailAddress(this.FromEmail, this.FromName), 57 | new EmailAddress(toEmail, toName), 58 | subject, 59 | message, 60 | message 61 | ); 62 | 63 | await _sendGridClient.SendEmailAsync(sendGridMessage); 64 | } 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.Infrastructure/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "CleanArchitectureIdentity": "Server=localhost\\SQLSERVER2016;Database=CleanArchitectureIdentity;Trusted_Connection=True;" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/CleanArchitectureCosmosDB.WebAPI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Config/AuthenticationConfig.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication; 2 | using Microsoft.AspNetCore.Authentication.JwtBearer; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.IdentityModel.Tokens; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Security.Claims; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace CleanArchitectureCosmosDB.WebAPI.Config 14 | { 15 | /// 16 | /// Authentication configuration 17 | /// 18 | public static class AuthenticationConfig 19 | { 20 | /// 21 | /// authentication configuration 22 | /// 23 | /// 24 | /// 25 | public static void SetupAuthentication(this IServiceCollection services, IConfiguration configuration) 26 | { 27 | Token token = configuration.GetSection("token").Get(); 28 | byte[] secret = Encoding.ASCII.GetBytes(token.Secret); 29 | 30 | services 31 | .AddAuthentication( 32 | options => 33 | { 34 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 35 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 36 | }) 37 | .AddJwtBearer( 38 | options => 39 | { 40 | options.RequireHttpsMetadata = true; 41 | options.SaveToken = true; 42 | options.ClaimsIssuer = token.Issuer; 43 | options.IncludeErrorDetails = true; 44 | options.Validate(JwtBearerDefaults.AuthenticationScheme); 45 | options.TokenValidationParameters = 46 | new TokenValidationParameters 47 | { 48 | ClockSkew = TimeSpan.Zero, 49 | ValidateIssuer = true, 50 | ValidateAudience = true, 51 | ValidateLifetime = true, 52 | ValidateIssuerSigningKey = true, 53 | ValidIssuer = token.Issuer, 54 | ValidAudience = token.Audience, 55 | IssuerSigningKey = new SymmetricSecurityKey(secret), 56 | NameClaimType = ClaimTypes.NameIdentifier, 57 | RequireSignedTokens = true, 58 | RequireExpirationTime = true 59 | }; 60 | }); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Config/AuthorizationConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.JwtBearer; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace CleanArchitectureCosmosDB.WebAPI.Config 6 | { 7 | /// 8 | /// Configure authorization 9 | /// 10 | public static class AuthorizationConfig 11 | { 12 | /// 13 | /// Authorization 14 | /// 15 | /// 16 | public static void SetAuthorization(this IServiceCollection services) 17 | { 18 | services.AddAuthorization( 19 | options => 20 | { 21 | options.AddPolicy( 22 | JwtBearerDefaults.AuthenticationScheme, 23 | new AuthorizationPolicyBuilder() 24 | .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) 25 | .RequireAuthenticatedUser() 26 | .Build()); 27 | }); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Config/CachingConfig.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Interfaces; 2 | using CleanArchitectureCosmosDB.WebAPI.Infrastructure.Services; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace CleanArchitectureCosmosDB.WebAPI.Config 6 | { 7 | /// 8 | /// Setup caching 9 | /// 10 | public static class CachingConfig 11 | { 12 | /// 13 | /// In-memory Caching 14 | /// 15 | /// 16 | public static void SetupInMemoryCaching(this IServiceCollection services) 17 | { 18 | // Non-distributed in-memory cache services 19 | services.AddMemoryCache(); 20 | services.AddScoped(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Config/DatabaseConfig.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Interfaces; 2 | using CleanArchitectureCosmosDB.Core.Interfaces.Persistence; 3 | using CleanArchitectureCosmosDB.Infrastructure.AppSettings; 4 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Repository; 5 | using CleanArchitectureCosmosDB.Infrastructure.Extensions; 6 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models; 7 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Services; 8 | using Microsoft.AspNetCore.Identity; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace CleanArchitectureCosmosDB.WebAPI.Config 14 | { 15 | /// 16 | /// Database related configurations 17 | /// 18 | public static class DatabaseConfig 19 | { 20 | /// 21 | /// Setup Cosmos DB 22 | /// 23 | /// 24 | /// 25 | public static void SetupCosmosDb(this IServiceCollection services, IConfiguration configuration) 26 | { 27 | // Bind database-related bindings 28 | CosmosDbSettings cosmosDbConfig = configuration.GetSection("ConnectionStrings:CleanArchitectureCosmosDB").Get(); 29 | // register CosmosDB client and data repositories 30 | services.AddCosmosDb(cosmosDbConfig.EndpointUrl, 31 | cosmosDbConfig.PrimaryKey, 32 | cosmosDbConfig.DatabaseName, 33 | cosmosDbConfig.Containers); 34 | 35 | services.AddScoped(); 36 | services.AddScoped(); 37 | } 38 | 39 | /// 40 | /// Setup ASP.NET Core Identity DB, including connection string, Identity options, token providers, and token services, etc.. 41 | /// 42 | /// 43 | /// 44 | public static void SetupIdentityDatabase(this IServiceCollection services, IConfiguration configuration) 45 | { 46 | services.AddDbContext(options => 47 | //options.UseSqlServer(configuration.GetConnectionString("CleanArchitectureIdentity")) 48 | options.UseInMemoryDatabase("CleanArchitectureIdentity") 49 | ); 50 | 51 | services.AddIdentity() 52 | .AddDefaultTokenProviders() 53 | .AddUserManager>() 54 | .AddSignInManager>() 55 | .AddEntityFrameworkStores(); 56 | services.Configure( 57 | options => 58 | { 59 | options.SignIn.RequireConfirmedEmail = true; 60 | options.User.RequireUniqueEmail = true; 61 | options.User.AllowedUserNameCharacters = 62 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; 63 | 64 | // Identity : Default password settings 65 | options.Password.RequireDigit = true; 66 | options.Password.RequireLowercase = true; 67 | options.Password.RequireNonAlphanumeric = true; 68 | options.Password.RequireUppercase = true; 69 | options.Password.RequiredLength = 6; 70 | options.Password.RequiredUniqueChars = 1; 71 | }); 72 | 73 | // services required using Identity 74 | services.AddScoped(); 75 | 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Config/MediatrConfig.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System.Reflection; 4 | 5 | namespace CleanArchitectureCosmosDB.WebAPI.Config 6 | { 7 | /// 8 | /// MediatR config 9 | /// 10 | public static class MediatrConfig 11 | { 12 | /// 13 | /// Setup mediatr to use command/query pattern and pipeline behaviors 14 | /// 15 | /// 16 | public static void SetupMediatr(this IServiceCollection services) 17 | { 18 | // MediatR, this will scan and register everything that inherits IRequest 19 | services.AddMediatR(Assembly.GetExecutingAssembly()); 20 | 21 | // Register MediatR pipeline behaviors, in the same order the behaviors should be called. 22 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(Infrastructure.Behaviours.ValidationBehaviour<,>)); 23 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(Infrastructure.Behaviours.UnhandledExceptionBehaviour<,>)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Config/MvcConfig.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.WebAPI.Infrastructure.Filters; 2 | using FluentValidation.AspNetCore; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using ZymLabs.NSwag.FluentValidation.AspNetCore; 5 | 6 | namespace CleanArchitectureCosmosDB.WebAPI.Config 7 | { 8 | /// 9 | /// Configure MVC options 10 | /// 11 | public static class MvcConfig 12 | { 13 | /// 14 | /// Configure controllers 15 | /// 16 | /// 17 | public static void SetupControllers(this IServiceCollection services) 18 | { 19 | // API controllers 20 | services.AddControllers(options => 21 | // handle exceptions thrown by an action 22 | options.Filters.Add(new ApiExceptionFilterAttribute())) 23 | .AddNewtonsoftJson(options => 24 | { 25 | // Serilize enum in string 26 | options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); 27 | }) 28 | .AddFluentValidation(options => 29 | { 30 | // In order to register FluentValidation to define Swagger schema 31 | // https://github.com/RicoSuter/NSwag/issues/1722#issuecomment-544202504 32 | // https://github.com/zymlabs/nswag-fluentvalidation 33 | options.RegisterValidatorsFromAssemblyContaining(); 34 | 35 | // Optionally set validator factory if you have problems with scope resolve inside validators. 36 | options.ValidatorFactoryType = typeof(HttpContextServiceProviderValidatorFactory); 37 | }) 38 | .AddMvcOptions(options => 39 | { 40 | // Clear the default MVC model binding and model validations, as we are registering all model binding and validation using FluentValidation. 41 | // See ApiExceptionFilterAttribute.cs 42 | // https://github.com/jasontaylordev/NorthwindTraders/issues/76 43 | options.ModelMetadataDetailsProviders.Clear(); 44 | options.ModelValidatorProviders.Clear(); 45 | }); 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Config/ODataConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNet.OData.Extensions; 2 | using Microsoft.AspNetCore.Mvc.Formatters; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Net.Http.Headers; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace CleanArchitectureCosmosDB.WebAPI.Config 11 | { 12 | /// 13 | /// OData 14 | /// 15 | public static class ODataConfig 16 | { 17 | /// 18 | /// Setup OData 19 | /// 20 | /// 21 | public static void SetupOData(this IServiceCollection services) 22 | { 23 | // OData Support 24 | services.AddOData(); 25 | 26 | // In order to make swagger work with OData 27 | services.AddMvcCore(options => 28 | { 29 | foreach (OutputFormatter outputFormatter in options.OutputFormatters.OfType().Where(x => x.SupportedMediaTypes.Count == 0)) 30 | { 31 | outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata")); 32 | } 33 | 34 | foreach (InputFormatter inputFormatter in options.InputFormatters.OfType().Where(x => x.SupportedMediaTypes.Count == 0)) 35 | { 36 | inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata")); 37 | } 38 | }); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Config/SwaggerConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using NSwag; 3 | using NSwag.Generation.Processors.Security; 4 | using ZymLabs.NSwag.FluentValidation; 5 | 6 | namespace CleanArchitectureCosmosDB.WebAPI.Config 7 | { 8 | /// 9 | /// Swagger 10 | /// 11 | public static class SwaggerConfig 12 | { 13 | /// 14 | /// NSwag for swagger 15 | /// 16 | /// 17 | public static void SetupNSwag(this IServiceCollection services) 18 | { 19 | // Register the Swagger services 20 | services.AddOpenApiDocument((options, serviceProvider) => 21 | { 22 | options.DocumentName = "v1"; 23 | options.Title = "Clean Architecture Cosmos DB API"; 24 | options.Version = "v1"; 25 | 26 | FluentValidationSchemaProcessor fluentValidationSchemaProcessor = serviceProvider.GetService(); 27 | // Add the fluent validations schema processor 28 | options.SchemaProcessors.Add(fluentValidationSchemaProcessor); 29 | 30 | // Add JWT token authorization 31 | options.OperationProcessors.Add(new OperationSecurityScopeProcessor("auth")); 32 | options.DocumentProcessors.Add(new SecurityDefinitionAppender("auth", new OpenApiSecurityScheme 33 | { 34 | Type = OpenApiSecuritySchemeType.Http, 35 | In = OpenApiSecurityApiKeyLocation.Header, 36 | Scheme = "bearer", 37 | BearerFormat = "jwt" 38 | })); 39 | 40 | }); 41 | 42 | // Add the FluentValidationSchemaProcessor as a singleton 43 | services.AddSingleton(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Controllers/AttachmentController.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.WebAPI.Models.Attachment; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace CleanArchitectureCosmosDB.WebAPI.Controllers 8 | { 9 | /// 10 | /// Controller 11 | /// 12 | [Route("api/[controller]")] 13 | [ApiController] 14 | public class AttachmentController : ControllerBase 15 | { 16 | private readonly IMediator _mediator; 17 | 18 | /// 19 | /// Controller ctor 20 | /// 21 | /// 22 | public AttachmentController(IMediator mediator) 23 | { 24 | this._mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 25 | } 26 | 27 | // GET: api/Attachment/Download?filePath=abc.JPG 28 | /// 29 | /// Download an item by path 30 | /// 31 | /// 32 | /// 33 | /// 34 | [HttpGet("Download", Name = "DownloadAttachment")] 35 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))] 36 | public async Task Download([FromQuery] string filePath, 37 | [FromQuery] string originalFileName = "") 38 | { 39 | var response = await _mediator.Send( 40 | new Download.DownloadQuery() 41 | { 42 | FilePath = filePath, 43 | OriginalFileName = originalFileName 44 | }); 45 | 46 | return File(response.Stream, response.ContentType, response.FileName); 47 | } 48 | 49 | // POST: api/Attachment 50 | /// 51 | /// Upload an item 52 | /// 53 | /// 54 | /// 55 | [HttpPost] 56 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Create))] 57 | public async Task> Upload([FromForm] Models.Attachment.Upload.UploadAttachmentCommand command) 58 | { 59 | var response = await _mediator.Send(command); 60 | return CreatedAtRoute("DownloadAttachment", 61 | new 62 | { 63 | filePath = response.Resource.FilePath, 64 | originalFileName = response.Resource.OriginalFileName 65 | }, 66 | response); 67 | } 68 | 69 | // POST: api/Attachment/Multiple 70 | /// 71 | /// Upload multiple files 72 | /// 73 | /// 74 | /// 75 | [HttpPost("Multiple", Name = "UploadMultipleAttachment")] 76 | [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status200OK)] 77 | [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest)] 78 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Create))] 79 | public async Task> UploadMultiple([FromForm] Models.Attachment.UploadMultiple.UploadMultipleAttachmentCommand command) 80 | { 81 | var response = await _mediator.Send(command); 82 | 83 | return Ok(response.UploadedAttachments); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Controllers/ToDoItemController.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Exceptions; 2 | using CleanArchitectureCosmosDB.WebAPI.Models.Shared; 3 | using CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem; 4 | using MediatR; 5 | using Microsoft.AspNet.OData; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Threading.Tasks; 11 | 12 | namespace CleanArchitectureCosmosDB.WebAPI.Controllers 13 | { 14 | /// 15 | /// ToDoItem Controller 16 | /// 17 | //[Authorize("Bearer")] 18 | [Route("api/[controller]")] 19 | [ApiController] 20 | public class ToDoItemController : ControllerBase 21 | { 22 | private readonly IMediator _mediator; 23 | 24 | /// 25 | /// Controller ctor 26 | /// 27 | /// 28 | public ToDoItemController(IMediator mediator) 29 | { 30 | this._mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 31 | } 32 | 33 | // GET: api/ToDoItem 34 | // OData: https://localhost:5001/api/ToDoItem?$select=title 35 | /// 36 | /// Get all 37 | /// 38 | /// 39 | [HttpGet] 40 | [EnableQuery] 41 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))] 42 | public async Task> GetAll() 43 | { 44 | GetAll.QueryResponse response = await _mediator.Send(new GetAll.GetAllQuery()); 45 | return response.Resource; 46 | } 47 | 48 | // GET: api/ToDoItem/5 49 | /// 50 | /// Get by id 51 | /// 52 | /// 53 | /// 54 | [HttpGet("{id}", Name = "GetToDoItem")] 55 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))] 56 | public async Task> Get(string id) 57 | { 58 | Get.QueryResponse response = await _mediator.Send(new Get.GetQuery() { Id = id }); 59 | 60 | return response.Resource; 61 | } 62 | 63 | // POST: api/ToDoItem 64 | /// 65 | /// Create 66 | /// 67 | /// 68 | /// 69 | [HttpPost] 70 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Create))] 71 | public async Task Create([FromBody] Create.CreateToDoItemCommand command) 72 | { 73 | Create.CommandResponse response = await _mediator.Send(command); 74 | return CreatedAtRoute("GetToDoItem", new { id = response.Id }, null); 75 | } 76 | 77 | // PUT: api/ToDoItem/5 78 | /// 79 | /// Update 80 | /// 81 | /// 82 | /// 83 | /// 84 | [HttpPut("{id}")] 85 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Update))] 86 | public async Task Update(string id, [FromBody] Update.UpdateCommand command) 87 | { 88 | if (id != command.Id) 89 | { 90 | return BadRequest(); 91 | } 92 | 93 | Update.CommandResponse response = await _mediator.Send(command); 94 | 95 | return NoContent(); 96 | } 97 | 98 | // DELETE: api/ToDoItem/5 99 | /// 100 | /// Delete 101 | /// 102 | /// 103 | /// 104 | [HttpDelete("{id}")] 105 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Delete))] 106 | public async Task Delete(string id) 107 | { 108 | await _mediator.Send(new Delete.DeleteToDoItemCommand() { Id = id }); 109 | 110 | return NoContent(); 111 | } 112 | 113 | // GET: api/ToDoItem/5/AuditHistory 114 | /// 115 | /// Get audit history of an item by id 116 | /// 117 | /// 118 | /// 119 | [HttpGet("{id}/AuditHistory", Name = "GetToDoItemAuditHistory")] 120 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))] 121 | public async Task> GetAuditHistory(string id) 122 | { 123 | GetAuditHistory.QueryResponse response = await _mediator.Send(new GetAuditHistory.GetQuery() { Id = id }); 124 | 125 | return response.Resource; 126 | } 127 | 128 | // Search: api/ToDoItem/Search 129 | /// 130 | /// Search 131 | /// 132 | /// 133 | /// 134 | [HttpPost("Search", Name = "SearchDefinition")] 135 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))] 136 | [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status200OK)] 137 | public async Task Search(Search.SearchToDoItemQuery query) 138 | { 139 | Search.QueryResponse response = await _mediator.Send(query); 140 | DataTablesResponse result = new DataTablesResponse() 141 | { 142 | Data = response.Resource, 143 | TotalRecords = response.TotalRecordsMatched, 144 | Page = response.CurrentPage 145 | }; 146 | 147 | return result; 148 | } 149 | 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Controllers/TokenController.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication; 2 | using CleanArchitectureCosmosDB.WebAPI.Models.Token; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using System.Threading.Tasks; 7 | 8 | namespace CleanArchitectureCosmosDB.WebAPI.Controllers 9 | { 10 | /// 11 | /// All token related actions. 12 | /// 13 | [ApiController] 14 | [Route("api/[controller]")] 15 | public class TokenController 16 | { 17 | private readonly IMediator _mediator; 18 | 19 | /// 20 | /// ctor 21 | /// 22 | /// 23 | public TokenController(IMediator mediator) 24 | { 25 | _mediator = mediator; 26 | } 27 | 28 | // POST: api/Token/Authenticate 29 | /// 30 | /// Validate that the user account is valid and return an auth token 31 | /// to the requesting app for use in the api. 32 | /// 33 | /// 34 | /// 35 | [AllowAnonymous] 36 | [HttpPost("Authenticate")] 37 | [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status200OK)] 38 | [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest)] 39 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))] 40 | public async Task AuthenticateAsync([FromBody] Authenticate.AuthenticateCommand command) 41 | { 42 | var response = await _mediator.Send(command); 43 | return response.Resource; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Infrastructure/ApiExceptions/ApiModelValidationException.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.Results; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace CleanArchitectureCosmosDB.WebAPI.Infrastructure.ApiExceptions 7 | { 8 | /// 9 | /// Api validation exception 10 | /// 11 | public class ApiModelValidationException : Exception 12 | { 13 | /// 14 | /// Validation errors 15 | /// 16 | public IDictionary Errors { get; } 17 | 18 | /// 19 | /// ctor 20 | /// 21 | public ApiModelValidationException() 22 | : base("One or more validation failures have occurred.") 23 | { 24 | Errors = new Dictionary(); 25 | } 26 | 27 | /// 28 | /// ctor 29 | /// 30 | /// 31 | public ApiModelValidationException(IEnumerable failures) 32 | : this() 33 | { 34 | Errors = failures 35 | .GroupBy(e => e.PropertyName, e => e.ErrorMessage) 36 | .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()); 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Infrastructure/Behaviours/UnhandledExceptionBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Serilog; 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace CleanArchitectureCosmosDB.WebAPI.Infrastructure.Behaviours 8 | { 9 | /// 10 | /// MediatR pipeline behavior to handle any unhandled exception. 11 | /// For more information: https://github.com/jbogard/MediatR/wiki/Behaviors 12 | /// 13 | /// The request object passed in through IMediator.Send. 14 | /// 15 | public class UnhandledExceptionBehaviour : IPipelineBehavior 16 | { 17 | /// 18 | /// ctor 19 | /// 20 | public UnhandledExceptionBehaviour() 21 | { 22 | } 23 | 24 | /// 25 | /// 26 | /// 27 | /// The request object passed in through IMediator.Send. 28 | /// Cancellation token. 29 | /// An async continuation for the next action in the behavior chain. 30 | /// 31 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 32 | { 33 | try 34 | { 35 | return await next(); 36 | } 37 | catch (Exception ex) 38 | { 39 | string requestName = typeof(TRequest).Name; 40 | 41 | Log.Error(ex, "Request: Unhandled Exception for Request {Name} {@Request}", requestName, request); 42 | 43 | throw; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Infrastructure/Behaviours/ValidationBehaviour.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.WebAPI.Infrastructure.ApiExceptions; 2 | using FluentValidation; 3 | using MediatR; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace CleanArchitectureCosmosDB.WebAPI.Infrastructure.Behaviours 10 | { 11 | /// 12 | /// MediatR pipeline behavior to run validation logic before the handlers handle the request. 13 | /// For more information: https://github.com/jbogard/MediatR/wiki/Behaviors 14 | /// 15 | /// 16 | /// 17 | public class ValidationBehaviour : IPipelineBehavior 18 | where TRequest : IRequest 19 | { 20 | private readonly IEnumerable> _validators; 21 | 22 | /// 23 | /// ctor 24 | /// 25 | /// 26 | public ValidationBehaviour(IEnumerable> validators) 27 | { 28 | _validators = validators; 29 | } 30 | 31 | /// 32 | /// pipeline handler 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 39 | { 40 | if (_validators.Any()) 41 | { 42 | ValidationContext context = new ValidationContext(request); 43 | 44 | FluentValidation.Results.ValidationResult[] validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); 45 | List failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList(); 46 | 47 | if (failures.Count != 0) 48 | throw new ApiModelValidationException(failures); 49 | } 50 | return await next(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Infrastructure/Services/InMemoryCachedToDoItemsService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities; 2 | using CleanArchitectureCosmosDB.Core.Interfaces; 3 | using CleanArchitectureCosmosDB.Infrastructure.Extensions; 4 | using Microsoft.Extensions.Caching.Memory; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace CleanArchitectureCosmosDB.WebAPI.Infrastructure.Services 9 | { 10 | /// 11 | /// Non-distributed in memory cache. 12 | /// 13 | public class InMemoryCachedToDoItemsService : ICachedToDoItemsService 14 | { 15 | private readonly IMemoryCache _cache; 16 | 17 | /// 18 | /// ctor 19 | /// 20 | /// 21 | public InMemoryCachedToDoItemsService(IMemoryCache cache) 22 | { 23 | _cache = cache ?? throw new ArgumentNullException(nameof(cache)); 24 | } 25 | 26 | /// 27 | /// Delete 28 | /// 29 | /// 30 | public void DeleteCachedToDoItems() 31 | { 32 | _cache.Remove(CacheHelpers.GenerateToDoItemsCacheKey()); 33 | } 34 | 35 | /// 36 | /// Get 37 | /// 38 | /// 39 | public IEnumerable GetCachedToDoItems() 40 | { 41 | IEnumerable toDoItems; 42 | 43 | _cache.TryGetValue>(CacheHelpers.GenerateToDoItemsCacheKey(), out toDoItems); 44 | 45 | return toDoItems; 46 | } 47 | 48 | /// 49 | /// Set 50 | /// 51 | /// 52 | public void SetCachedToDoItems(IEnumerable entry) 53 | { 54 | // Set cache options 55 | MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions() 56 | // Keep in cache for this time, reset time if accessed. 57 | .SetSlidingExpiration(TimeSpan.FromDays(1)); 58 | 59 | _cache.Set(CacheHelpers.GenerateToDoItemsCacheKey(), entry, cacheEntryOptions); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/Attachment/AttachmentModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Attachment 8 | { 9 | /// 10 | /// Attachment 11 | /// 12 | public class AttachmentModel 13 | { 14 | public AttachmentModel() 15 | { 16 | this.Id = Guid.NewGuid(); 17 | } 18 | 19 | [Required] 20 | public Guid Id { get; set; } 21 | 22 | public string FileName { get; set; } 23 | public string FileType { get; set; } 24 | public string FilePath { get; set; } 25 | public string OriginalFileName { get; set; } 26 | public string Name { get; set; } 27 | public string Description { get; set; } 28 | public string ContentType { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/Attachment/Download.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Exceptions; 3 | using CleanArchitectureCosmosDB.Core.Interfaces.Storage; 4 | using FluentValidation; 5 | using MediatR; 6 | using System; 7 | using System.IO; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Attachment 12 | { 13 | 14 | /// 15 | /// Download related query, validators, and handlers 16 | /// 17 | public class Download 18 | { 19 | /// 20 | /// Model to Download 21 | /// 22 | public class DownloadQuery : IRequest 23 | { 24 | /// 25 | /// Full Path 26 | /// 27 | public string FilePath { get; set; } 28 | 29 | /// 30 | /// Original name of the file 31 | /// 32 | public string OriginalFileName { get; set; } 33 | } 34 | 35 | /// 36 | /// Query Response 37 | /// 38 | public class QueryResponse 39 | { 40 | /// 41 | /// File Name 42 | /// 43 | public string FileName { get; set; } 44 | 45 | /// 46 | /// Content Type 47 | /// 48 | public string ContentType { get; set; } 49 | 50 | /// 51 | /// Stream 52 | /// 53 | public Stream Stream { get; set; } 54 | } 55 | 56 | /// 57 | /// Register Validation 58 | /// 59 | public class DownloadAttachmentQueryValidator : AbstractValidator 60 | { 61 | /// 62 | /// Validator ctor 63 | /// 64 | public DownloadAttachmentQueryValidator() 65 | { 66 | RuleFor(x => x.FilePath) 67 | .NotEmpty(); 68 | } 69 | 70 | } 71 | 72 | 73 | /// 74 | /// Handler 75 | /// 76 | public class QueryHandler : IRequestHandler 77 | { 78 | private readonly IStorageService _storageService; 79 | private readonly IMapper _mapper; 80 | 81 | /// 82 | /// Ctor 83 | /// 84 | /// 85 | /// 86 | public QueryHandler(IStorageService storageService, 87 | IMapper mapper) 88 | { 89 | this._storageService = storageService ?? throw new ArgumentNullException(nameof(storageService)); 90 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 91 | } 92 | 93 | /// 94 | /// Handle 95 | /// 96 | /// 97 | /// 98 | /// 99 | public async Task Handle(DownloadQuery query, CancellationToken cancellationToken) 100 | { 101 | QueryResponse response = new QueryResponse(); 102 | Stream ms = new MemoryStream(); 103 | 104 | try 105 | { 106 | ms = await _storageService.GetFileStream(query.FilePath); 107 | ms.Position = 0; // Have to reset the current position 108 | } 109 | catch(Exception ex) 110 | { 111 | // Exception and logging are handled in a centralized place, see ApiExceptionFilter. 112 | throw new EntityNotFoundException(nameof(AttachmentModel), query.FilePath); 113 | } 114 | 115 | // Content Type mapping 116 | string[] fileNameParts = query.FilePath.Split('/', StringSplitOptions.RemoveEmptyEntries); 117 | string fileName = fileNameParts[fileNameParts.Length - 1]; 118 | 119 | var contentTypeProvider = new Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider(); 120 | string contentType = "application/octet-stream"; 121 | if (!contentTypeProvider.TryGetContentType(fileName, out contentType)) 122 | { 123 | contentType = "application/octet-stream"; // fallback 124 | } 125 | 126 | response.FileName = String.IsNullOrEmpty(query.OriginalFileName) ? fileName : query.OriginalFileName; 127 | response.Stream = ms; 128 | response.ContentType = contentType; 129 | 130 | return response; 131 | } 132 | 133 | } 134 | 135 | 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/Attachment/Upload.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Interfaces.Storage; 3 | using FluentValidation; 4 | using MediatR; 5 | using Microsoft.AspNetCore.Http; 6 | using Storage.Net; 7 | using System; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Attachment 12 | { 13 | /// 14 | /// Upload related commands, validators and handlers 15 | /// 16 | public class Upload 17 | { 18 | /// 19 | /// Model to create a new Attachment 20 | /// 21 | public class UploadAttachmentCommand : IRequest 22 | { 23 | 24 | /// 25 | /// The binary file to upload 26 | /// 27 | public IFormFile File { get; set; } 28 | 29 | } 30 | 31 | /// 32 | /// Command Response 33 | /// 34 | public class CommandResponse 35 | { 36 | /// 37 | /// Attachment that is uploaded 38 | /// 39 | public AttachmentModel Resource { get; set; } 40 | } 41 | 42 | /// 43 | /// Register Validation 44 | /// 45 | public class UploadAttachmentCommandValidator : AbstractValidator 46 | { 47 | private readonly IStorageService _storageService; 48 | 49 | /// 50 | /// Validator ctor 51 | /// 52 | public UploadAttachmentCommandValidator(IStorageService storageService) 53 | { 54 | this._storageService = storageService ?? throw new ArgumentNullException(nameof(storageService)); 55 | 56 | // Add Validation rules here 57 | 58 | } 59 | 60 | 61 | } 62 | 63 | 64 | /// 65 | /// Handler 66 | /// 67 | public class CommandHandler : IRequestHandler 68 | { 69 | private readonly IStorageService _storageService; 70 | private readonly IMapper _mapper; 71 | 72 | /// 73 | /// Ctor 74 | /// 75 | /// 76 | /// 77 | public CommandHandler(IStorageService storageService, 78 | IMapper mapper) 79 | { 80 | this._storageService = storageService ?? throw new ArgumentNullException(nameof(storageService)); 81 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 82 | } 83 | 84 | /// 85 | /// Handle 86 | /// 87 | /// 88 | /// 89 | /// 90 | public async Task Handle(UploadAttachmentCommand command, CancellationToken cancellationToken) 91 | { 92 | CommandResponse response = new CommandResponse(); 93 | AttachmentModel uploadedAttachment = new AttachmentModel(); 94 | // prepare the upload 95 | string originalName = System.IO.Path.GetFileName(command.File.FileName); 96 | string extension = System.IO.Path.GetExtension(command.File.FileName).ToLower(); 97 | string storedAsFileName = $"{uploadedAttachment.Id}{extension}"; 98 | string fileFullPath = StoragePath.Combine("attachments", 99 | storedAsFileName); 100 | 101 | // upload 102 | string fullPath = await _storageService.UploadFile(command.File, fileFullPath); 103 | uploadedAttachment.FilePath = fullPath; 104 | 105 | // prepare response 106 | uploadedAttachment.FileName = storedAsFileName; 107 | uploadedAttachment.FileType = extension; 108 | uploadedAttachment.ContentType = command.File.ContentType; 109 | uploadedAttachment.OriginalFileName = originalName; 110 | uploadedAttachment.Name = originalName; 111 | uploadedAttachment.Description = $"Attachment {originalName}"; 112 | 113 | response.Resource = uploadedAttachment; 114 | 115 | return response; 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/Attachment/UploadMultiple.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Interfaces.Storage; 3 | using FluentValidation; 4 | using MediatR; 5 | using Microsoft.AspNetCore.Http; 6 | using Storage.Net; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Attachment 13 | { 14 | /// 15 | /// UploadMultiple related commands, validators and handlers 16 | /// 17 | public class UploadMultiple 18 | { 19 | /// 20 | /// Model to create a new Attachment 21 | /// 22 | public class UploadMultipleAttachmentCommand : IRequest 23 | { 24 | 25 | /// 26 | /// The binary file to upload 27 | /// 28 | public IEnumerable Files { get; set; } 29 | 30 | } 31 | 32 | /// 33 | /// Command Response 34 | /// 35 | public class CommandResponse 36 | { 37 | /// 38 | /// Attachments uploaded 39 | /// 40 | public List UploadedAttachments { get; set; } 41 | } 42 | 43 | /// 44 | /// Register Validation 45 | /// 46 | public class UploadMultipleAttachmentCommandValidator : AbstractValidator 47 | { 48 | private readonly IStorageService _storageService; 49 | 50 | /// 51 | /// Validator ctor 52 | /// 53 | public UploadMultipleAttachmentCommandValidator(IStorageService storageService) 54 | { 55 | this._storageService = storageService ?? throw new ArgumentNullException(nameof(storageService)); 56 | 57 | // Add Validation rules here 58 | 59 | } 60 | 61 | 62 | } 63 | 64 | 65 | /// 66 | /// Handler 67 | /// 68 | public class CommandHandler : IRequestHandler 69 | { 70 | private readonly IStorageService _storageService; 71 | private readonly IMapper _mapper; 72 | 73 | /// 74 | /// Ctor 75 | /// 76 | /// 77 | /// 78 | public CommandHandler(IStorageService storageService, 79 | IMapper mapper) 80 | { 81 | this._storageService = storageService ?? throw new ArgumentNullException(nameof(storageService)); 82 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 83 | } 84 | 85 | /// 86 | /// Handle 87 | /// 88 | /// 89 | /// 90 | /// 91 | public async Task Handle(UploadMultipleAttachmentCommand command, CancellationToken cancellationToken) 92 | { 93 | CommandResponse response = new CommandResponse() 94 | { 95 | UploadedAttachments = new List() 96 | }; 97 | 98 | foreach(var commandFile in command.Files) 99 | { 100 | AttachmentModel attachment = new 101 | AttachmentModel(); 102 | 103 | // prepare the upload 104 | string originalName = System.IO.Path.GetFileName(commandFile.FileName); 105 | string extension = System.IO.Path.GetExtension(commandFile.FileName).ToLower(); 106 | string storedAsFileName = $"{attachment.Id}{extension}"; 107 | string fileFullPath = StoragePath.Combine("attachments", 108 | storedAsFileName); 109 | 110 | // upload 111 | string fullPath = await _storageService.UploadFile(commandFile, fileFullPath); 112 | attachment.FilePath = fullPath; 113 | 114 | // prepare response 115 | attachment.FileName = storedAsFileName; 116 | attachment.FileType = extension; 117 | attachment.ContentType = commandFile.ContentType; 118 | attachment.OriginalFileName = originalName; 119 | attachment.Name = originalName; 120 | attachment.Description = $"Attachment {originalName}"; 121 | 122 | response.UploadedAttachments.Add(attachment); 123 | } 124 | 125 | return response; 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/Shared/DataTablesResponse.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Shared 4 | { 5 | public class DataTablesResponse 6 | { 7 | /// 8 | /// Total number of records available 9 | /// 10 | [Required] 11 | public int TotalRecords { get; set; } 12 | /// 13 | /// Data object 14 | /// 15 | [Required] 16 | public object Data { get; set; } 17 | 18 | /// 19 | /// Current page index 20 | /// 21 | [Required] 22 | public int Page { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/Create.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Interfaces; 3 | using CleanArchitectureCosmosDB.Core.Specifications; 4 | using FluentValidation; 5 | using MediatR; 6 | using System; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem 12 | { 13 | /// 14 | /// Create related commands, validators and handlers 15 | /// 16 | public class Create 17 | { 18 | /// 19 | /// Model to create an entity 20 | /// 21 | public class CreateToDoItemCommand : IRequest 22 | { 23 | /// 24 | /// Category 25 | /// 26 | public string Category { get; set; } 27 | 28 | /// 29 | /// Title 30 | /// 31 | public string Title { get; set; } 32 | 33 | 34 | } 35 | 36 | /// 37 | /// Command Response 38 | /// 39 | public class CommandResponse 40 | { 41 | /// 42 | /// Item Id 43 | /// 44 | public string Id { get; set; } 45 | } 46 | 47 | /// 48 | /// Register Validation 49 | /// 50 | public class CreateToDoItemCommandValidator : AbstractValidator 51 | { 52 | private readonly IToDoItemRepository _repo; 53 | 54 | /// 55 | /// Validator ctor 56 | /// 57 | public CreateToDoItemCommandValidator(IToDoItemRepository repo) 58 | { 59 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo)); 60 | 61 | RuleFor(x => x.Category) 62 | .NotEmpty(); 63 | RuleFor(x => x.Title) 64 | .Cascade(CascadeMode.Stop) 65 | .NotEmpty() 66 | .MustAsync(HasUniqueTitle).WithMessage("Title must be unique"); 67 | 68 | } 69 | 70 | /// 71 | /// Check uniqueness 72 | /// 73 | /// 74 | /// 75 | /// 76 | public async Task HasUniqueTitle(string title, CancellationToken cancellationToken) 77 | { 78 | ToDoItemSearchSpecification specification = new ToDoItemSearchSpecification(title, 79 | exactSearch: true); 80 | 81 | System.Collections.Generic.IEnumerable entities = await _repo.GetItemsAsync(specification); 82 | 83 | return entities == null || entities.Count() == 0; 84 | 85 | } 86 | } 87 | 88 | 89 | /// 90 | /// Handler 91 | /// 92 | public class CommandHandler : IRequestHandler 93 | { 94 | private readonly IToDoItemRepository _repo; 95 | private readonly IMapper _mapper; 96 | 97 | /// 98 | /// Ctor 99 | /// 100 | /// 101 | /// 102 | public CommandHandler(IToDoItemRepository repo, 103 | IMapper mapper) 104 | { 105 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo)); 106 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 107 | } 108 | 109 | /// 110 | /// Handle 111 | /// 112 | /// 113 | /// 114 | /// 115 | public async Task Handle(CreateToDoItemCommand command, CancellationToken cancellationToken) 116 | { 117 | CommandResponse response = new CommandResponse(); 118 | Core.Entities.ToDoItem entity = _mapper.Map(command); 119 | await _repo.AddItemAsync(entity); 120 | 121 | response.Id = entity.Id; 122 | return response; 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/Delete.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Exceptions; 3 | using CleanArchitectureCosmosDB.Core.Interfaces; 4 | using FluentValidation; 5 | using MediatR; 6 | using System; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem 11 | { 12 | /// 13 | /// Delete related commands, validators, and handlers 14 | /// 15 | public class Delete 16 | { 17 | /// 18 | /// Model to Delete an entity 19 | /// 20 | public class DeleteToDoItemCommand : IRequest 21 | { 22 | /// 23 | /// Id 24 | /// 25 | public string Id { get; set; } 26 | 27 | } 28 | 29 | /// 30 | /// Command Response 31 | /// 32 | public class CommandResponse 33 | { 34 | } 35 | 36 | /// 37 | /// Register Validation 38 | /// 39 | public class DeleteToDoItemCommandValidator : AbstractValidator 40 | { 41 | /// 42 | /// Validator ctor 43 | /// 44 | public DeleteToDoItemCommandValidator() 45 | { 46 | RuleFor(x => x.Id) 47 | .NotEmpty(); 48 | } 49 | 50 | } 51 | 52 | 53 | /// 54 | /// Handler 55 | /// 56 | public class CommandHandler : IRequestHandler 57 | { 58 | private readonly IToDoItemRepository _repo; 59 | private readonly IMapper _mapper; 60 | 61 | /// 62 | /// Ctor 63 | /// 64 | /// 65 | /// 66 | public CommandHandler(IToDoItemRepository repo, 67 | IMapper mapper) 68 | { 69 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo)); 70 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 71 | } 72 | 73 | /// 74 | /// Handle 75 | /// 76 | /// 77 | /// 78 | /// 79 | public async Task Handle(DeleteToDoItemCommand command, CancellationToken cancellationToken) 80 | { 81 | CommandResponse response = new CommandResponse(); 82 | 83 | Core.Entities.ToDoItem entity = await _repo.GetItemAsync(command.Id); 84 | if (entity == null) 85 | { 86 | throw new EntityNotFoundException(nameof(ToDoItem), command.Id); 87 | } 88 | 89 | await _repo.DeleteItemAsync(command.Id); 90 | 91 | return response; 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/Get.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Exceptions; 3 | using CleanArchitectureCosmosDB.Core.Interfaces; 4 | using FluentValidation; 5 | using MediatR; 6 | using System; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem 11 | { 12 | /// 13 | /// Get related query, validators, and handlers 14 | /// 15 | public class Get 16 | { 17 | /// 18 | /// Model to Get an entity 19 | /// 20 | public class GetQuery : IRequest 21 | { 22 | /// 23 | /// Id 24 | /// 25 | public string Id { get; set; } 26 | 27 | } 28 | 29 | /// 30 | /// Query Response 31 | /// 32 | public class QueryResponse 33 | { 34 | /// 35 | /// Resource 36 | /// 37 | public ToDoItemModel Resource { get; set; } 38 | } 39 | 40 | /// 41 | /// Register Validation 42 | /// 43 | public class GetToDoItemQueryValidator : AbstractValidator 44 | { 45 | /// 46 | /// Validator ctor 47 | /// 48 | public GetToDoItemQueryValidator() 49 | { 50 | RuleFor(x => x.Id) 51 | .NotEmpty(); 52 | } 53 | 54 | } 55 | 56 | 57 | /// 58 | /// Handler 59 | /// 60 | public class QueryHandler : IRequestHandler 61 | { 62 | private readonly IToDoItemRepository _repo; 63 | private readonly IMapper _mapper; 64 | 65 | /// 66 | /// Ctor 67 | /// 68 | /// 69 | /// 70 | public QueryHandler(IToDoItemRepository repo, 71 | IMapper mapper) 72 | { 73 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo)); 74 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 75 | } 76 | 77 | /// 78 | /// Handle 79 | /// 80 | /// 81 | /// 82 | /// 83 | public async Task Handle(GetQuery query, CancellationToken cancellationToken) 84 | { 85 | QueryResponse response = new QueryResponse(); 86 | 87 | Core.Entities.ToDoItem entity = await _repo.GetItemAsync(query.Id); 88 | if (entity == null) 89 | { 90 | throw new EntityNotFoundException(nameof(ToDoItem), query.Id); 91 | } 92 | 93 | response.Resource = _mapper.Map(entity); 94 | 95 | return response; 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/GetAll.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Interfaces; 3 | using CleanArchitectureCosmosDB.Core.Specifications; 4 | using FluentValidation; 5 | using MediatR; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem 13 | { 14 | /// 15 | /// GetAll related commands, validators, and handlers 16 | /// 17 | public class GetAll 18 | { 19 | /// 20 | /// Model to GetAll entities 21 | /// 22 | public class GetAllQuery : IRequest 23 | { 24 | 25 | } 26 | 27 | /// 28 | /// Query Response 29 | /// 30 | public class QueryResponse 31 | { 32 | /// 33 | /// Resource 34 | /// 35 | public IEnumerable Resource { get; set; } 36 | } 37 | 38 | /// 39 | /// Register Validation 40 | /// 41 | public class GetAllToDoItemQueryValidator : AbstractValidator 42 | { 43 | /// 44 | /// Validator ctor 45 | /// 46 | public GetAllToDoItemQueryValidator() 47 | { 48 | 49 | } 50 | 51 | } 52 | 53 | /// 54 | /// Handler 55 | /// 56 | public class QueryHandler : IRequestHandler 57 | { 58 | private readonly IToDoItemRepository _repo; 59 | private readonly IMapper _mapper; 60 | private readonly ICachedToDoItemsService _cachedToDoItemsService; 61 | 62 | /// 63 | /// Ctor 64 | /// 65 | /// 66 | /// 67 | /// 68 | public QueryHandler(IToDoItemRepository repo, 69 | IMapper mapper, 70 | ICachedToDoItemsService cachedToDoItemsService) 71 | { 72 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo)); 73 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 74 | this._cachedToDoItemsService = cachedToDoItemsService ?? throw new ArgumentNullException(nameof(cachedToDoItemsService)); 75 | } 76 | 77 | /// 78 | /// Handle 79 | /// 80 | /// 81 | /// 82 | /// 83 | public async Task Handle(GetAllQuery query, CancellationToken cancellationToken) 84 | { 85 | QueryResponse response = new QueryResponse(); 86 | 87 | // If needed, this is where to implement cache reading and setting logic 88 | //var cachedEntities = await _cachedToDoItemsService.GetCachedToDoItemsAsync(); 89 | 90 | //var entities = await _repo.GetItemsAsync($"SELECT * FROM c"); 91 | // Get all the incompleted todo items 92 | ToDoItemGetAllSpecification specification = new ToDoItemGetAllSpecification(false); 93 | IEnumerable entities = await _repo.GetItemsAsync(specification); 94 | response.Resource = entities.Select(x => _mapper.Map(x)); 95 | 96 | return response; 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/GetAuditHistory.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Interfaces.Persistence; 3 | using CleanArchitectureCosmosDB.Core.Specifications; 4 | using FluentValidation; 5 | using MediatR; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem 13 | { 14 | /// 15 | /// Get related query, validators, and handlers 16 | /// 17 | public class GetAuditHistory 18 | { 19 | /// 20 | /// Get 21 | /// 22 | public class GetQuery : IRequest 23 | { 24 | /// 25 | /// Entity Id 26 | /// 27 | public string Id { get; set; } 28 | 29 | } 30 | 31 | /// 32 | /// Query Response 33 | /// 34 | public class QueryResponse 35 | { 36 | /// 37 | /// Resource 38 | /// 39 | public IEnumerable Resource { get; set; } 40 | } 41 | 42 | /// 43 | /// Register Validation 44 | /// 45 | public class GetDefinitionQueryValidator : AbstractValidator 46 | { 47 | /// 48 | /// Validator ctor 49 | /// 50 | public GetDefinitionQueryValidator() 51 | { 52 | RuleFor(x => x.Id) 53 | .NotEmpty(); 54 | } 55 | 56 | } 57 | 58 | 59 | /// 60 | /// Handler 61 | /// 62 | public class QueryHandler : IRequestHandler 63 | { 64 | private readonly IAuditRepository _repo; 65 | private readonly IMapper _mapper; 66 | 67 | /// 68 | /// Ctor 69 | /// 70 | /// 71 | /// 72 | public QueryHandler(IAuditRepository repo, 73 | IMapper mapper) 74 | { 75 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo)); 76 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 77 | } 78 | 79 | /// 80 | /// Handle 81 | /// 82 | /// 83 | /// 84 | /// 85 | public async Task Handle(GetQuery query, CancellationToken cancellationToken) 86 | { 87 | QueryResponse response = new QueryResponse(); 88 | 89 | AuditFilterSpecification specification = new AuditFilterSpecification(query.Id); 90 | IEnumerable entities = await _repo.GetItemsAsync(specification); 91 | 92 | // Map audit records to entity-specific audit model 93 | response.Resource = entities.Select(x => _mapper.Map(x)); 94 | 95 | return response; 96 | } 97 | 98 | } 99 | 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem 4 | { 5 | /// 6 | /// Mapping Profile for AutoMapper 7 | /// 8 | public class MappingProfile : Profile 9 | { 10 | /// 11 | /// ctor 12 | /// 13 | public MappingProfile() 14 | { 15 | // Get 16 | CreateMap().ReverseMap(); 17 | 18 | // Create 19 | CreateMap(); 20 | 21 | // Audit 22 | CreateMap() 23 | .ForMember(t => t.ToDoItemModel, s => s.MapFrom(audit => Newtonsoft.Json.JsonConvert.DeserializeObject(audit.Entity))); 24 | } 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/Search.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Interfaces; 3 | using CleanArchitectureCosmosDB.Core.Specifications; 4 | using CleanArchitectureCosmosDB.Core.Specifications.Interfaces; 5 | using FluentValidation; 6 | using MediatR; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem 14 | { 15 | /// 16 | /// Search related commands, validators, and handlers 17 | /// 18 | public class Search 19 | { 20 | /// 21 | /// Model to Search 22 | /// 23 | public class SearchToDoItemQuery : IRequest, ISearchQuery 24 | { 25 | // Pagination and Sort 26 | /// 27 | /// Starting point (translates to OFFSET) 28 | /// 29 | public int Start { get; set; } 30 | /// 31 | /// Page Size (translates to LIMIT) 32 | /// 33 | public int PageSize { get; set; } 34 | /// 35 | /// Sort by Column 36 | /// 37 | public string SortColumn { get; set; } 38 | /// 39 | /// Sort direction 40 | /// 41 | public SortDirection? SortDirection { get; set; } 42 | 43 | // Search 44 | /// 45 | /// Title 46 | /// 47 | public string TitleFilter { get; set; } 48 | } 49 | 50 | /// 51 | /// Query Response 52 | /// 53 | public class QueryResponse 54 | { 55 | /// 56 | /// Current Page, 0-indexed 57 | /// 58 | public int CurrentPage { get; set; } 59 | 60 | /// 61 | /// Total Records Matched. For Pagination purpose. 62 | /// 63 | public int TotalRecordsMatched { get; set; } 64 | 65 | /// 66 | /// Resource 67 | /// 68 | public IEnumerable Resource { get; set; } 69 | } 70 | 71 | 72 | 73 | /// 74 | /// Register Validation 75 | /// 76 | public class SearchToDoItemQueryValidator : AbstractValidator 77 | { 78 | /// 79 | /// Validator ctor 80 | /// 81 | public SearchToDoItemQueryValidator() 82 | { 83 | RuleFor(x => x.PageSize) 84 | .NotEmpty() 85 | .GreaterThan(0); 86 | 87 | } 88 | 89 | } 90 | 91 | /// 92 | /// Handler 93 | /// 94 | public class QueryHandler : IRequestHandler 95 | { 96 | private readonly IToDoItemRepository _repo; 97 | private readonly IMapper _mapper; 98 | 99 | /// 100 | /// Ctor 101 | /// 102 | /// 103 | /// 104 | public QueryHandler(IToDoItemRepository repo, 105 | IMapper mapper) 106 | { 107 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo)); 108 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 109 | } 110 | 111 | /// 112 | /// Handle 113 | /// 114 | /// 115 | /// 116 | /// 117 | public async Task Handle(SearchToDoItemQuery query, CancellationToken cancellationToken) 118 | { 119 | QueryResponse response = new QueryResponse(); 120 | 121 | // records 122 | ToDoItemSearchSpecification specification = new ToDoItemSearchSpecification(query.TitleFilter, 123 | query.Start, 124 | query.PageSize, 125 | query.SortColumn, 126 | query.SortDirection ?? query.SortDirection.Value); 127 | 128 | IEnumerable entities = await _repo.GetItemsAsync(specification); 129 | response.Resource = entities.Select(x => _mapper.Map(x)); 130 | 131 | // count 132 | ToDoItemSearchAggregationSpecification countSpecification = new ToDoItemSearchAggregationSpecification(query.TitleFilter); 133 | response.TotalRecordsMatched = await _repo.GetItemsCountAsync(countSpecification); 134 | 135 | response.CurrentPage = (query.PageSize != 0) ? query.Start / query.PageSize : 0; 136 | 137 | return response; 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/ToDoItemAuditModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem 5 | { 6 | /// 7 | /// ToDoItem audit Model 8 | /// 9 | public class ToDoItemAuditModel 10 | { 11 | /// 12 | /// Snapshot of the ToDoItem 13 | /// 14 | [Required] 15 | public ToDoItemModel ToDoItemModel { get; set; } 16 | /// 17 | /// Date audit record created 18 | /// 19 | [Required] 20 | public DateTime DateCreatedUTC { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/ToDoItemModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem 4 | { 5 | /// 6 | /// ToDoItem Api Model 7 | /// 8 | public class ToDoItemModel 9 | { 10 | /// 11 | /// ToDoItem Id 12 | /// 13 | [Required] 14 | public string Id { get; set; } 15 | /// 16 | /// Category which the To-Do-Item belongs to 17 | /// 18 | [Required] 19 | public string Category { get; set; } 20 | /// 21 | /// Title of the To-Do-Item 22 | /// 23 | [Required] 24 | public string Title { get; set; } 25 | 26 | /// 27 | /// Whether the To-Do-Item is done 28 | /// 29 | [Required] 30 | public bool IsCompleted { get; private set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/Update.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Exceptions; 3 | using CleanArchitectureCosmosDB.Core.Interfaces; 4 | using CleanArchitectureCosmosDB.Core.Specifications; 5 | using FluentValidation; 6 | using MediatR; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | 14 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem 15 | { 16 | /// 17 | /// Update related commands, validators, and handlers 18 | /// 19 | public class Update 20 | { 21 | /// 22 | /// Model to Update an entity 23 | /// 24 | public class UpdateCommand : IRequest 25 | { 26 | /// 27 | /// Id 28 | /// 29 | public string Id { get; set; } 30 | 31 | /// 32 | /// Category 33 | /// 34 | public string Category { get; set; } 35 | 36 | /// 37 | /// Title 38 | /// 39 | public string Title { get; set; } 40 | 41 | } 42 | 43 | /// 44 | /// Command Response 45 | /// 46 | public class CommandResponse 47 | { 48 | 49 | } 50 | 51 | /// 52 | /// Register Validation 53 | /// 54 | public class UpdateToDoItemCommandValidator : AbstractValidator 55 | { 56 | private readonly IToDoItemRepository _repo; 57 | 58 | /// 59 | /// Validator ctor 60 | /// 61 | public UpdateToDoItemCommandValidator(IToDoItemRepository repo) 62 | { 63 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo)); 64 | 65 | RuleFor(x => x.Id) 66 | .NotEmpty(); 67 | 68 | RuleFor(x => x.Category) 69 | .NotEmpty(); 70 | 71 | RuleFor(x => x.Title) 72 | .NotEmpty(); 73 | } 74 | 75 | /// 76 | /// Check uniqueness 77 | /// 78 | /// 79 | /// 80 | /// 81 | /// 82 | public async Task HasUniqueName(UpdateCommand command, string title, CancellationToken cancellationToken) 83 | { 84 | ToDoItemSearchSpecification specification = new ToDoItemSearchSpecification(title, 85 | exactSearch: true); 86 | 87 | IEnumerable entities = await _repo.GetItemsAsync(specification); 88 | 89 | return entities == null || 90 | entities.Count() == 0 || 91 | // self 92 | entities.All(x => x.Id == command.Id); 93 | 94 | } 95 | } 96 | 97 | 98 | /// 99 | /// Handler 100 | /// 101 | public class CommandHandler : IRequestHandler 102 | { 103 | private readonly IToDoItemRepository _repo; 104 | private readonly IMapper _mapper; 105 | 106 | /// 107 | /// Ctor 108 | /// 109 | /// 110 | /// 111 | public CommandHandler(IToDoItemRepository repo, 112 | IMapper mapper) 113 | { 114 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo)); 115 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 116 | } 117 | 118 | /// 119 | /// Handle 120 | /// 121 | /// 122 | /// 123 | /// 124 | public async Task Handle(UpdateCommand command, CancellationToken cancellationToken) 125 | { 126 | CommandResponse response = new CommandResponse(); 127 | 128 | Core.Entities.ToDoItem entity = await _repo.GetItemAsync(command.Id); 129 | if (entity == null) 130 | { 131 | throw new EntityNotFoundException(nameof(ToDoItem), command.Id); 132 | } 133 | 134 | entity.Category = command.Category; 135 | entity.Title = command.Title; 136 | await _repo.UpdateItemAsync(command.Id, entity); 137 | 138 | return response; 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Models/Token/Authenticate.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Core.Exceptions; 3 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication; 4 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Services; 5 | using FluentValidation; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Http; 8 | using System; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Token 13 | { 14 | /// 15 | /// Authenticate 16 | /// 17 | public class Authenticate 18 | { 19 | /// 20 | /// command 21 | /// 22 | public class AuthenticateCommand : TokenRequest, IRequest 23 | { 24 | } 25 | 26 | /// 27 | /// Response 28 | /// 29 | public class CommandResponse 30 | { 31 | /// 32 | /// Resource 33 | /// 34 | public TokenResponse Resource { get; set; } 35 | } 36 | 37 | /// 38 | /// Register Validation 39 | /// 40 | public class AuthenticateCommandValidator : AbstractValidator 41 | { 42 | 43 | /// 44 | /// Validator ctor 45 | /// 46 | public AuthenticateCommandValidator() 47 | { 48 | RuleFor(x => x.Username) 49 | .Cascade(CascadeMode.Stop) 50 | .NotEmpty(); 51 | 52 | RuleFor(x => x.Password) 53 | .Cascade(CascadeMode.Stop) 54 | .NotEmpty(); 55 | } 56 | 57 | } 58 | 59 | /// 60 | /// Handler 61 | /// 62 | public class CommandHandler : IRequestHandler 63 | { 64 | private readonly ITokenService _tokenService; 65 | private readonly IMapper _mapper; 66 | private readonly HttpContext _httpContext; 67 | 68 | /// 69 | /// ctor 70 | /// 71 | /// 72 | /// 73 | /// 74 | public CommandHandler(ITokenService tokenService, 75 | IMapper mapper, 76 | IHttpContextAccessor httpContextAccessor) 77 | { 78 | this._tokenService = tokenService ?? throw new ArgumentNullException(nameof(tokenService)); 79 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 80 | this._httpContext = (httpContextAccessor != null) ? httpContextAccessor.HttpContext : throw new ArgumentNullException(nameof(httpContextAccessor)); 81 | 82 | } 83 | 84 | /// 85 | /// Handle 86 | /// 87 | /// 88 | /// 89 | /// 90 | public async Task Handle(AuthenticateCommand command, CancellationToken cancellationToken) 91 | { 92 | CommandResponse response = new CommandResponse(); 93 | 94 | string ipAddress = _httpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(); 95 | 96 | TokenResponse tokenResponse = await _tokenService.Authenticate(command, ipAddress); 97 | if (tokenResponse == null) 98 | { 99 | throw new InvalidCredentialsException(); 100 | } 101 | 102 | response.Resource = tokenResponse; 103 | return response; 104 | } 105 | } 106 | 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using Serilog; 5 | using System; 6 | using System.IO; 7 | 8 | namespace CleanArchitectureCosmosDB.WebAPI 9 | { 10 | /// 11 | /// Program 12 | /// 13 | public class Program 14 | { 15 | /// 16 | /// Configuration 17 | /// 18 | public static IConfiguration Configuration { get; private set; } 19 | 20 | /// 21 | /// Main entry point 22 | /// 23 | /// 24 | public static void Main(string[] args) 25 | { 26 | Configuration = new ConfigurationBuilder() 27 | .SetBasePath(Directory.GetCurrentDirectory()) 28 | .AddJsonFile("appsettings.json", false, true) 29 | .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true, 30 | true) 31 | .AddCommandLine(args) 32 | .AddEnvironmentVariables() 33 | .Build(); 34 | 35 | // configure serilog 36 | Log.Logger = new LoggerConfiguration() 37 | .ReadFrom.Configuration(Configuration) 38 | .Enrich.FromLogContext() 39 | .Enrich.WithMachineName() 40 | .CreateLogger(); 41 | 42 | 43 | try 44 | { 45 | Log.Information("Starting up..."); 46 | CreateHostBuilder(args).Build().Run(); 47 | Log.Information("Shutting down..."); 48 | } 49 | catch (Exception ex) 50 | { 51 | Log.Fatal(ex, "Host terminated unexpectedly"); 52 | } 53 | finally 54 | { 55 | Log.CloseAndFlush(); 56 | } 57 | } 58 | 59 | /// 60 | /// CreateHostBuilder 61 | /// 62 | /// 63 | /// 64 | public static IHostBuilder CreateHostBuilder(string[] args) => 65 | Host.CreateDefaultBuilder(args) 66 | .ConfigureWebHostDefaults(webBuilder => 67 | { 68 | webBuilder.UseStartup() 69 | .UseConfiguration(Configuration) 70 | .UseSerilog(); ; 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:52349", 7 | "sslPort": 44315 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "CleanArchitectureCosmosDB.WebAPI": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | }, 27 | "applicationUrl": "https://localhost:5001" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/README.md: -------------------------------------------------------------------------------- 1 | # API Project 2 | 3 | REST API project built with ASP.NET Core 3.1 with endpoints to work with the todo items. 4 | 5 | # Getting started 6 | 7 | **Prerequisites** 8 | * Azure Cosmos DB Emulator 9 | * appsettings.Development.json being updated with db connection strings 10 | 11 | # NOTES 12 | 1. Running the API project will ensure both Cosmos DB containers are created in Cosmos DB Emulator, and Identity database is created in the in-memory database or the SQL Server depending on the setup in DatabaseConfig.cs. 13 | 1. Running the API project will also seed application data and identity data. 14 | 15 | Run API 16 | 1. Run API using Visual Studio 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/Startup.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CleanArchitectureCosmosDB.Infrastructure.Extensions; 3 | using CleanArchitectureCosmosDB.Infrastructure.Identity; 4 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication; 5 | using CleanArchitectureCosmosDB.WebAPI.Config; 6 | using Microsoft.AspNet.OData.Extensions; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using System.Linq; 13 | using System.Reflection; 14 | 15 | namespace CleanArchitectureCosmosDB.WebAPI 16 | { 17 | /// 18 | /// Start up 19 | /// 20 | public class Startup 21 | { 22 | /// 23 | /// Configuration 24 | /// 25 | public IConfiguration Configuration { get; } 26 | 27 | /// 28 | /// ctor 29 | /// 30 | /// 31 | public Startup(IConfiguration configuration) 32 | { 33 | Configuration = configuration; 34 | } 35 | 36 | /// 37 | /// This method gets called by the runtime. Use this method to add services to the container. 38 | /// 39 | /// 40 | public void ConfigureServices(IServiceCollection services) 41 | { 42 | // Strongly-typed configurations using IOptions 43 | services.Configure(Configuration.GetSection("token")); 44 | services.Configure(Configuration.GetSection("TokenServiceProvider")); 45 | 46 | // Authentication and Authorization 47 | services.SetupAuthentication(Configuration); 48 | services.SetAuthorization(); 49 | 50 | // Cosmos DB for application data 51 | services.SetupCosmosDb(Configuration); 52 | // Identity DB for Identity data 53 | services.SetupIdentityDatabase(Configuration); 54 | 55 | // API controllers 56 | services.SetupControllers(); 57 | 58 | // HttpContext 59 | services.AddHttpContextAccessor(); 60 | 61 | // AutoMapper, this will scan and register everything that inherits AutoMapper.Profile 62 | services.AddAutoMapper(Assembly.GetExecutingAssembly()); 63 | 64 | // MediatR for Command/Query pattern and pipeline behaviours 65 | services.SetupMediatr(); 66 | 67 | // Caching 68 | services.SetupInMemoryCaching(); 69 | 70 | // NSwag Swagger 71 | services.SetupNSwag(); 72 | 73 | // OData 74 | services.SetupOData(); 75 | 76 | // Blob Storage 77 | // Since storage can be shared among projects, extension method is defined in Infrastructure project. 78 | services.SetupStorage(Configuration); 79 | 80 | } 81 | 82 | /// 83 | /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 84 | /// 85 | /// 86 | /// 87 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 88 | { 89 | if (env.IsDevelopment()) 90 | { 91 | app.UseDeveloperExceptionPage(); 92 | 93 | // ONLY automatically create development databases 94 | app.EnsureCosmosDbIsCreated(); 95 | app.SeedToDoContainerIfEmptyAsync().Wait(); 96 | // Optional: auto-create and seed Identity DB 97 | app.EnsureIdentityDbIsCreated(); 98 | app.SeedIdentityDataAsync().Wait(); 99 | } 100 | 101 | // NSwag Swagger 102 | app.UseOpenApi(); 103 | app.UseSwaggerUi3(); 104 | 105 | app.UseCors(options => options.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); 106 | app.UseHttpsRedirection(); 107 | 108 | app.UseRouting(); 109 | 110 | app.UseAuthorization(); 111 | 112 | app.UseEndpoints(endpointRouteBuilder => 113 | { 114 | endpointRouteBuilder.MapControllers(); 115 | 116 | // OData configuration 117 | endpointRouteBuilder.EnableDependencyInjection(); 118 | endpointRouteBuilder.Filter().Select().Count().OrderBy(); 119 | }); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "CleanArchitectureCosmosDB": { 4 | "EndpointUrl": "https://localhost:8081", 5 | // default primary key used by CosmosDB emulator 6 | "PrimaryKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", 7 | "DatabaseName": "CleanArchitectureCosmosDB", 8 | "Containers": [ 9 | { 10 | "Name": "Audit", 11 | "PartitionKey": "/EntityId" 12 | }, 13 | { 14 | "Name": "Todo", 15 | "PartitionKey": "/Category" 16 | } 17 | ] 18 | }, 19 | "CleanArchitectureIdentity": "Server=localhost\\SQLSERVER2016;Database=CleanArchitectureIdentity;Trusted_Connection=True;", 20 | "StorageConnectionString": "azure.blob://emu=true" 21 | }, 22 | /* Token Service Provider */ 23 | "TokenServiceProvider": { 24 | "Authority": "http://localhost:3000", 25 | "SetPasswordPath": "/verify", 26 | "ResetPasswordPath": "/resetPassword" 27 | }, 28 | /* For token issued by application*/ 29 | "token": { 30 | "secret": "rqkGzhVj8mne_GN3BREE!A4j7F69dR__tc!48EAG5ZTTQ&eN2m?LVD4g$-N!8xrH+m5!PPZPPE!WqpASHwmkA4Nt2q=&*?WZRzvGrgqkMp29zs7M8sm_V+VLvb7p+H8GSNr7?-_JywP$5cDm653!fH$CPvEzA64^L&AbqEExr7=zBchJLNESK&HeEjwTChT=qRcE$LtpS5%ec%s8qvY?8eEtH#$+xX-Z-Zcpq^n!3q5kZNQMD@5XGZ_7@e#Zy&pT", 31 | "issuer": "https://github.com/ShawnShiSS", 32 | "audience": "audience", 33 | "expiry": 120, 34 | "refreshExpiry": 10080 35 | }, 36 | "Serilog": { 37 | "Using": [ 38 | "Serilog.Sinks.Console", 39 | "Serilog.Sinks.File" 40 | ], 41 | "MinimumLevel": { 42 | "Default": "Information", 43 | "Override": { 44 | "Microsoft": "Error", 45 | "System": "Error" 46 | } 47 | }, 48 | "WriteTo": [ 49 | { 50 | "Name": "File", 51 | "Args": { 52 | "path": "C:\\Logs\\todo-api\\log-todo-api-.txt", 53 | "rollingInterval": "Day" 54 | } 55 | }, 56 | { 57 | "Name": "Console", 58 | "Args": { 59 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", 60 | "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {MachineName} ({ThreadId}) <{SourceContext}> {Message}{NewLine}{Exception}" 61 | } 62 | }, 63 | { 64 | "Name": "Seq", 65 | "Args": { 66 | "serverUrl": "http://localhost:5341" 67 | } 68 | } 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "CleanArchitectureCosmosDB": { 4 | "EndpointUrl": "https://localhost:8081", 5 | // default primary key used by CosmosDB emulator 6 | "PrimaryKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", 7 | "DatabaseName": "CleanArchitectureCosmosDB", 8 | "Containers": [ 9 | { 10 | "Name": "Audit", 11 | "PartitionKey": "/EntityId" 12 | }, 13 | { 14 | "Name": "Todo", 15 | "PartitionKey": "/Category" 16 | } 17 | ] 18 | }, 19 | "StorageConnectionString": "azure.blob://emu=true" 20 | }, 21 | "Serilog": { 22 | "Using": [ 23 | "Serilog.Sinks.Console", 24 | "Serilog.Sinks.File" 25 | ], 26 | "MinimumLevel": { 27 | "Default": "Information", 28 | "Override": { 29 | "Microsoft": "Error", 30 | "System": "Error" 31 | } 32 | }, 33 | "WriteTo": [ 34 | { 35 | "Name": "File", 36 | "Args": { 37 | "path": "D:\\home\\LogFiles\\http\\RawLogs\\log-todo-api-.txt", 38 | "rollingInterval": "Day" 39 | } 40 | }, 41 | { 42 | "Name": "Console", 43 | "Args": { 44 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", 45 | "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {MachineName} ({ThreadId}) <{SourceContext}> {Message}{NewLine}{Exception}" 46 | } 47 | } 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/CleanArchitectureCosmosDB.WebAPI/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /tests/CleanArchitectureCosmosDB.UnitTests/CleanArchitectureCosmosDB.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/CleanArchitectureCosmosDB.UnitTests/Core/Entities/ToDoItemTest.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitectureCosmosDB.Core.Entities; 2 | using Xunit; 3 | 4 | namespace CleanArchitectureCosmosDB.UnitTests.Core.Entities 5 | { 6 | public class ToDoItemTest 7 | { 8 | [Fact] 9 | public void SetIsCompletedToTrue() 10 | { 11 | ToDoItem item = new ToDoItem() 12 | { 13 | Category = "UnitTest", 14 | Title = "Mark me as completed", 15 | //IsCompleted = false // private property that can only be set by MarkComplete method 16 | }; 17 | 18 | item.MarkComplete(); 19 | 20 | Assert.True(item.IsCompleted); 21 | 22 | } 23 | } 24 | } 25 | --------------------------------------------------------------------------------