├── .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 | [](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 | [](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