├── .deployment ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── APPLICATION_CONFIGURATION.md ├── AzureResources.DeploymentScript ├── CosmosDeployPS.ps1 └── README.md ├── CONTRIBUTING.md ├── LICENSE.md ├── MANUAL_DEPLOYMENT.md ├── README.md ├── SECRET_MANAGEMENT.md ├── TodoService.Api.UnitTests ├── Controllers │ └── TodoItemsControllersTests.cs └── TodoService.Api.UnitTests.csproj ├── TodoService.Api ├── Connected Services │ └── Application Insights │ │ └── ConnectedService.json ├── Controllers │ └── TodoItemsController.cs ├── Extensions │ ├── ApplicationBuilderAppInsightsLoggerExtensions.cs │ ├── ApplicationBuilderGlobalErrorHandlerExtensions.cs │ ├── HealthCheckBuilderCosmosDbExtensions.cs │ ├── ServiceCollectionCosmosDbExtensions.cs │ └── ServiceCollectionCustomSwaggerExtensions.cs ├── Middleware │ ├── ErrorDetails.cs │ └── GlobalErrorHandlerMiddleware.cs ├── Options │ ├── ConnectionStringOptions.cs │ ├── ConnectionStringsOptions.cs │ ├── CosmosDbOptions.cs │ └── KeyVaultOptions.cs ├── Program.cs ├── Startup.cs ├── TodoService.Api.csproj ├── appsettings.Development.json └── appsettings.json ├── TodoService.Core ├── Exceptions │ ├── EntityAlreadyExistsException.cs │ └── EntityNotFoundException.cs ├── Interfaces │ ├── IRepository.cs │ └── ITodoItemRepository.cs ├── Models │ ├── Entity.cs │ └── TodoItem.cs └── TodoService.Core.csproj ├── TodoService.Infrastructure.UnitTests ├── Data │ ├── CosmosDbClientFactoryTests.cs │ ├── CosmosDbClientTests.cs │ └── CosmosDbRepositoryTests.cs └── TodoService.Infrastructure.UnitTests.csproj ├── TodoService.Infrastructure ├── Data │ ├── CosmosDbClient.cs │ ├── CosmosDbClientFactory.cs │ ├── CosmosDbRepository.cs │ ├── ICosmosDbClient.cs │ ├── ICosmosDbClientFactory.cs │ ├── IDocumentCollectionContext.cs │ └── TodoItemRepository.cs └── TodoService.Infrastructure.csproj ├── TodoService.sln └── global.json /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | project = ToDo.Api/ToDo.Api.csproj 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = crlf 5 | indent_style = space 6 | indent_size = 4 7 | insert_final_newline = true 8 | max_line_length = 120 9 | 10 | [*.json] 11 | indent_size = 2 12 | 13 | [*.{cs,vb}] 14 | dotnet_style_qualification_for_field = false:error 15 | dotnet_style_qualification_for_property = false:error 16 | dotnet_style_qualification_for_method = false:error 17 | dotnet_style_qualification_for_event = false:error 18 | 19 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:error 20 | 21 | dotnet_style_coalesce_expression = true:error 22 | dotnet_style_null_propagation = true:error 23 | 24 | csharp_style_var_for_built_in_types = true:error 25 | csharp_style_var_when_type_is_apparent = true:error 26 | 27 | dotnet_naming_rule.private_members_with_underscore.symbols = private_fields 28 | dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore 29 | dotnet_naming_rule.private_members_with_underscore.severity = error 30 | 31 | dotnet_naming_symbols.private_fields.applicable_kinds = field 32 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 33 | 34 | dotnet_naming_style.prefix_underscore.capitalization = camel_case 35 | dotnet_naming_style.prefix_underscore.required_prefix = _ 36 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /APPLICATION_CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # Application Configuration Settings 2 | This file describes the application configuration that the TodoService.Api project uses. 3 | 4 | The complete logical settings document is listed below: 5 | 6 | ```JSON 7 | { 8 | "ApplicationInsights": { 9 | "InstrumentationKey": "" 10 | }, 11 | "ConnectionStrings": { 12 | "ConnectionMode": "", 13 | "Azure": { 14 | "AuthKey": "", 15 | "ServiceEndpoint": "" 16 | }, 17 | "Emulator": { 18 | "AuthKey": "", 19 | "ServiceEndpoint": "" 20 | } 21 | }, 22 | "CosmosDb": { 23 | "DatabaseName": "", 24 | "CollectionNames": [ 25 | {"Name" : "", 26 | "PartitionKey" : "" 27 | } 28 | ] 29 | }, 30 | "Logging": { 31 | "LogLevel": { 32 | "Default": "Warning" 33 | } 34 | }, 35 | "Secrets": { 36 | "Mode": "", 37 | "KeyVaultUri": "", 38 | "ClientId": "", 39 | "ClientSecret": "" 40 | } 41 | } 42 | ``` 43 | Even though the settings above define the runtime configuration objects of the application, they are not provided by the same file. 44 | 45 | It is not advisable to have settings that contain secrets saved in the appSettings.json file that is checked into source control. 46 | 47 | Some configuration settings are removed from the appSettings.json file and moved to a secret store. This is documented in [SECRET_MANAGEMENT.md](./SECRET_MANAGEMENT.md). 48 | 49 | At runtime, the configuration system will read several locations and create a memory representation of the document shown above. 50 | 51 | This could mean that the same setting is defined in several locations. If that is the case, the order the files are read and the settings overriden is: 52 | 53 | 1. appSettings.json 54 | 1. appSettings.Development.json 55 | 1. Secret Store 56 | 57 | In particular, the settings that should be moved from appSettings.json (or appSettings.Development.json) to the secret store are: 58 | 59 | * ApplicationInsights:InstrumentationKey 60 | * ConnectionStrings:Azure:AuthKey 61 | * ConnectionStrings:Emulator:AuthKey 62 | * Secrets:ClientId 63 | * Secrets:ClientSecret 64 | -------------------------------------------------------------------------------- /AzureResources.DeploymentScript/CosmosDeployPS.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT license. 3 | 4 | # The script will create Cosmos Account / Database and Collections If it does not exists 5 | # 6 | # The script will required the following Parameters 7 | # 1- ResourceGroup: where the CosmosDB be created 8 | # 2- Location: where the ResourceGroup and CosmosDB will be hosted 9 | # 3- Cosmos Account: CosmosDB account name 10 | # 4- Cosmos Database: ComosDB database name 11 | # 5- Cosmos Collections: CosmosDB collection names separated by comma delimiater i.e col1,col2,col3 12 | 13 | param( 14 | [Parameter(Mandatory=$True)] 15 | [string]$resourceGroup = $(throw "-resourceGroup is required."), 16 | [Parameter(Mandatory=$True)] 17 | [string]$location = $(throw "-location is required."), 18 | [Parameter(Mandatory=$True)] 19 | [string]$cosmosAccount = $(throw "-cosmosAccount is required."), 20 | [Parameter(Mandatory=$True)] 21 | [string]$cosmosDatabase = $(throw "-cosmosDatabase is required."), 22 | [Parameter(Mandatory=$True)] 23 | [string]$cosmosCollections = $(throw "-cosmosCollections is required."), 24 | [Parameter(Mandatory=$True)] 25 | [string]$cosmosCollectionsPK = $(throw "-cosmosCollectionsPK is required.") 26 | ) 27 | 28 | 29 | echo "Logging into Azure Account" 30 | #Connect-AzureRmAccount 31 | 32 | echo "Install CosmosDB Modules" 33 | Install-Module -Name CosmosDB 34 | 35 | echo "Check if resourceGroup $resourceGroup exists" 36 | Get-AzureRmResourceGroup -Name $resourceGroup -ErrorVariable notPresent -ErrorAction SilentlyContinue 37 | if ($notPresent) 38 | { 39 | echo "ResourceGroup $resourceGroup at location $location does not exists. Create New Resource Group" 40 | 41 | New-AzureRmResourceGroup -Name $resourceGroup -Location $location 42 | } 43 | else 44 | { 45 | echo "ResourceGroup $resourceGroup exists" 46 | } 47 | 48 | echo "Check if CosmosDb Account $cosmosAccount exists" 49 | Get-CosmosDbAccount -Name $cosmosAccount -ResourceGroup $resourceGroup -ErrorVariable notPresent -ErrorAction SilentlyContinue 50 | if ($notPresent) 51 | { 52 | echo "CosmosDBAccount $cosmosAccount does not. Create new account" 53 | 54 | New-CosmosDbAccount -Name $cosmosAccount -ResourceGroup $resourceGroup -Location $location 55 | } 56 | else 57 | { 58 | echo "CosmosDbAccount $cosmosAccount exists" 59 | } 60 | 61 | 62 | echo "Retrieve Cosmos primary key and create Cosmos context object" 63 | $cosmosDbContext = New-CosmosDbContext -Account $cosmosAccount -ResourceGroup $resourceGroup -MasterKeyType 'PrimaryMasterKey' 64 | 65 | 66 | 67 | echo "Check if CosmosDb Database $cosmosDatabase exists" 68 | Get-CosmosDbDatabase -Context $cosmosDbContext -Id $cosmosDatabase -ErrorVariable notPresent -ErrorAction SilentlyContinue 69 | if ($notPresent) 70 | { 71 | echo "Create new CosmosDb Database $cosmosDatabase" 72 | New-CosmosDbDatabase -Context $cosmosDbContext -Id $cosmosDatabase 73 | } 74 | else 75 | { 76 | echo "CosmosDbDatabase $cosmosDatabase exists" 77 | } 78 | 79 | 80 | echo "Retrieve Cosmos primary key and create Cosmos context object for database $cosmosDatabase" 81 | $cosmosDbContext = New-CosmosDbContext -Account $cosmosAccount -Database $cosmosDatabase -ResourceGroup $resourceGroup -MasterKeyType 'PrimaryMasterKey' 82 | 83 | echo "Covert Collections string into array" 84 | $collectionslist = $cosmosCollections.split(","); 85 | 86 | echo "Covert Collections string into array" 87 | $pklist = $cosmosCollectionsPK.split(","); 88 | 89 | $i=0; 90 | foreach($collection in $collectionslist){ 91 | 92 | echo "Check if CosmosDb Collection $collection exists" 93 | Get-CosmosDbCollection -Context $cosmosDbContext -Id $collection -ErrorVariable notPresent -ErrorAction SilentlyContinue 94 | if ($notPresent) 95 | { 96 | echo "Create new CosmosDb collection $collection" 97 | New-CosmosDbCollection -Context $cosmosDbContext -Id $collection -PartitionKey $pklist[$i] 98 | } 99 | else 100 | { 101 | echo "CosmosDb collection $collection exists" 102 | } 103 | $i++; 104 | 105 | } 106 | -------------------------------------------------------------------------------- /AzureResources.DeploymentScript/README.md: -------------------------------------------------------------------------------- 1 | # Auto Create CosmosDB Collection(s) Script 2 | 3 | CosmosDB Collections script is used to check if the collections already exist otherwise the script will create it. 4 | The following resources will be created if it does not exists 5 | 6 | - Resource Group 7 | - Cosmos Account 8 | - Cosmos Database 9 | - Cosmos Collections 10 | 11 | 12 | ## Required Paramaters 13 | The following parameters are mandatory parameters: 14 | 15 | In order to run the script successfully the following are condition should be met 16 | 17 | - **resourceGroup**: where the CosmosDB be created 18 | - **location**: where the ResourceGroup and CosmosDB will be hoste 19 | - **cosmosAccount**: CosmosDB account name 20 | - **cosmosDatabase**: ComosDB database name 21 | - **cosmosCollections**: CosmosDB collection names separated by comma delimiater i.e col1,col2,col3 22 | - **cosmosCollectionsPartitionKeys**: CosmosDB collection PK names separated by comma delimiater i.e PK1,PK2,PK3 23 | 24 | ## Running the script 25 | 26 | The script will be executed in the following format 27 | 28 | 29 | 30 | ./CosmosDeployPS.ps1 -resourceGroup "" -location "" -cosmosAccount "" -cosmosDatabase "" -cosmosCollections "" - cosmosCollectionsPartitionKeys "" 31 | 32 | 33 | In a successful case, we will get the following result 34 | 35 | 36 | Scipt is completed successfully. 37 | 38 | 39 | and by reading $lastexistcode will give 0 40 | 41 | echo $lastexitcode 42 | 0 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 6 | 7 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 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 -------------------------------------------------------------------------------- /MANUAL_DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | ### Deploying to Azure Web App from Local Git Repository 2 | 3 | These instructions assume that you have a Web App instance on Azure. If you do not have a Web App, please follow the [Quick Start](https://docs.microsoft.com/en-us/azure/app-service/) for your desired language and select Local Git as the Deployment Option. 4 | 5 | Perform the following steps to deploy your app to Azure: 6 | 7 | 1. Set the App for Local Deployment. You can check if it is already set in the Portal by browsing to `[Your Web App] > Properties > Git URL`. If the Git URL is present then your Web App is set up for local deployment. If the URL is not there, go to `[Your Web App] > Deployment Options` and select Disconnect to remove the current deployment. Then click on Setup and choose Local Git as your source. When you return to Properties the Git URL should be present. 8 | 9 | 1. Set the Publishing Profile. If you do not know the deployment username and password for the Web App, they can be set in the portal at `[Your Web App] > Deployment Credentials`. The username will automatically be reflected in the Git URL from Step 1. 10 | 11 | 1. Deploy your local source code to your Azure Web App. First, set your Web App as a remote git repository with the URL generated in Step 1. The following commands must be run in the root of the local copy of this repository. 12 | 13 | ```bash 14 | git remote add azurepub https://@.scm.azurewebsites.net/.git 15 | ``` 16 | 17 | Then push your current branch to the master branch of the remote azure repository. Enter your deployment credential password from Step 2 if prompted. This step will take a few minutes to complete and should show the output of the server performing the build. 18 | 19 | ```bash 20 | git push azurepub :master 21 | ``` 22 | 23 | 1. Browse to the Swagger UI page for your new Web App to test the functionality. 24 | 25 | ``` 26 | http://.azurewebsites.net/swagger 27 | ``` 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - csharp 5 | - powershell 6 | products: 7 | - azure 8 | description: "This project implements a to-do service using ASP.NET Core. It demonstrates how to use the repository pattern with Azure Cosmos DB SQL API." 9 | urlFragment: repository-pattern-with-azure-cosmos-db-sql-api 10 | --- 11 | 12 | # Repository Pattern with Azure Cosmos DB SQL API 13 | 14 | This project implements a sample to-do service using ASP.NET core. It demonstrates how to use the repository pattern with Azure Cosmos DB SQL API. It also demonstrates other best practices for Azure hosted ASP.NET core web applications, such as logging and telemetry with Application Insights and key/secret management via Azure Key Vault. 15 | 16 | ### Repository Pattern 17 | This repository is based on the [repository design pattern](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/repository-pattern?view=aspnetcore-2.1) which isolates data access behind interface abstractions. Connecting to the database and manipulating data storage objects is performed through methods provided by the interface's implementation. Consequently, there is no need to call code to deal with database concerns, such as connections, commands, and readers. 18 | 19 | Azure Cosmos DB utilizes [partition keys](https://docs.microsoft.com/en-us/azure/cosmos-db/partition-data) to enable quick look ups in unlimited scaled databases. This repository implements the repository design pattern to support partition keys for Cosmos DB. For details on the execution, please see the code within `./TodoService.Infrastructure/Data`. 20 | 21 | ### Benefits of the Partitioned Repository Pattern Implementation 22 | 23 | * Easy reuse of the database access code, because the database communications is centralized in a single place. 24 | * The business domain can be unit tested independent off the database layer. 25 | * Integrated partition key support for large scale Cosmos DB projects. 26 | 27 | ## Getting Started 28 | 29 | ### Deploy Azure Resources Using a PowerShell Script 30 | A script for deploying the necessary Cosmos DB resources is located in the **AzureResources.DeploymentScript** folder. The script will check the existence of a resource group, a Cosmos DB account, a Cosmos DB database, and one or more Cosmos DB collections with partition keys. If the defined resources do not exist they will be created by the script. 31 | 32 | The following Parameters are inputs to the script: 33 | 34 | - **resourceGroup**: where the Cosmos DB instance be created 35 | - **location**: where the ResourceGroup and Cosmos DB will be hosted 36 | - **cosmosAccount**: Cosmos DB account name 37 | - **cosmosDatabase**: Cosmos DB database name 38 | - **cosmosCollections**: Cosmos DB collection names separated by comma delimiter i.e col1,col2,col3 39 | - **cosmosCollectionsPartitionKeys**: Cosmos DB collection partition key names separated by comma delimiter i.e PK1,PK2,PK3 40 | 41 | ### Prerequisites 42 | - [ASP.NET Core SDK v2.1.300](https://www.microsoft.com/net/download/thank-you/dotnet-sdk-2.1.300-windows-x64-installer) 43 | - [ASP.NET Core Runtime 2.1.0](https://www.microsoft.com/net/download/thank-you/dotnet-runtime-2.1.0-windows-hosting-bundle-installer) 44 | - [Visual Studio 2017 15.7 or newer](https://docs.microsoft.com/en-us/visualstudio/install/update-visual-studio) 45 | 46 | ### Build and Test 47 | The configuration for this solution depends on secrets, either stored locally or in Azure Key Vault. This is to prevent sensitive information from being stored in a public manner. Please see [SECRET_MANAGEMENT.md](./SECRET_MANAGEMENT.md) for more information on setting up the secret management system. 48 | 49 | ### Manually Deploying the Solution 50 | The solution can be run locally for development purposes. If you are looking to deploy the solution to an Azure Web App, follow the steps in [MANUAL_DEPLOYMENT.md](./MANUAL_DEPLOYMENT.md) 51 | 52 | 53 | ## Resources 54 | 55 | * Repository Design Pattern: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/repository-pattern?view=aspnetcore-2.1 56 | * Azure Free Account : https://azure.microsoft.com/en-us/free/ 57 | * Azure App Service : https://azure.microsoft.com/en-us/services/app-service/ 58 | * Azure Cosmos DB : https://azure.microsoft.com/en-us/services/cosmos-db/ 59 | * Swagger : https://swagger.io/ 60 | -------------------------------------------------------------------------------- /SECRET_MANAGEMENT.md: -------------------------------------------------------------------------------- 1 | # Secrets and the Configuration System 2 | 3 | This project takes advantage of the platform's configuration system. This allows us to store secrets transparently in a specialized secret store running locally and in Azure Key Vault. 4 | 5 | The appSettings.json file, as well as the environment dependent files (e.g. appSettings.Development.json), may still be used for configuration settings that are not deemed "sensitive information". 6 | 7 | Settings like connection strings, passwords, and other sensitive information, should be configured via a secret store. 8 | 9 | During development, you can choose to use secrets from a local secret store or from Key Vault. Once the application is running in a Web App, the only option is to use secrets from Key Vault, using the Web App's Managed Service Identity (MSI). This method provides the safest configuration since secrets are stored in Key Vault, and (after appropriate configuration of the Key Vault), the MSI does not require a set of credentials to access Key Vault. 10 | 11 | If you wish to test the application locally using Key Vault, you have to provide Key Vault client credentials. This is because MSI only runs in the context of Azure and not within the development machine. For this scenario, you can provide Key Vault client credentials via the local secret store. The benefit of using this approach is that the local secret store does not store this set of credentials in the source code, but rather in a path in the computer under the logged-in user's profile. 12 | 13 | To setup the configuration you need to: 14 | 15 | 1. Configure the local Secret Store 16 | 2. Add secrets to the Secret Store 17 | 3. Add secrets to the Key Vault 18 | 4. Adjust the appSettings.json file to establish the secret storage mode. 19 | 20 | ## 1. Configure the local secret store 21 | The local secret store uses a different format than a appSettings.json configuration file. 22 | Secrets are stored in a file called `secrets.json`, which must be created under the user's profile at the following locations: 23 | 24 | * Windows: `%APPDATA%\Microsoft\UserSecrets\\secrets.json` 25 | * macOS: `~/.microsoft/usersecrets//secrets.json` 26 | * Linux: `~/.microsoft/usersecrets//secrets.json` 27 | 28 | Note that the GUID is project specific and defined in the .csproj file as UserSecretsId. Please see [Safe storage of app secrets in development in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-2.1&tabs=macos) for more information on setting up a local secret store. 29 | 30 | ## 2. Add secrets to the secret store 31 | 32 | The secrets.json file uses a flat schema to store information. To simulate a hierarchy, colons `:` must be used. 33 | 34 | For instance, if `appSettings.json` looks like this: 35 | ```json 36 | { 37 | "Application": { 38 | "DefaultLanguage" : "English" 39 | }, 40 | "ConnectionStrings": { 41 | "CosmosDbEndpoint": "https://acosmosname.documents.azure.com:443/", 42 | "CosmosDbName": "todo" 43 | }, 44 | "Logging": { 45 | "LogLevel": { 46 | "Default": "Warning" 47 | } 48 | }, 49 | ... 50 | } 51 | ``` 52 | 53 | and we want to store a setting called AdminPassword for the Application section, then `secrets.json` should be: 54 | ```json 55 | { 56 | "Application:AdminPassword" : "passwordHere" 57 | } 58 | ``` 59 | 60 | ## 3. Add Secrets to Key Vault 61 | 62 | The same secrets that appear in `secrets.json` should exist in Key Vault. The only exceptions are ClientId and ClientSecret, which are used to connect to Key Vault, so there is little point in storing them in Key Vault itself. 63 | 64 | Secrets in Key Vault are not stored in hierarchies, so their names must be flattened like `secrets.json` . The only difference is that colons are not allowed in Key Vault secrets because the secret name is part of its URI. 65 | 66 | To work around this problem, replace colons with double dashes `--`. For example, the Admin Password shown above would be stored in Key Vault as: `Application--AdminPassword` 67 | 68 | ## 4. Adjust the appSettings.json file to establish the secret storage mode 69 | A setting in `appSettings.json` is used to tell the application where to find secrets. The following snippet shows the setting that controls the secret storage approach: 70 | ```JSON 71 | { 72 | ... 73 | "Secrets": { 74 | "Mode": "UseMsi", 75 | "KeyVaultUri": "https://.vault.azure.net/" 76 | } 77 | ... 78 | } 79 | ``` 80 | 81 | The Mode configuration entry can have one of three values: 82 | 83 | * UseLocalSecretStore 84 | * UseClientSecret 85 | * UseMsi 86 | 87 | To use Key Vault when developing locally, you need to set Key Vault connection credentials. Set the Mode to `UseClientSecret` in `appSettings.json`, and add the following entries in `secrets.json`: 88 | ```JSON 89 | { 90 | ... 91 | "Secrets:ClientId": "[AppId]", 92 | "Secrets:ClientSecret": "[Secret]" 93 | ... 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /TodoService.Api.UnitTests/Controllers/TodoItemsControllersTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Moq; 8 | using TodoService.Api.Controllers; 9 | using TodoService.Core.Exceptions; 10 | using TodoService.Core.Interfaces; 11 | using TodoService.Core.Models; 12 | using Xunit; 13 | 14 | namespace TodoService.Api.UnitTests.Controllers 15 | { 16 | public class TodoItemsControllersTests 17 | { 18 | private readonly Mock _mockRepository; 19 | private readonly TodoItemsController _controller; 20 | private readonly string _toDoId; 21 | private TodoItem _savedTodoItem; 22 | 23 | public TodoItemsControllersTests() 24 | { 25 | _toDoId = Guid.NewGuid().ToString(); 26 | _savedTodoItem = null; 27 | _mockRepository = new Mock(); 28 | _mockRepository.Setup(repo => repo.UpdateAsync(It.IsAny())) 29 | .Returns(Task.CompletedTask) 30 | .Callback(x => _savedTodoItem = x); 31 | _controller = new TodoItemsController(_mockRepository.Object); 32 | } 33 | 34 | [Fact] 35 | public async Task CreateToDo_WhenValid_ReturnsGuid() 36 | { 37 | // Arrange 38 | var newToDo = new TodoItem {Id = _toDoId, Name = "fake item Id"}; 39 | 40 | _mockRepository.Setup(repo => repo.AddAsync(It.IsAny())) 41 | .ReturnsAsync(newToDo); 42 | 43 | // Act 44 | var result = await _controller.CreateItem(newToDo); 45 | 46 | // Assert 47 | var createdAtActionResult = Assert.IsType(result); 48 | Assert.IsType(createdAtActionResult.Value); 49 | } 50 | 51 | [Fact] 52 | public async Task CreateToDo_WhenValid_AddsCorrectToDo() 53 | { 54 | // Arrange 55 | var newToDo = new TodoItem {Id = _toDoId, Name = "fake item Id"}; 56 | 57 | _mockRepository.Setup(repo => repo.AddAsync(It.IsAny())) 58 | .ReturnsAsync((TodoItem x) => { return x; }) 59 | .Callback(x => _savedTodoItem = x); 60 | 61 | // Act 62 | var result = await _controller.CreateItem(newToDo); 63 | 64 | // Assert 65 | Assert.Equal(newToDo.Id, _savedTodoItem.Id); 66 | Assert.Equal(newToDo.Name, _savedTodoItem.Name); 67 | } 68 | 69 | [Fact] 70 | public async Task CreateToDo_WhenUserIdIsInvalid_ReturnsBadRequest() 71 | { 72 | // Arrange 73 | _controller.ModelState.AddModelError("error", "Missing Id"); 74 | var newToDoDto = new TodoItem {Id = "", Name = "fake item Id"}; 75 | 76 | // Act 77 | var result = await _controller.CreateItem(newToDoDto); 78 | 79 | // Assert 80 | var badRequestResult = Assert.IsType(result); 81 | } 82 | 83 | [Fact] 84 | public async Task GetToDo_WithNonExistingToDoId_ShouldReturnNotFound() 85 | { 86 | // Arrange 87 | _mockRepository.Setup(repo => repo.GetByIdAsync(_toDoId)) 88 | .ThrowsAsync(new EntityNotFoundException()); 89 | 90 | // Act 91 | var result = await _controller.GetItem(_toDoId); 92 | 93 | // Assert 94 | var notFoundObjectResult = Assert.IsType(result); 95 | Assert.Equal(_toDoId, notFoundObjectResult.Value); 96 | } 97 | 98 | [Fact] 99 | public async Task GetToDo_WithValidId_ReturnsOk() 100 | { 101 | // Arrange 102 | var toDo = new TodoItem {Id = _toDoId}; 103 | _mockRepository.Setup(repo => repo.GetByIdAsync(_toDoId)) 104 | .Returns(Task.FromResult(toDo)); 105 | 106 | // Act 107 | var result = await _controller.GetItem(_toDoId); 108 | 109 | // Assert 110 | var okResult = Assert.IsType(result); 111 | Assert.Equal(okResult.Value, toDo); 112 | _mockRepository.Verify(repo => repo.GetByIdAsync(_toDoId), Times.Once); 113 | } 114 | 115 | [Fact] 116 | public async Task UpdateToDo_WhenReplacingName_ReturnOK() 117 | { 118 | // Arrange 119 | var toDo = new TodoItem 120 | { 121 | Id = _toDoId, 122 | Name = "Original Name" 123 | }; 124 | 125 | var updatedToDo = new TodoItem 126 | { 127 | Id = _toDoId, 128 | Name = "New Name" 129 | }; 130 | 131 | 132 | _mockRepository.Setup(repo => repo.UpdateAsync(It.IsAny())) 133 | .Returns(Task.FromResult(updatedToDo)); 134 | 135 | 136 | // Act 137 | var result = await _controller.UpdateItem(toDo.Id, updatedToDo); 138 | var okResult = Assert.IsType(result); 139 | // Assert 140 | Assert.Equal(200,okResult.StatusCode); 141 | } 142 | 143 | [Fact] 144 | public async Task UpdateToDo_WhenToDoIdDoesNotMatch_ReturnsBadRequest() 145 | { 146 | // Arrange 147 | _mockRepository.Setup(repo => repo.UpdateAsync(It.IsAny())) 148 | .ThrowsAsync(new EntityNotFoundException()); 149 | 150 | // Act 151 | var result = await _controller.UpdateItem(_toDoId, new TodoItem()); 152 | 153 | // Assert 154 | var badRequestObjectResult = Assert.IsType(result); 155 | Assert.Null(badRequestObjectResult.Value); 156 | } 157 | 158 | [Fact] 159 | public async Task UpdateToDo_WhenToDoIdNotPresent_ReturnsBadRequest() 160 | { 161 | // Arrange 162 | _mockRepository.Setup(repo => repo.UpdateAsync(It.IsAny())) 163 | .ThrowsAsync(new EntityNotFoundException()); 164 | 165 | // Act 166 | var result = await _controller.UpdateItem(null, new TodoItem()); 167 | 168 | // Assert 169 | var notFoundObjectResult = Assert.IsType(result); 170 | Assert.Null(notFoundObjectResult.Value); 171 | } 172 | 173 | 174 | 175 | 176 | [Fact] 177 | public async Task RemoveItem_WithItemId_RemovesItem() 178 | { 179 | // Arrange 180 | 181 | var existingToDo = new TodoItem {Id = _toDoId}; 182 | 183 | _mockRepository.Setup(repo => repo.GetByIdAsync(It.IsAny())) 184 | .Returns(Task.FromResult(existingToDo)); 185 | 186 | _mockRepository.Setup(repo => repo.DeleteAsync(It.IsAny())) 187 | .Returns(Task.FromResult(new TodoItem())); 188 | 189 | // Act 190 | var result = await _controller.RemoveItem(existingToDo.Id); 191 | 192 | // Assert 193 | Assert.IsType(result); 194 | } 195 | 196 | 197 | [Fact] 198 | public async Task RemoveItem_WithWrongItemId_ReturnsNotFound() 199 | { 200 | // Arrange 201 | var nonExistingToDoId = Guid.NewGuid().ToString(); 202 | 203 | _mockRepository.Setup(repo => repo.GetByIdAsync(nonExistingToDoId)) 204 | .Returns(Task.FromResult(_savedTodoItem)); 205 | 206 | 207 | // Act 208 | var result = await _controller.RemoveItem(nonExistingToDoId); 209 | 210 | // Assert 211 | var notFoundObjectResult = Assert.IsType(result); 212 | Assert.Equal(nonExistingToDoId, notFoundObjectResult.Value); 213 | } 214 | 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /TodoService.Api.UnitTests/TodoService.Api.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /TodoService.Api/Connected Services/Application Insights/ConnectedService.json: -------------------------------------------------------------------------------- 1 | { 2 | "ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider", 3 | "Version": "8.12.10405.1", 4 | "GettingStartedDocument": { 5 | "Uri": "https://go.microsoft.com/fwlink/?LinkID=798432" 6 | } 7 | } -------------------------------------------------------------------------------- /TodoService.Api/Controllers/TodoItemsController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System; 3 | 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using TodoService.Core.Exceptions; 8 | using TodoService.Core.Interfaces; 9 | using TodoService.Core.Models; 10 | 11 | namespace TodoService.Api.Controllers 12 | { 13 | [ApiVersion("1")] 14 | [Route("api/[controller]")] 15 | [ApiController] 16 | public class TodoItemsController : ControllerBase 17 | { 18 | private readonly ITodoItemRepository _repo; 19 | 20 | public TodoItemsController(ITodoItemRepository repo) 21 | { 22 | _repo = repo; 23 | } 24 | 25 | /// 26 | /// Creating a new TodoItem Item 27 | /// 28 | /// JSON New TodoItem document 29 | /// Returns the new TodoItem Id 30 | /// Returns 201 Created success 31 | /// Returns 400 Bad Request error 32 | /// Returns 500 Internal Server Error 33 | [ProducesResponseType(StatusCodes.Status201Created, Type = typeof(string))] 34 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 35 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 36 | [HttpPost] 37 | public async Task CreateItem([FromBody] TodoItem newTodoItem) 38 | { 39 | if (!ModelState.IsValid) 40 | { 41 | return BadRequest(); 42 | } 43 | var toDo = await _repo.AddAsync(newTodoItem); 44 | 45 | return Ok(toDo); 46 | } 47 | 48 | /// 49 | /// Retrieving a TodoItem using its TodoItem Id 50 | /// 51 | /// 52 | /// Retrieves a TodoItem using its TodoItem Id 53 | /// 54 | /// The Id of the TodoItem item to be retrieved 55 | /// Returns the full TodoItem document 56 | /// Returns 200 OK success 57 | /// Returns 404 Not Found error 58 | /// Returns 500 Internal Server Error 59 | [HttpGet("{todoId}")] 60 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TodoItem))] 61 | [ProducesResponseType(StatusCodes.Status404NotFound)] 62 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 63 | public async Task GetItem(string toDoId) 64 | { 65 | try 66 | { 67 | var toDo = await _repo.GetByIdAsync(toDoId); 68 | return Ok(toDo); 69 | } 70 | catch (EntityNotFoundException) 71 | { 72 | return NotFound(toDoId); 73 | } 74 | } 75 | 76 | 77 | /// 78 | /// Updating TodoItem item 79 | /// 80 | /// Updates an existing item 81 | /// Id of an existing TodoItem that needs to be updated 82 | /// JSON TodoItem document to be updated in an existing TodoItem 83 | /// Returns 200 OK success 84 | /// Returns 400 Bad Request error 85 | /// Returns 404 Not Found error 86 | /// Returns 500 Internal Server Error 87 | [ProducesResponseType(StatusCodes.Status200OK)] 88 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 89 | [ProducesResponseType(StatusCodes.Status404NotFound)] 90 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 91 | [HttpPut("{todoId}")] 92 | public async Task UpdateItem(string toDoId, [FromBody]TodoItem updatedItem) 93 | { 94 | if (updatedItem.Id != toDoId) 95 | { 96 | return BadRequest(updatedItem.Id); 97 | } 98 | 99 | try 100 | { 101 | if (toDoId == null) 102 | { 103 | return NotFound(toDoId); 104 | } 105 | 106 | await _repo.UpdateAsync(updatedItem); 107 | return Ok(); 108 | } 109 | catch (EntityNotFoundException) 110 | { 111 | return NotFound(toDoId); 112 | } 113 | } 114 | 115 | /// 116 | /// Deleting an existing TodoItem 117 | /// 118 | /// Deletes an existing TodoItem item list 119 | /// Id of an existing TodoItem that needs to be deleting 120 | /// Returns 204 No Content success 121 | /// Returns 404 Not Found error 122 | /// Returns 500 Internal Server Error 123 | [ProducesResponseType(StatusCodes.Status204NoContent)] 124 | [ProducesResponseType(StatusCodes.Status404NotFound)] 125 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 126 | [HttpDelete("{toDoId}")] 127 | public async Task RemoveItem(string toDoId) 128 | { 129 | try 130 | { 131 | var toDoToUpdate = await _repo.GetByIdAsync(toDoId); 132 | 133 | if (toDoToUpdate == null) 134 | { 135 | return NotFound(toDoId); 136 | } 137 | 138 | 139 | await _repo.DeleteAsync(toDoToUpdate); 140 | 141 | return NoContent(); 142 | } 143 | catch (EntityNotFoundException) 144 | { 145 | return NotFound(toDoId); 146 | } 147 | } 148 | 149 | 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /TodoService.Api/Extensions/ApplicationBuilderAppInsightsLoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace TodoService.Api.Extensions 8 | { 9 | public static class ApplicationBuilderAppInsightsLoggerExtensions 10 | { 11 | public static IApplicationBuilder UseAppInsightsLogger(this IApplicationBuilder builder, 12 | ILoggerFactory loggerFactory) 13 | { 14 | loggerFactory.AddApplicationInsights(builder.ApplicationServices, LogLevel.Warning); 15 | return builder; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TodoService.Api/Extensions/ApplicationBuilderGlobalErrorHandlerExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using Microsoft.AspNetCore.Builder; 5 | using TodoService.Api.Middleware; 6 | 7 | namespace TodoService.Api.Extensions 8 | { 9 | public static class ApplicationBuilderGlobalErrorHandlerExtensions 10 | { 11 | public static IApplicationBuilder UseGlobalErrorHandler(this IApplicationBuilder builder) 12 | { 13 | return builder.UseMiddleware(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TodoService.Api/Extensions/HealthCheckBuilderCosmosDbExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using Microsoft.Azure.Documents.Client; 6 | using Microsoft.Extensions.HealthChecks; 7 | 8 | namespace TodoService.Api.Extensions 9 | { 10 | // For more information, visit: 11 | // - https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/monitor-app-health 12 | // - https://github.com/dotnet-architecture/HealthChecks 13 | public static class HealthCheckBuilderCosmosDbExtensions 14 | { 15 | public static HealthCheckBuilder AddCosmosDbCheck(this HealthCheckBuilder builder, Uri serviceEndpoint, 16 | string authKey) 17 | { 18 | return AddCosmosDbCheck(builder, serviceEndpoint, authKey, builder.DefaultCacheDuration); 19 | } 20 | 21 | public static HealthCheckBuilder AddCosmosDbCheck(this HealthCheckBuilder builder, Uri serviceEndpoint, 22 | string authKey, TimeSpan cacheDuration) 23 | { 24 | var checkName = $"CosmosDbCheck({serviceEndpoint})"; 25 | 26 | builder.AddCheck(checkName, async () => 27 | { 28 | try 29 | { 30 | using (var documentClient = new DocumentClient(serviceEndpoint, authKey)) 31 | { 32 | await documentClient.OpenAsync(); 33 | return HealthCheckResult.Healthy($"{checkName}: Healthy"); 34 | } 35 | } 36 | catch (Exception ex) 37 | { 38 | // Failed to connect to CosmosDB. 39 | return HealthCheckResult.Unhealthy($"{checkName}: Exception during check: ${ex.Message}"); 40 | } 41 | }, cacheDuration); 42 | 43 | return builder; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TodoService.Api/Extensions/ServiceCollectionCosmosDbExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Microsoft.Azure.Documents.Client; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Serialization; 10 | using TodoService.Infrastructure.Data; 11 | 12 | namespace TodoService.Api.Extensions 13 | { 14 | public static class ServiceCollectionCosmosDbExtensions 15 | { 16 | public static IServiceCollection AddCosmosDb(this IServiceCollection services, Uri serviceEndpoint, 17 | string authKey, string databaseName, List collectionNames) 18 | { 19 | var documentClient = new DocumentClient(serviceEndpoint, authKey, new JsonSerializerSettings 20 | { 21 | NullValueHandling = NullValueHandling.Ignore, 22 | DefaultValueHandling = DefaultValueHandling.Ignore, 23 | ContractResolver = new CamelCasePropertyNamesContractResolver() 24 | }); 25 | documentClient.OpenAsync().Wait(); 26 | 27 | var cosmosDbClientFactory = new CosmosDbClientFactory(databaseName, collectionNames, documentClient); 28 | cosmosDbClientFactory.EnsureDbSetupAsync().Wait(); 29 | 30 | services.AddSingleton(cosmosDbClientFactory); 31 | 32 | return services; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /TodoService.Api/Extensions/ServiceCollectionCustomSwaggerExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System.IO; 5 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Swashbuckle.AspNetCore.Swagger; 8 | 9 | namespace TodoService.Api.Extensions 10 | { 11 | public static class ServiceCollectionCustomSwaggerExtensions 12 | { 13 | public static IServiceCollection AddCustomSwagger(this IServiceCollection services) 14 | { 15 | services.AddSwaggerGen(options => 16 | { 17 | var provider = services.BuildServiceProvider().GetRequiredService(); 18 | 19 | foreach (var description in provider.ApiVersionDescriptions) 20 | { 21 | options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); 22 | } 23 | 24 | options.IncludeXmlComments(Path.Combine(System.AppContext.BaseDirectory, "TodoService.Api.xml")); 25 | options.IncludeXmlComments(Path.Combine(System.AppContext.BaseDirectory, "TodoService.Core.xml")); 26 | }); 27 | 28 | return services; 29 | } 30 | 31 | private static Info CreateInfoForApiVersion(ApiVersionDescription description) 32 | { 33 | var info = new Info 34 | { 35 | Title = $"To-do REST API {description.ApiVersion}", 36 | Version = description.ApiVersion.ToString(), 37 | Description = "To-do example for partitioned repository ASP.NET Core Web API with Azure CosmosDB Backend" 38 | }; 39 | 40 | if (description.IsDeprecated) 41 | { 42 | info.Description += " This API version has been deprecated."; 43 | } 44 | 45 | return info; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /TodoService.Api/Middleware/ErrorDetails.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using Newtonsoft.Json; 5 | 6 | namespace TodoService.Api.Middleware 7 | { 8 | public class ErrorDetails 9 | { 10 | public int StatusCode { get; set; } 11 | public string Message { get; set; } 12 | public string ErrorType { get; set; } 13 | 14 | public override string ToString() 15 | { 16 | return JsonConvert.SerializeObject(this); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TodoService.Api/Middleware/GlobalErrorHandlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace TodoService.Api.Middleware 11 | { 12 | public class GlobalErrorHandlerMiddleware 13 | { 14 | private readonly RequestDelegate _next; 15 | private readonly ILogger _logger; 16 | 17 | public GlobalErrorHandlerMiddleware(RequestDelegate next, ILogger logger) 18 | { 19 | _next = next; 20 | _logger = logger; 21 | } 22 | 23 | public async Task InvokeAsync(HttpContext context) 24 | { 25 | try 26 | { 27 | await _next(context); 28 | } 29 | catch (Exception ex) 30 | { 31 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 32 | context.Response.ContentType = "application/json"; 33 | 34 | await context.Response.WriteAsync(new ErrorDetails() 35 | { 36 | StatusCode = context.Response.StatusCode, 37 | Message = $"Message: {ex.Message}", 38 | ErrorType = ex.GetType().ToString() 39 | }.ToString()); 40 | 41 | _logger.LogError(ex, "Unhandled error caught by Global Error Handler"); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /TodoService.Api/Options/ConnectionStringOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | 6 | namespace TodoService.Api.Options 7 | { 8 | public class ConnectionStringOptions 9 | { 10 | public Uri ServiceEndpoint { get; set; } 11 | public string AuthKey { get; set; } 12 | 13 | public void Deconstruct(out Uri serviceEndpoint, out string authKey) 14 | { 15 | serviceEndpoint = ServiceEndpoint; 16 | authKey = AuthKey; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TodoService.Api/Options/ConnectionStringsOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | namespace TodoService.Api.Options 5 | { 6 | public enum ConnectionStringMode 7 | { 8 | Azure, 9 | Emulator 10 | } 11 | 12 | public class ConnectionStringsOptions 13 | { 14 | public ConnectionStringMode Mode { get; set; } 15 | public ConnectionStringOptions Azure { get; set; } 16 | public ConnectionStringOptions Emulator { get; set; } 17 | 18 | public ConnectionStringOptions ActiveConnectionStringOptions => 19 | Mode == ConnectionStringMode.Azure ? Azure : Emulator; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TodoService.Api/Options/CosmosDbOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace TodoService.Api.Options 7 | { 8 | public class CosmosDbOptions 9 | { 10 | public string DatabaseName { get; set; } 11 | public List CollectionNames { get; set; } 12 | 13 | public void Deconstruct(out string databaseName, out List collectionNames) 14 | { 15 | databaseName = DatabaseName; 16 | collectionNames = CollectionNames; 17 | } 18 | } 19 | 20 | public class CollectionInfo 21 | { 22 | public string Name { get; set; } 23 | public string PartitionKey { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /TodoService.Api/Options/KeyVaultOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | namespace TodoService.Api.Options 5 | { 6 | public enum KeyVaultUsage 7 | { 8 | UseLocalSecretStore, 9 | UseClientSecret, 10 | UseMsi 11 | } 12 | 13 | public class KeyVaultOptions 14 | { 15 | public KeyVaultUsage Mode { get; set; } 16 | public string KeyVaultUri { get; set; } 17 | public string ClientId { get; set; } 18 | public string ClientSecret { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TodoService.Api/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using Microsoft.AspNetCore; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Azure.KeyVault; 8 | using Microsoft.Azure.Services.AppAuthentication; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.Configuration.AzureKeyVault; 11 | using TodoService.Api.Options; 12 | 13 | namespace TodoService.Api 14 | { 15 | public class Program 16 | { 17 | public static void Main(string[] args) 18 | { 19 | CreateWebHostBuilder(args).Build().Run(); 20 | } 21 | 22 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 23 | WebHost.CreateDefaultBuilder(args) 24 | .ConfigureAppConfiguration((ctx, builder) => 25 | { 26 | var config = builder.Build(); 27 | var mode = (KeyVaultUsage)Enum.Parse(typeof(KeyVaultUsage), (config["Secrets:Mode"])); 28 | if (mode != KeyVaultUsage.UseLocalSecretStore) 29 | { 30 | KeyVaultOptions kvc = config.GetSection("Secrets").Get(); 31 | if (mode == KeyVaultUsage.UseClientSecret) 32 | { 33 | builder.AddAzureKeyVault(kvc.KeyVaultUri, kvc.ClientId, kvc.ClientSecret); 34 | } 35 | else //UseMsi 36 | { 37 | var tokenProvider = new AzureServiceTokenProvider(); 38 | //Create the Key Vault client 39 | var kvClient = new KeyVaultClient((authority, resource, scope) => tokenProvider.KeyVaultTokenCallback(authority, resource, scope)); 40 | //Add Key Vault to configuration pipeline 41 | builder.AddAzureKeyVault(kvc.KeyVaultUri, kvClient, new DefaultKeyVaultSecretManager()); 42 | } 43 | } 44 | }) 45 | .UseHealthChecks("/") 46 | .UseApplicationInsights() 47 | .UseStartup(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /TodoService.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using System.Linq; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Logging; 13 | using Newtonsoft.Json.Serialization; 14 | using TodoService.Api.Extensions; 15 | using TodoService.Api.Options; 16 | using TodoService.Core.Interfaces; 17 | using TodoService.Infrastructure.Data; 18 | 19 | namespace TodoService.Api 20 | { 21 | public class Startup 22 | { 23 | public Startup(IConfiguration configuration) 24 | { 25 | Configuration = configuration; 26 | } 27 | 28 | public IConfiguration Configuration { get; } 29 | 30 | // This method gets called by the runtime. Use this method to add services to the container. 31 | public void ConfigureServices(IServiceCollection services) 32 | { 33 | // Bind database options. Invalid configuration will terminate the application startup. 34 | var connectionStringsOptions = 35 | Configuration.GetSection("ConnectionStrings").Get(); 36 | var cosmosDbOptions = Configuration.GetSection("CosmosDb").Get(); 37 | var (serviceEndpoint, authKey) = connectionStringsOptions.ActiveConnectionStringOptions; 38 | var (databaseName, collectionData) = cosmosDbOptions; 39 | var collectionNames = collectionData.Select(c => c.Name).ToList(); 40 | 41 | // Add Mvc. 42 | services.AddMvc() 43 | .SetCompatibilityVersion(CompatibilityVersion.Version_2_1) 44 | .AddJsonOptions(options => 45 | { 46 | options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); 47 | }); 48 | 49 | // Add version support. 50 | services.AddMvcCore().AddVersionedApiExplorer(o => o.GroupNameFormat = "'v'VVV"); 51 | services.AddApiVersioning(options => 52 | { 53 | options.ReportApiVersions = true; 54 | options.AssumeDefaultVersionWhenUnspecified = true; 55 | }); 56 | 57 | // Add CosmosDb. This verifies database and collections existence. 58 | services.AddCosmosDb(serviceEndpoint, authKey, databaseName, collectionNames); 59 | 60 | // Add health check by checking CosmosDb connection. Cache the result for 1 minute. 61 | services.AddHealthChecks(checks => 62 | { 63 | checks.AddCosmosDbCheck(serviceEndpoint, authKey, TimeSpan.FromMinutes(1)); 64 | }); 65 | 66 | services.AddCustomSwagger(); 67 | 68 | services.AddScoped(); 69 | } 70 | 71 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 72 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApiVersionDescriptionProvider provider, ILoggerFactory loggerFactory) 73 | { 74 | if (env.IsDevelopment()) 75 | { 76 | app.UseDeveloperExceptionPage(); 77 | } 78 | else 79 | { 80 | app.UseAppInsightsLogger(loggerFactory); 81 | app.UseGlobalErrorHandler(); 82 | app.UseHsts(); 83 | } 84 | 85 | app.UseHttpsRedirection(); 86 | app.UseMvc(); 87 | 88 | app.UseSwagger(); 89 | app.UseSwaggerUI(options => 90 | { 91 | foreach (var description in provider.ApiVersionDescriptions) 92 | { 93 | options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", 94 | description.GroupName.ToUpperInvariant()); 95 | } 96 | }); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /TodoService.Api/TodoService.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | 9 | 10 | true 11 | $(NoWarn);1591 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 | 48 | 49 | -------------------------------------------------------------------------------- /TodoService.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | }, 8 | "Console": { 9 | "IncludeScopes": "true" 10 | } 11 | }, 12 | "Secrets": { 13 | "Mode": "UseLocalSecretStore" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /TodoService.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "ConnectionMode": "Azure", 4 | "Azure": { 5 | "ServiceEndpoint": "" 6 | }, 7 | "Emulator": { 8 | "ServiceEndpoint": "https://localhost:8081" 9 | } 10 | }, 11 | "CosmosDb": { 12 | "DatabaseName": "todo", 13 | "CollectionNames": [ 14 | { 15 | "Name": "todoItems", 16 | "PartitionKey": "/category" 17 | } 18 | ] 19 | }, 20 | "Logging": { 21 | "LogLevel": { 22 | "Default": "Warning" 23 | } 24 | }, 25 | "Secrets": { 26 | "Mode": "UseMsi", 27 | "KeyVaultUri": "" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /TodoService.Core/Exceptions/EntityAlreadyExistsException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | 6 | namespace TodoService.Core.Exceptions 7 | { 8 | public class EntityAlreadyExistsException : Exception 9 | { 10 | public EntityAlreadyExistsException() { } 11 | 12 | public EntityAlreadyExistsException(string message): base(message) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TodoService.Core/Exceptions/EntityNotFoundException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | 6 | namespace TodoService.Core.Exceptions 7 | { 8 | public class EntityNotFoundException : Exception 9 | { 10 | public EntityNotFoundException() { } 11 | 12 | public EntityNotFoundException(string message): base(message) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TodoService.Core/Interfaces/IRepository.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System.Threading.Tasks; 5 | using TodoService.Core.Models; 6 | 7 | namespace TodoService.Core.Interfaces 8 | { 9 | public interface IRepository where T : Entity 10 | { 11 | Task GetByIdAsync(string id); 12 | Task AddAsync(T entity); 13 | Task UpdateAsync(T entity); 14 | Task DeleteAsync(T entity); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TodoService.Core/Interfaces/ITodoItemRepository.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using TodoService.Core.Models; 5 | 6 | namespace TodoService.Core.Interfaces 7 | { 8 | public interface ITodoItemRepository : IRepository { } 9 | } 10 | -------------------------------------------------------------------------------- /TodoService.Core/Models/Entity.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | namespace TodoService.Core.Models 5 | { 6 | public abstract class Entity 7 | { 8 | /// 9 | /// Entity identifier 10 | /// 11 | /// 5fe3fc2a-cbac-4df0-8031-fdca0f682989 12 | public string Id { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TodoService.Core/Models/TodoItem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | namespace TodoService.Core.Models 5 | { 6 | public class TodoItem : Entity 7 | { 8 | /// 9 | /// TodoItem item name 10 | /// 11 | /// Grocery 12 | public string Name { get; set; } 13 | 14 | /// 15 | /// TodoItem item Description 16 | /// 17 | /// Pick Bread 18 | public string Description { get; set; } 19 | 20 | /// 21 | /// TodoItem item category 22 | /// 23 | /// Shopping 24 | public string Category { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TodoService.Core/TodoService.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | true 9 | $(NoWarn);1591 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /TodoService.Infrastructure.UnitTests/Data/CosmosDbClientFactoryTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Microsoft.Azure.Documents; 8 | using Microsoft.Azure.Documents.Client; 9 | using Moq; 10 | using TodoService.Infrastructure.Data; 11 | using Xunit; 12 | 13 | namespace TodoService.Infrastructure.UnitTests.Data 14 | { 15 | public class CosmosDbClientFactoryFixture : IDisposable 16 | { 17 | public string DatabaseName { get; } = "foobar"; 18 | public List CollectionNames { get; } = new List { "foo", "bar" }; 19 | 20 | public CosmosDbClientFactory CreateCosmosDbClientFactoryForTesting(IDocumentClient documentClient) 21 | { 22 | return new CosmosDbClientFactory(DatabaseName, CollectionNames, documentClient); 23 | } 24 | 25 | public void Dispose() { } 26 | } 27 | 28 | public class CosmosDbClientFactoryTests : IClassFixture 29 | { 30 | private readonly CosmosDbClientFactoryFixture _fixture; 31 | 32 | public CosmosDbClientFactoryTests(CosmosDbClientFactoryFixture fixture) 33 | { 34 | _fixture = fixture; 35 | } 36 | 37 | [Theory] 38 | [InlineData(null, null, null, "databaseName")] 39 | [InlineData("foo", null, null, "collectionNames")] 40 | [InlineData("foo", new[] { "bar" }, null, "documentClient")] 41 | public void CosmosDbClientFactory_WithNullArgument_ShouldThrowArgumentNullException(string databaseName, 42 | IEnumerable collectionNames, DocumentClient documentClient, string paramName) 43 | { 44 | var ex = Assert.Throws(() => 45 | new CosmosDbClientFactory(databaseName, collectionNames?.ToList(), documentClient)); 46 | 47 | Assert.Equal(paramName, ex.ParamName); 48 | } 49 | 50 | [Fact] 51 | public void CosmosClientFactory_WithNonNullArguments_ShouldCreateNewInstance() 52 | { 53 | var documentClientStub = new Mock(); 54 | var sut = _fixture.CreateCosmosDbClientFactoryForTesting(documentClientStub.Object); 55 | 56 | Assert.NotNull(sut); 57 | } 58 | 59 | [Fact] 60 | public void GetClient_WithNonExistingCollectionName_ShouldThrowArgumentException() 61 | { 62 | var documentClientStub = new Mock(); 63 | var sut = _fixture.CreateCosmosDbClientFactoryForTesting(documentClientStub.Object); 64 | const string collectionName = "abc"; 65 | 66 | var ex = Assert.Throws(() => sut.GetClient(collectionName)); 67 | 68 | Assert.Equal($"Unable to find collection: {collectionName}", ex.Message); 69 | } 70 | 71 | [Fact] 72 | public void GetClient_WithExistingCollectionName_ShouldReturnNewCosmosClient() 73 | { 74 | var documentClientStub = new Mock(); 75 | var sut = _fixture.CreateCosmosDbClientFactoryForTesting(documentClientStub.Object); 76 | var collectionName = _fixture.CollectionNames[0]; 77 | 78 | var result = sut.GetClient(collectionName); 79 | 80 | Assert.NotNull(result); 81 | } 82 | 83 | [Fact] 84 | public async void EnsureDbSetupAsync_WhenCalled_ShouldVerifyDatabaseAndCollectionsExistence() 85 | { 86 | var documentClientMock = new Mock(); 87 | documentClientMock.Setup(x => x.ReadDatabaseAsync(It.IsAny(), null)) 88 | .ReturnsAsync(new ResourceResponse()); 89 | documentClientMock.Setup(x => x.ReadDocumentCollectionAsync(It.IsAny(), null)) 90 | .ReturnsAsync(new ResourceResponse()); 91 | var sut = _fixture.CreateCosmosDbClientFactoryForTesting(documentClientMock.Object); 92 | 93 | await sut.EnsureDbSetupAsync(); 94 | 95 | var databaseUri = UriFactory.CreateDatabaseUri(_fixture.DatabaseName); 96 | var collectionUris = _fixture.CollectionNames 97 | .Select(x => UriFactory.CreateDocumentCollectionUri(_fixture.DatabaseName, x)) 98 | .ToList(); 99 | 100 | documentClientMock.Verify( 101 | x => x.ReadDatabaseAsync(It.Is(uri => uri.Equals(databaseUri)), null), Times.Once); 102 | documentClientMock.Verify( 103 | x => x.ReadDocumentCollectionAsync(It.Is(uri => uri.Equals(collectionUris[0])), null), Times.Once); 104 | documentClientMock.Verify( 105 | x => x.ReadDocumentCollectionAsync(It.Is(uri => uri.Equals(collectionUris[1])), null), Times.Once); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /TodoService.Infrastructure.UnitTests/Data/CosmosDbClientTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading; 7 | using Microsoft.Azure.Documents; 8 | using Microsoft.Azure.Documents.Client; 9 | using Moq; 10 | using Newtonsoft.Json; 11 | using TodoService.Infrastructure.Data; 12 | using Xunit; 13 | 14 | namespace TodoService.Infrastructure.UnitTests.Data 15 | { 16 | public class CosmosDbClientFixture : IDisposable 17 | { 18 | public CosmosDbClientFixture() 19 | { 20 | DocumentUri = UriFactory.CreateDocumentUri(DatabaseName, CollectionName, DocumentId); 21 | 22 | CreateDocumentResponse(); 23 | } 24 | 25 | private void CreateDocumentResponse() 26 | { 27 | var documentJson = JsonConvert.SerializeObject(Document); 28 | var jsonReader = new JsonTextReader(new StringReader(documentJson)); 29 | 30 | var document = new Document(); 31 | document.LoadFrom(jsonReader); 32 | 33 | DocumentResponse = new ResourceResponse(document); 34 | } 35 | 36 | public string DatabaseName { get; } = "foo"; 37 | public string CollectionName { get; } = "bar"; 38 | public string DocumentId { get; } = "foobar"; 39 | public object Document { get; } = new { Id = "foobar", Note = "Note" }; 40 | public Uri DocumentUri { get; } 41 | public ResourceResponse DocumentResponse { get; private set; } 42 | 43 | public CosmosDbClient CreateCosmosDbClientForTesting(IDocumentClient documentClient) 44 | { 45 | return new CosmosDbClient(DatabaseName, CollectionName, documentClient); 46 | } 47 | 48 | public void Dispose() { } 49 | } 50 | 51 | public class CosmosDbClientTests : IClassFixture 52 | { 53 | private readonly CosmosDbClientFixture _fixture; 54 | 55 | public CosmosDbClientTests(CosmosDbClientFixture fixture) 56 | { 57 | _fixture = fixture; 58 | } 59 | 60 | [Theory] 61 | [InlineData(null, null, null, "databaseName")] 62 | [InlineData("foo", null, null, "collectionName")] 63 | [InlineData("foo", "bar", null, "documentClient")] 64 | public void CosmosDbClient_WithNullArgument_ShouldThrowArgumentNullException(string databaseName, 65 | string collectionName, IDocumentClient documentClient, string paramName) 66 | { 67 | var ex = Assert.Throws(() => 68 | new CosmosDbClient(databaseName, collectionName, documentClient)); 69 | 70 | Assert.Equal(paramName, ex.ParamName); 71 | } 72 | 73 | [Fact] 74 | public void CosmosDbClient_WithNonNullArguments_ShouldReturnNewInstance() 75 | { 76 | var documentClientStub = new Mock(); 77 | var sut = _fixture.CreateCosmosDbClientForTesting(documentClientStub.Object); 78 | 79 | Assert.NotNull(sut); 80 | } 81 | 82 | [Fact] 83 | public async void ReadDocumentAsync_WhenCalled_ShouldCallReadDocumentAsyncOnDocumentClient() 84 | { 85 | var documentClientMock = new Mock(); 86 | documentClientMock.Setup(x => x.ReadDocumentAsync(It.IsAny(), null, default(CancellationToken))) 87 | .ReturnsAsync(_fixture.DocumentResponse); 88 | var sut = _fixture.CreateCosmosDbClientForTesting(documentClientMock.Object); 89 | 90 | await sut.ReadDocumentAsync(_fixture.DocumentId); 91 | 92 | documentClientMock.Verify( 93 | x => x.ReadDocumentAsync( 94 | It.Is(uri => uri == _fixture.DocumentUri), 95 | null, 96 | default(CancellationToken)), 97 | Times.Once); 98 | } 99 | 100 | [Fact] 101 | public async void CreateDocumentAsync_WhenCalled_ShouldCallCreateDocumentAsyncOnDocumentClient() 102 | { 103 | var documentClientMock = new Mock(); 104 | documentClientMock.Setup(x => 105 | x.CreateDocumentAsync(It.IsAny(), It.IsAny(), null, false, default(CancellationToken))) 106 | .ReturnsAsync(_fixture.DocumentResponse); 107 | var sut = _fixture.CreateCosmosDbClientForTesting(documentClientMock.Object); 108 | 109 | await sut.CreateDocumentAsync(_fixture.Document); 110 | 111 | documentClientMock.Verify( 112 | x => x.CreateDocumentAsync( 113 | It.Is(uri => 114 | uri == UriFactory.CreateDocumentCollectionUri(_fixture.DatabaseName, _fixture.CollectionName)), 115 | It.Is(document => document == _fixture.Document), 116 | null, 117 | false, 118 | default(CancellationToken)), 119 | Times.Once); 120 | } 121 | 122 | [Fact] 123 | public async void ReplaceAsync_WhenCalled_ShouldCallReplaceDocumentAsyncOnDocumentClient() 124 | { 125 | var documentClientMock = new Mock(); 126 | documentClientMock.Setup(x => 127 | x.ReplaceDocumentAsync(It.IsAny(), It.IsAny(), null, default(CancellationToken))) 128 | .ReturnsAsync(_fixture.DocumentResponse); 129 | var sut = _fixture.CreateCosmosDbClientForTesting(documentClientMock.Object); 130 | 131 | await sut.ReplaceDocumentAsync(_fixture.DocumentId, _fixture.Document); 132 | 133 | documentClientMock.Verify( 134 | x => x.ReplaceDocumentAsync( 135 | It.Is(uri => uri == _fixture.DocumentUri), 136 | It.Is(document => document == _fixture.Document), 137 | null, 138 | default(CancellationToken)), 139 | Times.Once); 140 | } 141 | 142 | [Fact] 143 | public async void DeleteAsync_WhenCalled_ShouldCallDeleteDocumentAsyncOnDocumentClient() 144 | { 145 | var documentClientMock = new Mock(); 146 | documentClientMock.Setup(x => x.DeleteDocumentAsync(It.IsAny(), null, default(CancellationToken))) 147 | .ReturnsAsync(_fixture.DocumentResponse); 148 | var sut = _fixture.CreateCosmosDbClientForTesting(documentClientMock.Object); 149 | 150 | await sut.DeleteDocumentAsync(_fixture.DocumentId); 151 | 152 | documentClientMock.Verify( 153 | x => x.DeleteDocumentAsync( 154 | It.Is(uri => uri == _fixture.DocumentUri), 155 | null, 156 | default(CancellationToken)), 157 | Times.Once); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /TodoService.Infrastructure.UnitTests/Data/CosmosDbRepositoryTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using System.IO; 6 | using System.Net; 7 | using System.Reflection; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.Azure.Documents; 11 | using Microsoft.Azure.Documents.Client; 12 | using Moq; 13 | using Newtonsoft.Json; 14 | using TodoService.Core.Exceptions; 15 | using TodoService.Core.Models; 16 | using TodoService.Infrastructure.Data; 17 | using Xunit; 18 | 19 | namespace TodoService.Infrastructure.UnitTests.Data 20 | { 21 | public class FakeEntity : Entity 22 | { 23 | public string Note { get; set; } 24 | } 25 | 26 | public class CosmosDbRepositoryFixture : IDisposable 27 | { 28 | public string CollectionName { get; } = "fakeCollection"; 29 | 30 | public FakeEntity FakeEntity { get; } = new FakeEntity { Note = "fakeNote" }; 31 | 32 | public CosmosDbRepository CreateCosmosDbRepositoryForTesting(ICosmosDbClient cosmosDbClient) 33 | { 34 | var factoryStub = new Mock(); 35 | factoryStub.Setup(x => x.GetClient(CollectionName)).Returns(cosmosDbClient); 36 | 37 | var sut = new Mock>(factoryStub.Object); 38 | sut.Setup(x => x.CollectionName).Returns(CollectionName); 39 | sut.CallBase = true; 40 | 41 | return sut.Object; 42 | } 43 | 44 | // A workaround due to https://github.com/Azure/azure-cosmosdb-dotnet/issues/121. 45 | public DocumentClientException CreateDocumentClientExceptionForTesting(HttpStatusCode statusCode) 46 | { 47 | var type = typeof(DocumentClientException); 48 | 49 | var documentClientExceptionInstance = type.Assembly.CreateInstance(type.FullName, false, 50 | BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { new Error(), null, statusCode }, 51 | null, 52 | null); 53 | 54 | return (DocumentClientException)documentClientExceptionInstance; 55 | } 56 | 57 | public Document CreateDocument(FakeEntity fakeEntity) 58 | { 59 | var modelString = JsonConvert.SerializeObject(fakeEntity); 60 | var jsonReader = new JsonTextReader(new StringReader(modelString)); 61 | var document = new Document(); 62 | document.LoadFrom(jsonReader); 63 | 64 | return document; 65 | } 66 | public void Dispose() { } 67 | } 68 | 69 | public class CosmosDbRepositoryTests : IClassFixture 70 | { 71 | private readonly CosmosDbRepositoryFixture _fixture; 72 | 73 | public CosmosDbRepositoryTests(CosmosDbRepositoryFixture fixture) 74 | { 75 | _fixture = fixture; 76 | } 77 | 78 | [Fact] 79 | public async Task GetByIdAsync_WhenDocumentClientExceptionWithStatusCodeNotFoundIsCaught_ShouldThrowEntityNotFoundException() 80 | { 81 | var clientStub = new Mock(); 82 | clientStub.Setup(x => 83 | x.ReadDocumentAsync(It.IsAny(), It.IsAny(), It.IsAny())) 84 | .Throws(_fixture.CreateDocumentClientExceptionForTesting(HttpStatusCode.NotFound)); 85 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientStub.Object); 86 | 87 | await Assert.ThrowsAsync(async () => await sut.GetByIdAsync("")); 88 | } 89 | 90 | [Fact] 91 | public async Task GetByIdAsync_WhenDocumentClientExceptionWithStatusCodeBesidesNotFoundIsCaught_ShouldRethrow() 92 | { 93 | var clientStub = new Mock(); 94 | clientStub.Setup(x => 95 | x.ReadDocumentAsync(It.IsAny(), It.IsAny(), It.IsAny())) 96 | .Throws(_fixture.CreateDocumentClientExceptionForTesting(HttpStatusCode.BadRequest)); 97 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientStub.Object); 98 | 99 | var dce = await Assert.ThrowsAsync(async () => await sut.GetByIdAsync("")); 100 | 101 | Assert.Equal(HttpStatusCode.BadRequest, dce.StatusCode); 102 | } 103 | 104 | [Fact] 105 | public async Task GetByIdAsync_WhenIdExists_ShouldReturnDocumentWithTheId() 106 | { 107 | var clientStub = new Mock(); 108 | clientStub.Setup(x => 109 | x.ReadDocumentAsync(_fixture.FakeEntity.Id, It.IsAny(), It.IsAny())) 110 | .ReturnsAsync(() => _fixture.CreateDocument(_fixture.FakeEntity)); 111 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientStub.Object); 112 | 113 | var result = await sut.GetByIdAsync(_fixture.FakeEntity.Id); 114 | 115 | Assert.NotNull(result); 116 | Assert.Equal(_fixture.FakeEntity.Id, result.Id); 117 | Assert.Equal(_fixture.FakeEntity.Note, result.Note); 118 | } 119 | 120 | [Fact] 121 | public async Task AddAsync_WhenDocumentClientExceptionWithStatusCodeConflictIsCaught_ShouldThrowEntityAlreadyExistsException() 122 | { 123 | var clientStub = new Mock(); 124 | clientStub.Setup( 125 | x => x.CreateDocumentAsync(It.IsAny(), null, false, It.IsAny())) 126 | .Throws(_fixture.CreateDocumentClientExceptionForTesting(HttpStatusCode.Conflict)); 127 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientStub.Object); 128 | 129 | await Assert.ThrowsAsync(async () => await sut.AddAsync(new FakeEntity())); 130 | } 131 | 132 | [Fact] 133 | 134 | public async Task AddAsync_WhenDocumentClientExceptionWithStatusCodeBesidesConflictIsCaught_ShouldRethrow() 135 | { 136 | var clientStub = new Mock(); 137 | clientStub.Setup( 138 | x => x.CreateDocumentAsync(It.IsAny(), null, false, It.IsAny())) 139 | .Throws(_fixture.CreateDocumentClientExceptionForTesting(HttpStatusCode.BadRequest)); 140 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientStub.Object); 141 | 142 | var dce = await Assert.ThrowsAsync( 143 | async () => await sut.AddAsync(new FakeEntity())); 144 | 145 | Assert.Equal(HttpStatusCode.BadRequest, dce.StatusCode); 146 | } 147 | 148 | [Fact] 149 | public async Task AddAsync_GivenAnEntity_ShouldAddTheEntityAndReturnIt() 150 | { 151 | var clientStub = new Mock(); 152 | clientStub.Setup(x => x.CreateDocumentAsync(_fixture.FakeEntity, null, false, It.IsAny())) 153 | .ReturnsAsync(() => _fixture.CreateDocument(_fixture.FakeEntity)); 154 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientStub.Object); 155 | 156 | var result = await sut.AddAsync(_fixture.FakeEntity); 157 | 158 | Assert.NotNull(result); 159 | Assert.True(Guid.TryParse(result.Id, out _)); 160 | Assert.Equal(_fixture.FakeEntity.Note, result.Note); 161 | } 162 | 163 | [Fact] 164 | public async Task UpdateAsync_WhenDocumentClientExceptionIsCaughtWithStatusCodeNotFound_ShouldThrowEntityNotFoundExistsException() 165 | { 166 | var clientStub = new Mock(); 167 | clientStub.Setup(x => 168 | x.ReplaceDocumentAsync(It.IsAny(), It.IsAny(), null, 169 | It.IsAny())) 170 | .Throws(_fixture.CreateDocumentClientExceptionForTesting(HttpStatusCode.NotFound)); 171 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientStub.Object); 172 | 173 | await Assert.ThrowsAsync(async () => await sut.UpdateAsync(new FakeEntity())); 174 | } 175 | 176 | [Fact] 177 | public async Task UpdateAsync_WhenDocumentClientExceptionIsCaughtWithStatusCodeBesidesNotFound_ShouldRethrow() 178 | { 179 | var clientStub = new Mock(); 180 | clientStub.Setup(x => 181 | x.ReplaceDocumentAsync(It.IsAny(), It.IsAny(), null, 182 | It.IsAny())) 183 | .Throws(_fixture.CreateDocumentClientExceptionForTesting(HttpStatusCode.BadRequest)); 184 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientStub.Object); 185 | 186 | var dce = await Assert.ThrowsAsync(async () => 187 | await sut.UpdateAsync(new FakeEntity())); 188 | 189 | Assert.Equal(HttpStatusCode.BadRequest, dce.StatusCode); 190 | } 191 | 192 | [Fact] 193 | public async Task UpdateAsync_GivenAnEntity_ShouldCallReplaceDocumentAsync() 194 | { 195 | var clientMock = new Mock(); 196 | clientMock.Setup( 197 | x => x.ReplaceDocumentAsync( 198 | It.IsAny(), 199 | It.IsAny(), 200 | null, 201 | It.IsAny())) 202 | .ReturnsAsync(new Document()); 203 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientMock.Object); 204 | 205 | await sut.UpdateAsync(_fixture.FakeEntity); 206 | 207 | clientMock.Verify( 208 | x => x.ReplaceDocumentAsync( 209 | It.Is(entityId => entityId == _fixture.FakeEntity.Id), 210 | It.Is(entity => 211 | entity.Id == _fixture.FakeEntity.Id && entity.Note == _fixture.FakeEntity.Note), 212 | null, 213 | It.IsAny()), 214 | Times.Once); 215 | } 216 | 217 | [Fact] 218 | public async Task DeleteAsync_WhenDocumentClientExceptionWithStatusCodeNotFoundIsCaught_ShouldThrowEntityNotFoundException() 219 | { 220 | var clientStub = new Mock(); 221 | clientStub.Setup(x => x.DeleteDocumentAsync(It.IsAny(), It.IsAny(), It.IsAny())) 222 | .Throws(_fixture.CreateDocumentClientExceptionForTesting(HttpStatusCode.NotFound)); 223 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientStub.Object); 224 | 225 | await Assert.ThrowsAsync(async () => await sut.DeleteAsync(new FakeEntity())); 226 | } 227 | 228 | [Fact] 229 | public async Task DeleteAsync_WhenDocumentClientExceptionWithStatusCodeBesidesNotFoundIsCaught_ShouldRethrow() 230 | { 231 | var clientStub = new Mock(); 232 | clientStub.Setup(x => x.DeleteDocumentAsync(It.IsAny(), It.IsAny(), It.IsAny())) 233 | .Throws(_fixture.CreateDocumentClientExceptionForTesting(HttpStatusCode.BadRequest)); 234 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientStub.Object); 235 | 236 | var dce = await Assert.ThrowsAsync(async () => 237 | await sut.DeleteAsync(new FakeEntity())); 238 | 239 | Assert.Equal(HttpStatusCode.BadRequest, dce.StatusCode); 240 | } 241 | 242 | [Fact] 243 | public async Task DeleteAsync_GivenAnEntity_ShouldCallDeleteDocumentAsync() 244 | { 245 | var clientMock = new Mock(); 246 | clientMock.Setup(x => x.DeleteDocumentAsync(It.IsAny(), It.IsAny(), It.IsAny())) 247 | .ReturnsAsync(new Document()); 248 | var sut = _fixture.CreateCosmosDbRepositoryForTesting(clientMock.Object); 249 | 250 | await sut.DeleteAsync(_fixture.FakeEntity); 251 | 252 | clientMock.Verify( 253 | x => x.DeleteDocumentAsync( 254 | It.Is(entityId => entityId == _fixture.FakeEntity.Id), 255 | It.IsAny(), 256 | It.IsAny()), 257 | Times.Once); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /TodoService.Infrastructure.UnitTests/TodoService.Infrastructure.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ..\..\..\..\.nuget\packages\moq\4.9.0\lib\netstandard1.3\Moq.dll 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /TodoService.Infrastructure/Data/CosmosDbClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Azure.Documents; 8 | using Microsoft.Azure.Documents.Client; 9 | 10 | namespace TodoService.Infrastructure.Data 11 | { 12 | public class CosmosDbClient : ICosmosDbClient 13 | { 14 | private readonly string _databaseName; 15 | private readonly string _collectionName; 16 | private readonly IDocumentClient _documentClient; 17 | 18 | public CosmosDbClient(string databaseName, string collectionName, IDocumentClient documentClient) 19 | { 20 | _databaseName = databaseName ?? throw new ArgumentNullException(nameof(databaseName)); 21 | _collectionName = collectionName ?? throw new ArgumentNullException(nameof(collectionName)); 22 | _documentClient = documentClient ?? throw new ArgumentNullException(nameof(documentClient)); 23 | } 24 | 25 | public async Task ReadDocumentAsync(string documentId, RequestOptions options = null, 26 | CancellationToken cancellationToken = default(CancellationToken)) 27 | { 28 | return await _documentClient.ReadDocumentAsync( 29 | UriFactory.CreateDocumentUri(_databaseName, _collectionName, documentId), options, cancellationToken); 30 | } 31 | 32 | public async Task CreateDocumentAsync(object document, RequestOptions options = null, 33 | bool disableAutomaticIdGeneration = false, CancellationToken cancellationToken = default(CancellationToken)) 34 | { 35 | return await _documentClient.CreateDocumentAsync( 36 | UriFactory.CreateDocumentCollectionUri(_databaseName, _collectionName), document, options, 37 | disableAutomaticIdGeneration, cancellationToken); 38 | } 39 | 40 | public async Task ReplaceDocumentAsync(string documentId, object document, 41 | RequestOptions options = null, CancellationToken cancellationToken = default(CancellationToken)) 42 | { 43 | return await _documentClient.ReplaceDocumentAsync( 44 | UriFactory.CreateDocumentUri(_databaseName, _collectionName, documentId), document, options, 45 | cancellationToken); 46 | } 47 | 48 | public async Task DeleteDocumentAsync(string documentId, RequestOptions options = null, 49 | CancellationToken cancellationToken = default(CancellationToken)) 50 | { 51 | return await _documentClient.DeleteDocumentAsync( 52 | UriFactory.CreateDocumentUri(_databaseName, _collectionName, documentId), options, cancellationToken); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /TodoService.Infrastructure/Data/CosmosDbClientFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Microsoft.Azure.Documents; 8 | using Microsoft.Azure.Documents.Client; 9 | 10 | namespace TodoService.Infrastructure.Data 11 | { 12 | public class CosmosDbClientFactory : ICosmosDbClientFactory 13 | { 14 | private readonly string _databaseName; 15 | private readonly List _collectionNames; 16 | private readonly IDocumentClient _documentClient; 17 | 18 | public CosmosDbClientFactory(string databaseName, List collectionNames, IDocumentClient documentClient) 19 | { 20 | _databaseName = databaseName ?? throw new ArgumentNullException(nameof(databaseName)); 21 | _collectionNames = collectionNames ?? throw new ArgumentNullException(nameof(collectionNames)); 22 | _documentClient = documentClient ?? throw new ArgumentNullException(nameof(documentClient)); 23 | } 24 | 25 | public ICosmosDbClient GetClient(string collectionName) 26 | { 27 | if (!_collectionNames.Contains(collectionName)) 28 | { 29 | throw new ArgumentException($"Unable to find collection: {collectionName}"); 30 | } 31 | 32 | return new CosmosDbClient(_databaseName, collectionName, _documentClient); 33 | } 34 | 35 | public async Task EnsureDbSetupAsync() 36 | { 37 | await _documentClient.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(_databaseName)); 38 | 39 | foreach (var collectionName in _collectionNames) 40 | { 41 | await _documentClient.ReadDocumentCollectionAsync( 42 | UriFactory.CreateDocumentCollectionUri(_databaseName, collectionName)); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TodoService.Infrastructure/Data/CosmosDbRepository.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | using Microsoft.Azure.Documents; 8 | using Microsoft.Azure.Documents.Client; 9 | using Newtonsoft.Json; 10 | using TodoService.Core.Exceptions; 11 | using TodoService.Core.Interfaces; 12 | using TodoService.Core.Models; 13 | 14 | namespace TodoService.Infrastructure.Data 15 | { 16 | public abstract class CosmosDbRepository : IRepository, IDocumentCollectionContext where T : Entity 17 | { 18 | private readonly ICosmosDbClientFactory _cosmosDbClientFactory; 19 | 20 | protected CosmosDbRepository(ICosmosDbClientFactory cosmosDbClientFactory) 21 | { 22 | _cosmosDbClientFactory = cosmosDbClientFactory; 23 | } 24 | 25 | public async Task GetByIdAsync(string id) 26 | { 27 | try 28 | { 29 | var cosmosDbClient = _cosmosDbClientFactory.GetClient(CollectionName); 30 | var document = await cosmosDbClient.ReadDocumentAsync(id, new RequestOptions 31 | { 32 | PartitionKey = ResolvePartitionKey(id) 33 | }); 34 | 35 | return JsonConvert.DeserializeObject(document.ToString()); 36 | } 37 | catch (DocumentClientException e) 38 | { 39 | if (e.StatusCode == HttpStatusCode.NotFound) 40 | { 41 | throw new EntityNotFoundException(); 42 | } 43 | 44 | throw; 45 | } 46 | } 47 | 48 | public async Task AddAsync(T entity) 49 | { 50 | try 51 | { 52 | entity.Id = GenerateId(entity); 53 | var cosmosDbClient = _cosmosDbClientFactory.GetClient(CollectionName); 54 | var document = await cosmosDbClient.CreateDocumentAsync(entity); 55 | return JsonConvert.DeserializeObject(document.ToString()); 56 | } 57 | catch (DocumentClientException e) 58 | { 59 | if (e.StatusCode == HttpStatusCode.Conflict) 60 | { 61 | throw new EntityAlreadyExistsException(); 62 | } 63 | 64 | throw; 65 | } 66 | } 67 | 68 | public async Task UpdateAsync(T entity) 69 | { 70 | try 71 | { 72 | var cosmosDbClient = _cosmosDbClientFactory.GetClient(CollectionName); 73 | await cosmosDbClient.ReplaceDocumentAsync(entity.Id, entity); 74 | } 75 | catch (DocumentClientException e) 76 | { 77 | if (e.StatusCode == HttpStatusCode.NotFound) 78 | { 79 | throw new EntityNotFoundException(); 80 | } 81 | 82 | throw; 83 | } 84 | } 85 | 86 | public async Task DeleteAsync(T entity) 87 | { 88 | try 89 | { 90 | var cosmosDbClient = _cosmosDbClientFactory.GetClient(CollectionName); 91 | await cosmosDbClient.DeleteDocumentAsync(entity.Id, new RequestOptions 92 | { 93 | PartitionKey = ResolvePartitionKey(entity.Id) 94 | }); 95 | } 96 | catch (DocumentClientException e) 97 | { 98 | if (e.StatusCode == HttpStatusCode.NotFound) 99 | { 100 | throw new EntityNotFoundException(); 101 | } 102 | 103 | throw; 104 | } 105 | } 106 | 107 | public abstract string CollectionName { get; } 108 | public virtual string GenerateId(T entity) => Guid.NewGuid().ToString(); 109 | public virtual PartitionKey ResolvePartitionKey(string entityId) => null; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /TodoService.Infrastructure/Data/ICosmosDbClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Azure.Documents; 7 | using Microsoft.Azure.Documents.Client; 8 | 9 | namespace TodoService.Infrastructure.Data 10 | { 11 | public interface ICosmosDbClient 12 | { 13 | Task ReadDocumentAsync(string documentId, RequestOptions options = null, 14 | CancellationToken cancellationToken = default(CancellationToken)); 15 | 16 | Task CreateDocumentAsync(object document, RequestOptions options = null, 17 | bool disableAutomaticIdGeneration = false, 18 | CancellationToken cancellationToken = default(CancellationToken)); 19 | 20 | Task ReplaceDocumentAsync(string documentId, object document, RequestOptions options = null, 21 | CancellationToken cancellationToken = default(CancellationToken)); 22 | 23 | Task DeleteDocumentAsync(string documentId, RequestOptions options = null, 24 | CancellationToken cancellationToken = default(CancellationToken)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TodoService.Infrastructure/Data/ICosmosDbClientFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | namespace TodoService.Infrastructure.Data 5 | { 6 | public interface ICosmosDbClientFactory 7 | { 8 | ICosmosDbClient GetClient(string collectionName); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /TodoService.Infrastructure/Data/IDocumentCollectionContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using Microsoft.Azure.Documents; 5 | using TodoService.Core.Models; 6 | 7 | namespace TodoService.Infrastructure.Data 8 | { 9 | public interface IDocumentCollectionContext where T : Entity 10 | { 11 | string CollectionName { get; } 12 | 13 | string GenerateId(T entity); 14 | 15 | PartitionKey ResolvePartitionKey(string entityId); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TodoService.Infrastructure/Data/TodoItemRepository.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license.using System 3 | 4 | using System; 5 | using Microsoft.Azure.Documents; 6 | using TodoService.Core.Interfaces; 7 | using TodoService.Core.Models; 8 | 9 | namespace TodoService.Infrastructure.Data 10 | { 11 | public class TodoItemRepository : CosmosDbRepository , ITodoItemRepository 12 | { 13 | public TodoItemRepository(ICosmosDbClientFactory factory) : base(factory) { } 14 | 15 | public override string CollectionName { get; } = "todoItems"; 16 | public override string GenerateId(TodoItem entity) => $"{entity.Category}:{Guid.NewGuid()}"; 17 | public override PartitionKey ResolvePartitionKey(string entityId) => new PartitionKey(entityId.Split(':')[0]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TodoService.Infrastructure/TodoService.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ..\..\..\..\.nuget\packages\microsoft.extensions.options\2.0.0\lib\netstandard2.0\Microsoft.Extensions.Options.dll 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /TodoService.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2015 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoService.Api", "TodoService.Api\TodoService.Api.csproj", "{57F1FAAA-E920-46F2-847A-9C2B2023DD74}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoService.Infrastructure", "TodoService.Infrastructure\TodoService.Infrastructure.csproj", "{9BA1A6DB-5332-409D-9E42-BBE228DA27D6}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoService.Api.UnitTests", "TodoService.Api.UnitTests\TodoService.Api.UnitTests.csproj", "{B51FA2DD-C067-4554-B646-AB5B773FAACC}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoService.Infrastructure.UnitTests", "TodoService.Infrastructure.UnitTests\TodoService.Infrastructure.UnitTests.csproj", "{EC3EFCF0-4132-480C-A629-4F018FDF27AB}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{72CD320A-0E35-47F4-A1BD-8C69489E64B6}" 15 | ProjectSection(SolutionItems) = preProject 16 | .editorconfig = .editorconfig 17 | global.json = global.json 18 | EndProjectSection 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoService.Core", "TodoService.Core\TodoService.Core.csproj", "{AA00ADFF-57DB-474A-B3AB-4AE17D70248B}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {57F1FAAA-E920-46F2-847A-9C2B2023DD74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {57F1FAAA-E920-46F2-847A-9C2B2023DD74}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {57F1FAAA-E920-46F2-847A-9C2B2023DD74}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {57F1FAAA-E920-46F2-847A-9C2B2023DD74}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {9BA1A6DB-5332-409D-9E42-BBE228DA27D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {9BA1A6DB-5332-409D-9E42-BBE228DA27D6}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {9BA1A6DB-5332-409D-9E42-BBE228DA27D6}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {9BA1A6DB-5332-409D-9E42-BBE228DA27D6}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {B51FA2DD-C067-4554-B646-AB5B773FAACC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {B51FA2DD-C067-4554-B646-AB5B773FAACC}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {B51FA2DD-C067-4554-B646-AB5B773FAACC}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {B51FA2DD-C067-4554-B646-AB5B773FAACC}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {EC3EFCF0-4132-480C-A629-4F018FDF27AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {EC3EFCF0-4132-480C-A629-4F018FDF27AB}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {EC3EFCF0-4132-480C-A629-4F018FDF27AB}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {EC3EFCF0-4132-480C-A629-4F018FDF27AB}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {AA00ADFF-57DB-474A-B3AB-4AE17D70248B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {AA00ADFF-57DB-474A-B3AB-4AE17D70248B}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {AA00ADFF-57DB-474A-B3AB-4AE17D70248B}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {AA00ADFF-57DB-474A-B3AB-4AE17D70248B}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {CB79937A-1420-46D1-92C5-D1218DDF13A1} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "2.1.300" 4 | } 5 | } --------------------------------------------------------------------------------