├── .github └── workflows │ └── fluentpos-cicd.yaml ├── .gitignore ├── README.md ├── fluentpos ├── .editorconfig ├── .env ├── FluentPos.sln ├── Makefile ├── README.md ├── docker-compose.infrastructure.yml ├── docker-compose.yml ├── events │ ├── CartCheckedOutEvent.cs │ ├── Events.csproj │ └── ProductCreatedEvent.cs ├── gateways │ └── Gateway │ │ ├── Gateway.csproj │ │ ├── Program.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.docker.json │ │ └── appsettings.json ├── services │ ├── cart │ │ ├── Api │ │ │ ├── Api.csproj │ │ │ ├── Controllers │ │ │ │ └── CartsController.cs │ │ │ ├── Program.cs │ │ │ ├── Properties │ │ │ │ └── launchSettings.json │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.docker.json │ │ │ └── appsettings.json │ │ ├── Application │ │ │ ├── Application.csproj │ │ │ ├── CartApplication.cs │ │ │ ├── Dtos │ │ │ │ ├── CheckoutCartRequestDto.cs │ │ │ │ └── UpdateCartRequestDto.cs │ │ │ ├── Endpoints.cs │ │ │ ├── Exceptions │ │ │ │ └── CartNotFoundException.cs │ │ │ ├── Features │ │ │ │ ├── CheckoutCart.cs │ │ │ │ ├── DeleteCart.cs │ │ │ │ ├── GetCart.cs │ │ │ │ └── UpdateCart.cs │ │ │ └── ICartRepository.cs │ │ ├── Domain │ │ │ ├── CartItem.cs │ │ │ ├── CustomerCart.cs │ │ │ └── Domain.csproj │ │ └── Infrastructure │ │ │ ├── Extensions.cs │ │ │ ├── Infrastructure.csproj │ │ │ └── Repositories │ │ │ └── CartRepository.cs │ ├── catalog │ │ ├── Api │ │ │ ├── Api.csproj │ │ │ ├── Controllers │ │ │ │ └── ProductsController.cs │ │ │ ├── Program.cs │ │ │ ├── Properties │ │ │ │ └── launchSettings.json │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.docker.json │ │ │ └── appsettings.json │ │ ├── Application │ │ │ ├── Application.csproj │ │ │ ├── CatalogApplication.cs │ │ │ ├── Consumers │ │ │ │ ├── CartCheckedOutEventConsumer.cs │ │ │ │ └── ProductCreatedEventConsumer.cs │ │ │ └── Products │ │ │ │ ├── Dtos │ │ │ │ ├── AddProductDto.cs │ │ │ │ ├── ProductDetailsDto.cs │ │ │ │ ├── ProductDto.cs │ │ │ │ ├── ProductsParametersDto.cs │ │ │ │ └── UpdateProductDto.cs │ │ │ │ ├── Exceptions │ │ │ │ └── ProductNotFoundException.cs │ │ │ │ ├── Features │ │ │ │ ├── AddProduct.cs │ │ │ │ ├── DeleteProduct.cs │ │ │ │ ├── GetProductDetails.cs │ │ │ │ ├── GetProducts.cs │ │ │ │ └── UpdateProduct.cs │ │ │ │ ├── IProductRepository.cs │ │ │ │ └── Mappings │ │ │ │ └── ProductMappings.cs │ │ ├── Domain │ │ │ ├── Domain.csproj │ │ │ └── Products │ │ │ │ ├── Product.cs │ │ │ │ └── ProductCreatedEvent.cs │ │ └── Infrastructure │ │ │ ├── Extensions.cs │ │ │ ├── Infrastructure.csproj │ │ │ └── Repositories │ │ │ └── ProductRepository.cs │ └── identity │ │ ├── Api │ │ ├── Api.csproj │ │ ├── Controllers │ │ │ ├── TokensController.cs │ │ │ └── UsersController.cs │ │ ├── Extensions │ │ │ └── AsyncEnumerableExtensions.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.docker.json │ │ └── appsettings.json │ │ ├── Application │ │ ├── Application.csproj │ │ ├── IdentityCore.cs │ │ └── Users │ │ │ ├── Dtos │ │ │ ├── AddUserDto.cs │ │ │ └── UserDto.cs │ │ │ ├── Exceptions │ │ │ └── UserRegistrationException.cs │ │ │ ├── Features │ │ │ └── AddUser.cs │ │ │ └── Mappings │ │ │ └── UserMappings.cs │ │ ├── Domain │ │ ├── Domain.csproj │ │ └── Users │ │ │ └── AppUser.cs │ │ └── Infrastructure │ │ ├── Extensions.cs │ │ ├── Infrastructure.csproj │ │ ├── Migrations │ │ ├── 20230429093516_initial.Designer.cs │ │ ├── 20230429093516_initial.cs │ │ └── AppDbContextModelSnapshot.cs │ │ └── Persistence │ │ ├── AppDbContext.cs │ │ ├── Constants.cs │ │ ├── IdentityConfiguration.cs │ │ └── SeedClientsAndScopes.cs └── tye.yaml ├── framework ├── Core │ ├── Caching │ │ └── ICacheService.cs │ ├── Database │ │ └── IRepository.cs │ ├── Domain │ │ ├── BaseEntity.cs │ │ └── IBaseEntity.cs │ ├── Events │ │ ├── DomainEvent.cs │ │ ├── IDomainEvent.cs │ │ ├── IEvent.cs │ │ ├── IEventPublisher.cs │ │ ├── IIntegrationEvent.cs │ │ └── IntegrationEvent.cs │ ├── Exceptions │ │ ├── ConfigurationMissingException.cs │ │ ├── CustomException.cs │ │ ├── ForbiddenException.cs │ │ ├── NotFoundException.cs │ │ └── UnauthorizedException.cs │ ├── FSH.Framework.Core.csproj │ ├── GlobalUsings.cs │ ├── Identity │ │ └── Constants.cs │ ├── Pagination │ │ ├── PagedList.cs │ │ └── PaginationParameters.cs │ ├── Serializers │ │ └── ISerializerService.cs │ ├── Services │ │ ├── IDateTimeService.cs │ │ ├── IScopedService.cs │ │ └── ITransientService.cs │ └── Validation │ │ └── CustomValidator.cs ├── Infrastructure │ ├── Auth │ │ ├── OpenId │ │ │ ├── Extensions.cs │ │ │ ├── HasScopeHandler.cs │ │ │ ├── HasScopeRequirement.cs │ │ │ └── OpenIdOptions.cs │ │ └── OpenIddict │ │ │ ├── Extensions.cs │ │ │ └── OpenIddictOptions.cs │ ├── Behaviors │ │ ├── Extensions.cs │ │ └── ValidationBehavior.cs │ ├── Caching │ │ ├── CachingOptions.cs │ │ ├── DistributedCacheService.cs │ │ ├── Extensions.cs │ │ └── InMemoryCacheService.cs │ ├── Controllers │ │ └── BaseApiController.cs │ ├── Extensions.cs │ ├── FSH.Framework.Infrastructure.csproj │ ├── Logging │ │ └── Serilog │ │ │ ├── Extensions.cs │ │ │ └── SerilogOptions.cs │ ├── Mapping │ │ └── Mapster │ │ │ └── Extensions.cs │ ├── Messaging │ │ ├── EventPublisher.cs │ │ └── Extensions.cs │ ├── Middlewares │ │ ├── ExceptionDetails.cs │ │ ├── ExceptionMiddleware.cs │ │ └── Extensions.cs │ ├── Options │ │ ├── AppOptions.cs │ │ ├── Extensions.cs │ │ └── IOptionsRoot.cs │ ├── Serializers │ │ └── NewtonSoftService.cs │ ├── Services │ │ ├── DateTimeService.cs │ │ └── Extensions.cs │ └── Swagger │ │ ├── Extensions.cs │ │ └── SwaggerOptions.cs ├── Persistence.EntityFrameworkCore │ └── FSH.Framework.Persistence.EntityFrameworkCore.csproj └── Persistence.NoSQL │ ├── Extensions.cs │ ├── FSH.Framework.Persistence.Mongo.csproj │ ├── IMongoDbContext.cs │ ├── MongoDbContext.cs │ ├── MongoOptions.cs │ ├── MongoRepository.cs │ └── QueryableExtensions.cs └── thunder-tests ├── thunderActivity.json ├── thunderCollection.json ├── thunderEnvironment.json └── thunderclient.json /.github/workflows/fluentpos-cicd.yaml: -------------------------------------------------------------------------------- 1 | name: fluentpos 2 | on: 3 | push: 4 | branches: [ main ] 5 | workflow_dispatch: 6 | inputs: 7 | build_gateway_image: 8 | type: boolean 9 | description: push gateway image 10 | build_identity_image: 11 | type: boolean 12 | description: push identity image 13 | build_catalog_image: 14 | type: boolean 15 | description: push catalog image 16 | build_cart_image: 17 | type: boolean 18 | description: push cart image 19 | jobs: 20 | build: 21 | name: Build 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Setup .NET 26 | uses: actions/setup-dotnet@v3 27 | with: 28 | dotnet-version: 7.0.x 29 | - name: Restore dependencies 30 | working-directory: ./fluentpos/ 31 | run: dotnet restore 32 | - name: Build 33 | working-directory: ./fluentpos/ 34 | run: dotnet build --no-restore 35 | - name: Test 36 | working-directory: ./fluentpos/ 37 | run: dotnet test --no-build --verbosity normal 38 | docker: 39 | name: Docker 40 | needs: build 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v3 45 | - name: Docker Login 46 | uses: docker/login-action@v2 47 | with: 48 | username: ${{ secrets.DOCKER_USERNAME }} 49 | password: ${{ secrets.DOCKER_PASSWORD }} 50 | - if: github.event.inputs.build_gateway_image == 'true' || github.ref == 'refs/heads/main' 51 | name: Build Gateway Image 52 | working-directory: ./ 53 | run: | 54 | dotnet publish fluentpos/gateways/Gateway/Gateway.csproj --os linux --arch x64 -c Release -p:PublishProfile=DefaultContainer -p:ContainerImageName=${{ secrets.DOCKER_USERNAME }}/fluentpos.gateway 55 | docker push ${{ secrets.DOCKER_USERNAME }}/fluentpos.gateway --all-tags 56 | 57 | - if: github.event.inputs.build_identity_image == 'true' || github.ref == 'refs/heads/main' 58 | name: Build Identity Server Image 59 | working-directory: ./ 60 | run: | 61 | dotnet publish fluentpos/services/identity/Api/Api.csproj --os linux --arch x64 -c Release -p:PublishProfile=DefaultContainer -p:ContainerImageName=${{ secrets.DOCKER_USERNAME }}/fluentpos.identity 62 | docker push ${{ secrets.DOCKER_USERNAME }}/fluentpos.identity --all-tags 63 | 64 | - if: github.event.inputs.build_catalog_image == 'true' || github.ref == 'refs/heads/main' 65 | name: Build Catalog Service Image 66 | working-directory: ./ 67 | run: | 68 | dotnet publish fluentpos/services/catalog/Api/Api.csproj --os linux --arch x64 -c Release -p:PublishProfile=DefaultContainer -p:ContainerImageName=${{ secrets.DOCKER_USERNAME }}/fluentpos.catalog 69 | docker push ${{ secrets.DOCKER_USERNAME }}/fluentpos.catalog --all-tags 70 | 71 | - if: github.event.inputs.build_cart_image == 'true' || github.ref == 'refs/heads/main' 72 | name: Build Cart Service Image 73 | working-directory: ./ 74 | run: | 75 | dotnet publish fluentpos/services/cart/Api/Api.csproj --os linux --arch x64 -c Release -p:PublishProfile=DefaultContainer -p:ContainerImageName=${{ secrets.DOCKER_USERNAME }}/fluentpos.cart 76 | docker push ${{ secrets.DOCKER_USERNAME }}/fluentpos.cart --all-tags -------------------------------------------------------------------------------- /.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/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | 403 | ## 404 | ## Visual studio for Mac 405 | ## 406 | 407 | 408 | # globs 409 | Makefile.in 410 | *.userprefs 411 | *.usertasks 412 | config.make 413 | config.status 414 | aclocal.m4 415 | install-sh 416 | autom4te.cache/ 417 | *.tar.gz 418 | tarballs/ 419 | test-results/ 420 | 421 | # Mac bundle stuff 422 | *.dmg 423 | *.app 424 | 425 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 426 | # General 427 | .DS_Store 428 | .AppleDouble 429 | .LSOverride 430 | 431 | # Icon must end with two \r 432 | Icon 433 | 434 | 435 | # Thumbnails 436 | ._* 437 | 438 | # Files that might appear in the root of a volume 439 | .DocumentRevisions-V100 440 | .fseventsd 441 | .Spotlight-V100 442 | .TemporaryItems 443 | .Trashes 444 | .VolumeIcon.icns 445 | .com.apple.timemachine.donotpresent 446 | 447 | # Directories potentially created on remote AFP share 448 | .AppleDB 449 | .AppleDesktop 450 | Network Trash Folder 451 | Temporary Items 452 | .apdisk 453 | 454 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 455 | # Windows thumbnail cache files 456 | Thumbs.db 457 | ehthumbs.db 458 | ehthumbs_vista.db 459 | 460 | # Dump file 461 | *.stackdump 462 | 463 | # Folder config file 464 | [Dd]esktop.ini 465 | 466 | # Recycle Bin used on file shares 467 | $RECYCLE.BIN/ 468 | 469 | # Windows Installer files 470 | *.cab 471 | *.msi 472 | *.msix 473 | *.msm 474 | *.msp 475 | 476 | # Windows shortcuts 477 | *.lnk 478 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # .NET Microservices Boilerplate 2 | 3 | [![fluentpos](https://github.com/fullstackhero/dotnet-microservices-boilerplate/actions/workflows/fluentpos-cicd.yaml/badge.svg?branch=main)](https://github.com/fullstackhero/dotnet-microservices-boilerplate/actions/workflows/fluentpos-cicd.yaml) 4 | 5 | The goal is to build a framework that can make building microservices in .NET easy for developers. This project follows Vertical Slice Architecture along with other latest best practices and tools like CQRS, NoSQL, SQL, MediatR, Serilog, FluentValidations and more.🚀 6 | 7 | # Table of Contents 8 | 9 | - [.NET Microservices Boilerplate](#net-microservices-boilerplate) 10 | - [Table of Contents](#table-of-contents) 11 | - [Goals](#goals) 12 | - [FluentPos](#fluentpos) 13 | - [How to Run ?](#how-to-run-) 14 | - [Tye](#tye) 15 | - [Docker \& Docker-Compose](#docker--docker-compose) 16 | - [Technologies \& Libraries](#technologies--libraries) 17 | - [Documentation](#documentation) 18 | - [Changelogs](#changelogs) 19 | - [Community](#community) 20 | - [License](#license) 21 | - [Support ⭐](#support-) 22 | - [Code Contributors](#code-contributors) 23 | - [Financial Contributors](#financial-contributors) 24 | 25 | 26 | ## Goals 27 | 28 | - :sparkle: Using `Vertical Slice Architecture` for architecture level. 29 | - :sparkle: Using `Domain Driven Design (DDD)` to implement all business processes in microservices. 30 | - :sparkle: Using `Rabbitmq` on top of `MassTranit` for `Event Driven Architecture` between our microservices. 31 | - :sparkle: Using `CQRS` implementation with `MediatR` library. 32 | - :sparkle: Using `Entity Framework Core` for some microservices. 33 | - :sparkle: Using `MongoDB` for some microservices. 34 | - :sparkle: Using `Fluent Validation` and a `Validation Pipeline Behaviour` on top of `MediatR`. 35 | - :sparkle: Using `Minimal API` for all endpoints. 36 | - :sparkle: Using `Health Check` for reporting the health of app infrastructure components. 37 | - :sparkle: Using `Tye` for local development and debugging. 38 | - :sparkle: Using `Built-In Containerization` for `Docker` images. 39 | - :sparkle: Using `Zipkin` for distributed tracing. 40 | - :sparkle: Using `OpenIddict` for authentication and authorization base on `OpenID-Connect` and `OAuth2`. 41 | - :sparkle: Using `Yarp` as a microservices gateway. 42 | 43 | ## FluentPos 44 | 45 | FluentPos is a sample project that consumes the microservice framework. You will learn a lot by exploring this project, which is located under the `./fluentpos` folder. 46 | 47 | 48 | | Services | Status | 49 | | ----------------- | -------------- | 50 | | Gateway | Completed ✔️ | 51 | | Identity | Completed ✔️ | 52 | | Catalog | Completed ✔️ | 53 | | Cart | WIP 🚧 | 54 | | People | WIP 🚧 | 55 | | Ordering | WIP 🚧 | 56 | | Payment | WIP 🚧 | 57 | 58 | ## How to Run ? 59 | 60 | ### Tye 61 | Tye is a super-awesome way to run your applications quickly. The `fluentpos` project already has this support. Simply run the following at the `./fluentpos` directory : 62 | 63 | ``` 64 | make tye 65 | ``` 66 | 67 | That's it! 68 | 69 | This will spin up all the services required. 70 | - Gateway will be available on `https://localhost:7002`. 71 | - Identity Service will be available on `https://localhost:7001`. 72 | - Catalog Service will be available on `https://localhost:7003`. 73 | 74 | To Test these APIs, you can use open up Visual Code from the `./fluentpos` directory, install the `Thunder Client` extension. I have already included the required Test collections at `./fluentpos/thunder-tests`. 75 | 76 | > You can find the specification of services under the ./fluentpos/tye.yaml file. 77 | ### Docker & Docker-Compose 78 | The `fluentpos` project comes included with the required docker-compose.yaml and makefile file for your reference. 79 | 80 | There are some prerequisites for using these included docker-compose.yml files: 81 | 82 | 1) Make sure you have docker installed (on windows install docker desktop) 83 | 84 | 2) Create and install an https certificate: 85 | 86 | ``` 87 | dotnet dev-certs https -ep $env:USERPROFILE\.aspnet\https\cert.pfx -p password! 88 | ``` 89 | 90 | Note that the certificate name and password should match the ones that are mentioned in the docker-compose.yaml file. 91 | 92 | 3) It's possible that the above step gives you an `A valid HTTPS certificate is already present` error. 93 | In that case you will have to run the following command, and then `Re-Run Step #2`. 94 | 95 | ``` 96 | dotnet dev-certs https --clean 97 | ``` 98 | 99 | 4) Trust the certificate 100 | 101 | ``` 102 | dotnet dev-certs https --trust 103 | ``` 104 | Once your certificate is trusted, simply navigate into the `./fluentpos` folder of the project and run the following command. 105 | 106 | ``` 107 | make docker-up 108 | ``` 109 | 110 | This will spin up all the containers required. Your Gateway URL will be available on `https://localhost:7002`. 111 | 112 | To bring down all the `fluentpos` container, simply run the following. 113 | 114 | ``` 115 | make docker-down 116 | ``` 117 | 118 | *Note that the default Docker Images that will be pulled are from my public Image Repository (for eg, `iammukeshm/fluentpos.identity:latest`). You can switch it your variants if required.* 119 | 120 | ## Technologies & Libraries 121 | 122 | - **[`.NET 7`](https://dotnet.microsoft.com/download)** - .NET Framework and .NET Core, including ASP.NET and ASP.NET Core 123 | - **[`MVC Versioning API`](https://github.com/microsoft/aspnet-api-versioning)** - Set of libraries which add service API versioning to ASP.NET Web API, OData with ASP.NET Web API, and ASP.NET Core 124 | - **[`EF Core`](https://github.com/dotnet/efcore)** - Modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations 125 | - **[`MediatR`](https://github.com/jbogard/MediatR)** - Simple, unambitious mediator implementation in .NET. 126 | - **[`FluentValidation`](https://github.com/FluentValidation/FluentValidation)** - Popular .NET validation library for building strongly-typed validation rules 127 | - **[`Swagger & Swagger UI`]()** - Swagger tools for documenting API's built on ASP.NET Core 128 | - **[`Serilog`](https://github.com/serilog/serilog)** - Simple .NET logging with fully-structured events 129 | - **[`OpenIddict`](https://github.com/openiddict/openiddict-core)** - OpenIddict aims at providing a versatile solution to implement OpenID Connect client, server and token validation support. 130 | - **[`Mapster`](https://github.com/MapsterMapper/Mapster)** - Convention-based object-object mapper in .NET. 131 | - **[`Yarp`](https://github.com/microsoft/reverse-proxy)** - Reverse proxy toolkit for building fast proxy servers in .NET 132 | - **[`Tye`](https://github.com/dotnet/tye)** - Developer tool that makes developing, testing, and deploying microservices and distributed applications easier. 133 | - **[`MongoDB.Driver`](https://github.com/mongodb/mongo-csharp-driver)** - .NET Driver for MongoDB. 134 | 135 | ## Documentation 136 | 137 | Read Documentation related to this Boilerplate here - https://fullstackhero.net/dotnet-microservices-boilerplate/ 138 | > Feel free to contribute to the Documentation Repository - https://github.com/fullstackhero/docs 139 | > Docs are not yet updated. 140 | 141 | ## Changelogs 142 | 143 | [View Complete Changelogs.](https://github.com/fullstackhero/dotnet-microservices-boilerplate/blob/main/CHANGELOGS.md) 144 | 145 | ## Community 146 | 147 | - Discord [@fullstackhero](https://discord.gg/gdgHRt4mMw) 148 | - Facebook Page [@codewithmukesh](https://facebook.com/codewithmukesh) 149 | - Youtube Channel [@codewithmukesh](https://youtube.com/c/codewithmukesh) 150 | 151 | ## License 152 | 153 | This project is licensed with the [MIT license](LICENSE). 154 | 155 | 156 | ## Support ⭐ 157 | 158 | Has this Project helped you learn something New? or Helped you at work? 159 | Here are a few ways by which you can support. 160 | 161 | - Leave a star! ⭐ 162 | - Recommend this awesome project to your colleagues. 🥇 163 | - Do consider endorsing me on LinkedIn for ASP.NET Core - [Connect via LinkedIn](https://codewithmukesh.com/linkedin) 🦸 164 | - Sponsor the project - [opencollective/fullstackhero](https://opencollective.com/fullstackhero) ❤️ 165 | - Or, [consider buying me a coffee](https://www.buymeacoffee.com/codewithmukesh)! ☕ 166 | 167 | 168 | ## Code Contributors 169 | 170 | This project exists thanks to all the people who contribute. [Submit your PR and join the elite list!](CONTRIBUTING.md) 171 | 172 | [![fsh dotnet microservices contributors](https://contrib.rocks/image?repo=fullstackhero/dotnet-microservices-boilerplate "fsh dotnet microservices contributors")](https://github.com/fullstackhero/dotnet-microservices-boilerplate/graphs/contributors) 173 | 174 | 175 | ## Financial Contributors 176 | 177 | Become a financial contributor and help me sustain the project. [Support the Project!](https://opencollective.com/fullstackhero/contribute) 178 | 179 | 180 | -------------------------------------------------------------------------------- /fluentpos/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | roslynator_accessibility_modifiers = explicit 5 | roslynator_use_anonymous_function_or_method_group = anonymous_function|method_group 6 | roslynator_enum_has_flag_style = method 7 | roslynator_object_creation_type_style = explicit|implicit|implicit_when_type_is_obvious 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | insert_final_newline = false 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.json] 16 | indent_size = 2 17 | 18 | [*.cs] 19 | dotnet_sort_system_directives_first = true:warning 20 | csharp_style_namespace_declarations = file_scoped 21 | csharp_style_var_for_built_in_types = false:warning 22 | csharp_style_var_when_type_is_apparent = true:warning 23 | csharp_style_var_elsewhere = true:warning 24 | csharp_new_line_before_members_in_anonymous_types = true:warning 25 | 26 | # Asynchronous method name should end with 'Async'. 27 | dotnet_diagnostic.RCS1046.severity = none 28 | 29 | dotnet_diagnostic.RCS1194.severity = none 30 | 31 | # SA1623: Property summary documentation should match accessors 32 | dotnet_diagnostic.SA1623.severity = none 33 | 34 | # SA1101: Prefix local calls with this 35 | dotnet_diagnostic.SA1101.severity = none 36 | 37 | # SA1642: Constructor summary documentation should begin with standard text 38 | dotnet_diagnostic.SA1642.severity = none 39 | 40 | # SA1309: Field names should not begin with underscore 41 | dotnet_diagnostic.SA1309.severity = none 42 | 43 | # RCS1194: Implement exception constructors. 44 | dotnet_diagnostic.RCS1194.severity = none 45 | 46 | # SA1000: Keywords should be spaced correctly 47 | dotnet_diagnostic.SA1000.severity = none 48 | 49 | # SA1124: Do not use regions 50 | dotnet_diagnostic.SA1124.severity = none 51 | 52 | # SA1413: Use trailing comma in multi-line initializers 53 | dotnet_diagnostic.SA1413.severity = none 54 | 55 | # SA1201: Elements should appear in the correct order 56 | dotnet_diagnostic.SA1201.severity = suggestion 57 | 58 | # SA1638: File header file name documentation should match file name 59 | dotnet_diagnostic.SA1638.severity = warning 60 | 61 | # SA1633: File should have header 62 | dotnet_diagnostic.SA1633.severity = none 63 | 64 | # SA1404: Code analysis suppression should have justification 65 | dotnet_diagnostic.SA1404.severity = none 66 | 67 | # SA1206: Declaration keywords should follow order 68 | dotnet_diagnostic.SA1206.severity = none 69 | 70 | # CA1040: Avoid empty interfaces 71 | dotnet_diagnostic.CA1040.severity = none 72 | 73 | # RCS1012: Use explicit type instead of 'var' 74 | dotnet_diagnostic.RCS1012.severity = none 75 | 76 | # RCS1008: Use explicit type instead of 'var' 77 | dotnet_diagnostic.RCS1008.severity = none 78 | 79 | # CA1725 80 | dotnet_diagnostic.CA1725.severity = none 81 | 82 | # RCS1009: Use explicit type instead of 'var' 83 | dotnet_diagnostic.RCS1009.severity = none 84 | 85 | # SA1402: File may only contain a single type 86 | dotnet_diagnostic.SA1402.severity = suggestion 87 | 88 | # CA1711 89 | dotnet_diagnostic.CA1711.severity = none 90 | 91 | # CA1720: Identifier contains type name 92 | dotnet_diagnostic.CA1720.severity = none 93 | 94 | # IDE0022: Use block body for methods 95 | dotnet_diagnostic.IDE0022.severity = none 96 | 97 | # SA1011: Closing square brackets should be spaced correctly 98 | dotnet_diagnostic.SA1011.severity = none 99 | 100 | # CA1721 101 | dotnet_diagnostic.CA1721.severity = none 102 | 103 | # SA1313: Parameter names should begin with lower-case letter 104 | dotnet_diagnostic.SA1313.severity = none 105 | 106 | # SecurityIntelliSenseCS: MS Security rules violation 107 | dotnet_diagnostic.SecurityIntelliSenseCS.severity = suggestion 108 | 109 | # SA1123: Do not place regions within elements 110 | dotnet_diagnostic.SA1123.severity = none 111 | 112 | # RCS1046: Add suffix 'Async' to asynchronous method name 113 | dotnet_diagnostic.RCS1046.severity = warning 114 | 115 | # SA1625: Element documentation should not be copied and pasted 116 | dotnet_diagnostic.SA1625.severity = none 117 | 118 | # SCS9999 119 | dotnet_diagnostic.SCS9999.severity = none 120 | 121 | # RCS1090 Add call to 'ConfigureAwait' 122 | dotnet_diagnostic.RCS1090.severity = none 123 | 124 | # RCS1170 Use read-only auto-implemented property 125 | dotnet_diagnostic.RCS1170.severity = none 126 | 127 | # SA1649 128 | dotnet_diagnostic.SA1649.severity = none 129 | 130 | # RCS1021 Use expression-bodied lambda. 131 | dotnet_diagnostic.RCS1021.severity = none 132 | 133 | # RCS1047 Remove suffix 'Async' from non-asynchronous method name. 134 | # dotnet_diagnostic.RCS1047.severity = silent 135 | 136 | # SA1600 Elements should be documented 137 | dotnet_diagnostic.SA1600.severity = silent 138 | 139 | # CS1591 Missing XML comment for publicly visible type or member 140 | dotnet_diagnostic.CS1591.severity = none 141 | 142 | # SA1602: Enumeration items should be documented 143 | dotnet_diagnostic.SA1602.severity = none 144 | 145 | # CA1720 Identifier 'Decimal' contains type name 146 | dotnet_diagnostic.CA1720.severity = none 147 | 148 | # SA1601: Partial elements should be documented 149 | dotnet_diagnostic.SA1601.severity = silent 150 | 151 | # CA1711 Rename type name UserEventHandler so that it does not end in 'EventHandler' 152 | dotnet_diagnostic.CA1711.severity = none 153 | 154 | # CA1307: Specify StringComparison for clarity 155 | dotnet_diagnostic.CA1307.severity = none 156 | -------------------------------------------------------------------------------- /fluentpos/.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhero/dotnet-microservices-boilerplate/3f506d7ebf023aee63c73fd34bd198ad5972e31e/fluentpos/.env -------------------------------------------------------------------------------- /fluentpos/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @dotnet build /ll 3 | 4 | tye: 5 | @tye run 6 | 7 | docker-up: 8 | @docker-compose -f docker-compose.infrastructure.yml -f docker-compose.yml pull 9 | @docker-compose -f docker-compose.infrastructure.yml -f docker-compose.yml up -d 10 | 11 | docker-down: 12 | @docker-compose -f docker-compose.infrastructure.yml -f docker-compose.yml down 13 | 14 | dotnet-publish: 15 | @cd ./gateways/gateway && dotnet publish --os linux --arch x64 -c Release --self-contained 16 | @cd ./services/catalog/api && dotnet publish --os linux --arch x64 -c Release --self-contained 17 | @cd ./services/identity/api && dotnet publish --os linux --arch x64 -c Release --self-contained 18 | @cd ./services/cart/api && dotnet publish --os linux --arch x64 -c Release --self-contained -------------------------------------------------------------------------------- /fluentpos/README.md: -------------------------------------------------------------------------------- 1 | ## FluentPos 2 | 3 | FluentPos is a sample project that consumes the microservice framework. You will learn a lot by exploring this project, which is located under the `./fluentpos` folder. 4 | 5 | 6 | | Services | Status | 7 | | ----------------- | -------------- | 8 | | Gateway | Completed ✔️ | 9 | | Identity | Completed ✔️ | 10 | | Catalog | Completed ✔️ | 11 | | Cart | WIP 🚧 | 12 | | Ordering | WIP 🚧 | 13 | | Payment | WIP 🚧 | 14 | 15 | ## How to Run ? 16 | 17 | ### Tye 18 | Tye is a super-awesome way to run your applications quickly. The `fluentpos` project already has this support. Simply run the following at the `./fluentpos` directory : 19 | 20 | ``` 21 | make tye 22 | ``` 23 | 24 | That's it! 25 | 26 | This will spin up all the services required. 27 | - Gateway will be available on `https://localhost:7002`. 28 | - Identity Service will be available on `https://localhost:7001`. 29 | - Catalog Service will be available on `https://localhost:7003`. 30 | 31 | To Test these APIs, you can use open up Visual Code from the `./fluentpos` directory, install the `Thunder Client` extension. I have already included the required Test collections at `./fluentpos/thunder-tests`. 32 | 33 | > You can find the specification of services under the ./fluentpos/tye.yaml file. 34 | ### Docker & Docker-Compose 35 | The `fluentpos` project comes included with the required docker-compose.yaml and makefile file for your reference. 36 | 37 | There are some prerequisites for using these included docker-compose.yml files: 38 | 39 | 1) Make sure you have docker installed (on windows install docker desktop) 40 | 41 | 2) Create and install an https certificate: 42 | 43 | ``` 44 | dotnet dev-certs https -ep $env:USERPROFILE\.aspnet\https\cert.pfx -p password! 45 | ``` 46 | 47 | Note that the certificate name and password should match the ones that are mentioned in the docker-compose.yaml file. 48 | 49 | 3) It's possible that the above step gives you an `A valid HTTPS certificate is already present` error. 50 | In that case you will have to run the following command, and then `Re-Run Step #2`. 51 | 52 | ``` 53 | dotnet dev-certs https --clean 54 | ``` 55 | 56 | 4) Trust the certificate 57 | 58 | ``` 59 | dotnet dev-certs https --trust 60 | ``` 61 | Once your certificate is trusted, simply navigate into the `./fluentpos` folder of the project and run the following command. 62 | 63 | ``` 64 | make docker-up 65 | ``` 66 | 67 | This will spin up all the containers required. Your Gateway URL will be available on `https://localhost:7002`. 68 | 69 | To bring down all the `fluentpos` container, simply run the following. 70 | 71 | ``` 72 | make docker-down 73 | ``` 74 | 75 | *Note that the default Docker Images that will be pulled are from my public Image Repository (for eg, `iammukeshm/fluentpos.identity:latest`). You can switch it your variants if required.* -------------------------------------------------------------------------------- /fluentpos/docker-compose.infrastructure.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | name: fluentpos 3 | 4 | services: 5 | postgres: 6 | container_name: postgres 7 | image: postgres:15-alpine 8 | environment: 9 | - POSTGRES_USER=postgres 10 | - POSTGRES_PASSWORD=admin 11 | - PGPORT=5430 12 | ports: 13 | - 5430:5430 14 | volumes: 15 | - postgres-data:/data/db 16 | healthcheck: 17 | test: ["CMD-SHELL", "pg_isready -U postgres"] 18 | interval: 10s 19 | timeout: 5s 20 | retries: 5 21 | networks: 22 | - fluentpos 23 | 24 | mongo: 25 | image: mongo 26 | container_name: mongo 27 | restart: on-failure 28 | ports: 29 | - 27018:27017 30 | volumes: 31 | - mongo-data:/data/db 32 | networks: 33 | - fluentpos 34 | 35 | redis: 36 | container_name: redis 37 | environment: 38 | - ALLOW_EMPTY_PASSWORD=yes 39 | - DISABLE_COMMANDS=FLUSHDB,FLUSHALL,CONFIG 40 | image: redis:alpine 41 | ports: 42 | - 6380:6379 43 | volumes: 44 | - redis-data:/data/redis 45 | networks: 46 | - fluentpos 47 | 48 | rabbitmq: 49 | container_name: rabbitmq 50 | image: rabbitmq:3-management-alpine 51 | volumes: 52 | - rabbitmq-data:/data/rabbitmq 53 | networks: 54 | - fluentpos 55 | 56 | # elk: 57 | # container_name: elasticsearch-kibana 58 | # image: sebp/elk:oss-8.5.3 59 | # environment: 60 | # - LOGSTASH_START=0 61 | # volumes: 62 | # - sebp-elk-data:/usr/share/elk/data 63 | # ports: 64 | # - 5601:5601 65 | # - 9200:9200 66 | # networks: 67 | # - fluentpos 68 | 69 | volumes: 70 | postgres-data: 71 | redis-data: 72 | rabbitmq-data: 73 | mongo-data: 74 | sebp-elk-data: 75 | 76 | networks: 77 | fluentpos: 78 | name: fluentpos 79 | -------------------------------------------------------------------------------- /fluentpos/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | name: fluentpos 3 | 4 | services: 5 | gateway: 6 | container_name: gateway 7 | image: iammukeshm/fluentpos.gateway:latest 8 | ports: 9 | - 7002:7002 10 | - 5002:5002 11 | environment: 12 | - ASPNETCORE_ENVIRONMENT=docker 13 | - ASPNETCORE_URLS=https://+:7002;http://+:5002 14 | - ASPNETCORE_HTTPS_PORT=7002 15 | - ASPNETCORE_Kestrel__Certificates__Default__Password=password! 16 | - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/cert.pfx 17 | volumes: 18 | - ~/.aspnet/https:/https:ro 19 | networks: 20 | - fluentpos 21 | identity: 22 | container_name: identity 23 | image: iammukeshm/fluentpos.identity:latest 24 | environment: 25 | - ASPNETCORE_ENVIRONMENT=docker 26 | depends_on: 27 | postgres: 28 | condition: service_healthy 29 | networks: 30 | - fluentpos 31 | catalog: 32 | container_name: catalog 33 | image: iammukeshm/fluentpos.catalog:latest 34 | depends_on: 35 | - mongo 36 | environment: 37 | - ASPNETCORE_ENVIRONMENT=docker 38 | networks: 39 | - fluentpos 40 | cart: 41 | container_name: cart 42 | image: iammukeshm/fluentpos.cart:latest 43 | environment: 44 | - ASPNETCORE_ENVIRONMENT=docker 45 | networks: 46 | - fluentpos -------------------------------------------------------------------------------- /fluentpos/events/CartCheckedOutEvent.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Events; 2 | 3 | namespace FluentPos.Shared.Events; 4 | public class CartCheckedOutEvent : IntegrationEvent 5 | { 6 | public Guid CustomerId { get; } 7 | public string CreditCardNumber { get; } 8 | 9 | public CartCheckedOutEvent(Guid customerId, string creditCardNumber) 10 | { 11 | CustomerId = customerId; 12 | CreditCardNumber = creditCardNumber; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /fluentpos/events/Events.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | FluentPos.Shared.Events 6 | FluentPos.Shared.Events 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /fluentpos/events/ProductCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Events; 2 | 3 | namespace FluentPos.Shared.Events; 4 | 5 | public class ProductCreatedEvent : DomainEvent 6 | { 7 | public Guid ProductId { get; } 8 | public string ProductName { get; } 9 | 10 | public ProductCreatedEvent(Guid productId, string productName) 11 | { 12 | ProductId = productId; 13 | ProductName = productName; 14 | } 15 | } -------------------------------------------------------------------------------- /fluentpos/gateways/Gateway/Gateway.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | FluentPos.Gateway 6 | FluentPos.Gateway 7 | enable 8 | enable 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | fluentpos.gateway 20 | 1.0.0;latest 21 | DefaultContainer 22 | 23 | 24 | -------------------------------------------------------------------------------- /fluentpos/gateways/Gateway/Program.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure; 2 | using FSH.Framework.Infrastructure.Auth.OpenId; 3 | using Microsoft.AspNetCore.Authentication; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | bool enableSwagger = false; 7 | 8 | var policyNames = new List 9 | { 10 | "catalog:read", 11 | "catalog:write", 12 | "cart:read", 13 | "cart:write" 14 | }; 15 | 16 | builder.Services.AddOpenIdAuth(builder.Configuration, policyNames); 17 | 18 | builder.AddInfrastructure(enableSwagger: enableSwagger); 19 | builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); 20 | var app = builder.Build(); 21 | app.UseInfrastructure(builder.Environment, enableSwagger: enableSwagger); 22 | app.MapGet("/", () => "Hello From Gateway"); 23 | app.UseRouting(); 24 | app.MapReverseProxy(config => 25 | { 26 | config.Use(async (context, next) => 27 | { 28 | string? token = await context.GetTokenAsync("access_token"); 29 | context.Request.Headers["Authorization"] = $"Bearer {token}"; 30 | 31 | await next().ConfigureAwait(false); 32 | }); 33 | }); 34 | 35 | app.Run(); -------------------------------------------------------------------------------- /fluentpos/gateways/Gateway/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": false, 8 | "applicationUrl": "https://localhost:7002;http://localhost:5002", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /fluentpos/gateways/Gateway/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "OpenIdOptions": { 3 | "Authority": "https://localhost:7001/", 4 | "Audience": "gateway.resource.server" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fluentpos/gateways/Gateway/appsettings.docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "OpenIdOptions": { 3 | "Authority": "http://identity/", 4 | "Audience": "gateway.resource.server" 5 | }, 6 | "ReverseProxy": { 7 | "clusters": { 8 | "catalog": { 9 | "destinations": { 10 | "catalog": { 11 | "address": "http://catalog" 12 | } 13 | } 14 | }, 15 | "identity": { 16 | "destinations": { 17 | "identity": { 18 | "address": "http://identity" 19 | } 20 | } 21 | }, 22 | "cart": { 23 | "destinations": { 24 | "cart": { 25 | "address": "http://cart" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /fluentpos/gateways/Gateway/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "SerilogOptions": { 4 | "WriteToFile": true, 5 | "StructuredConsoleLogging": false 6 | }, 7 | "AppOptions": { 8 | "Name": "Gateway" 9 | }, 10 | "CachingOptions": { 11 | "EnableDistributedCaching": false, 12 | "SlidingExpirationInMinutes": 5, 13 | "AbsoluteExpirationInMinutes": 10 14 | }, 15 | "OpenIdOptions": { 16 | "Authority": "https://localhost:7001/", 17 | "Audience": "gateway.resource.server" 18 | }, 19 | "ReverseProxy": { 20 | "routes": { 21 | "catalog": { 22 | "clusterId": "catalog", 23 | "match": { 24 | "path": "/api/catalog/{**catch-all}" 25 | }, 26 | "transforms": [ 27 | { 28 | "pathPattern": "{**catch-all}" 29 | } 30 | ] 31 | }, 32 | "identity": { 33 | "clusterId": "identity", 34 | "match": { 35 | "path": "/api/identity/{**catch-all}" 36 | }, 37 | "transforms": [ 38 | { 39 | "pathPattern": "{**catch-all}" 40 | } 41 | ] 42 | }, 43 | "cart": { 44 | "clusterId": "cart", 45 | "match": { 46 | "path": "/api/cart/{**catch-all}" 47 | }, 48 | "transforms": [ 49 | { 50 | "pathPattern": "{**catch-all}" 51 | } 52 | ] 53 | } 54 | }, 55 | "clusters": { 56 | "catalog": { 57 | "destinations": { 58 | "catalog": { 59 | "address": "https://localhost:7003" 60 | } 61 | } 62 | }, 63 | "identity": { 64 | "destinations": { 65 | "identity": { 66 | "address": "https://localhost:7001" 67 | } 68 | } 69 | }, 70 | "cart": { 71 | "destinations": { 72 | "cart": { 73 | "address": "https://localhost:7004" 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Api/Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | FluentPos.Cart.Api 6 | FluentPos.Cart.Api 7 | enable 8 | enable 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | fluentpos.cart 19 | 1.0.0;latest 20 | DefaultContainer 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Api/Controllers/CartsController.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Controllers; 2 | 3 | namespace FluentPos.Cart.Api.Controllers; 4 | 5 | public class CartsController : BaseApiController 6 | { 7 | //private readonly ILogger _logger; 8 | 9 | //public CartsController(ILogger logger) 10 | //{ 11 | // _logger = logger; 12 | //} 13 | 14 | //[HttpGet("/{id:guid}", Name = nameof(GetCartAsync))] 15 | //[Authorize("cart:read")] 16 | //[ProducesResponseType((int)HttpStatusCode.OK)] 17 | //[ProducesResponseType((int)HttpStatusCode.BadRequest)] 18 | //[ProducesResponseType(200, Type = typeof(CustomerCart))] 19 | //public async Task GetCartAsync(Guid id) 20 | //{ 21 | // var query = new GetCart.Query(id); 22 | // var response = await Mediator.Send(query); 23 | 24 | // return Ok(response); 25 | //} 26 | 27 | //[HttpPut("/{id:guid}", Name = nameof(UpdateCartAsync))] 28 | //[Authorize("cart:write")] 29 | //[ProducesResponseType((int)HttpStatusCode.OK)] 30 | //[ProducesResponseType((int)HttpStatusCode.BadRequest)] 31 | //public async Task UpdateCartAsync(Guid id, UpdateCartRequestDto updateRequest) 32 | //{ 33 | // var command = new UpdateCart.Command(updateRequest, id); 34 | // var response = await Mediator.Send(command); 35 | 36 | // return Ok(response); 37 | //} 38 | 39 | //[HttpPost("/{id:guid}/checkout", Name = nameof(CheckoutCartAsync))] 40 | //[Authorize("cart:write")] 41 | //[ProducesResponseType((int)HttpStatusCode.Accepted)] 42 | //[ProducesResponseType((int)HttpStatusCode.BadRequest)] 43 | //public async Task CheckoutCartAsync(Guid id, CheckoutCartRequestDto checkoutRequest) 44 | //{ 45 | // var command = new CheckoutCart.Command(checkoutRequest, id); 46 | // await Mediator.Send(command); 47 | 48 | // return Ok(); 49 | //} 50 | } 51 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Api/Program.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Cart.Application; 2 | using FluentPos.Cart.Infrastructure; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | builder.AddCartInfrastructure(); 6 | 7 | var app = builder.Build(); 8 | app.UseCartInfrastructure(); 9 | app.MapCartEnpoints(); 10 | app.Run(); -------------------------------------------------------------------------------- /fluentpos/services/cart/Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": false, 8 | "applicationUrl": "https://localhost:7004;http://localhost:5004", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "OpenIdOptions": { 9 | "Authority": "https://localhost:7001/", 10 | "Audience": "cart.resource.server" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Api/appsettings.docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "OpenIdOptions": { 9 | "Authority": "http://identity/", 10 | "Audience": "cart.resource.server" 11 | }, 12 | "RabbitMqOptions": { 13 | "Host": "amqp://guest:guest@rabbitmq" 14 | } 15 | } -------------------------------------------------------------------------------- /fluentpos/services/cart/Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "OpenIdOptions": { 4 | "Authority": "https://localhost:7001/", 5 | "Audience": "cart.resource.server" 6 | }, 7 | "SerilogOptions": { 8 | "WriteToFile": true, 9 | "StructuredConsoleLogging": false, 10 | "EnableErichers": false, 11 | "MinimumLogLevel": "Information" 12 | }, 13 | "AppOptions": { 14 | "Name": "Cart Service" 15 | }, 16 | "SwaggerOptions": { 17 | "Title": "Cart Service", 18 | "Description": "Open API Documentation of Cart Service API.", 19 | "Name": "Mukesh Murugan", 20 | "Email": "hello@codewithmukesh.com" 21 | }, 22 | "RabbitMqOptions": { 23 | "Host": "amqp://guest:guest@localhost:5672" 24 | }, 25 | "CachingOptions": { 26 | "EnableDistributedCaching": true, 27 | "SlidingExpirationInMinutes": 5, 28 | "AbsoluteExpirationInMinutes": 10 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/Application.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | FluentPos.Cart.Application 6 | FluentPos.Cart.Application 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/CartApplication.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Cart.Application; 2 | 3 | public class CartApplication 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/Dtos/CheckoutCartRequestDto.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Cart.Application.Dtos; 2 | public class CheckoutCartRequestDto 3 | { 4 | public string? CreditCardNumber { get; set; } 5 | } 6 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/Dtos/UpdateCartRequestDto.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Cart.Domain; 2 | 3 | namespace FluentPos.Cart.Application.Dtos; 4 | public class UpdateCartRequestDto 5 | { 6 | public List Items { get; set; } = new List(); 7 | } 8 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/Endpoints.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Cart.Application.Dtos; 2 | using FluentPos.Cart.Application.Features; 3 | using FluentPos.Cart.Domain; 4 | using MediatR; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FluentPos.Cart.Application; 10 | public static class Endpoints 11 | { 12 | public static void MapCartEnpoints(this IEndpointRouteBuilder builder) 13 | { 14 | builder.MapGet("/", () => "Hello!") 15 | .AllowAnonymous() 16 | .Produces(200); 17 | 18 | // Get Customer Cart Details 19 | builder.MapGet("/{id:guid}", async (Guid id, ISender _mediatr) => 20 | { 21 | var query = new GetCart.Query(id); 22 | return Results.Ok(await _mediatr.Send(query)); 23 | }) 24 | .RequireAuthorization("cart:read") 25 | .Produces(200, responseType: typeof(CustomerCart)) 26 | .Produces(400); 27 | 28 | // Update Customer Cart 29 | builder.MapPut("/{id:guid}", async (Guid id, UpdateCartRequestDto updateRequest, ISender _mediatr) => 30 | { 31 | var command = new UpdateCart.Command(updateRequest, id); 32 | return Results.Ok(await _mediatr.Send(command)); 33 | }) 34 | .RequireAuthorization("cart:write") 35 | .Produces(200) 36 | .Produces(400); 37 | 38 | // Checkout Customer Cart 39 | builder.MapPost("/{id:guid}/checkout", async (Guid id, CheckoutCartRequestDto checkoutRequest, ISender _mediatr) => 40 | { 41 | var command = new CheckoutCart.Command(checkoutRequest, id); 42 | await _mediatr.Send(command); 43 | return Results.Ok(); 44 | }) 45 | .RequireAuthorization("cart:write") 46 | .Produces(202) 47 | .Produces(400); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/Exceptions/CartNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Exceptions; 2 | 3 | namespace FluentPos.Cart.Application.Exceptions; 4 | internal class CartNotFoundException : NotFoundException 5 | { 6 | public CartNotFoundException(object customerId) : base($"Cart for Customer '{customerId}' is not found.") 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/Features/CheckoutCart.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Cart.Application.Dtos; 2 | using FluentPos.Cart.Application.Exceptions; 3 | using FluentPos.Shared.Events; 4 | using FluentValidation; 5 | using FSH.Framework.Core.Events; 6 | using MediatR; 7 | 8 | namespace FluentPos.Cart.Application.Features; 9 | public static class CheckoutCart 10 | { 11 | public sealed record Command : IRequest 12 | { 13 | public readonly Guid CustomerId; 14 | public readonly CheckoutCartRequestDto CheckoutRequest; 15 | 16 | public Command(CheckoutCartRequestDto checkoutRequest, Guid customerId) 17 | { 18 | CheckoutRequest = checkoutRequest; 19 | CustomerId = customerId; 20 | } 21 | } 22 | public sealed class Validator : AbstractValidator 23 | { 24 | public Validator() 25 | { 26 | RuleFor(x => x.CustomerId).NotEmpty(); 27 | } 28 | } 29 | public sealed class Handler : IRequestHandler 30 | { 31 | private readonly ICartRepository _cartRepository; 32 | private readonly IEventPublisher _eventBus; 33 | 34 | public Handler(IEventPublisher eventBus, ICartRepository cartRepository) 35 | { 36 | _eventBus = eventBus; 37 | _cartRepository = cartRepository; 38 | } 39 | 40 | public async Task Handle(Command request, CancellationToken cancellationToken) 41 | { 42 | _ = await _cartRepository.GetCustomerCartAsync(request.CustomerId.ToString(), cancellationToken) ?? throw new CartNotFoundException(request.CustomerId); 43 | var cartCheckedOutEvent = new CartCheckedOutEvent(request.CustomerId, request.CheckoutRequest.CreditCardNumber!); 44 | await _eventBus.PublishAsync(cartCheckedOutEvent, token: cancellationToken); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/Features/DeleteCart.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Cart.Application.Features; 2 | internal class DeleteCart 3 | { 4 | } 5 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/Features/GetCart.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Cart.Application.Exceptions; 2 | using FluentPos.Cart.Domain; 3 | using FluentValidation; 4 | using MediatR; 5 | 6 | namespace FluentPos.Cart.Application.Features; 7 | public static class GetCart 8 | { 9 | public sealed record Query : IRequest 10 | { 11 | public readonly Guid CustomerId; 12 | 13 | public Query(Guid customerId) 14 | { 15 | CustomerId = customerId; 16 | } 17 | } 18 | public sealed class Validator : AbstractValidator 19 | { 20 | public Validator() 21 | { 22 | RuleFor(p => p.CustomerId).NotEmpty(); 23 | } 24 | } 25 | public sealed class Handler : IRequestHandler 26 | { 27 | private readonly ICartRepository _cartRepository; 28 | 29 | public Handler(ICartRepository cartRepository) 30 | { 31 | _cartRepository = cartRepository; 32 | } 33 | 34 | public async Task Handle(Query request, CancellationToken cancellationToken) 35 | { 36 | var cart = await _cartRepository.GetCustomerCartAsync(request.CustomerId.ToString(), cancellationToken); 37 | if (cart == null) throw new CartNotFoundException(request.CustomerId.ToString()); 38 | return cart; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/Features/UpdateCart.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Cart.Application.Dtos; 2 | using FluentPos.Cart.Domain; 3 | using FluentValidation; 4 | using MediatR; 5 | 6 | namespace FluentPos.Cart.Application.Features; 7 | public static class UpdateCart 8 | { 9 | public sealed record Command : IRequest 10 | { 11 | public readonly Guid CustomerId; 12 | public readonly UpdateCartRequestDto UpdateCartDto; 13 | 14 | public Command(UpdateCartRequestDto updateCartDto, Guid customerId) 15 | { 16 | UpdateCartDto = updateCartDto; 17 | CustomerId = customerId; 18 | } 19 | } 20 | public sealed class Validator : AbstractValidator 21 | { 22 | public Validator() 23 | { 24 | RuleFor(x => x.CustomerId).NotEmpty(); 25 | RuleFor(dto => dto.UpdateCartDto.Items) 26 | .NotEmpty().WithMessage("At least one item must be specified.") 27 | .Must(items => items.All(item => item.Quantity > 0)) 28 | .WithMessage("Quantity of each product must be greater than 0."); 29 | } 30 | } 31 | public sealed class Handler : IRequestHandler 32 | { 33 | private readonly ICartRepository _cartRepository; 34 | 35 | public Handler(ICartRepository cartRepository) 36 | { 37 | _cartRepository = cartRepository; 38 | } 39 | 40 | public async Task Handle(Command request, CancellationToken cancellationToken) 41 | { 42 | string customerId = request.CustomerId.ToString(); 43 | var cart = await _cartRepository.GetCustomerCartAsync(customerId, cancellationToken) ?? new CustomerCart(request.CustomerId); 44 | foreach (var item in request.UpdateCartDto.Items) 45 | { 46 | cart.AddItem(item.ProductId, item.Quantity); 47 | } 48 | await _cartRepository.UpdateCustomerCartAsync(customerId, cart, cancellationToken); 49 | return cart!; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Application/ICartRepository.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Cart.Domain; 2 | 3 | namespace FluentPos.Cart.Application; 4 | public interface ICartRepository 5 | { 6 | Task GetCustomerCartAsync(string customerId, CancellationToken cancellationToken); 7 | Task UpdateCustomerCartAsync(string customerId, CustomerCart cart, CancellationToken cancellationToken); 8 | } 9 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Domain/CartItem.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Cart.Domain; 2 | public class CartItem 3 | { 4 | public Guid ProductId { get; set; } 5 | public int Quantity { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Domain/CustomerCart.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Domain; 2 | 3 | namespace FluentPos.Cart.Domain; 4 | 5 | public class CustomerCart : BaseEntity 6 | { 7 | public Guid CustomerId { get; set; } 8 | public List Items { get; set; } = new List(); 9 | 10 | public CustomerCart(Guid customerId) 11 | { 12 | CustomerId = customerId; 13 | } 14 | 15 | public CustomerCart AddItem(Guid productId, int quantity) 16 | { 17 | var existingItem = Items.Find(i => i.ProductId == productId); 18 | if (existingItem != null) 19 | { 20 | existingItem.Quantity += quantity; 21 | } 22 | else 23 | { 24 | Items.Add(new CartItem { ProductId = productId, Quantity = quantity }); 25 | } 26 | UpdateModifiedProperties(DateTime.UtcNow, null!); 27 | return this; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | FluentPos.Cart.Domain 6 | FluentPos.Cart.Domain 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Infrastructure/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Cart.Application; 2 | using FluentPos.Cart.Infrastructure.Repositories; 3 | using FSH.Framework.Core.Events; 4 | using FSH.Framework.Infrastructure; 5 | using FSH.Framework.Infrastructure.Auth.OpenId; 6 | using FSH.Framework.Infrastructure.Messaging; 7 | using MassTransit; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | namespace FluentPos.Cart.Infrastructure; 12 | 13 | public static class Extensions 14 | { 15 | public static void AddCartInfrastructure(this WebApplicationBuilder builder) 16 | { 17 | var applicationAssembly = typeof(CartApplication).Assembly; 18 | var policyNames = new List { "cart:read", "cart:write" }; 19 | builder.Services.AddOpenIdAuth(builder.Configuration, policyNames); 20 | 21 | builder.Services.AddMassTransit(config => 22 | { 23 | config.AddConsumers(applicationAssembly); 24 | config.UsingRabbitMq((ctx, cfg) => 25 | { 26 | cfg.Host(builder.Configuration["RabbitMqOptions:Host"]); 27 | cfg.ConfigureEndpoints(ctx, new KebabCaseEndpointNameFormatter("cart", false)); 28 | }); 29 | }); 30 | 31 | builder.AddInfrastructure(applicationAssembly); 32 | builder.Services.AddTransient(); 33 | builder.Services.AddTransient(); 34 | } 35 | public static void UseCartInfrastructure(this WebApplication app) 36 | { 37 | app.UseInfrastructure(app.Environment); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | FluentPos.Cart.Infrastructure 6 | FluentPos.Cart.Infrastructure 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /fluentpos/services/cart/Infrastructure/Repositories/CartRepository.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Cart.Application; 2 | using FluentPos.Cart.Domain; 3 | using FSH.Framework.Core.Caching; 4 | 5 | namespace FluentPos.Cart.Infrastructure.Repositories; 6 | public class CartRepository : ICartRepository 7 | { 8 | private readonly ICacheService _cacheService; 9 | 10 | public CartRepository(ICacheService cacheService) 11 | { 12 | _cacheService = cacheService; 13 | } 14 | 15 | public async Task GetCustomerCartAsync(string customerId, CancellationToken cancellationToken) 16 | { 17 | string cacheKey = $"cart-{customerId}"; 18 | return await _cacheService.GetAsync(cacheKey, token: cancellationToken); 19 | } 20 | 21 | public async Task UpdateCustomerCartAsync(string customerId, CustomerCart cart, CancellationToken cancellationToken) 22 | { 23 | string cacheKey = $"cart-{customerId}"; 24 | await _cacheService.SetAsync(cacheKey, cart, cancellationToken: cancellationToken); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Api/Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0 4 | FluentPos.Catalog.Api 5 | FluentPos.Catalog.Api 6 | enable 7 | enable 8 | true 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | fluentpos.catalog 17 | 1.0.0;latest 18 | DefaultContainer 19 | 20 | 21 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Api/Controllers/ProductsController.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Application.Products.Dtos; 2 | using FluentPos.Catalog.Application.Products.Features; 3 | using FSH.Framework.Core.Pagination; 4 | using FSH.Framework.Infrastructure.Controllers; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace FluentPos.Catalog.Api.Controllers; 9 | 10 | public class ProductsController : BaseApiController 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public ProductsController(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | [HttpPost(Name = nameof(AddProductAsync))] 20 | [Authorize("catalog:write")] 21 | [ProducesResponseType(201, Type = typeof(ProductDto))] 22 | public async Task AddProductAsync(AddProductDto request) 23 | { 24 | var command = new AddProduct.Command(request); 25 | var commandResponse = await Mediator.Send(command); 26 | 27 | return CreatedAtRoute(nameof(GetProductAsync), new { commandResponse.Id }, commandResponse); 28 | } 29 | 30 | [HttpGet("{id:guid}", Name = nameof(GetProductAsync))] 31 | [Authorize("catalog:read")] 32 | [ProducesResponseType(200, Type = typeof(ProductDetailsDto))] 33 | public async Task GetProductAsync(Guid id) 34 | { 35 | var query = new GetProductDetails.Query(id); 36 | var queryResponse = await Mediator.Send(query); 37 | 38 | return Ok(queryResponse); 39 | } 40 | 41 | [HttpGet(Name = nameof(GetProductsAsync))] 42 | [Authorize("catalog:read")] 43 | [ProducesResponseType(200, Type = typeof(PagedList))] 44 | public async Task GetProductsAsync([FromQuery] ProductsParametersDto parameters) 45 | { 46 | var query = new GetProducts.Query(parameters); 47 | var queryResponse = await Mediator.Send(query); 48 | 49 | return Ok(queryResponse); 50 | } 51 | 52 | [HttpDelete("{id:guid}", Name = nameof(DeleteProductsAsync))] 53 | [Authorize("catalog:write")] 54 | [ProducesResponseType(204)] 55 | public async Task DeleteProductsAsync(Guid id) 56 | { 57 | var command = new DeleteProduct.Command(id); 58 | await Mediator.Send(command); 59 | 60 | return NoContent(); 61 | } 62 | 63 | [HttpPut("{id:guid}", Name = nameof(UpdateProductsAsync))] 64 | [Authorize("catalog:write")] 65 | [ProducesResponseType(204)] 66 | public async Task UpdateProductsAsync(Guid id, UpdateProductDto updateProductDto) 67 | { 68 | var command = new UpdateProduct.Command(updateProductDto, id); 69 | await Mediator.Send(command); 70 | 71 | return NoContent(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Api/Program.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Infrastructure; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | builder.AddCatalogInfrastructure(); 5 | var app = builder.Build(); 6 | app.UseCatalogInfrastructure(); 7 | 8 | app.MapGet("/", () => "Hello From Catalog Service").AllowAnonymous(); 9 | app.Run(); -------------------------------------------------------------------------------- /fluentpos/services/catalog/Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": false, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "https://localhost:7003;http://localhost:5003", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "OpenIdOptions": { 4 | "Authority": "https://localhost:7001/", 5 | "Audience": "catalog.resource.server" 6 | }, 7 | "MongoOptions": { 8 | "ConnectionString": "mongodb://localhost:27017", 9 | "DatabaseName": "catalog-db" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Api/appsettings.docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "OpenIdOptions": { 4 | "Authority": "http://identity/", 5 | "Audience": "catalog.resource.server" 6 | }, 7 | "MongoOptions": { 8 | "ConnectionString": "mongodb://mongo", 9 | "DatabaseName": "catalog-db" 10 | }, 11 | "RabbitMqOptions": { 12 | "Host": "amqp://guest:guest@rabbitmq" 13 | } 14 | } -------------------------------------------------------------------------------- /fluentpos/services/catalog/Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "OpenIdOptions": { 4 | "Authority": "https://localhost:7001/", 5 | "Audience": "catalog.resource.server" 6 | }, 7 | "SerilogOptions": { 8 | "WriteToFile": true, 9 | "StructuredConsoleLogging": false, 10 | "EnableErichers": false, 11 | "MinimumLogLevel": "Information" 12 | }, 13 | "AppOptions": { 14 | "Name": "Catalog Service" 15 | }, 16 | "SwaggerOptions": { 17 | "Title": "Catalog Service", 18 | "Description": "Open API Documentation of Catalog Service API.", 19 | "Name": "Mukesh Murugan", 20 | "Email": "hello@codewithmukesh.com" 21 | }, 22 | "MongoOptions": { 23 | "ConnectionString": "mongodb://localhost:27017", 24 | "DatabaseName": "catalog-db" 25 | }, 26 | "RabbitMqOptions": { 27 | "Host": "amqp://guest:guest@localhost:5672" 28 | }, 29 | "CachingOptions": { 30 | "EnableDistributedCaching": "false", 31 | "SlidingExpirationInMinutes": 5, 32 | "AbsoluteExpirationInMinutes": 10 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Application.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | FluentPos.Catalog.Application 6 | FluentPos.Catalog.Application 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/CatalogApplication.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Catalog.Application; 2 | public static class CatalogApplication 3 | { 4 | } 5 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Consumers/CartCheckedOutEventConsumer.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Shared.Events; 2 | using MassTransit; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace FluentPos.Catalog.Application.Consumers; 6 | public class CartCheckedOutEventConsumer : IConsumer 7 | { 8 | private readonly ILogger _logger; 9 | 10 | public CartCheckedOutEventConsumer(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | public Task Consume(ConsumeContext context) 16 | { 17 | _logger.LogInformation("CC Numbers is {ccNo}", context.Message.CreditCardNumber); 18 | return Task.CompletedTask; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Consumers/ProductCreatedEventConsumer.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Domain.Products; 2 | using MassTransit; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace FluentPos.Catalog.Application.Consumers; 6 | public class ProductCreatedEventConsumer : IConsumer 7 | { 8 | private readonly ILogger _logger; 9 | 10 | public ProductCreatedEventConsumer(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | public Task Consume(ConsumeContext context) 16 | { 17 | _logger.LogInformation("Message is {message}", context.Message); 18 | return Task.CompletedTask; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Dtos/AddProductDto.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Catalog.Application.Products.Dtos; 2 | public sealed class AddProductDto 3 | { 4 | public string? Name { get; set; } 5 | public string? Details { get; set; } 6 | public string? Code { get; set; } 7 | public decimal Cost { get; set; } 8 | public decimal Price { get; set; } 9 | public decimal Quantity { get; set; } = 0; 10 | public decimal AlertQuantity { get; set; } = 10; 11 | public bool TrackQuantity { get; set; } = true; 12 | } -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Dtos/ProductDetailsDto.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Catalog.Application.Products.Dtos; 2 | public class ProductDetailsDto 3 | { 4 | public Guid Id { get; set; } 5 | public string? Name { get; set; } 6 | public string? Details { get; set; } 7 | public string? Code { get; set; } 8 | public string? Slug { get; set; } 9 | public decimal? Price { get; set; } 10 | public decimal? Quantity { get; set; } 11 | public DateTime CreatedOn { get; set; } 12 | public DateTime? LastModifiedOn { get; set; } = null; 13 | } 14 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Dtos/ProductDto.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Catalog.Application.Products.Dtos; 2 | public class ProductDto 3 | { 4 | public Guid Id { get; set; } 5 | public string? Name { get; set; } 6 | public string? Details { get; set; } 7 | public string? Code { get; set; } 8 | public string? Slug { get; set; } 9 | public decimal? Price { get; set; } 10 | public decimal? Quantity { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Dtos/ProductsParametersDto.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Pagination; 2 | 3 | namespace FluentPos.Catalog.Application.Products.Dtos; 4 | public class ProductsParametersDto : PaginationParameters 5 | { 6 | public string? Keyword { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Dtos/UpdateProductDto.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Catalog.Application.Products.Dtos; 2 | public sealed class UpdateProductDto 3 | { 4 | public string? Name { get; init; } 5 | public string? Details { get; init; } 6 | public decimal? Cost { get; init; } = null; 7 | public decimal? Price { get; init; } = null; 8 | public decimal? Quantity { get; init; } = null; 9 | public decimal? AlertQuantity { get; init; } = null; 10 | public bool? TrackQuantity { get; init; } = null; 11 | } -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Exceptions/ProductNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Exceptions; 2 | 3 | namespace FluentPos.Catalog.Application.Products.Exceptions; 4 | internal class ProductNotFoundException : NotFoundException 5 | { 6 | public ProductNotFoundException(object productId) : base($"Product with ID '{productId}' is not found.") 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Features/AddProduct.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Application.Products.Dtos; 2 | using FluentPos.Catalog.Domain.Products; 3 | using FluentValidation; 4 | using FSH.Framework.Core.Events; 5 | using MapsterMapper; 6 | using MediatR; 7 | 8 | namespace FluentPos.Catalog.Application.Products.Features; 9 | public static class AddProduct 10 | { 11 | public sealed record Command : IRequest 12 | { 13 | public readonly AddProductDto AddProductDto; 14 | public Command(AddProductDto addProductDto) 15 | { 16 | AddProductDto = addProductDto; 17 | } 18 | } 19 | public sealed class Validator : AbstractValidator 20 | { 21 | public Validator(IProductRepository _repository) 22 | { 23 | RuleFor(p => p.AddProductDto.Name) 24 | .NotEmpty() 25 | .MaximumLength(75) 26 | .WithName("Name"); 27 | 28 | RuleFor(p => p.AddProductDto.Cost) 29 | .GreaterThanOrEqualTo(1) 30 | .WithName("Cost"); 31 | 32 | RuleFor(p => p.AddProductDto.Price) 33 | .GreaterThanOrEqualTo(1) 34 | .GreaterThanOrEqualTo(p => p.AddProductDto.Cost) 35 | .WithName("Price"); 36 | 37 | RuleFor(p => p.AddProductDto.Code) 38 | .NotEmpty() 39 | .MaximumLength(75) 40 | .WithName("Code") 41 | .MustAsync(async (code, ct) => !await _repository.ExistsAsync(p => p.Code == code, ct)) 42 | .WithMessage((_, code) => $"Product with Code '{code}' already Exists."); 43 | } 44 | } 45 | public sealed class Handler : IRequestHandler 46 | { 47 | private readonly IProductRepository _repository; 48 | private readonly IMapper _mapper; 49 | private readonly IEventPublisher _eventBus; 50 | 51 | public Handler(IProductRepository repository, IMapper mapper, IEventPublisher eventBus) 52 | { 53 | _repository = repository; 54 | _mapper = mapper; 55 | _eventBus = eventBus; 56 | } 57 | 58 | public async Task Handle(Command request, CancellationToken cancellationToken) 59 | { 60 | var productToAdd = Product.Create( 61 | request.AddProductDto.Name, 62 | request.AddProductDto.Details, 63 | request.AddProductDto.Code, 64 | request.AddProductDto.Cost, 65 | request.AddProductDto.Price, 66 | request.AddProductDto.AlertQuantity, 67 | request.AddProductDto.TrackQuantity, 68 | request.AddProductDto.Quantity); 69 | 70 | await _repository.AddAsync(productToAdd, cancellationToken); 71 | foreach (var @event in productToAdd.DomainEvents) 72 | { 73 | await _eventBus.PublishAsync(@event, token: cancellationToken); 74 | } 75 | return _mapper.Map(productToAdd); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Features/DeleteProduct.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Domain.Products; 2 | using FSH.Framework.Core.Caching; 3 | using MediatR; 4 | 5 | namespace FluentPos.Catalog.Application.Products.Features; 6 | public static class DeleteProduct 7 | { 8 | public sealed record Command : IRequest 9 | { 10 | public readonly Guid Id; 11 | public Command(Guid id) 12 | { 13 | Id = id; 14 | } 15 | } 16 | public sealed class Handler : IRequestHandler 17 | { 18 | private readonly IProductRepository _repository; 19 | private readonly ICacheService _cacheService; 20 | 21 | public Handler(IProductRepository repository, ICacheService cacheService) 22 | { 23 | _repository = repository; 24 | _cacheService = cacheService; 25 | } 26 | 27 | public async Task Handle(Command request, CancellationToken cancellationToken) 28 | { 29 | await _repository.DeleteByIdAsync(request.Id, cancellationToken); 30 | await _cacheService.RemoveAsync(Product.GetCacheKey(request.Id), cancellationToken); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Features/GetProductDetails.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Application.Products.Dtos; 2 | using FluentPos.Catalog.Application.Products.Exceptions; 3 | using FluentPos.Catalog.Domain.Products; 4 | using FSH.Framework.Core.Caching; 5 | using MapsterMapper; 6 | using MediatR; 7 | 8 | namespace FluentPos.Catalog.Application.Products.Features; 9 | public static class GetProductDetails 10 | { 11 | public sealed record Query : IRequest 12 | { 13 | public readonly Guid Id; 14 | 15 | public Query(Guid id) 16 | { 17 | Id = id; 18 | } 19 | } 20 | 21 | public sealed class Handler : IRequestHandler 22 | { 23 | private readonly IProductRepository _repository; 24 | private readonly ICacheService _cache; 25 | private readonly IMapper _mapper; 26 | 27 | public Handler(IProductRepository repository, IMapper mapper, ICacheService cache) 28 | { 29 | _repository = repository; 30 | _mapper = mapper; 31 | _cache = cache; 32 | } 33 | 34 | public async Task Handle(Query request, CancellationToken cancellationToken) 35 | { 36 | string cacheKey = Product.GetCacheKey(request.Id); 37 | var productDto = await _cache.GetAsync(cacheKey, cancellationToken); 38 | if (productDto == null) 39 | { 40 | var product = await _repository.FindByIdAsync(request.Id, cancellationToken) ?? throw new ProductNotFoundException(request.Id); 41 | productDto = _mapper.Map(product); 42 | await _cache.SetAsync(cacheKey, productDto, cancellationToken: cancellationToken); 43 | } 44 | return productDto; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Features/GetProducts.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Application.Products.Dtos; 2 | using FSH.Framework.Core.Pagination; 3 | using MediatR; 4 | 5 | namespace FluentPos.Catalog.Application.Products.Features; 6 | public static class GetProducts 7 | { 8 | public sealed record Query : IRequest> 9 | { 10 | public readonly ProductsParametersDto Parameters; 11 | 12 | public Query(ProductsParametersDto parameters) 13 | { 14 | Parameters = parameters; 15 | } 16 | } 17 | 18 | public sealed class Handler : IRequestHandler> 19 | { 20 | private readonly IProductRepository _repository; 21 | 22 | public Handler(IProductRepository repository) 23 | { 24 | _repository = repository; 25 | } 26 | 27 | public async Task> Handle(Query request, CancellationToken cancellationToken) 28 | { 29 | return await _repository.GetPagedProductsAsync(request.Parameters, cancellationToken); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Features/UpdateProduct.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Application.Products.Dtos; 2 | using FluentPos.Catalog.Application.Products.Exceptions; 3 | using FluentPos.Catalog.Domain.Products; 4 | using FSH.Framework.Core.Caching; 5 | using MapsterMapper; 6 | using MediatR; 7 | 8 | namespace FluentPos.Catalog.Application.Products.Features; 9 | public static class UpdateProduct 10 | { 11 | public sealed record Command : IRequest 12 | { 13 | public readonly UpdateProductDto UpdateProductDto; 14 | public readonly Guid Id; 15 | public Command(UpdateProductDto updateProductDto, Guid id) 16 | { 17 | UpdateProductDto = updateProductDto; 18 | Id = id; 19 | } 20 | } 21 | public sealed class Handler : IRequestHandler 22 | { 23 | private readonly IProductRepository _repository; 24 | private readonly IMapper _mapper; 25 | private readonly ICacheService _cacheService; 26 | 27 | public Handler(IProductRepository repository, IMapper mapper, ICacheService cacheService) 28 | { 29 | _repository = repository; 30 | _mapper = mapper; 31 | _cacheService = cacheService; 32 | } 33 | 34 | public async Task Handle(Command request, CancellationToken cancellationToken) 35 | { 36 | var productToBeUpdated = await _repository.FindByIdAsync(request.Id, cancellationToken) ?? throw new ProductNotFoundException(request.Id); 37 | productToBeUpdated.Update( 38 | request.UpdateProductDto.Name, 39 | request.UpdateProductDto.Details, 40 | request.UpdateProductDto.Price, 41 | request.UpdateProductDto.Cost, 42 | request.UpdateProductDto.TrackQuantity, 43 | request.UpdateProductDto.AlertQuantity, 44 | request.UpdateProductDto.Quantity); 45 | 46 | await _repository.UpdateAsync(productToBeUpdated, cancellationToken); 47 | await _cacheService.RemoveAsync(Product.GetCacheKey(request.Id), cancellationToken); 48 | return _mapper.Map(productToBeUpdated); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/IProductRepository.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Application.Products.Dtos; 2 | using FluentPos.Catalog.Domain.Products; 3 | using FSH.Framework.Core.Database; 4 | using FSH.Framework.Core.Pagination; 5 | 6 | namespace FluentPos.Catalog.Application.Products; 7 | public interface IProductRepository : IRepository 8 | { 9 | Task> GetPagedProductsAsync(ProductsParametersDto parameters, CancellationToken cancellationToken = default); 10 | } -------------------------------------------------------------------------------- /fluentpos/services/catalog/Application/Products/Mappings/ProductMappings.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Application.Products.Dtos; 2 | using FluentPos.Catalog.Domain.Products; 3 | using Mapster; 4 | 5 | namespace FluentPos.Catalog.Application.Products.Mappings; 6 | public sealed class ProductMappings : IRegister 7 | { 8 | public void Register(TypeAdapterConfig config) 9 | { 10 | config.NewConfig(); 11 | config.NewConfig(); 12 | } 13 | } -------------------------------------------------------------------------------- /fluentpos/services/catalog/Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | FluentPos.Catalog.Domain 6 | FluentPos.Catalog.Domain 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Domain/Products/Product.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Domain; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace FluentPos.Catalog.Domain.Products; 5 | 6 | public class Product : BaseEntity 7 | { 8 | public string Name { get; private set; } = default!; 9 | public string Details { get; private set; } = default!; 10 | public string Code { get; private set; } = default!; 11 | public string Slug { get; private set; } = default!; 12 | public bool Active { get; private set; } = true; 13 | public decimal? Cost { get; private set; } 14 | public decimal? Price { get; private set; } 15 | public decimal? Quantity { get; private set; } 16 | public decimal? AlertQuantity { get; private set; } 17 | public bool? TrackQuantity { get; private set; } 18 | 19 | public Product Update( 20 | string? name, 21 | string? details, 22 | decimal? price, 23 | decimal? cost, 24 | bool? trackQuantity, 25 | decimal? alertQuantity, 26 | decimal? quantity) 27 | { 28 | if (name is not null && Name?.Equals(name) is not true) Name = name; 29 | if (details is not null && Details?.Equals(details) is not true) Details = details; 30 | if (price.HasValue && Price != price.Value) Price = price.Value; 31 | if (cost.HasValue && Cost != cost.Value) Cost = cost.Value; 32 | if (trackQuantity.HasValue && TrackQuantity != trackQuantity) TrackQuantity = trackQuantity.Value; 33 | if (alertQuantity.HasValue && AlertQuantity != alertQuantity.Value) AlertQuantity = alertQuantity.Value; 34 | if (quantity.HasValue && Quantity != quantity.Value) Quantity = quantity.Value; 35 | return this; 36 | } 37 | public static Product Create( 38 | string? name, 39 | string? details, 40 | string? code, 41 | decimal? cost, 42 | decimal? price, 43 | decimal? alertQuantity, 44 | bool? trackQuantity, 45 | decimal? quantity) 46 | { 47 | Product product = new() 48 | { 49 | Name = name!, 50 | Details = details!, 51 | Code = code!, 52 | Slug = GetProductSlug(name!), 53 | Cost = cost, 54 | AlertQuantity = alertQuantity, 55 | TrackQuantity = trackQuantity, 56 | Quantity = quantity, 57 | Active = true, 58 | Price = price 59 | }; 60 | 61 | var @event = new ProductCreatedEvent(product.Id, product.Name); 62 | product.AddDomainEvent(@event); 63 | 64 | return product; 65 | } 66 | 67 | private static string GetProductSlug(string name) 68 | { 69 | name = name.Trim(); 70 | name = name.ToLower(); 71 | name = Regex.Replace(name, "[^a-z0-9]+", "-"); 72 | name = Regex.Replace(name, "--+", "-"); 73 | name = name.Trim('-'); 74 | return name; 75 | } 76 | 77 | public static string GetCacheKey(Guid id) 78 | { 79 | return $"Product:{id}"; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Domain/Products/ProductCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Events; 2 | 3 | namespace FluentPos.Catalog.Domain.Products; 4 | public class ProductCreatedEvent : DomainEvent 5 | { 6 | public Guid ProductId { get; } 7 | public string ProductName { get; } 8 | 9 | public ProductCreatedEvent(Guid productId, string productName) 10 | { 11 | ProductId = productId; 12 | ProductName = productName; 13 | } 14 | } -------------------------------------------------------------------------------- /fluentpos/services/catalog/Infrastructure/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Application; 2 | using FluentPos.Catalog.Application.Products; 3 | using FluentPos.Catalog.Infrastructure.Repositories; 4 | using FSH.Framework.Core.Events; 5 | using FSH.Framework.Infrastructure; 6 | using FSH.Framework.Infrastructure.Auth.OpenId; 7 | using FSH.Framework.Infrastructure.Messaging; 8 | using FSH.Framework.Persistence.Mongo; 9 | using MassTransit; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace FluentPos.Catalog.Infrastructure; 14 | public static class Extensions 15 | { 16 | public static void AddCatalogInfrastructure(this WebApplicationBuilder builder) 17 | { 18 | var applicationAssembly = typeof(CatalogApplication).Assembly; 19 | var policyNames = new List { "catalog:read", "catalog:write" }; 20 | builder.Services.AddOpenIdAuth(builder.Configuration, policyNames); 21 | builder.Services.AddMassTransit(config => 22 | { 23 | config.AddConsumers(applicationAssembly); 24 | config.UsingRabbitMq((ctx, cfg) => 25 | { 26 | cfg.Host(builder.Configuration["RabbitMqOptions:Host"]); 27 | cfg.ConfigureEndpoints(ctx, new KebabCaseEndpointNameFormatter("catalog", false)); 28 | }); 29 | }); 30 | builder.AddInfrastructure(applicationAssembly); 31 | builder.Services.AddTransient(); 32 | builder.Services.AddMongoDbContext(builder.Configuration); 33 | builder.Services.AddTransient(); 34 | } 35 | public static void UseCatalogInfrastructure(this WebApplication app) 36 | { 37 | app.UseInfrastructure(app.Environment); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | FluentPos.Catalog.Infrastructure 6 | FluentPos.Catalog.Infrastructure 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /fluentpos/services/catalog/Infrastructure/Repositories/ProductRepository.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Catalog.Application.Products; 2 | using FluentPos.Catalog.Application.Products.Dtos; 3 | using FluentPos.Catalog.Domain.Products; 4 | using FSH.Framework.Core.Pagination; 5 | using FSH.Framework.Core.Services; 6 | using FSH.Framework.Persistence.Mongo; 7 | using MongoDB.Driver; 8 | using MongoDB.Driver.Linq; 9 | 10 | namespace FluentPos.Catalog.Infrastructure.Repositories; 11 | 12 | public class ProductRepository : MongoRepository, IProductRepository 13 | { 14 | private readonly IMongoDbContext _dbContext; 15 | public ProductRepository(IMongoDbContext context, IDateTimeService dateTimeService) : base(context, dateTimeService) 16 | { 17 | _dbContext = context; 18 | } 19 | 20 | public async Task> GetPagedProductsAsync(ProductsParametersDto parameters, CancellationToken cancellationToken = default) 21 | { 22 | var queryable = _dbContext.GetCollection().AsQueryable(); 23 | if (!string.IsNullOrEmpty(parameters.Keyword)) 24 | { 25 | string keyword = parameters.Keyword.ToLower(); 26 | queryable = queryable.Where(t => t.Name.ToLower().Contains(keyword) 27 | || t.Details.ToLower().Contains(keyword) 28 | || t.Code.ToLower().Contains(keyword)); 29 | } 30 | queryable = queryable.OrderBy(p => p.CreatedOn); 31 | return await queryable.ApplyPagingAsync(parameters.PageNumber, parameters.PageSize, cancellationToken); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Api/Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | FluentPos.Identity.Api 8 | FluentPos.Identity.Api 9 | true 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | fluentpos.identity 27 | 1.0.0;latest 28 | DefaultContainer 29 | 30 | 31 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Api/Controllers/TokensController.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using FluentPos.Identity.Api.Extensions; 3 | using Microsoft.AspNetCore; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.IdentityModel.Tokens; 6 | using OpenIddict.Abstractions; 7 | using OpenIddict.Server.AspNetCore; 8 | using static OpenIddict.Abstractions.OpenIddictConstants; 9 | 10 | namespace FluentPos.Identity.Api.Controllers; 11 | 12 | [ApiController] 13 | public class TokensController : ControllerBase 14 | { 15 | private readonly IOpenIddictApplicationManager _applicationManager; 16 | private readonly IOpenIddictScopeManager _scopeManager; 17 | 18 | public TokensController(IOpenIddictApplicationManager applicationManager, IOpenIddictScopeManager scopeManager) 19 | { 20 | _applicationManager = applicationManager; 21 | _scopeManager = scopeManager; 22 | } 23 | 24 | [HttpPost("~/connect/token"), IgnoreAntiforgeryToken, Produces("application/json")] 25 | public async Task Exchange() 26 | { 27 | var request = HttpContext.GetOpenIddictServerRequest() ?? throw new ArgumentNullException(); 28 | if (request.IsClientCredentialsGrantType()) 29 | { 30 | return await HandleClientCredentialsGrantType(request); 31 | } 32 | throw new NotImplementedException("The specified grant type is not implemented."); 33 | } 34 | 35 | private async Task HandleClientCredentialsGrantType(OpenIddictRequest? request) 36 | { 37 | object? application = await _applicationManager.FindByClientIdAsync(request!.ClientId!) ?? throw new InvalidOperationException("The application details cannot be found in the database."); 38 | var identity = new ClaimsIdentity( 39 | authenticationType: TokenValidationParameters.DefaultAuthenticationType, 40 | nameType: Claims.Name, 41 | roleType: Claims.Role); 42 | identity.SetClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application)); 43 | identity.SetClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application)); 44 | identity.SetScopes(request!.GetScopes()); 45 | identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); 46 | identity.SetDestinations(GetDestinations); 47 | return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); 48 | } 49 | 50 | private static IEnumerable GetDestinations(Claim claim) 51 | { 52 | return claim.Type switch 53 | { 54 | Claims.Name or 55 | Claims.Subject 56 | => new[] { Destinations.AccessToken, Destinations.IdentityToken }, 57 | 58 | _ => new[] { Destinations.AccessToken }, 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Api/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Identity.Application.Users.Dtos; 2 | using FluentPos.Identity.Application.Users.Features; 3 | using FSH.Framework.Infrastructure.Controllers; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace FluentPos.Identity.Api.Controllers; 8 | 9 | public class UsersController : BaseApiController 10 | { 11 | [HttpPost(Name = nameof(AddUserAsync))] 12 | [AllowAnonymous] 13 | public async Task AddUserAsync(AddUserDto request) 14 | { 15 | var command = new AddUser.Command(request); 16 | var userDto = await Mediator.Send(command); 17 | 18 | return CreatedAtRoute(nameof(AddUserAsync), userDto); 19 | } 20 | 21 | //[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] 22 | //[HttpGet("~/connect/userinfo"), HttpPost("~/connect/userinfo"), Produces("application/json")] 23 | //public async Task GetCurrentUserInfoAsync() 24 | //{ 25 | // var command = new GetCurrentUserInfo.Query(User); 26 | // var userDto = await Mediator.Send(command); 27 | 28 | // return Ok(userDto); 29 | //} 30 | } 31 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Api/Extensions/AsyncEnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Identity.Api.Extensions; 2 | 3 | public static class AsyncEnumerableExtensions 4 | { 5 | public static Task> ToListAsync(this IAsyncEnumerable source) 6 | { 7 | if (source == null) 8 | { 9 | throw new ArgumentNullException(nameof(source)); 10 | } 11 | return ExecuteAsync(); 12 | async Task> ExecuteAsync() 13 | { 14 | var list = new List(); 15 | await foreach (var element in source) 16 | { 17 | list.Add(element); 18 | } 19 | return list; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Api/Program.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Identity.Infrastructure; 2 | var builder = WebApplication.CreateBuilder(args); 3 | builder.AddIdentityInfrastructure(); 4 | var app = builder.Build(); 5 | app.UseIdentityInfrastructure(); 6 | app.Run(); 7 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": false, 8 | "applicationUrl": "https://localhost:7001;http://localhost:5001", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Host=localhost;Port=5432;Database=authDb;Username=postgres;Password=admin;Include Error Detail=true" 4 | }, 5 | "SerilogOptions": { 6 | "WriteToFile": false, 7 | "StructuredConsoleLogging": false, 8 | "EnableErichers": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Api/appsettings.docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=postgres;Port=5430;Database=identityDb;User Id=postgres;Password=admin" 4 | }, 5 | "SerilogOptions": { 6 | "WriteToFile": true, 7 | "StructuredConsoleLogging": false, 8 | "EnableErichers": false 9 | } 10 | } -------------------------------------------------------------------------------- /fluentpos/services/identity/Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*", 3 | "ConnectionStrings": { 4 | "DefaultConnection": "Host=localhost;Port=5432;Database=authDb;Username=postgres;Password=admin;Include Error Detail=true" 5 | }, 6 | "SerilogOptions": { 7 | "WriteToFile": true, 8 | "StructuredConsoleLogging": false, 9 | "EnableErichers": false 10 | }, 11 | "AppOptions": { 12 | "Name": "Identity Service" 13 | }, 14 | "RabbitMqOptions": { 15 | "Host": "amqp://guest:guest@localhost:5672" 16 | }, 17 | "CachingOptions": { 18 | "EnableDistributedCaching": false, 19 | "SlidingExpirationInMinutes": 5, 20 | "AbsoluteExpirationInMinutes": 10 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Application/Application.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | FluentPos.Identity.Core 6 | FluentPos.Identity.Core 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Application/IdentityCore.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Identity.Application; 2 | public class IdentityCore 3 | { 4 | } 5 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Application/Users/Dtos/AddUserDto.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Identity.Application.Users.Dtos; 2 | public class AddUserDto 3 | { 4 | public string? UserName { get; set; } 5 | public string? Email { get; set; } 6 | public string? Password { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Application/Users/Dtos/UserDto.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Identity.Application.Users.Dtos; 2 | public class UserDto 3 | { 4 | public string? Id { get; set; } 5 | public string? UserName { get; set; } 6 | public string? Email { get; set; } 7 | public bool EmailConfirmed { get; set; } 8 | public string? PhoneNumber { get; set; } 9 | public bool PhoneNumberConfirmed { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Application/Users/Exceptions/UserRegistrationException.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Exceptions; 2 | using System.Net; 3 | 4 | namespace FluentPos.Identity.Application.Users.Exceptions; 5 | public class UserRegistrationException : CustomException 6 | { 7 | public UserRegistrationException(string message, HttpStatusCode statusCode = HttpStatusCode.BadRequest) : base(message, statusCode) 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Application/Users/Features/AddUser.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Identity.Application.Users.Dtos; 2 | using FluentPos.Identity.Application.Users.Exceptions; 3 | using FluentPos.Identity.Domain.Users; 4 | using FluentValidation; 5 | using MapsterMapper; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace FluentPos.Identity.Application.Users.Features; 11 | 12 | public static class AddUser 13 | { 14 | public sealed record Command : IRequest 15 | { 16 | public readonly AddUserDto AddUserDto; 17 | public Command(AddUserDto addUserDto) 18 | { 19 | AddUserDto = addUserDto; 20 | } 21 | } 22 | public sealed class Validator : AbstractValidator 23 | { 24 | public Validator() 25 | { 26 | RuleFor(p => p.AddUserDto.UserName) 27 | .NotEmpty() 28 | .Matches("\\w+").WithMessage("The {0} must only contain letters and numbers") 29 | .MaximumLength(75) 30 | .WithName("UserName"); 31 | 32 | RuleFor(p => p.AddUserDto.Email) 33 | .NotEmpty() 34 | .EmailAddress(); 35 | 36 | RuleFor(p => p.AddUserDto.Password) 37 | .NotEmpty() 38 | .MinimumLength(5); 39 | } 40 | } 41 | public sealed class Handler : IRequestHandler 42 | { 43 | private readonly UserManager _userManager; 44 | private readonly IMapper _mapper; 45 | private readonly ILogger _logger; 46 | 47 | public Handler(UserManager userManager, IMapper mapper, ILogger logger) 48 | { 49 | _userManager = userManager; 50 | _mapper = mapper; 51 | _logger = logger; 52 | } 53 | 54 | public async Task Handle(Command request, CancellationToken cancellationToken) 55 | { 56 | var userWithSameName = await _userManager.FindByNameAsync(request.AddUserDto.UserName!); 57 | if (userWithSameName != null) throw new UserRegistrationException(string.Format("Username {0} is already taken.", request.AddUserDto.UserName)); 58 | var userWithSameEmail = await _userManager.FindByEmailAsync(request.AddUserDto.Email!); 59 | if (userWithSameEmail != null) throw new UserRegistrationException(string.Format("Email {0} is already registered.", request.AddUserDto.Email)); 60 | 61 | AppUser user = new() { UserName = request.AddUserDto.UserName, Email = request.AddUserDto.Email }; 62 | var result = await _userManager.CreateAsync(user, request.AddUserDto.Password!); 63 | if (result.Succeeded) 64 | { 65 | return _mapper.Map(user); 66 | } 67 | 68 | foreach (var error in result.Errors) 69 | { 70 | _logger.LogError("{error}", error.Description); 71 | } 72 | 73 | throw new UserRegistrationException("Identity Exception"); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Application/Users/Mappings/UserMappings.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Identity.Application.Users.Dtos; 2 | using FluentPos.Identity.Domain.Users; 3 | using Mapster; 4 | 5 | namespace FluentPos.Identity.Application.Users.Mappings; 6 | public sealed class UserMappings : IRegister 7 | { 8 | public void Register(TypeAdapterConfig config) 9 | { 10 | config.NewConfig(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | FluentPos.Identity.Domain 6 | FluentPos.Identity.Domain 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Domain/Users/AppUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace FluentPos.Identity.Domain.Users; 4 | public class AppUser : IdentityUser { } -------------------------------------------------------------------------------- /fluentpos/services/identity/Infrastructure/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Identity.Application; 2 | using FluentPos.Identity.Domain.Users; 3 | using FluentPos.Identity.Infrastructure.Persistence; 4 | using FSH.Framework.Infrastructure; 5 | using FSH.Framework.Infrastructure.Auth.OpenIddict; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using static OpenIddict.Abstractions.OpenIddictConstants; 10 | 11 | namespace FluentPos.Identity.Infrastructure; 12 | public static class Extensions 13 | { 14 | internal static bool enableSwagger = false; 15 | public static void AddIdentityInfrastructure(this WebApplicationBuilder builder) 16 | { 17 | var coreAssembly = typeof(IdentityCore).Assembly; 18 | var dbContextAssembly = typeof(AppDbContext).Assembly; 19 | 20 | builder.Services.AddIdentityExtensions(); 21 | builder.AddInfrastructure(applicationAssembly: coreAssembly, enableSwagger: enableSwagger); 22 | builder.ConfigureAuthServer(dbContextAssembly); 23 | builder.Services.AddHostedService(); 24 | } 25 | 26 | public static void UseIdentityInfrastructure(this WebApplication app) 27 | { 28 | app.UseInfrastructure(app.Environment, enableSwagger); 29 | } 30 | internal static IServiceCollection AddIdentityExtensions(this IServiceCollection services) 31 | { 32 | services 33 | .AddIdentity(options => 34 | { 35 | options.Password.RequiredLength = 6; 36 | options.Password.RequireDigit = false; 37 | options.Password.RequireLowercase = false; 38 | options.Password.RequireNonAlphanumeric = false; 39 | options.Password.RequireUppercase = false; 40 | options.User.RequireUniqueEmail = true; 41 | }) 42 | .AddEntityFrameworkStores() 43 | .AddDefaultTokenProviders(); 44 | 45 | services.Configure(options => 46 | { 47 | // Configure Identity to use the same JWT claims as OpenIddict instead 48 | // of the legacy WS-Federation claims it uses by default (ClaimTypes), 49 | // which saves you from doing the mapping in your authorization controller. 50 | options.ClaimsIdentity.UserNameClaimType = Claims.Name; 51 | options.ClaimsIdentity.UserIdClaimType = Claims.Subject; 52 | options.ClaimsIdentity.RoleClaimType = Claims.Role; 53 | options.ClaimsIdentity.EmailClaimType = Claims.Email; 54 | 55 | // Note: to require account confirmation before login, 56 | // register an email sender service (IEmailSender) and 57 | // set options.SignIn.RequireConfirmedAccount to true. 58 | // 59 | // For more information, visit https://aka.ms/aspaccountconf. 60 | options.SignIn.RequireConfirmedAccount = false; 61 | }); 62 | 63 | return services; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0 4 | FluentPos.Identity.Infrastructure 5 | FluentPos.Identity.Infrastructure 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Infrastructure/Persistence/AppDbContext.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Identity.Domain.Users; 2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace FluentPos.Identity.Infrastructure.Persistence; 6 | 7 | public class AppDbContext : IdentityDbContext 8 | { 9 | public AppDbContext(DbContextOptions options) : base(options) { } 10 | 11 | protected override void OnModelCreating(ModelBuilder modelBuilder) 12 | { 13 | base.OnModelCreating(modelBuilder); 14 | modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Infrastructure/Persistence/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace FluentPos.Identity.Infrastructure.Persistence; 2 | 3 | public static class Constants 4 | { 5 | public const string Client = "console"; 6 | public const string ClientSecret = "858b39fd-3908-45cb-ab14-aa58220f6d69"; 7 | public const string ClientDisplayName = "console"; 8 | public const string CatalogResourceServer = "catalog.resource.server"; 9 | public const string CatalogResourceServerSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB342"; 10 | public const string CatalogReadScope = "catalog:read"; 11 | public const string CatalogWriteScope = "catalog:write"; 12 | public const string CartResourceServer = "cart.resource.server"; 13 | public const string CartResourceServerSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB344"; 14 | public const string CartReadScope = "cart:read"; 15 | public const string CartWriteScope = "cart:write"; 16 | public const string GatewayResourceServer = "gateway.resource.server"; 17 | public const string GatewayResourceServerSecret = "846B62D0-DEF9-4215-A99D-86E6B8DAB343"; 18 | } 19 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Infrastructure/Persistence/IdentityConfiguration.cs: -------------------------------------------------------------------------------- 1 | using FluentPos.Identity.Domain.Users; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace FluentPos.Identity.Infrastructure.Persistence; 6 | internal class AppUserConfiguration : IEntityTypeConfiguration 7 | { 8 | public void Configure(EntityTypeBuilder builder) 9 | { 10 | const string IdentitySchemaName = "Identity"; 11 | builder.ToTable("Users", IdentitySchemaName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fluentpos/services/identity/Infrastructure/Persistence/SeedClientsAndScopes.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using OpenIddict.Abstractions; 4 | using static OpenIddict.Abstractions.OpenIddictConstants; 5 | 6 | namespace FluentPos.Identity.Infrastructure.Persistence; 7 | 8 | public class SeedClientsAndScopes : IHostedService 9 | { 10 | private readonly IServiceProvider _serviceProvider; 11 | 12 | public SeedClientsAndScopes(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; 13 | 14 | public async Task StartAsync(CancellationToken cancellationToken) 15 | { 16 | await using var scope = _serviceProvider.CreateAsyncScope(); 17 | 18 | var context = scope.ServiceProvider.GetRequiredService(); 19 | _ = await context.Database.EnsureCreatedAsync(cancellationToken); 20 | 21 | var manager = scope.ServiceProvider.GetRequiredService(); 22 | if (await manager.FindByClientIdAsync(Constants.Client, cancellationToken) is null) 23 | { 24 | await manager.CreateAsync(new OpenIddictApplicationDescriptor 25 | { 26 | ClientId = Constants.Client, 27 | ClientSecret = Constants.ClientSecret, 28 | DisplayName = Constants.ClientDisplayName, 29 | Permissions = 30 | { 31 | Permissions.Endpoints.Token, 32 | Permissions.GrantTypes.ClientCredentials, 33 | Permissions.ResponseTypes.Token, 34 | Permissions.Scopes.Email, 35 | Permissions.Scopes.Profile, 36 | Permissions.Scopes.Roles, 37 | Permissions.Prefixes.Scope + Constants.CatalogReadScope, 38 | Permissions.Prefixes.Scope + Constants.CatalogWriteScope, 39 | Permissions.Prefixes.Scope + Constants.CartReadScope, 40 | Permissions.Prefixes.Scope + Constants.CartWriteScope 41 | } 42 | }, cancellationToken); 43 | } 44 | 45 | if (await manager.FindByClientIdAsync(Constants.GatewayResourceServer, cancellationToken) is null) 46 | { 47 | await manager.CreateAsync(new OpenIddictApplicationDescriptor 48 | { 49 | ClientId = Constants.GatewayResourceServer, 50 | ClientSecret = Constants.GatewayResourceServerSecret, 51 | Permissions = 52 | { 53 | Permissions.Endpoints.Introspection 54 | } 55 | }, cancellationToken); 56 | } 57 | 58 | if (await manager.FindByClientIdAsync(Constants.CatalogResourceServer, cancellationToken) is null) 59 | { 60 | await manager.CreateAsync(new OpenIddictApplicationDescriptor 61 | { 62 | ClientId = Constants.CatalogResourceServer, 63 | ClientSecret = Constants.CatalogResourceServerSecret, 64 | Permissions = 65 | { 66 | Permissions.Endpoints.Introspection 67 | } 68 | }, cancellationToken); 69 | } 70 | 71 | var scopesManager = scope.ServiceProvider.GetRequiredService(); 72 | 73 | if (await scopesManager.FindByNameAsync(Constants.CatalogWriteScope, cancellationToken) is null) 74 | { 75 | await scopesManager.CreateAsync(new OpenIddictScopeDescriptor 76 | { 77 | Name = Constants.CatalogWriteScope, 78 | Resources = 79 | { 80 | Constants.CatalogResourceServer, 81 | Constants.GatewayResourceServer 82 | } 83 | }, cancellationToken); 84 | } 85 | 86 | if (await scopesManager.FindByNameAsync(Constants.CatalogReadScope, cancellationToken) is null) 87 | { 88 | await scopesManager.CreateAsync(new OpenIddictScopeDescriptor 89 | { 90 | Name = Constants.CatalogReadScope, 91 | Resources = 92 | { 93 | Constants.CatalogResourceServer, 94 | Constants.GatewayResourceServer 95 | } 96 | }, cancellationToken); 97 | } 98 | 99 | if (await scopesManager.FindByNameAsync(Constants.CartWriteScope, cancellationToken) is null) 100 | { 101 | await scopesManager.CreateAsync(new OpenIddictScopeDescriptor 102 | { 103 | Name = Constants.CartWriteScope, 104 | Resources = 105 | { 106 | Constants.CartResourceServer, 107 | Constants.GatewayResourceServer 108 | } 109 | }, cancellationToken); 110 | } 111 | 112 | if (await scopesManager.FindByNameAsync(Constants.CartReadScope, cancellationToken) is null) 113 | { 114 | await scopesManager.CreateAsync(new OpenIddictScopeDescriptor 115 | { 116 | Name = Constants.CartReadScope, 117 | Resources = 118 | { 119 | Constants.CartResourceServer, 120 | Constants.GatewayResourceServer 121 | } 122 | }, cancellationToken); 123 | } 124 | } 125 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 126 | } 127 | -------------------------------------------------------------------------------- /fluentpos/tye.yaml: -------------------------------------------------------------------------------- 1 | name: fluentpos 2 | containerEngine: docker 3 | 4 | services: 5 | - name: identity 6 | project: services/identity/Api/Api.csproj 7 | bindings: 8 | - port: 7001 9 | name: secure 10 | protocol: https 11 | - port: 5001 12 | name: non-secure 13 | protocol: http 14 | - name: gateway 15 | project: gateways/Gateway/Gateway.csproj 16 | bindings: 17 | - port: 7002 18 | name: secure 19 | protocol: https 20 | - port: 5002 21 | name: non-secure 22 | protocol: http 23 | - name: catalog 24 | project: services/catalog/Api/Api.csproj 25 | bindings: 26 | - port: 7003 27 | name: secure 28 | protocol: https 29 | - port: 5003 30 | name: non-secure 31 | protocol: http 32 | - name: cart 33 | project: services/cart/Api/Api.csproj 34 | bindings: 35 | - port: 7004 36 | name: secure 37 | protocol: https 38 | - port: 5004 39 | name: non-secure 40 | protocol: http 41 | -------------------------------------------------------------------------------- /framework/Core/Caching/ICacheService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Caching; 2 | 3 | public interface ICacheService 4 | { 5 | T Get(string key); 6 | Task GetAsync(string key, CancellationToken token = default); 7 | 8 | void Refresh(string key); 9 | Task RefreshAsync(string key, CancellationToken token = default); 10 | 11 | void Remove(string key); 12 | Task RemoveAsync(string key, CancellationToken token = default); 13 | 14 | void Set(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null); 15 | Task SetAsync(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null, CancellationToken cancellationToken = default); 16 | } 17 | -------------------------------------------------------------------------------- /framework/Core/Database/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace FSH.Framework.Core.Database; 4 | 5 | public interface IRepository : IReadRepository, IWriteRepository, IDisposable where TDocument : class 6 | { 7 | } 8 | 9 | public interface IReadRepository where TDocument : class 10 | { 11 | Task FindByIdAsync(TId id, CancellationToken cancellationToken = default); 12 | 13 | Task FindOneAsync(Expression> predicate, CancellationToken cancellationToken = default); 14 | 15 | Task> FindAsync(Expression> predicate, CancellationToken cancellationToken = default); 16 | 17 | Task> GetAllAsync(CancellationToken cancellationToken = default); 18 | Task ExistsAsync(Expression> predicate, CancellationToken cancellationToken = default); 19 | } 20 | 21 | public interface IWriteRepository where TDocument : class 22 | { 23 | Task AddAsync(TDocument entity, CancellationToken cancellationToken = default); 24 | Task UpdateAsync(TDocument entity, CancellationToken cancellationToken = default); 25 | Task DeleteRangeAsync(IReadOnlyList entities, CancellationToken cancellationToken = default); 26 | Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default); 27 | Task DeleteAsync(TDocument entity, CancellationToken cancellationToken = default); 28 | Task DeleteByIdAsync(TId id, CancellationToken cancellationToken = default); 29 | } 30 | -------------------------------------------------------------------------------- /framework/Core/Domain/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Events; 2 | using MassTransit; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace FSH.Framework.Core.Domain; 6 | public abstract class BaseEntity : BaseEntity 7 | { 8 | protected BaseEntity() => Id = NewId.Next().ToGuid(); 9 | } 10 | 11 | public abstract class BaseEntity : IBaseEntity 12 | { 13 | [JsonPropertyOrder(-1)] 14 | public TId Id { get; protected set; } = default!; 15 | public DateTime CreatedOn { get; private set; } = DateTime.UtcNow; 16 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 17 | public string? CreatedBy { get; private set; } 18 | public DateTime? LastModifiedOn { get; private set; } = DateTime.UtcNow; 19 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 20 | public string? LastModifiedBy { get; private set; } 21 | [JsonIgnore] 22 | public bool IsDeleted { get; private set; } 23 | [JsonIgnore] 24 | private readonly List _domainEvents = new(); 25 | [JsonIgnore] 26 | public IReadOnlyList DomainEvents => _domainEvents.AsReadOnly(); 27 | public void UpdateIsDeleted(bool isDeleted) 28 | { 29 | IsDeleted = isDeleted; 30 | } 31 | public void UpdateModifiedProperties(DateTime? lastModifiedOn, string lastModifiedBy) 32 | { 33 | LastModifiedOn = lastModifiedOn; 34 | LastModifiedBy = lastModifiedBy; 35 | } 36 | public void AddDomainEvent(IDomainEvent @event) 37 | { 38 | _domainEvents.Add(@event); 39 | } 40 | 41 | public IDomainEvent[] ClearDomainEvents() 42 | { 43 | var dequeuedEvents = _domainEvents.ToArray(); 44 | _domainEvents.Clear(); 45 | return dequeuedEvents; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /framework/Core/Domain/IBaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | 3 | public interface IBaseEntity 4 | { 5 | // Add Domain Event here 6 | } 7 | 8 | public interface IBaseEntity : IBaseEntity 9 | { 10 | TId Id { get; } 11 | string? CreatedBy { get; } 12 | DateTime? LastModifiedOn { get; } 13 | string? LastModifiedBy { get; } 14 | bool IsDeleted { get; } 15 | void UpdateIsDeleted(bool isDeleted); 16 | void UpdateModifiedProperties(DateTime? lastModifiedOn, string lastModifiedBy); 17 | } -------------------------------------------------------------------------------- /framework/Core/Events/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Events; 2 | 3 | public abstract class DomainEvent : IDomainEvent 4 | { 5 | public DefaultIdType Id { get; } 6 | public DateTime CreationDate { get; } 7 | 8 | public IDictionary MetaData { get; } 9 | 10 | protected DomainEvent() 11 | { 12 | Id = DefaultIdType.NewGuid(); 13 | CreationDate = DateTime.UtcNow; 14 | MetaData = new Dictionary(); 15 | } 16 | } -------------------------------------------------------------------------------- /framework/Core/Events/IDomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Events; 2 | public interface IDomainEvent : IEvent 3 | { 4 | } 5 | -------------------------------------------------------------------------------- /framework/Core/Events/IEvent.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace FSH.Framework.Core.Events; 4 | public interface IEvent : INotification 5 | { 6 | DefaultIdType Id { get; } 7 | DateTime CreationDate { get; } 8 | IDictionary MetaData { get; } 9 | } 10 | -------------------------------------------------------------------------------- /framework/Core/Events/IEventPublisher.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Events; 2 | public interface IEventPublisher 3 | { 4 | Task PublishAsync(TEvent @event, CancellationToken token = default) where TEvent : IEvent; 5 | } 6 | -------------------------------------------------------------------------------- /framework/Core/Events/IIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Events; 2 | public interface IIntegrationEvent : IEvent 3 | { 4 | } 5 | -------------------------------------------------------------------------------- /framework/Core/Events/IntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Events; 2 | 3 | public class IntegrationEvent : IIntegrationEvent 4 | { 5 | public Guid Id { get; } 6 | public DateTime CreationDate { get; } 7 | 8 | public IDictionary MetaData { get; } 9 | 10 | protected IntegrationEvent() 11 | { 12 | Id = Guid.NewGuid(); 13 | CreationDate = DateTime.UtcNow; 14 | MetaData = new Dictionary(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /framework/Core/Exceptions/ConfigurationMissingException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | 5 | public class ConfigurationMissingException : CustomException 6 | { 7 | public ConfigurationMissingException(string sectionName) : base($"{sectionName} Missing in Configurations", HttpStatusCode.NotFound) 8 | { 9 | } 10 | 11 | public ConfigurationMissingException(string message, HttpStatusCode statusCode = HttpStatusCode.NotFound) : base(message, statusCode) 12 | { 13 | } 14 | } -------------------------------------------------------------------------------- /framework/Core/Exceptions/CustomException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | 5 | public class CustomException : Exception 6 | { 7 | public HttpStatusCode StatusCode { get; } 8 | 9 | public CustomException(string message, HttpStatusCode statusCode = HttpStatusCode.InternalServerError) 10 | : base(message) 11 | { 12 | StatusCode = statusCode; 13 | } 14 | } -------------------------------------------------------------------------------- /framework/Core/Exceptions/ForbiddenException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | public class ForbiddenException : CustomException 5 | { 6 | public ForbiddenException() : base("You do not have permissions to access this resource.", HttpStatusCode.Forbidden) 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /framework/Core/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | public class NotFoundException : CustomException 5 | { 6 | public NotFoundException(string message) : base(message, HttpStatusCode.NotFound) 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /framework/Core/Exceptions/UnauthorizedException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | 5 | public class UnauthorizedException : CustomException 6 | { 7 | public string Error { get; set; } 8 | public string Description { get; set; } 9 | public UnauthorizedException(string error = default!, string description = default!) : base(error, HttpStatusCode.Unauthorized) 10 | { 11 | Error = error; 12 | Description = description; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /framework/Core/FSH.Framework.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /framework/Core/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using DefaultIdType = global::System.Guid; -------------------------------------------------------------------------------- /framework/Core/Identity/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Identity; 2 | public static class RoleConstants 3 | { 4 | public const string AdministratorRole = "Administrator"; 5 | public const string BasicRole = "Basic"; 6 | } 7 | 8 | public static class UserConstants 9 | { 10 | public const string DefaultPassword = "123Pa$$word!"; 11 | } -------------------------------------------------------------------------------- /framework/Core/Pagination/PagedList.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Pagination; 2 | public class PagedList 3 | { 4 | public IList Data { get; } 5 | public PagedList(IEnumerable items, int totalItems, int pageNumber, int pageSize) 6 | { 7 | PageNumber = pageNumber; 8 | PageSize = pageSize; 9 | TotalItems = totalItems; 10 | if (totalItems > 0) 11 | { 12 | TotalPages = (int)Math.Ceiling(totalItems / (double)pageSize); 13 | } 14 | Data = items as IList ?? new List(items); 15 | } 16 | public int PageNumber { get; } 17 | public int PageSize { get; } 18 | public int TotalPages { get; } 19 | public int TotalItems { get; } 20 | public bool IsFirstPage => PageNumber == 1; 21 | public bool IsLastPage => PageNumber == TotalPages && TotalPages > 0; 22 | } -------------------------------------------------------------------------------- /framework/Core/Pagination/PaginationParameters.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Pagination; 2 | public abstract class PaginationParameters 3 | { 4 | internal virtual int MaxPageSize { get; } = 20; 5 | internal virtual int DefaultPageSize { get; set; } = 10; 6 | public virtual int PageNumber { get; set; } = 1; 7 | public int PageSize 8 | { 9 | get 10 | { 11 | return DefaultPageSize; 12 | } 13 | set 14 | { 15 | DefaultPageSize = value > MaxPageSize ? MaxPageSize : value; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /framework/Core/Serializers/ISerializerService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Services; 2 | 3 | namespace FSH.Framework.Core.Serializers; 4 | 5 | public interface ISerializerService : ITransientService 6 | { 7 | string Serialize(T obj); 8 | 9 | string Serialize(T obj, Type type); 10 | 11 | T Deserialize(string text); 12 | } 13 | -------------------------------------------------------------------------------- /framework/Core/Services/IDateTimeService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Services; 2 | 3 | public interface IDateTimeService : IScopedService 4 | { 5 | public DateTime DateTimeUtcNow { get; } 6 | public DateOnly DateOnlyUtcNow { get; } 7 | } 8 | -------------------------------------------------------------------------------- /framework/Core/Services/IScopedService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Services; 2 | 3 | public interface IScopedService 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /framework/Core/Services/ITransientService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Services; 2 | 3 | public interface ITransientService 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /framework/Core/Validation/CustomValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace FSH.Framework.Core.Validation; 4 | public class CustomValidator : AbstractValidator 5 | { 6 | } -------------------------------------------------------------------------------- /framework/Infrastructure/Auth/OpenId/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Exceptions; 2 | using FSH.Framework.Infrastructure.Options; 3 | using Microsoft.AspNetCore.Authentication.JwtBearer; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace FSH.Framework.Infrastructure.Auth.OpenId; 9 | public static class Extensions 10 | { 11 | public static IServiceCollection AddOpenIdAuth(this IServiceCollection services, IConfiguration config, List policyNames) 12 | { 13 | var authOptions = services.BindValidateReturn(config); 14 | 15 | services.AddAuthentication(options => 16 | { 17 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 18 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 19 | }).AddJwtBearer(options => 20 | { 21 | options.Authority = authOptions.Authority; 22 | options.Audience = authOptions.Audience; 23 | options.RequireHttpsMetadata = false; 24 | options.SaveToken = true; 25 | options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters 26 | { 27 | RequireAudience = true, 28 | ValidateAudience = true, 29 | }; 30 | options.Events = new JwtBearerEvents 31 | { 32 | OnChallenge = context => 33 | { 34 | context.HandleResponse(); 35 | if (!context.Response.HasStarted) 36 | { 37 | throw new UnauthorizedException(context.Error!, context.ErrorDescription!); 38 | } 39 | 40 | return Task.CompletedTask; 41 | }, 42 | OnForbidden = _ => throw new ForbiddenException() 43 | }; 44 | }); 45 | 46 | if (policyNames?.Count > 0) 47 | { 48 | services.AddAuthorization(options => 49 | { 50 | foreach (string policyName in policyNames) 51 | { 52 | options.AddPolicy(policyName, policy => policy.Requirements.Add(new HasScopeRequirement(policyName, authOptions.Authority!))); 53 | } 54 | }); 55 | } 56 | 57 | services.AddSingleton(); 58 | return services; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /framework/Infrastructure/Auth/OpenId/HasScopeHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace FSH.Framework.Infrastructure.Auth.OpenId; 4 | public class HasScopeHandler : AuthorizationHandler 5 | { 6 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement) 7 | { 8 | // If user does not have the scope claim, get out of here 9 | if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer)) 10 | return Task.CompletedTask; 11 | 12 | // Split the scopes string into an array 13 | string[] scopes = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer)!.Value.Split(' '); 14 | 15 | // Succeed if the scope array contains the required scope 16 | if (scopes.Any(s => s == requirement.Scope)) 17 | context.Succeed(requirement); 18 | 19 | return Task.CompletedTask; 20 | } 21 | } -------------------------------------------------------------------------------- /framework/Infrastructure/Auth/OpenId/HasScopeRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace FSH.Framework.Infrastructure.Auth.OpenId; 4 | public class HasScopeRequirement : IAuthorizationRequirement 5 | { 6 | public string Issuer { get; } 7 | public string Scope { get; } 8 | 9 | public HasScopeRequirement(string scope, string issuer) 10 | { 11 | Scope = scope ?? throw new ArgumentNullException(nameof(scope)); 12 | Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer)); 13 | } 14 | } -------------------------------------------------------------------------------- /framework/Infrastructure/Auth/OpenId/OpenIdOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace FSH.Framework.Infrastructure.Auth.OpenId; 5 | public class OpenIdOptions : IOptionsRoot 6 | { 7 | [Required(AllowEmptyStrings = false)] 8 | public string? Authority { get; set; } = string.Empty; 9 | [Required(AllowEmptyStrings = false)] 10 | public string? Audience { get; set; } = string.Empty; 11 | } 12 | -------------------------------------------------------------------------------- /framework/Infrastructure/Auth/OpenIddict/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using FSH.Framework.Infrastructure.Options; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using OpenIddict.Validation.AspNetCore; 9 | using static OpenIddict.Abstractions.OpenIddictConstants; 10 | 11 | namespace FSH.Framework.Infrastructure.Auth.OpenIddict; 12 | 13 | public static class Extensions 14 | { 15 | public static IServiceCollection AddAuthValidation(this IServiceCollection services, IConfiguration config) 16 | { 17 | var authOptions = services.BindValidateReturn(config); 18 | 19 | services.AddOpenIddict() 20 | .AddValidation(options => 21 | { 22 | options.SetIssuer(authOptions.IssuerUrl!); 23 | options.UseIntrospection() 24 | .SetClientId(authOptions.ClientId!) 25 | .SetClientSecret(authOptions.ClientSecret!); 26 | options.UseSystemNetHttp(); 27 | options.UseAspNetCore(); 28 | }); 29 | 30 | services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); 31 | services.AddAuthorization(); 32 | return services; 33 | } 34 | 35 | public static void ConfigureAuthServer(this WebApplicationBuilder builder, Assembly dbContextAssembly, string connectionName = "DefaultConnection") where T : DbContext 36 | { 37 | builder.Services.AddOpenIddict() 38 | .AddCore(options => options.UseEntityFrameworkCore().UseDbContext()) 39 | .AddServer(options => 40 | { 41 | options.SetAuthorizationEndpointUris("/connect/authorize") 42 | .SetIntrospectionEndpointUris("/connect/introspect") 43 | .SetUserinfoEndpointUris("connect/userinfo") 44 | .SetTokenEndpointUris("/connect/token"); 45 | options.AllowClientCredentialsFlow(); 46 | options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles); 47 | options.DisableAccessTokenEncryption(); 48 | options.AddDevelopmentEncryptionCertificate().AddDevelopmentSigningCertificate(); 49 | options.UseAspNetCore().EnableTokenEndpointPassthrough().DisableTransportSecurityRequirement(); 50 | }) 51 | .AddValidation(options => 52 | { 53 | options.UseLocalServer(); 54 | options.UseAspNetCore(); 55 | }); 56 | 57 | builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); 58 | builder.Services.AddAuthorization(); 59 | 60 | string? connectionString = builder.Configuration.GetConnectionString(connectionName); 61 | if (!builder.Environment.IsDevelopment() && connectionString == null) 62 | throw new ArgumentNullException(nameof(connectionString)); 63 | 64 | builder.Services.AddDbContext(options => 65 | { 66 | if (builder.Environment.IsDevelopment()) 67 | { 68 | options.UseInMemoryDatabase("authDb"); 69 | } 70 | else 71 | { 72 | options.UseNpgsql(connectionString, m => m.MigrationsAssembly(dbContextAssembly.FullName)); 73 | } 74 | options.UseOpenIddict(); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /framework/Infrastructure/Auth/OpenIddict/OpenIddictOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace FSH.Framework.Infrastructure.Auth.OpenIddict; 5 | 6 | public class OpenIddictOptions : IOptionsRoot 7 | { 8 | [Required(AllowEmptyStrings = false)] 9 | public string? ClientId { get; set; } = string.Empty; 10 | [Required(AllowEmptyStrings = false)] 11 | public string? ClientSecret { get; set; } = string.Empty; 12 | [Required(AllowEmptyStrings = false)] 13 | public string? IssuerUrl { get; set; } = string.Empty; 14 | } 15 | -------------------------------------------------------------------------------- /framework/Infrastructure/Behaviors/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using MediatR; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace FSH.Framework.Infrastructure.Behaviors; 6 | public static class Extensions 7 | { 8 | public static IServiceCollection AddBehaviors(this IServiceCollection services) 9 | { 10 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); 11 | return services; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /framework/Infrastructure/Behaviors/ValidationBehavior.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using MediatR; 3 | 4 | namespace FSH.Framework.Infrastructure.Behaviors; 5 | public class ValidationBehavior : IPipelineBehavior 6 | where TRequest : IRequest 7 | { 8 | private readonly IEnumerable> _validators; 9 | 10 | public ValidationBehavior(IEnumerable> validators) 11 | { 12 | _validators = validators; 13 | } 14 | 15 | public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) 16 | { 17 | if (_validators.Any()) 18 | { 19 | var context = new ValidationContext(request); 20 | var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); 21 | var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList(); 22 | 23 | if (failures.Count != 0) 24 | throw new ValidationException(failures); 25 | } 26 | 27 | return await next(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /framework/Infrastructure/Caching/CachingOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | 3 | namespace FSH.Framework.Infrastructure.Caching; 4 | public class CachingOptions : IOptionsRoot 5 | { 6 | public bool EnableDistributedCaching { get; set; } = false; 7 | public int SlidingExpirationInMinutes { get; set; } = 2; 8 | public int AbsoluteExpirationInMinutes { get; set; } = 5; 9 | public bool PreferRedis { get; set; } = false; 10 | public string? RedisURL { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /framework/Infrastructure/Caching/DistributedCacheService.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using FSH.Framework.Core.Caching; 3 | using FSH.Framework.Core.Serializers; 4 | using Microsoft.Extensions.Caching.Distributed; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace FSH.Framework.Infrastructure.Caching 8 | { 9 | internal class DistributedCacheService : ICacheService 10 | { 11 | private readonly IDistributedCache _cache; 12 | private readonly ILogger _logger; 13 | private readonly ISerializerService _serializer; 14 | 15 | public DistributedCacheService(IDistributedCache cache, ISerializerService serializer, ILogger logger) => 16 | (_cache, _serializer, _logger) = (cache, serializer, logger); 17 | 18 | public T Get(string key) => 19 | Get(key) is { } data 20 | ? Deserialize(data) 21 | : default!; 22 | 23 | private byte[] Get(string key) 24 | { 25 | ArgumentNullException.ThrowIfNull(key); 26 | 27 | try 28 | { 29 | return _cache.Get(key)!; 30 | } 31 | catch 32 | { 33 | return default!; 34 | } 35 | } 36 | 37 | public async Task GetAsync(string key, CancellationToken token = default) => 38 | await GetAsync(key, token) is { } data 39 | ? Deserialize(data) 40 | : default!; 41 | 42 | private async Task GetAsync(string key, CancellationToken token = default) 43 | { 44 | try 45 | { 46 | byte[]? data = await _cache.GetAsync(key, token)!; 47 | return data!; 48 | } 49 | catch 50 | { 51 | return default!; 52 | } 53 | } 54 | 55 | public void Refresh(string key) 56 | { 57 | try 58 | { 59 | _cache.Refresh(key); 60 | } 61 | catch 62 | { 63 | } 64 | } 65 | 66 | public async Task RefreshAsync(string key, CancellationToken token = default) 67 | { 68 | try 69 | { 70 | await _cache.RefreshAsync(key, token); 71 | _logger.LogDebug("Cache Refreshed : {key}", key); 72 | } 73 | catch 74 | { 75 | } 76 | } 77 | 78 | public void Remove(string key) 79 | { 80 | try 81 | { 82 | _cache.Remove(key); 83 | } 84 | catch 85 | { 86 | } 87 | } 88 | 89 | public async Task RemoveAsync(string key, CancellationToken token = default) 90 | { 91 | try 92 | { 93 | await _cache.RemoveAsync(key, token); 94 | } 95 | catch 96 | { 97 | } 98 | } 99 | 100 | public void Set(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null) => 101 | Set(key, Serialize(value), slidingExpiration); 102 | 103 | private void Set(string key, byte[] value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null) 104 | { 105 | try 106 | { 107 | _cache.Set(key, value, GetOptions(slidingExpiration, absoluteExpiration)); 108 | _logger.LogDebug("Added to Cache : {key}", key); 109 | } 110 | catch 111 | { 112 | } 113 | } 114 | 115 | public Task SetAsync(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null, CancellationToken cancellationToken = default) => 116 | SetAsync(key, Serialize(value), slidingExpiration, absoluteExpiration, cancellationToken); 117 | 118 | private async Task SetAsync(string key, byte[] value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null, CancellationToken token = default) 119 | { 120 | try 121 | { 122 | await _cache.SetAsync(key, value, GetOptions(slidingExpiration, absoluteExpiration), token); 123 | _logger.LogDebug("Added to Cache : {key}", key); 124 | } 125 | catch 126 | { 127 | } 128 | } 129 | 130 | private byte[] Serialize(T item) 131 | { 132 | return Encoding.Default.GetBytes(_serializer.Serialize(item)); 133 | } 134 | 135 | private T Deserialize(byte[] cachedData) => 136 | _serializer.Deserialize(Encoding.Default.GetString(cachedData)); 137 | 138 | private static DistributedCacheEntryOptions GetOptions(TimeSpan? slidingExpiration, DateTimeOffset? absoluteExpiration) 139 | { 140 | var options = new DistributedCacheEntryOptions(); 141 | if (slidingExpiration.HasValue) 142 | { 143 | options.SetSlidingExpiration(slidingExpiration.Value); 144 | } 145 | else 146 | { 147 | options.SetSlidingExpiration(TimeSpan.FromMinutes(10)); // Default expiration time of 10 minutes. 148 | } 149 | 150 | if (absoluteExpiration.HasValue) 151 | { 152 | options.SetAbsoluteExpiration(absoluteExpiration.Value); 153 | } 154 | else 155 | { 156 | options.SetAbsoluteExpiration(TimeSpan.FromMinutes(15)); // Default expiration time of 10 minutes. 157 | } 158 | 159 | return options; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /framework/Infrastructure/Caching/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Caching; 2 | using FSH.Framework.Infrastructure.Options; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace FSH.Framework.Infrastructure.Caching; 7 | public static class Extensions 8 | { 9 | public static IServiceCollection AddCachingService(this IServiceCollection services, IConfiguration configuration) 10 | { 11 | var cacheOptions = services.BindValidateReturn(configuration); 12 | if (cacheOptions.EnableDistributedCaching) 13 | { 14 | if (cacheOptions.PreferRedis) 15 | { 16 | services.AddStackExchangeRedisCache(options => 17 | { 18 | options.Configuration = cacheOptions.RedisURL; 19 | options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions() 20 | { 21 | AbortOnConnectFail = true, 22 | EndPoints = { cacheOptions.RedisURL } 23 | }; 24 | }); 25 | } 26 | else 27 | { 28 | services.AddDistributedMemoryCache(); 29 | } 30 | 31 | services.AddTransient(); 32 | } 33 | else 34 | { 35 | services.AddTransient(); 36 | } 37 | services.AddMemoryCache(); 38 | 39 | return services; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /framework/Infrastructure/Caching/InMemoryCacheService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Caching; 2 | using Microsoft.Extensions.Caching.Memory; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace FSH.Framework.Infrastructure.Caching; 7 | 8 | public class InMemoryCacheService : ICacheService 9 | { 10 | private readonly ILogger _logger; 11 | private readonly IMemoryCache _cache; 12 | private readonly CachingOptions _cacheOptions; 13 | public InMemoryCacheService(IMemoryCache cache, ILogger logger, IOptions cacheOptions) 14 | { 15 | _cache = cache; 16 | _logger = logger; 17 | _cacheOptions = cacheOptions.Value; 18 | } 19 | 20 | public T Get(string key) => _cache.Get(key)!; 21 | 22 | public Task GetAsync(string key, CancellationToken token = default) 23 | { 24 | var data = Get(key)!; 25 | if (data != null) 26 | { 27 | _logger.LogDebug("Get From Cache : {key}", key); 28 | } 29 | else 30 | { 31 | _logger.LogDebug("Key Not Found in Cache : {key}", key); 32 | } 33 | return Task.FromResult(data); 34 | } 35 | 36 | public void Refresh(string key) => _cache.TryGetValue(key, out _); 37 | 38 | public Task RefreshAsync(string key, CancellationToken token = default) 39 | { 40 | Refresh(key); 41 | return Task.CompletedTask; 42 | } 43 | 44 | public void Remove(string key) => _cache.Remove(key); 45 | 46 | public Task RemoveAsync(string key, CancellationToken token = default) 47 | { 48 | Remove(key); 49 | return Task.CompletedTask; 50 | } 51 | 52 | public void Set(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null) 53 | { 54 | slidingExpiration ??= TimeSpan.FromMinutes(_cacheOptions.SlidingExpirationInMinutes); 55 | absoluteExpiration ??= DateTime.UtcNow.AddMinutes(_cacheOptions.AbsoluteExpirationInMinutes); 56 | _cache.Set(key, value, new MemoryCacheEntryOptions { SlidingExpiration = slidingExpiration, AbsoluteExpiration = absoluteExpiration }); 57 | _logger.LogDebug("Added to Cache : {key}", key); 58 | } 59 | 60 | public Task SetAsync(string key, T value, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null, CancellationToken token = default) 61 | { 62 | Set(key, value, slidingExpiration); 63 | return Task.CompletedTask; 64 | } 65 | } -------------------------------------------------------------------------------- /framework/Infrastructure/Controllers/BaseApiController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace FSH.Framework.Infrastructure.Controllers; 8 | 9 | [ApiController] 10 | [Authorize] 11 | [Route("[controller]")] 12 | public class BaseApiController : ControllerBase 13 | { 14 | private ISender _mediator = null!; 15 | 16 | protected ISender Mediator => _mediator ??= HttpContext.RequestServices.GetRequiredService(); 17 | } -------------------------------------------------------------------------------- /framework/Infrastructure/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Framework.Infrastructure.Behaviors; 3 | using FSH.Framework.Infrastructure.Caching; 4 | using FSH.Framework.Infrastructure.Logging.Serilog; 5 | using FSH.Framework.Infrastructure.Mapping.Mapster; 6 | using FSH.Framework.Infrastructure.Middlewares; 7 | using FSH.Framework.Infrastructure.Options; 8 | using FSH.Framework.Infrastructure.Services; 9 | using FSH.Framework.Infrastructure.Swagger; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Hosting; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using System.Reflection; 14 | 15 | namespace FSH.Framework.Infrastructure; 16 | 17 | public static class Extensions 18 | { 19 | public const string AllowAllOrigins = "AllowAll"; 20 | public static void AddInfrastructure(this WebApplicationBuilder builder, Assembly? applicationAssembly = null, bool enableSwagger = true) 21 | { 22 | var config = builder.Configuration; 23 | var appOptions = builder.Services.BindValidateReturn(config); 24 | 25 | builder.Services.AddCors(options => 26 | { 27 | options.AddPolicy(name: AllowAllOrigins, 28 | builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); 29 | }); 30 | builder.Services.AddExceptionMiddleware(); 31 | builder.Services.AddControllers(); 32 | builder.Services.AddEndpointsApiExplorer(); 33 | builder.ConfigureSerilog(appOptions.Name); 34 | builder.Services.AddRouting(options => options.LowercaseUrls = true); 35 | if (applicationAssembly != null) 36 | { 37 | builder.Services.AddMapsterExtension(applicationAssembly); 38 | builder.Services.AddBehaviors(); 39 | builder.Services.AddValidatorsFromAssembly(applicationAssembly); 40 | builder.Services.AddMediatR(o => o.RegisterServicesFromAssembly(applicationAssembly)); 41 | } 42 | 43 | if (enableSwagger) builder.Services.AddSwaggerExtension(config); 44 | builder.Services.AddCachingService(config); 45 | builder.Services.AddInternalServices(); 46 | } 47 | 48 | public static void UseInfrastructure(this WebApplication app, IWebHostEnvironment env, bool enableSwagger = true) 49 | { 50 | //Preserve Order 51 | app.UseCors(AllowAllOrigins); 52 | app.UseExceptionMiddleware(); 53 | app.UseAuthentication(); 54 | app.UseAuthorization(); 55 | app.MapControllers(); 56 | if (enableSwagger) app.UseSwaggerExtension(env); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /framework/Infrastructure/FSH.Framework.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /framework/Infrastructure/Logging/Serilog/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | using Microsoft.AspNetCore.Builder; 3 | using Serilog; 4 | using Serilog.Core; 5 | using Serilog.Events; 6 | using Serilog.Exceptions; 7 | using Serilog.Formatting.Compact; 8 | 9 | namespace FSH.Framework.Infrastructure.Logging.Serilog; 10 | 11 | public static class Extensions 12 | { 13 | public static void ConfigureSerilog(this WebApplicationBuilder builder, string appName) 14 | { 15 | var config = builder.Configuration; 16 | var serilogOptions = builder.Services.BindValidateReturn(config); 17 | _ = builder.Host.UseSerilog((_, _, serilogConfig) => 18 | { 19 | if (serilogOptions.EnableErichers) ConfigureEnrichers(serilogConfig, appName); 20 | ConfigureConsoleLogging(serilogConfig, serilogOptions.StructuredConsoleLogging); 21 | ConfigureWriteToFile(serilogConfig, serilogOptions.WriteToFile, serilogOptions.RetentionFileCount, appName); 22 | SetMinimumLogLevel(serilogConfig, serilogOptions.MinimumLogLevel); 23 | if (serilogOptions.OverideMinimumLogLevel) OverideMinimumLogLevel(serilogConfig); 24 | }); 25 | } 26 | 27 | private static void ConfigureEnrichers(LoggerConfiguration config, string appName) 28 | { 29 | config 30 | .Enrich.FromLogContext() 31 | .Enrich.WithProperty("Application", appName) 32 | .Enrich.WithExceptionDetails() 33 | .Enrich.WithMachineName() 34 | .Enrich.WithProcessId() 35 | .Enrich.WithThreadId(); 36 | } 37 | 38 | private static void ConfigureConsoleLogging(LoggerConfiguration serilogConfig, bool structuredConsoleLogging) 39 | { 40 | if (structuredConsoleLogging) 41 | { 42 | serilogConfig.WriteTo.Async(wt => wt.Console(new CompactJsonFormatter())); 43 | } 44 | else 45 | { 46 | serilogConfig.WriteTo.Async(wt => wt.Console()); 47 | } 48 | } 49 | 50 | private static void ConfigureWriteToFile(LoggerConfiguration serilogConfig, bool writeToFile, int retainedFileCount, string appName) 51 | { 52 | if (writeToFile) 53 | { 54 | serilogConfig.WriteTo.File( 55 | new CompactJsonFormatter(), 56 | $"Logs/{appName.ToLower()}.logs.json", 57 | restrictedToMinimumLevel: LogEventLevel.Information, 58 | rollingInterval: RollingInterval.Day, 59 | retainedFileCountLimit: retainedFileCount); 60 | } 61 | } 62 | 63 | private static void SetMinimumLogLevel(LoggerConfiguration serilogConfig, string minLogLevel) 64 | { 65 | var loggingLevelSwitch = new LoggingLevelSwitch 66 | { 67 | MinimumLevel = minLogLevel.ToLower() switch 68 | { 69 | "debug" => LogEventLevel.Debug, 70 | "information" => LogEventLevel.Information, 71 | "warning" => LogEventLevel.Warning, 72 | _ => LogEventLevel.Information, 73 | } 74 | }; 75 | serilogConfig.MinimumLevel.ControlledBy(loggingLevelSwitch); 76 | } 77 | 78 | private static void OverideMinimumLogLevel(LoggerConfiguration serilogConfig) 79 | { 80 | serilogConfig 81 | .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 82 | .MinimumLevel.Override("Hangfire", LogEventLevel.Warning) 83 | .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) 84 | .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Error) 85 | .MinimumLevel.Override("OpenIddict.Validation", LogEventLevel.Error) 86 | .MinimumLevel.Override("System.Net.Http.HttpClient.OpenIddict", LogEventLevel.Error); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /framework/Infrastructure/Logging/Serilog/SerilogOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | 3 | namespace FSH.Framework.Infrastructure.Logging.Serilog; 4 | 5 | public class SerilogOptions : IOptionsRoot 6 | { 7 | public string ElasticSearchUrl { get; set; } = string.Empty; 8 | public bool WriteToFile { get; set; } = false; 9 | public int RetentionFileCount { get; set; } = 5; 10 | public bool StructuredConsoleLogging { get; set; } = false; 11 | public string MinimumLogLevel { get; set; } = "Information"; 12 | public bool EnableErichers { get; set; } = true; 13 | public bool OverideMinimumLogLevel { get; set; } = true; 14 | } -------------------------------------------------------------------------------- /framework/Infrastructure/Mapping/Mapster/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Mapster; 2 | using MapsterMapper; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System.Reflection; 5 | 6 | namespace FSH.Framework.Infrastructure.Mapping.Mapster; 7 | public static class Extensions 8 | { 9 | public static IServiceCollection AddMapsterExtension(this IServiceCollection services, Assembly coreAssembly) 10 | { 11 | var typeAdapterConfig = TypeAdapterConfig.GlobalSettings; 12 | typeAdapterConfig.Scan(coreAssembly); 13 | var mapperConfig = new Mapper(typeAdapterConfig); 14 | services.AddSingleton(mapperConfig); 15 | return services; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /framework/Infrastructure/Messaging/EventPublisher.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Events; 2 | using MassTransit; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace FSH.Framework.Infrastructure.Messaging; 6 | 7 | public class EventPublisher : IEventPublisher 8 | { 9 | private readonly IPublishEndpoint _publisher; 10 | private readonly ILogger _logger; 11 | 12 | public EventPublisher(IPublishEndpoint publisher, ILogger logger) 13 | { 14 | _publisher = publisher; 15 | _logger = logger; 16 | } 17 | 18 | public Task PublishAsync(TEvent @event, CancellationToken token = default) where TEvent : IEvent 19 | { 20 | return _publisher.Publish(@event, token); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /framework/Infrastructure/Messaging/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Infrastructure.Messaging 2 | { 3 | internal class Extensions 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /framework/Infrastructure/Middlewares/ExceptionDetails.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using FSH.Framework.Core.Exceptions; 3 | using Microsoft.AspNetCore.WebUtilities; 4 | 5 | namespace FSH.Framework.Infrastructure.Middlewares; 6 | 7 | public class ExceptionDetails 8 | { 9 | public string? Title { get; set; } 10 | public string? Detail { get; set; } 11 | public Guid TraceId { get; set; } = Guid.NewGuid(); 12 | public List? Errors { get; private set; } 13 | public int? Status { get; set; } 14 | public string? StackTrace { get; set; } 15 | 16 | internal static ExceptionDetails HandleFluentValidationException(FluentValidation.ValidationException exception) 17 | { 18 | var errorResult = new ExceptionDetails() 19 | { 20 | Title = "Validation Failed", 21 | Detail = "One or More Validations failed", 22 | Status = (int)HttpStatusCode.BadRequest, 23 | Errors = new(), 24 | }; 25 | if (exception.Errors.Count() == 1) 26 | { 27 | errorResult.Detail = exception.Errors.FirstOrDefault()!.ErrorMessage; 28 | } 29 | foreach (var error in exception.Errors) 30 | { 31 | errorResult.Errors.Add(error.ErrorMessage); 32 | } 33 | return errorResult; 34 | } 35 | 36 | internal static ExceptionDetails HandleDefaultException(Exception exception) 37 | { 38 | var errorResult = new ExceptionDetails() 39 | { 40 | Title = ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.InternalServerError), 41 | Detail = exception.Message.Trim(), 42 | Status = (int)HttpStatusCode.InternalServerError, 43 | }; 44 | return errorResult; 45 | } 46 | 47 | internal static ExceptionDetails HandleNotFoundException(NotFoundException exception) 48 | { 49 | var errorResult = new ExceptionDetails() 50 | { 51 | Title = ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.NotFound), 52 | Detail = exception.Message.Trim(), 53 | Status = (int)HttpStatusCode.NotFound 54 | }; 55 | return errorResult; 56 | } 57 | 58 | internal static ExceptionDetails HandleUnauthorizedException(UnauthorizedException unauthorizedException) 59 | { 60 | return new ExceptionDetails() 61 | { 62 | Title = string.IsNullOrEmpty(unauthorizedException.Error) ? ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.Unauthorized) : unauthorizedException.Error, 63 | Detail = string.IsNullOrEmpty(unauthorizedException.Description) ? unauthorizedException.Message.Trim() : unauthorizedException.Description, 64 | Status = (int)HttpStatusCode.Unauthorized 65 | }; 66 | } 67 | 68 | internal static ExceptionDetails HandleForbiddenException(ForbiddenException forbiddenException) 69 | { 70 | return new ExceptionDetails() 71 | { 72 | Title = ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.Forbidden), 73 | Detail = forbiddenException.Message.Trim(), 74 | Status = ((int)HttpStatusCode.Forbidden) 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /framework/Infrastructure/Middlewares/ExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Exceptions; 2 | using FSH.Framework.Core.Serializers; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace FSH.Framework.Infrastructure.Middlewares; 9 | 10 | internal class ExceptionMiddleware : IMiddleware 11 | { 12 | private readonly ILogger _logger; 13 | private readonly ISerializerService _serializer; 14 | private readonly IWebHostEnvironment _env; 15 | 16 | public ExceptionMiddleware(ILogger logger, ISerializerService serializer, IWebHostEnvironment env) 17 | { 18 | _logger = logger; 19 | _serializer = serializer; 20 | _env = env; 21 | } 22 | 23 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 24 | { 25 | try 26 | { 27 | await next(context); 28 | } 29 | catch (Exception exception) 30 | { 31 | var errorResult = exception switch 32 | { 33 | FluentValidation.ValidationException fluentException => ExceptionDetails.HandleFluentValidationException(fluentException), 34 | UnauthorizedException unauthorizedException => ExceptionDetails.HandleUnauthorizedException(unauthorizedException), 35 | ForbiddenException forbiddenException => ExceptionDetails.HandleForbiddenException(forbiddenException), 36 | NotFoundException notFoundException => ExceptionDetails.HandleNotFoundException(notFoundException), 37 | _ => ExceptionDetails.HandleDefaultException(exception), 38 | }; 39 | 40 | var errorLogLevel = exception switch 41 | { 42 | FluentValidation.ValidationException or UnauthorizedException => LogLevel.Warning, 43 | _ => LogLevel.Error 44 | }; 45 | 46 | LogErrorMessage(errorLogLevel, exception, errorResult); 47 | 48 | var response = context.Response; 49 | if (!response.HasStarted) 50 | { 51 | response.ContentType = "application/json"; 52 | response.StatusCode = errorResult.Status!.Value; 53 | await response.WriteAsync(_serializer.Serialize(errorResult)); 54 | } 55 | else 56 | { 57 | _logger.LogWarning("Can't write error response. Response has already started."); 58 | } 59 | } 60 | } 61 | 62 | private void LogErrorMessage(LogLevel errorLogLevel, Exception exception, ExceptionDetails details) 63 | { 64 | var properties = new Dictionary 65 | { 66 | { "TraceId", details.TraceId } 67 | }; 68 | 69 | if (details.Errors != null) 70 | { 71 | properties.Add("Errors", details.Errors); 72 | } 73 | 74 | if (_env.IsDevelopment()) 75 | { 76 | properties.Add("StackTrace", exception.StackTrace!.Trim()); 77 | } 78 | 79 | using (_logger.BeginScope(properties)) 80 | { 81 | _logger.Log(errorLogLevel, "{title} | {details} | {traceId}", details.Title, details.Detail, details.TraceId); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /framework/Infrastructure/Middlewares/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace FSH.Framework.Infrastructure.Middlewares; 5 | 6 | public static class Extensions 7 | { 8 | public static IServiceCollection AddExceptionMiddleware(this IServiceCollection services) 9 | { 10 | return services.AddScoped(); 11 | } 12 | 13 | public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder app) 14 | { 15 | return app.UseMiddleware(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /framework/Infrastructure/Options/AppOptions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace FSH.Framework.Infrastructure.Options 4 | { 5 | public class AppOptions : IOptionsRoot 6 | { 7 | [Required(AllowEmptyStrings = false)] 8 | public string Name { get; set; } = "FSH.WebAPI"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /framework/Infrastructure/Options/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Exceptions; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace FSH.Framework.Infrastructure.Options; 6 | 7 | public static class Extensions 8 | { 9 | public static T LoadOptions(this IConfiguration configuration, string sectionName) where T : IOptionsRoot 10 | { 11 | var options = configuration.GetSection(sectionName).Get() ?? throw new ConfigurationMissingException(sectionName); 12 | return options; 13 | } 14 | 15 | public static T BindValidateReturn(this IServiceCollection services, IConfiguration configuration) where T : class, IOptionsRoot 16 | { 17 | services.AddOptions() 18 | .BindConfiguration(typeof(T).Name) 19 | .ValidateDataAnnotations() 20 | .ValidateOnStart(); 21 | return configuration.LoadOptions(typeof(T).Name); 22 | } 23 | public static void BindValidate(this IServiceCollection services) where T : class, IOptionsRoot 24 | { 25 | services.AddOptions() 26 | .BindConfiguration(typeof(T).Name) 27 | .ValidateDataAnnotations() 28 | .ValidateOnStart(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /framework/Infrastructure/Options/IOptionsRoot.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Infrastructure.Options; 2 | 3 | public interface IOptionsRoot 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /framework/Infrastructure/Serializers/NewtonSoftService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Serializers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | using Newtonsoft.Json.Serialization; 5 | 6 | namespace FSH.Framework.Infrastructure.Serializers; 7 | 8 | public class NewtonSoftService : ISerializerService 9 | { 10 | public T Deserialize(string text) 11 | { 12 | return JsonConvert.DeserializeObject(text)!; 13 | } 14 | 15 | public string Serialize(T obj) 16 | { 17 | return JsonConvert.SerializeObject(obj, new JsonSerializerSettings 18 | { 19 | ContractResolver = new CamelCasePropertyNamesContractResolver(), 20 | NullValueHandling = NullValueHandling.Ignore, 21 | 22 | Converters = new List 23 | { 24 | new StringEnumConverter(new CamelCaseNamingStrategy()) 25 | } 26 | }); 27 | } 28 | 29 | public string Serialize(T obj, Type type) 30 | { 31 | return JsonConvert.SerializeObject(obj, type, new()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /framework/Infrastructure/Services/DateTimeService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Services; 2 | 3 | namespace FSH.Framework.Infrastructure.Services; 4 | 5 | public class DateTimeService : IDateTimeService 6 | { 7 | public DateTime DateTimeUtcNow => DateTime.UtcNow; 8 | public DateOnly DateOnlyUtcNow => DateOnly.FromDateTime(DateTimeUtcNow); 9 | } 10 | -------------------------------------------------------------------------------- /framework/Infrastructure/Services/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Services; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace FSH.Framework.Infrastructure.Services; 5 | 6 | internal static class Extensions 7 | { 8 | internal static IServiceCollection AddInternalServices(this IServiceCollection services) => 9 | services 10 | .AddServices(typeof(ITransientService), ServiceLifetime.Transient) 11 | .AddServices(typeof(IScopedService), ServiceLifetime.Scoped); 12 | 13 | internal static IServiceCollection AddServices(this IServiceCollection services, Type interfaceType, ServiceLifetime lifetime) 14 | { 15 | var interfaceTypes = 16 | AppDomain.CurrentDomain.GetAssemblies() 17 | .SelectMany(s => s.GetTypes()) 18 | .Where(t => interfaceType.IsAssignableFrom(t) 19 | && t.IsClass && !t.IsAbstract) 20 | .Select(t => new 21 | { 22 | Service = t.GetInterfaces().FirstOrDefault(), 23 | Implementation = t 24 | }) 25 | .Where(t => t.Service is not null 26 | && interfaceType.IsAssignableFrom(t.Service)); 27 | 28 | foreach (var type in interfaceTypes) 29 | { 30 | services.AddService(type.Service!, type.Implementation, lifetime); 31 | } 32 | 33 | return services; 34 | } 35 | 36 | internal static IServiceCollection AddService(this IServiceCollection services, Type serviceType, Type implementationType, ServiceLifetime lifetime) => 37 | lifetime switch 38 | { 39 | ServiceLifetime.Transient => services.AddTransient(serviceType, implementationType), 40 | ServiceLifetime.Scoped => services.AddScoped(serviceType, implementationType), 41 | ServiceLifetime.Singleton => services.AddSingleton(serviceType, implementationType), 42 | _ => throw new ArgumentException("Invalid lifeTime", nameof(lifetime)) 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /framework/Infrastructure/Swagger/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | using Microsoft.AspNetCore.Authentication.JwtBearer; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.OpenApi.Models; 9 | using Swashbuckle.AspNetCore.SwaggerUI; 10 | 11 | namespace FSH.Framework.Infrastructure.Swagger 12 | { 13 | internal static class Extensions 14 | { 15 | public static void UseSwaggerExtension(this IApplicationBuilder app, IWebHostEnvironment env) 16 | { 17 | if (!env.IsProduction()) 18 | { 19 | app.UseSwagger(c => 20 | { 21 | c.RouteTemplate = "docs/{documentName}/openapi.json"; 22 | c.PreSerializeFilters.Add((swagger, httpReq) => swagger.Servers = new List { new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}{httpReq.PathBase.Value}" } }); 23 | }); 24 | app.UseSwaggerUI(config => 25 | { 26 | config.SwaggerEndpoint("v1/openapi.json", "Version 1"); 27 | config.RoutePrefix = "docs"; 28 | config.DocExpansion(DocExpansion.List); 29 | config.DisplayRequestDuration(); 30 | config.DefaultModelsExpandDepth(-1); 31 | }); 32 | } 33 | } 34 | internal static void AddSwaggerExtension(this IServiceCollection services, IConfiguration configuration) 35 | { 36 | var swaggerOptions = services.BindValidateReturn(configuration); 37 | _ = services.AddSwaggerGen(config => 38 | { 39 | config.CustomSchemaIds(type => type.ToString()); 40 | config.MapType(() => new OpenApiSchema 41 | { 42 | Type = "string", 43 | Format = "date" 44 | }); 45 | 46 | config.SwaggerDoc( 47 | "v1", 48 | new OpenApiInfo 49 | { 50 | Version = "v1", 51 | Title = swaggerOptions.Title, 52 | Description = swaggerOptions.Description, 53 | Contact = new OpenApiContact 54 | { 55 | Name = swaggerOptions.Name, 56 | Email = swaggerOptions.Email, 57 | }, 58 | }); 59 | 60 | config.AddSecurityRequirement(new OpenApiSecurityRequirement { 61 | { 62 | new OpenApiSecurityScheme { 63 | Reference = new OpenApiReference { 64 | Type = ReferenceType.SecurityScheme, 65 | Id = JwtBearerDefaults.AuthenticationScheme 66 | } 67 | }, 68 | Array.Empty() 69 | }}); 70 | 71 | config.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme 72 | { 73 | Name = "Authorization", 74 | Description = "Input your Bearer token to access this API", 75 | In = ParameterLocation.Header, 76 | Type = SecuritySchemeType.Http, 77 | Scheme = JwtBearerDefaults.AuthenticationScheme, 78 | BearerFormat = "JWT", 79 | }); 80 | }); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /framework/Infrastructure/Swagger/SwaggerOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | 3 | namespace FSH.Framework.Infrastructure.Swagger 4 | { 5 | public class SwaggerOptions : IOptionsRoot 6 | { 7 | public string? Title { get; set; } 8 | public string? Description { get; set; } 9 | public string? Name { get; set; } 10 | public string? Email { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /framework/Persistence.EntityFrameworkCore/FSH.Framework.Persistence.EntityFrameworkCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /framework/Persistence.NoSQL/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Database; 2 | using FSH.Framework.Infrastructure.Options; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace FSH.Framework.Persistence.Mongo; 7 | public static class Extensions 8 | { 9 | public static IServiceCollection AddMongoDbContext( 10 | this IServiceCollection services, IConfiguration configuration) 11 | where TContext : MongoDbContext 12 | { 13 | return services.AddMongoDbContext(configuration); 14 | } 15 | 16 | public static IServiceCollection AddMongoDbContext( 17 | this IServiceCollection services, IConfiguration configuration) 18 | where TContextService : IMongoDbContext 19 | where TContextImplementation : MongoDbContext, TContextService 20 | { 21 | var options = services.BindValidateReturn(configuration); 22 | if (string.IsNullOrEmpty(options.DatabaseName)) throw new ArgumentNullException(nameof(options.DatabaseName)); 23 | if (string.IsNullOrEmpty(options.ConnectionString)) throw new ArgumentNullException(nameof(options.ConnectionString)); 24 | services.AddScoped(typeof(TContextService), typeof(TContextImplementation)); 25 | services.AddScoped(typeof(TContextImplementation)); 26 | services.AddScoped(sp => sp.GetRequiredService()); 27 | services.AddTransient(typeof(IRepository<,>), typeof(MongoRepository<,>)); 28 | 29 | return services; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /framework/Persistence.NoSQL/FSH.Framework.Persistence.Mongo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /framework/Persistence.NoSQL/IMongoDbContext.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Driver; 2 | 3 | namespace FSH.Framework.Persistence.Mongo; 4 | public interface IMongoDbContext : IDisposable 5 | { 6 | IMongoCollection GetCollection(string? name = null); 7 | } -------------------------------------------------------------------------------- /framework/Persistence.NoSQL/MongoDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using MongoDB.Bson; 3 | using MongoDB.Bson.Serialization.Conventions; 4 | using MongoDB.Driver; 5 | 6 | namespace FSH.Framework.Persistence.Mongo; 7 | public class MongoDbContext : IMongoDbContext 8 | { 9 | public IMongoDatabase Database { get; } 10 | public IMongoClient MongoClient { get; } 11 | 12 | public MongoDbContext(IOptions options) 13 | { 14 | RegisterConventions(); 15 | 16 | MongoClient = new MongoClient(options.Value.ConnectionString); 17 | string databaseName = options.Value.DatabaseName; 18 | Database = MongoClient.GetDatabase(databaseName); 19 | } 20 | 21 | private static void RegisterConventions() 22 | { 23 | ConventionRegistry.Register( 24 | "conventions", 25 | new ConventionPack 26 | { 27 | new CamelCaseElementNameConvention(), 28 | new IgnoreExtraElementsConvention(true), 29 | new IgnoreIfNullConvention(true), 30 | new EnumRepresentationConvention(BsonType.String), 31 | new IgnoreIfDefaultConvention(false) 32 | }, _ => true); 33 | } 34 | 35 | public IMongoCollection GetCollection(string? name = null) 36 | { 37 | return Database.GetCollection(name ?? typeof(T).Name.ToLower()); 38 | } 39 | 40 | public void Dispose() 41 | { 42 | GC.SuppressFinalize(this); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /framework/Persistence.NoSQL/MongoOptions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Infrastructure.Options; 2 | 3 | namespace FSH.Framework.Persistence.Mongo; 4 | 5 | public class MongoOptions : IOptionsRoot 6 | { 7 | public string ConnectionString { get; set; } = null!; 8 | public string DatabaseName { get; set; } = null!; 9 | } 10 | -------------------------------------------------------------------------------- /framework/Persistence.NoSQL/MongoRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using FSH.Framework.Core.Database; 3 | using FSH.Framework.Core.Domain; 4 | using FSH.Framework.Core.Services; 5 | using MongoDB.Driver; 6 | 7 | namespace FSH.Framework.Persistence.Mongo; 8 | public class MongoRepository : IRepository where TDocument : class, IBaseEntity 9 | { 10 | private readonly IMongoDbContext _context; 11 | private readonly IMongoCollection _collection; 12 | private readonly IDateTimeService _dateTimeProvider; 13 | 14 | public MongoRepository(IMongoDbContext context, IDateTimeService dateTimeProvider) 15 | { 16 | _context = context; 17 | _collection = _context.GetCollection(); 18 | _dateTimeProvider = dateTimeProvider; 19 | } 20 | 21 | public async Task ExistsAsync(Expression> predicate, CancellationToken cancellationToken = default) 22 | { 23 | return await _collection.Find(predicate).AnyAsync(cancellationToken: cancellationToken)!; 24 | } 25 | 26 | public async Task> FindAsync(Expression> predicate, CancellationToken cancellationToken = default) 27 | { 28 | return await _collection.Find(predicate).ToListAsync(cancellationToken: cancellationToken)!; 29 | } 30 | 31 | public Task FindOneAsync(Expression> predicate, CancellationToken cancellationToken = default) 32 | { 33 | return _collection.Find(predicate).SingleOrDefaultAsync(cancellationToken: cancellationToken)!; 34 | } 35 | 36 | public Task FindByIdAsync(TId id, CancellationToken cancellationToken = default) 37 | { 38 | return FindOneAsync(e => e.Id!.Equals(id), cancellationToken); 39 | } 40 | 41 | public async Task> GetAllAsync(CancellationToken cancellationToken = default) 42 | { 43 | return await _collection.AsQueryable().ToListAsync(cancellationToken); 44 | } 45 | 46 | public async Task AddAsync(TDocument document, CancellationToken cancellationToken = default) 47 | { 48 | await _collection.InsertOneAsync(document, new InsertOneOptions(), cancellationToken); 49 | } 50 | 51 | public async Task UpdateAsync(TDocument entity, CancellationToken cancellationToken = default) 52 | { 53 | entity.UpdateModifiedProperties(_dateTimeProvider.DateTimeUtcNow, string.Empty); 54 | _ = await _collection.ReplaceOneAsync(x => x.Id!.Equals(entity.Id), entity, cancellationToken: cancellationToken); 55 | } 56 | 57 | public Task DeleteRangeAsync(IReadOnlyList entities, CancellationToken cancellationToken = default) 58 | { 59 | throw new NotImplementedException(); 60 | } 61 | 62 | public Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default) 63 | { 64 | throw new NotImplementedException(); 65 | } 66 | 67 | public Task DeleteAsync(TDocument entity, CancellationToken cancellationToken = default) 68 | { 69 | throw new NotImplementedException(); 70 | } 71 | 72 | public async Task DeleteByIdAsync(TId id, CancellationToken cancellationToken = default) 73 | { 74 | await _collection.DeleteOneAsync(d => d.Id!.Equals(id), cancellationToken); 75 | } 76 | 77 | public void Dispose() 78 | { 79 | _context?.Dispose(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /framework/Persistence.NoSQL/QueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Pagination; 2 | using Mapster; 3 | using MongoDB.Driver.Linq; 4 | 5 | namespace FSH.Framework.Persistence.Mongo; 6 | public static class QueryableExtensions 7 | { 8 | public static async Task> ApplyPagingAsync(this IMongoQueryable collection, int page = 1, int resultsPerPage = 10, CancellationToken cancellationToken = default) 9 | { 10 | if (page <= 0) page = 1; 11 | if (resultsPerPage <= 0) resultsPerPage = 10; 12 | int skipSize = (page - 1) * resultsPerPage; 13 | bool isEmpty = !await collection.AnyAsync(cancellationToken: cancellationToken); 14 | if (isEmpty) return new(Enumerable.Empty(), 0, 0, 0); 15 | int totalItems = await collection.CountAsync(cancellationToken: cancellationToken); 16 | var data = collection.Skip(skipSize).Take(resultsPerPage).ToList(); 17 | return new PagedList(data, totalItems, page, resultsPerPage); 18 | } 19 | public static async Task> ApplyPagingAsync(this IMongoQueryable collection, int page = 1, int resultsPerPage = 10, CancellationToken cancellationToken = default) 20 | { 21 | if (page <= 0) page = 1; 22 | if (resultsPerPage <= 0) resultsPerPage = 10; 23 | int skipSize = (page - 1) * resultsPerPage; 24 | bool isEmpty = !await collection.AnyAsync(cancellationToken: cancellationToken); 25 | if (isEmpty) return new(Enumerable.Empty(), 0, 0, 0); 26 | int totalItems = await collection.CountAsync(cancellationToken: cancellationToken); 27 | var data = collection.Skip(skipSize).Take(resultsPerPage).ProjectToType(); 28 | return new PagedList(data, totalItems, page, resultsPerPage); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /thunder-tests/thunderActivity.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /thunder-tests/thunderCollection.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "0f68643a-8467-4469-8cc1-47839e89ca8c", 4 | "colName": "fluentpos", 5 | "created": "2023-04-27T16:23:27.257Z", 6 | "sortNum": 20000, 7 | "folders": [ 8 | { 9 | "_id": "eaf54ff4-b545-4688-b323-87d0277ad3b3", 10 | "name": "auth", 11 | "containerId": "", 12 | "created": "2023-04-27T16:23:27.261Z", 13 | "sortNum": 10000 14 | }, 15 | { 16 | "_id": "b77142ba-65a9-4071-97b1-474e15013955", 17 | "name": "catalog", 18 | "containerId": "", 19 | "created": "2023-04-27T16:23:27.262Z", 20 | "sortNum": 20000 21 | }, 22 | { 23 | "_id": "7a625064-1e04-4bca-895a-3ab51e8e8f71", 24 | "name": "gateway", 25 | "containerId": "", 26 | "created": "2023-05-05T19:00:11.688Z", 27 | "sortNum": 30000 28 | }, 29 | { 30 | "_id": "d3986885-e28a-4557-bbf5-a05eed19169c", 31 | "name": "cart", 32 | "containerId": "", 33 | "created": "2023-05-07T11:55:20.177Z", 34 | "sortNum": 40000 35 | } 36 | ], 37 | "settings": { 38 | "auth": { 39 | "type": "bearer", 40 | "bearer": "{{access_token}}" 41 | }, 42 | "envId": "42790850-814d-4065-89da-b37693eb75f5" 43 | } 44 | } 45 | ] -------------------------------------------------------------------------------- /thunder-tests/thunderEnvironment.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "42790850-814d-4065-89da-b37693eb75f5", 4 | "name": "local", 5 | "default": true, 6 | "sortNum": 10000, 7 | "created": "2023-04-13T00:39:48.305Z", 8 | "modified": "2023-04-13T00:39:48.305Z", 9 | "data": [ 10 | { 11 | "name": "auth_service", 12 | "value": "https://localhost:7001" 13 | }, 14 | { 15 | "name": "catalog_service", 16 | "value": "https://localhost:7003" 17 | }, 18 | { 19 | "name": "access_token", 20 | "value": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg2QzBFNTUwRDU3MjU0QUQwQTE4QUZBQ0Y0RDY2MTNGOUMyQjgwNTgiLCJ4NXQiOiJoc0RsVU5WeVZLMEtHSy1zOU5aaFA1d3JnRmciLCJ0eXAiOiJhdCtqd3QifQ.eyJzdWIiOiJjb25zb2xlIiwibmFtZSI6ImNvbnNvbGUiLCJvaV9wcnN0IjoiY29uc29sZSIsImNsaWVudF9pZCI6ImNvbnNvbGUiLCJvaV90a25faWQiOiJjM2M4MWNlYi02ZDk4LTRiZTMtOGRhMy0wOGEyZmQ2ZTk1YjciLCJhdWQiOlsiY2F0YWxvZy5yZXNvdXJjZS5zZXJ2ZXIiLCJnYXRld2F5LnJlc291cmNlLnNlcnZlciIsImNhcnQucmVzb3VyY2Uuc2VydmVyIl0sInNjb3BlIjoiY2F0YWxvZzpyZWFkIGNhcnQ6cmVhZCBjYXJ0OndyaXRlIiwianRpIjoiN2I2YjcxZGQtMDdkZi00NjYyLWE2MWItMGU5OTM0MWJiY2NmIiwiZXhwIjoxNjg0NDQwODU1LCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MDAxLyIsImlhdCI6MTY4NDQzNzI1NX0.hdWPa_ZQZIyZNNlBo7o2piOHUbQ3tBY58w7LDs6XJg2ZE3Dva7OmCBzz0KgcUvFFjgs2AnGCwXimgNKtJoeI_DqfE_DUYXICSyosFgbuqtZcZskJjEQQafK5bdOEW07oTiQP8-qH4fW1MJTmSsB0ehEP54SnLn8C7xXDEVqFmxvUaOWIE8e2JKuyufD4NlKYXx3vXs1Jh8vxHYynGEj3PjfUghH41JEmdZYTXx14mk_mQ90s4fqwdO0anHyA0xNyju0S5vK2uY0NQkeNHO_Z_W7HmcsjbTggkBnPBHmlhfapYAtUCm5j0ro_ItuOF2wO82ldcj8n5N0k8FH2qoHujA" 21 | }, 22 | { 23 | "name": "gateway_url", 24 | "value": "https://localhost:7002" 25 | }, 26 | { 27 | "name": "cart_service", 28 | "value": "https://localhost:7004" 29 | } 30 | ] 31 | }, 32 | { 33 | "_id": "856a1423-5f3c-4d76-835f-b917dd0cb954", 34 | "name": "aws.staging", 35 | "default": false, 36 | "sortNum": 20000, 37 | "created": "2023-04-13T00:40:05.757Z", 38 | "modified": "2023-04-13T00:40:05.757Z", 39 | "data": [] 40 | } 41 | ] -------------------------------------------------------------------------------- /thunder-tests/thunderclient.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "ad4b65df-fc23-4cfe-ad6d-992f61004c8f", 4 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 5 | "containerId": "eaf54ff4-b545-4688-b323-87d0277ad3b3", 6 | "name": "token", 7 | "url": "{{auth_service}}/connect/token", 8 | "method": "POST", 9 | "sortNum": 35000, 10 | "created": "2023-04-27T16:23:27.261Z", 11 | "modified": "2023-05-18T18:52:07.298Z", 12 | "headers": [ 13 | { 14 | "name": "Content-Type", 15 | "value": "application/x-www-form-urlencoded" 16 | } 17 | ], 18 | "params": [], 19 | "body": { 20 | "type": "formencoded", 21 | "raw": "", 22 | "form": [ 23 | { 24 | "name": "grant_type", 25 | "value": "client_credentials" 26 | }, 27 | { 28 | "name": "client_id", 29 | "value": "console" 30 | }, 31 | { 32 | "name": "client_secret", 33 | "value": "858b39fd-3908-45cb-ab14-aa58220f6d69" 34 | }, 35 | { 36 | "name": "scope", 37 | "value": "catalog:read cart:read cart:write" 38 | } 39 | ] 40 | }, 41 | "auth": { 42 | "type": "none" 43 | }, 44 | "tests": [ 45 | { 46 | "type": "set-env-var", 47 | "custom": "json.access_token", 48 | "action": "setto", 49 | "value": "{{access_token}}" 50 | } 51 | ] 52 | }, 53 | { 54 | "_id": "2c3d2f84-635b-4e15-86fc-d26e9f87b0fa", 55 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 56 | "containerId": "b77142ba-65a9-4071-97b1-474e15013955", 57 | "name": "get-product-details", 58 | "url": "{{catalog_service}}/products/623e0000-3f5a-3c7c-b35b-08db4f2c9456", 59 | "method": "GET", 60 | "sortNum": 20000, 61 | "created": "2023-04-27T16:23:27.262Z", 62 | "modified": "2023-05-07T18:55:38.822Z", 63 | "headers": [], 64 | "params": [], 65 | "tests": [] 66 | }, 67 | { 68 | "_id": "48c0c105-9922-4e6d-adaf-b71539c13388", 69 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 70 | "containerId": "b77142ba-65a9-4071-97b1-474e15013955", 71 | "name": "get-products", 72 | "url": "{{catalog_service}}/products/", 73 | "method": "GET", 74 | "sortNum": 30000, 75 | "created": "2023-04-27T16:23:27.263Z", 76 | "modified": "2023-05-06T04:39:53.621Z", 77 | "headers": [], 78 | "params": [], 79 | "tests": [] 80 | }, 81 | { 82 | "_id": "03ff2ffb-36a6-45dd-bfc0-885837a0ceff", 83 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 84 | "containerId": "b77142ba-65a9-4071-97b1-474e15013955", 85 | "name": "add-product", 86 | "url": "{{catalog_service}}/products", 87 | "method": "POST", 88 | "sortNum": 40000, 89 | "created": "2023-04-27T16:23:27.264Z", 90 | "modified": "2023-05-07T18:55:23.375Z", 91 | "headers": [], 92 | "params": [], 93 | "body": { 94 | "type": "json", 95 | "raw": "{\n \"name\":\"Crucial DDR4 32GB\",\n \"details\":\"Crucial DDR4 32GB\",\n \"code\": \"CD32af\",\n \"cost\":6000,\n \"price\":7000\n}", 96 | "form": [] 97 | }, 98 | "tests": [] 99 | }, 100 | { 101 | "_id": "af5dd60c-ff64-48f0-9ea5-918b7ffa46a1", 102 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 103 | "containerId": "b77142ba-65a9-4071-97b1-474e15013955", 104 | "name": "update-product", 105 | "url": "{{catalog_service}}/products/623e0000-3f5a-3c7c-02fe-08db4debf69b", 106 | "method": "PUT", 107 | "sortNum": 50000, 108 | "created": "2023-04-27T16:23:27.265Z", 109 | "modified": "2023-05-06T04:40:32.357Z", 110 | "headers": [], 111 | "params": [], 112 | "body": { 113 | "type": "json", 114 | "raw": "{\n \"name\":\"Crucial DDR4 32GB\",\n \"details\":\"Crucial DDR4 32GB\",\n \"code\": \"CD3201\",\n \"cost\":6000,\n \"price\":70\n}", 115 | "form": [] 116 | }, 117 | "tests": [] 118 | }, 119 | { 120 | "_id": "33b82e08-585b-47a9-be05-a4e19753966c", 121 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 122 | "containerId": "b77142ba-65a9-4071-97b1-474e15013955", 123 | "name": "delete-product", 124 | "url": "{{catalog_service}}/products/623e0000-3f5a-3c7c-02fe-08db4debf69b", 125 | "method": "DELETE", 126 | "sortNum": 60000, 127 | "created": "2023-04-27T16:23:27.266Z", 128 | "modified": "2023-05-06T04:40:40.804Z", 129 | "headers": [], 130 | "params": [], 131 | "tests": [] 132 | }, 133 | { 134 | "_id": "44290d95-2e77-461b-adc4-2062052b3770", 135 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 136 | "containerId": "eaf54ff4-b545-4688-b323-87d0277ad3b3", 137 | "name": "register", 138 | "url": "{{auth_service}}/users", 139 | "method": "POST", 140 | "sortNum": 70000, 141 | "created": "2023-04-29T09:42:38.186Z", 142 | "modified": "2023-05-06T04:39:34.434Z", 143 | "headers": [], 144 | "params": [], 145 | "body": { 146 | "type": "json", 147 | "raw": "{\n \"userName\":\"mukesh.murugan\",\n \"email\":\"mukesh@cwm.com\",\n \"password\":\"123P@$$word!\"\n}", 148 | "form": [] 149 | }, 150 | "tests": [] 151 | }, 152 | { 153 | "_id": "f92c86ab-8574-4371-b090-4c93e0df5f9d", 154 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 155 | "containerId": "7a625064-1e04-4bca-895a-3ab51e8e8f71", 156 | "name": "token", 157 | "url": "{{gateway_url}}/api/identity/connect/token", 158 | "method": "POST", 159 | "sortNum": 20000, 160 | "created": "2023-05-05T19:00:19.591Z", 161 | "modified": "2023-05-09T18:38:40.434Z", 162 | "headers": [ 163 | { 164 | "name": "Content-Type", 165 | "value": "application/x-www-form-urlencoded" 166 | } 167 | ], 168 | "params": [], 169 | "body": { 170 | "type": "formencoded", 171 | "raw": "", 172 | "form": [ 173 | { 174 | "name": "grant_type", 175 | "value": "client_credentials" 176 | }, 177 | { 178 | "name": "client_id", 179 | "value": "console" 180 | }, 181 | { 182 | "name": "client_secret", 183 | "value": "858b39fd-3908-45cb-ab14-aa58220f6d69" 184 | }, 185 | { 186 | "name": "scope", 187 | "value": "catalog:write catalog:read cart:write cart:read" 188 | } 189 | ] 190 | }, 191 | "tests": [ 192 | { 193 | "type": "set-env-var", 194 | "custom": "json.access_token", 195 | "action": "setto", 196 | "value": "{{access_token}}" 197 | } 198 | ] 199 | }, 200 | { 201 | "_id": "df85d61d-7c31-4b98-b3bb-351f8300545b", 202 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 203 | "containerId": "7a625064-1e04-4bca-895a-3ab51e8e8f71", 204 | "name": "get-products", 205 | "url": "{{gateway_url}}/api/catalog/products", 206 | "method": "GET", 207 | "sortNum": 10000, 208 | "created": "2023-05-05T19:01:46.617Z", 209 | "modified": "2023-05-08T18:16:40.120Z", 210 | "headers": [], 211 | "params": [], 212 | "tests": [] 213 | }, 214 | { 215 | "_id": "ed6de0d7-5eab-462c-9890-3d0616064749", 216 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 217 | "containerId": "7a625064-1e04-4bca-895a-3ab51e8e8f71", 218 | "name": "get-product-by-id", 219 | "url": "{{gateway_url}}/api/catalog/products/623e0000-3f5a-3c7c-c127-08db44708474", 220 | "method": "GET", 221 | "sortNum": 15000, 222 | "created": "2023-05-05T19:02:22.178Z", 223 | "modified": "2023-05-08T18:14:52.980Z", 224 | "headers": [], 225 | "params": [], 226 | "tests": [] 227 | }, 228 | { 229 | "_id": "3ea133f0-ba64-441e-aaf9-e5a4f740dd61", 230 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 231 | "containerId": "7a625064-1e04-4bca-895a-3ab51e8e8f71", 232 | "name": "add-product", 233 | "url": "{{gateway_url}}/api/catalog/products/", 234 | "method": "POST", 235 | "sortNum": 12500, 236 | "created": "2023-05-06T19:28:31.708Z", 237 | "modified": "2023-05-08T18:15:57.764Z", 238 | "headers": [], 239 | "params": [], 240 | "body": { 241 | "type": "json", 242 | "raw": "{\n \"name\":\"Crucial DDR4 32GB\",\n \"details\":\"Crucial DDR4 32GB\",\n \"code\": \"CD3211\",\n \"cost\":6000,\n \"price\":7000\n}", 243 | "form": [] 244 | }, 245 | "tests": [] 246 | }, 247 | { 248 | "_id": "64d8f15f-c4f9-4e84-afc7-9407ccb60b15", 249 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 250 | "containerId": "d3986885-e28a-4557-bbf5-a05eed19169c", 251 | "name": "get-cart", 252 | "url": "{{cart_service}}/623e0000-3f5a-3c7c-02fe-08db4debf69c", 253 | "method": "GET", 254 | "sortNum": 80000, 255 | "created": "2023-05-07T11:55:32.804Z", 256 | "modified": "2023-05-08T18:17:36.906Z", 257 | "headers": [], 258 | "params": [], 259 | "tests": [] 260 | }, 261 | { 262 | "_id": "51426385-7a98-4171-85c5-810fae111918", 263 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 264 | "containerId": "d3986885-e28a-4557-bbf5-a05eed19169c", 265 | "name": "update-cart", 266 | "url": "{{cart_service}}/623e0000-3f5a-3c7c-02fe-08db4debf69c", 267 | "method": "PUT", 268 | "sortNum": 40000, 269 | "created": "2023-05-07T13:21:38.495Z", 270 | "modified": "2023-05-08T18:17:11.073Z", 271 | "headers": [], 272 | "params": [], 273 | "body": { 274 | "type": "json", 275 | "raw": "{\n \"items\": [\n {\n \"productId\":\"623e0000-3f5a-3c7c-02fe-08db4debf692\",\n \"quantity\":100\n }\n ]\n}", 276 | "form": [] 277 | }, 278 | "tests": [] 279 | }, 280 | { 281 | "_id": "56ec2a3a-575f-4007-9d90-591457eb58b0", 282 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 283 | "containerId": "7a625064-1e04-4bca-895a-3ab51e8e8f71", 284 | "name": "update-cart", 285 | "url": "{{gateway_url}}/api/cart/623e0000-3f5a-3c7c-02fe-08db4debf69c", 286 | "method": "PUT", 287 | "sortNum": 17500, 288 | "created": "2023-05-07T19:07:15.529Z", 289 | "modified": "2023-05-09T18:47:23.268Z", 290 | "headers": [], 291 | "params": [], 292 | "body": { 293 | "type": "json", 294 | "raw": "{\n \"items\": [\n {\n \"productId\":\"623e0000-3f5a-3c7c-02fe-081db4debf69\",\n \"quantity\":1\n }\n ]\n}", 295 | "form": [] 296 | }, 297 | "tests": [] 298 | }, 299 | { 300 | "_id": "af227df8-4fa3-424d-a876-51923dc30464", 301 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 302 | "containerId": "7a625064-1e04-4bca-895a-3ab51e8e8f71", 303 | "name": "get-cart", 304 | "url": "{{gateway_url}}/api/cart/623e0000-3f5a-3c7c-02fe-08db4debf69c", 305 | "method": "GET", 306 | "sortNum": 18750, 307 | "created": "2023-05-07T19:07:19.607Z", 308 | "modified": "2023-05-08T18:52:23.248Z", 309 | "headers": [], 310 | "params": [], 311 | "tests": [] 312 | }, 313 | { 314 | "_id": "6a1f7a7b-2cdb-4b93-a734-1285c55b6f60", 315 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 316 | "containerId": "d3986885-e28a-4557-bbf5-a05eed19169c", 317 | "name": "checkout", 318 | "url": "{{cart_service}}/623e0000-3f5a-3c7c-02fe-08db4debf69c/checkout", 319 | "method": "POST", 320 | "sortNum": 60000, 321 | "created": "2023-05-09T16:02:20.992Z", 322 | "modified": "2023-05-09T16:03:11.835Z", 323 | "headers": [], 324 | "params": [], 325 | "body": { 326 | "type": "json", 327 | "raw": "{\n \"creditCardNumber\": \"350176793563689\"\n}", 328 | "form": [] 329 | }, 330 | "tests": [] 331 | }, 332 | { 333 | "_id": "d0462d20-1f61-467c-8274-f9c11537b26a", 334 | "colId": "0f68643a-8467-4469-8cc1-47839e89ca8c", 335 | "containerId": "7a625064-1e04-4bca-895a-3ab51e8e8f71", 336 | "name": "checkout", 337 | "url": "{{gateway_url}}/api/cart/623e0000-3f5a-3c7c-02fe-08db4debf69c/checkout", 338 | "method": "POST", 339 | "sortNum": 18125, 340 | "created": "2023-05-09T16:04:09.494Z", 341 | "modified": "2023-05-09T18:46:48.733Z", 342 | "headers": [], 343 | "params": [], 344 | "body": { 345 | "type": "json", 346 | "raw": "{\n \"creditCardNumber\": \"350176793563689\"\n}", 347 | "form": [] 348 | }, 349 | "tests": [] 350 | } 351 | ] --------------------------------------------------------------------------------