This commit is contained in:
hamid
2026-06-16 01:32:43 +03:30
commit 69bbd28bb0
298 changed files with 24728 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
+342
View File
@@ -0,0 +1,342 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- Backup*.rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
/nuget.exe
/src/API/CleanArc.Web.Api/logs/log.json
+109
View File
@@ -0,0 +1,109 @@
# AGENTS.md — Balinyaar Server
Agent-oriented guide to the backend. For human setup/run instructions see [README.md](README.md).
## Stack
- **ASP.NET Core / .NET 10** (`net10.0`), Web API
- **Clean Architecture** (Domain → Application → Infrastructure → API)
- **CQRS** with **MediatR** (source-generated `Mediator`)
- **EF Core 10** + **SQL Server** (Repository + Unit of Work)
- **ASP.NET Core Identity** with **JWE** (signed + AES-encrypted JWT), **OTP**, and **dynamic permission** authorization
- **Mapster** (mapping), **FluentValidation** (validation), **Serilog** (logging), **OpenTelemetry** + **prometheus-net** (observability), **NSwag/Swagger** (OpenAPI), **Asp.Versioning** (API versioning)
- **xUnit** + **NSubstitute** (tests)
- Centralized NuGet versions in `Directory.Packages.props`
## Commands (run from `server/`)
| Task | Command |
| ---------- | --------------------------------------------------------------------------------- |
| Restore | `dotnet restore CleanArcTemplate.sln` |
| Build | `dotnet build CleanArcTemplate.sln` |
| Run API | `dotnet run --project src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj` |
| Test | `dotnet test CleanArcTemplate.sln` |
| Add migration | `dotnet ef migrations add <Name> --project src/Infrastructure/CleanArc.Infrastructure.Persistence --startup-project src/API/CleanArc.Web.Api` |
**Startup project:** `src/API/CleanArc.Web.Api`. Default URL `https://localhost:5002`, Swagger at `/swagger`. On boot the app applies EF migrations and seeds default users (`Program.cs``ApplyMigrationsAsync()` / `SeedDefaultUsersAsync()`), so a reachable DB is required.
## Projects by layer
```
src/
├── Core/
│ ├── CleanArc.Domain Entities (User, Order, Role...), BaseEntity, IEntity, ITimeModification
│ └── CleanArc.Application CQRS Features/, Contracts/ (interfaces), Models/, MediatR pipeline (Common/)
├── Infrastructure/
│ ├── CleanArc.Infrastructure.Persistence ApplicationDbContext, Configuration/, Repositories/, Migrations/
│ ├── CleanArc.Infrastructure.Identity Jwt/, Identity/ (Managers, Stores, PermissionManager, Seed), ServiceConfiguration/
│ ├── CleanArc.Infrastructure.CrossCutting Logging (Serilog)
│ └── CleanArc.Infrastructure.Monitoring HealthCheck / OpenTelemetry / Prometheus configs
├── API/
│ ├── CleanArc.Web.Api Program.cs, Controllers/V1/, appsettings*.json
│ ├── CleanArc.WebFramework BaseController, Filters/, Middlewares/, Swagger/, Attributes/
│ └── Plugins/CleanArc.Web.Plugins.Grpc GrpcPluginStartup, Services/, ProtoModels/
├── Shared/CleanArc.SharedKernel Extensions + validation base used by all layers
└── Tests/ CleanArc.Tests.Setup + CleanArc.Test.Infrastructure.Identity
```
Dependency direction points **inward**: Domain depends on nothing; Application depends on Domain; Infrastructure and API implement/consume Application's contracts. Never make Domain or Application reference Infrastructure or the API.
## Startup wiring — `src/API/CleanArc.Web.Api/Program.cs`
Service registration is composed from per-layer extension methods (in each project's `ServiceConfiguration`):
```
ConfigureHealthChecks() · SetupOpenTelemetry()
AddApplicationServices() // MediatR + validators + pipeline behaviors
RegisterIdentityServices(...) // Identity, JWT/JWE, authorization policies
AddPersistenceServices(...) // DbContext, UnitOfWork, repositories
AddWebFrameworkServices() // API versioning
AddSwagger("v1","v1.1") · RegisterValidatorsAsServices() · AddMapster()
ConfigureGrpcPluginServices() // gRPC plugin
```
Pipeline order: exception handling → Swagger → routing → **authentication → authorization** → controllers → metrics → health checks → `ConfigureGrpcPipeline()`.
When adding infrastructure, expose it as an extension method and call it here rather than inlining into `Program.cs`.
## CQRS — how a feature is shaped
Features live under `CleanArc.Application/Features/<Area>/{Commands|Queries}/<Name>/`. A query example (`Features/Order/Queries/GetAllOrders/`):
- `GetAllOrdersQuery.cs``record ... : IRequest<OperationResult<...>>`
- `GetAllOrdersQueryHandler.cs``internal` handler; depends on `IUnitOfWork`, `IMapper`; returns `OperationResult<T>`
- `GetAllOrdersQueryResult.cs` — the DTO returned
Commands additionally implement `IValidatableModel<T>` and declare FluentValidation rules; the `ValidateCommandBehavior` MediatR pipeline (`Application/Common/`) runs validators before the handler and surfaces errors in `OperationResult`.
**To add a feature:** create the folder with the request + handler (+ result/validator), then call it from a controller via `_sender.Send(...)`. Contracts the handler needs go in `Application/Contracts/` and are implemented in Infrastructure.
## Controllers & results
- Controllers live in `CleanArc.Web.Api/Controllers/V1/` and inherit `BaseController` (`CleanArc.WebFramework/BaseController/BaseController.cs`), which exposes `UserId`/`UserName`/etc. from claims and maps `OperationResult<T>``IActionResult`.
- All responses are wrapped in `OperationResult<T>` (`Application/Models/Common/`): `Result`, `IsSuccess`, `ErrorMessages`, `IsNotFound`, `IsException`. Use the factory methods (`SuccessResult`, `FailureResult`, `NotFoundResult`).
- Protected endpoints use `[Authorize(ConstantPolicies.DynamicPermission)]`.
## Persistence
- `ApplicationDbContext` (`CleanArc.Infrastructure.Persistence/ApplicationDbContext.cs`) extends `IdentityDbContext<...>`; it auto-registers `IEntity` types and applies all `IEntityTypeConfiguration` from the assembly.
- Per-entity config in `Configuration/<Area>Config/`. Repositories in `Repositories/` derive from `BaseAsyncRepository<T>`; expose them through `IUnitOfWork` (interface in `Application/Contracts/Persistence/`). Commit via `unitOfWork.CommitAsync()`.
- Migrations in `Migrations/`. Add new ones with the `dotnet ef` command above.
## Identity & auth
- Token service: `CleanArc.Infrastructure.Identity/Jwt/JwtService.cs` (`IJwtService`) — issues JWE (HMAC-SHA256 signed, AES-128 encrypted), refresh tokens, and OTP/phone-based tokens.
- Custom Identity managers/stores under `Identity/Manager/` and `Identity/Store/`.
- Dynamic permissions: `Identity/PermissionManager/` (`DynamicPermissionService`, `DynamicPermissionHandler`, `ConstantPolicies`).
- Settings from `appsettings.json``IdentitySettings` (`SecretKey`, `Encryptkey` = 16 chars, `Issuer`, `Audience`, lifetimes).
## gRPC plugin
`Plugins/CleanArc.Web.Plugins.Grpc` is a self-contained module mounted via Application Parts. `GrpcPluginStartup.cs` provides `ConfigureGrpcPluginServices()` / `ConfigureGrpcPipeline()` (called from `Program.cs`). Proto contracts in `ProtoModels/*.proto`, services in `Services/`. The host uses HTTP/2 (`Kestrel` config) for gRPC.
## Conventions
- Add cross-layer wiring as `ServiceConfiguration` extension methods, not inline in `Program.cs`.
- Keep handlers `internal`; return `OperationResult<T>`; don't throw for expected failures (use `FailureResult`/`NotFoundResult`).
- Use Mapster for entity↔DTO mapping; FluentValidation for input validation.
- Centralize package versions in `Directory.Packages.props` (no inline `Version=` in `.csproj`).
- The `CleanArc*` namespace/`.sln` naming is internal project naming, **not** template branding — don't rename it without an explicit request (it touches every file and the EF migrations).
+139
View File
@@ -0,0 +1,139 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33103.184
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{42CAB060-5D50-4E18-8F85-EBA5EB85B268}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{0E679B58-1D8A-4F5B-8838-6E4DD9258215}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{0E86739A-769C-4597-84D3-7D53BA1D1E3C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{2373AFFC-1389-4D78-8465-074AB22084AF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{DF0CD4C6-B53D-452D-867E-3E3BD24F883F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{542840FF-B0CC-4F8A-9F6E-1898BE0573D7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.SharedKernel", "src\Shared\CleanArc.SharedKernel\CleanArc.SharedKernel.csproj", "{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Infrastructure.CrossCutting", "src\Infrastructure\CleanArc.Infrastructure.CrossCutting\CleanArc.Infrastructure.CrossCutting.csproj", "{09E81356-0531-42A0-9F7F-00C495F1226E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Infrastructure.Identity", "src\Infrastructure\CleanArc.Infrastructure.Identity\CleanArc.Infrastructure.Identity.csproj", "{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Infrastructure.Persistence", "src\Infrastructure\CleanArc.Infrastructure.Persistence\CleanArc.Infrastructure.Persistence.csproj", "{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Application", "src\Core\CleanArc.Application\CleanArc.Application.csproj", "{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Domain", "src\Core\CleanArc.Domain\CleanArc.Domain.csproj", "{DC49CD3F-840E-4634-B9DA-595F160E9499}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Web.Plugins.Grpc", "src\API\Plugins\CleanArc.Web.Plugins.Grpc\CleanArc.Web.Plugins.Grpc.csproj", "{8F7135E8-68C9-4DA8-AA06-04518EBB403B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.Web.Api", "src\API\CleanArc.Web.Api\CleanArc.Web.Api.csproj", "{BE13FF32-B8D5-4AE7-B173-6CA96040B788}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArc.WebFramework", "src\API\CleanArc.WebFramework\CleanArc.WebFramework.csproj", "{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{77986571-8153-4120-AD08-36729310A56B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BaseSetup", "BaseSetup", "{34B1F72E-A991-4705-ACC5-08E65E46D26E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArc.Tests.Setup", "src\Tests\CleanArc.Tests.Setup\CleanArc.Tests.Setup.csproj", "{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{45FA88C0-9986-40E5-A2E2-7742302518D2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArc.Test.Infrastructure.Identity", "src\Tests\CleanArc.Test.Infrastructure.Identity\CleanArc.Test.Infrastructure.Identity\CleanArc.Test.Infrastructure.Identity.csproj", "{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArc.Infrastructure.Monitoring", "src\Infrastructure\CleanArc.Infrastructure.Monitoring\CleanArc.Infrastructure.Monitoring.csproj", "{7699705C-2C15-467F-957D-4C5EBE4FD92E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{704FAE1E-F0D2-468E-8B3D-E9E6F323ABE8}"
ProjectSection(SolutionItems) = preProject
Directory.Packages.props = Directory.Packages.props
Dockerfile = Dockerfile
docker-compose.yml = docker-compose.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398}.Release|Any CPU.Build.0 = Release|Any CPU
{09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09E81356-0531-42A0-9F7F-00C495F1226E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09E81356-0531-42A0-9F7F-00C495F1226E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09E81356-0531-42A0-9F7F-00C495F1226E}.Release|Any CPU.Build.0 = Release|Any CPU
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F}.Release|Any CPU.Build.0 = Release|Any CPU
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8}.Release|Any CPU.Build.0 = Release|Any CPU
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153}.Release|Any CPU.Build.0 = Release|Any CPU
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC49CD3F-840E-4634-B9DA-595F160E9499}.Release|Any CPU.Build.0 = Release|Any CPU
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F7135E8-68C9-4DA8-AA06-04518EBB403B}.Release|Any CPU.Build.0 = Release|Any CPU
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE13FF32-B8D5-4AE7-B173-6CA96040B788}.Release|Any CPU.Build.0 = Release|Any CPU
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9}.Release|Any CPU.Build.0 = Release|Any CPU
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1}.Release|Any CPU.Build.0 = Release|Any CPU
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60}.Release|Any CPU.Build.0 = Release|Any CPU
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7699705C-2C15-467F-957D-4C5EBE4FD92E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0E679B58-1D8A-4F5B-8838-6E4DD9258215} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268}
{0E86739A-769C-4597-84D3-7D53BA1D1E3C} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268}
{2373AFFC-1389-4D78-8465-074AB22084AF} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268}
{DF0CD4C6-B53D-452D-867E-3E3BD24F883F} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268}
{542840FF-B0CC-4F8A-9F6E-1898BE0573D7} = {0E679B58-1D8A-4F5B-8838-6E4DD9258215}
{56C4DDD2-4F8C-4D35-85D4-CC9064C52398} = {DF0CD4C6-B53D-452D-867E-3E3BD24F883F}
{09E81356-0531-42A0-9F7F-00C495F1226E} = {2373AFFC-1389-4D78-8465-074AB22084AF}
{3AFD5AAD-8DCD-44D6-86B9-078FBE8F2A1F} = {2373AFFC-1389-4D78-8465-074AB22084AF}
{9F3B3E49-3E3C-4244-AE88-D209B18B28B8} = {2373AFFC-1389-4D78-8465-074AB22084AF}
{9C0BCB6F-614C-4FA9-83A2-E95834E3C153} = {0E86739A-769C-4597-84D3-7D53BA1D1E3C}
{DC49CD3F-840E-4634-B9DA-595F160E9499} = {0E86739A-769C-4597-84D3-7D53BA1D1E3C}
{8F7135E8-68C9-4DA8-AA06-04518EBB403B} = {542840FF-B0CC-4F8A-9F6E-1898BE0573D7}
{BE13FF32-B8D5-4AE7-B173-6CA96040B788} = {0E679B58-1D8A-4F5B-8838-6E4DD9258215}
{44DD0A96-BA65-476E-BC59-C8D2CFA703B9} = {0E679B58-1D8A-4F5B-8838-6E4DD9258215}
{77986571-8153-4120-AD08-36729310A56B} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268}
{34B1F72E-A991-4705-ACC5-08E65E46D26E} = {77986571-8153-4120-AD08-36729310A56B}
{33AF382A-9E22-42F0-82E5-4F78BCFD40C1} = {34B1F72E-A991-4705-ACC5-08E65E46D26E}
{45FA88C0-9986-40E5-A2E2-7742302518D2} = {77986571-8153-4120-AD08-36729310A56B}
{54203B4F-3CE8-4EBA-B5E2-F7C985FACE60} = {45FA88C0-9986-40E5-A2E2-7742302518D2}
{7699705C-2C15-467F-957D-4C5EBE4FD92E} = {2373AFFC-1389-4D78-8465-074AB22084AF}
{704FAE1E-F0D2-468E-8B3D-E9E6F323ABE8} = {42CAB060-5D50-4E18-8F85-EBA5EB85B268}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {05C223B9-EA89-44B2-B9F5-D01181F85DFE}
EndGlobalSection
EndGlobal
+57
View File
@@ -0,0 +1,57 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Asp.Versioning.Http" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageVersion Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageVersion Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="FluentValidation" Version="12.1.0" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.71.0" />
<PackageVersion Include="Grpc.AspNetCore.Server.Reflection" Version="2.71.0" />
<PackageVersion Include="Mapster" Version="7.4.0" />
<PackageVersion Include="Mapster.DependencyInjection" Version="1.0.1" />
<PackageVersion Include="Mediator.Abstractions" Version="3.0.1" />
<PackageVersion Include="Mediator.SourceGenerator" Version="3.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NSwag.AspNetCore" Version="14.6.2" />
<PackageVersion Include="NuGet.Packaging" Version="7.0.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.1" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
<PackageVersion Include="Pluralize.NET" Version="1.0.2" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Span" Version="3.1.0" />
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.Elasticsearch" Version="10.0.0" />
<PackageVersion Include="Serilog.Sinks.MSSqlServer" Version="9.0.2" />
<PackageVersion Include="Serilog.Sinks.PeriodicBatching" Version="5.0.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
</Project>
+36
View File
@@ -0,0 +1,36 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["../Directory.Packages.props", "./"]
COPY ["src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj", "src/API/CleanArc.Web.Api/"]
COPY ["src/API/CleanArc.WebFramework/CleanArc.WebFramework.csproj", "src/API/CleanArc.WebFramework/"]
COPY ["src/API/Plugins/CleanArc.Web.Plugins.Grpc/CleanArc.Web.Plugins.Grpc.csproj", "src/API/Plugins/CleanArc.Web.Plugins.Grpc/"]
COPY ["src/Core/CleanArc.Application/CleanArc.Application.csproj", "src/Core/CleanArc.Application/"]
COPY ["src/Core/CleanArc.Domain/CleanArc.Domain.csproj", "src/Core/CleanArc.Domain/"]
COPY ["src/Infrastructure/CleanArc.Infrastructure.CrossCutting/CleanArc.Infrastructure.CrossCutting.csproj", "src/Infrastructure/CleanArc.Infrastructure.CrossCutting/"]
COPY ["src/Infrastructure/CleanArc.Infrastructure.Identity/CleanArc.Infrastructure.Identity.csproj", "src/Infrastructure/CleanArc.Infrastructure.Identity/"]
COPY ["src/Infrastructure/CleanArc.Infrastructure.Persistence/CleanArc.Infrastructure.Persistence.csproj", "src/Infrastructure/CleanArc.Infrastructure.Persistence/"]
COPY ["src/Infrastructure/CleanArc.Infrastructure.Monitoring/CleanArc.Infrastructure.Monitoring.csproj", "src/Infrastructure/CleanArc.Infrastructure.Monitoring/"]
COPY ["src/Shared/CleanArc.SharedKernel/CleanArc.SharedKernel.csproj", "src/Shared/CleanArc.SharedKernel/"]
COPY ["src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity.csproj", "src/Tests/CleanArc.Test.Infrastructure.Identity/CleanArc.Test.Infrastructure.Identity/"]
COPY ["src/Tests/CleanArc.Tests.Setup/CleanArc.Tests.Setup.csproj", "src/Tests/CleanArc.Tests.Setup/"]
RUN dotnet restore "src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj"
COPY . .
WORKDIR "src/API/CleanArc.Web.Api"
RUN dotnet build "CleanArc.Web.Api.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "CleanArc.Web.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "CleanArc.Web.Api.dll"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Babak Taremi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+121
View File
@@ -0,0 +1,121 @@
# Balinyaar — Server (ASP.NET Core, Clean Architecture)
Backend API for the Balinyaar application. It is an **ASP.NET Core (.NET 10)** solution organized around **Clean Architecture**, with:
- **CQRS** via MediatR (source-generated)
- **ASP.NET Core Identity** with **JWE** (signed + encrypted JWT) and **OTP** authentication
- **Dynamic, permission-based authorization**
- **EF Core** persistence (SQL Server) with the Repository + Unit of Work patterns
- A modular **gRPC plugin** mounted via Application Parts
- Observability out of the box: Serilog, OpenTelemetry, Prometheus metrics, health checks
> Looking for an architecture/file map to navigate the code? See [AGENTS.md](AGENTS.md).
## Requirements
- [.NET 10 SDK](https://dotnet.microsoft.com/)
- SQL Server (local instance, or the containerized one from `docker-compose.yml`)
## Running locally
From the `server/` folder:
```bash
dotnet restore CleanArcTemplate.sln
dotnet build CleanArcTemplate.sln
dotnet run --project src/API/CleanArc.Web.Api/CleanArc.Web.Api.csproj
```
By default the API listens on **https://localhost:5002** and serves Swagger UI at **/swagger**.
On startup the app **applies EF Core migrations** and **seeds default users** automatically, so a reachable database (see `appsettings.json``ConnectionStrings`) is required.
### Configuration
Settings live in `src/API/CleanArc.Web.Api/appsettings.json` (+ `appsettings.Development.json`):
- `ConnectionStrings:SqlServer` — main application database
- `ConnectionStrings:logDb` — Serilog SQL sink database
- `IdentitySettings``SecretKey` (signing), `Encryptkey` (AES-128 encryption, exactly 16 chars), `Issuer`, `Audience`, token lifetimes
## Running with Docker
Generate a development HTTPS certificate (used by the container):
```bash
dotnet dev-certs https -ep $env:USERPROFILE/.aspnet/https/cleanarc.pfx -p Strong@Password
dotnet dev-certs https --trust
```
Build and start the API together with SQL Server 2022:
```bash
docker build -t balinyaar-server -f Dockerfile .
docker-compose up -d
```
The compose stack exposes the API on `http://localhost:5000` / `https://localhost:5001` and SQL Server on `localhost:1435`.
## Solution layout
```
src/
├── Core/
│ ├── CleanArc.Domain Entities + domain primitives (the core / "brain")
│ └── CleanArc.Application CQRS features, contracts (interfaces), MediatR pipeline
├── Infrastructure/
│ ├── CleanArc.Infrastructure.Persistence EF Core DbContext, configs, repositories, UoW, migrations
│ ├── CleanArc.Infrastructure.Identity Identity, JWE/JWT, OTP, dynamic permissions
│ ├── CleanArc.Infrastructure.CrossCutting Cross-cutting concerns (logging)
│ └── CleanArc.Infrastructure.Monitoring Health checks, OpenTelemetry, Prometheus
├── API/
│ ├── CleanArc.Web.Api Presentation: REST controllers, Program.cs (startup)
│ ├── CleanArc.WebFramework Reusable web config: base controller, filters, middleware, Swagger
│ └── Plugins/CleanArc.Web.Plugins.Grpc Self-contained gRPC plugin
├── Shared/
│ └── CleanArc.SharedKernel Shared extensions/helpers referenced by every layer
└── Tests/ xUnit test setup + Identity tests
```
## The layers (why they exist)
### Domain
The core of the project. Each entity may carry its own behavior. Entities derive from a common `BaseEntity`, which lets the persistence layer discover models via reflection to register them and drive migrations.
### Application
Routes requests and defines the **contracts** (interfaces) the system depends on, without knowing their implementations. This is where **CQRS** lives: Commands and Queries are kept separate and dispatched to their handlers by **MediatR**. Cross-cutting concerns (validation, metrics) are applied as MediatR pipeline behaviors.
### Infrastructure
Implements the contracts the Application layer declares — the parts needed to run in the real world:
- **Persistence** — the chosen database (SQL Server via EF Core). Repositories give self-describing, persistence-agnostic data access; Unit of Work keeps multi-step writes atomic and consistent.
- **Identity** — registration, authentication and authorization using ASP.NET Core Identity, with JWE tokens, OTP login, and a dynamic access-control system.
- **CrossCutting** — services used across the whole app, such as logging.
- **Monitoring** — health checks, distributed tracing/metrics, and Prometheus.
### WebFramework
Keeps `Program.cs` thin by moving each piece of configuration into its own reusable class (filters, middleware, Swagger, API versioning, the base controller, etc.).
### Web.Api
The presentation layer — an ASP.NET Core Web API exposing versioned REST controllers under `Controllers/V1`.
### Web.Plugins.Grpc
A standalone module that adds gRPC endpoints to the same host via **Application Parts**, giving modularity (the "plugin" middle ground between a monolith and microservices) without a separate deployment. It registers its services and pipeline through extension methods called from `Program.cs`.
## Tests
```bash
dotnet test CleanArcTemplate.sln
```
Each layer is designed to be testable in isolation; `CleanArc.Tests.Setup` provides the shared test scaffolding.
## License
See [LICENSE.md](LICENSE.md).
+22
View File
@@ -0,0 +1,22 @@
version: "3.9" # optional since v1.27.0
services:
web_api:
image: bobby-cleanarc
container_name: bobby-cleanarc-app
environment:
"ASPNETCORE_URLS": "https://+;http://+"
"ASPNETCORE_Kestrel__Certificates__Default__Password": "Strong@Password"
"ASPNETCORE_Kestrel__Certificates__Default__Path": "/https/cleanarc.pfx"
ports:
- "5000:80"
- "5001:443"
volumes:
- ~/.aspnet/https:/https
sql:
image: "mcr.microsoft.com/mssql/server:2022-latest"
container_name: sql_server2022
ports: # not actually needed, because the two services are on the same network
- "1435:1433"
environment:
- ACCEPT_EULA=y
- SA_PASSWORD=A&VeryComplex123Password
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>true</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\CleanArc.Infrastructure.Monitoring\CleanArc.Infrastructure.Monitoring.csproj" />
<ProjectReference Include="..\CleanArc.WebFramework\CleanArc.WebFramework.csproj" />
<ProjectReference Include="..\Plugins\CleanArc.Web.Plugins.Grpc\CleanArc.Web.Plugins.Grpc.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,37 @@
using Asp.Versioning;
using CleanArc.Application.Features.Admin.Commands.AddAdminCommand;
using CleanArc.Application.Features.Admin.Queries.GetToken;
using CleanArc.Application.Models.Jwt;
using CleanArc.WebFramework.Attributes;
using CleanArc.WebFramework.BaseController;
using Mediator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace CleanArc.Web.Api.Controllers.V1.Admin
{
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/AdminManager")]
public class AdminManagerController(ISender sender) : BaseController
{
[HttpPost("Login")]
[ProducesOkApiResponseType<AccessToken>]
public async Task<IActionResult> AdminLogin(AdminGetTokenQuery model)
{
var query = await sender.Send(model);
return base.OperationResult(query);
}
[Authorize(Roles = "admin")]
[HttpPost("NewAdmin")]
[ProducesOkApiResponseType]
public async Task<IActionResult> AddNewAdmin(AddAdminCommand model)
{
var commandResult = await sender.Send(model);
return base.OperationResult(commandResult);
}
}
}
@@ -0,0 +1,36 @@
using CleanArc.Infrastructure.Identity.Identity.PermissionManager;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using Asp.Versioning;
using CleanArc.Application.Features.Order.Queries.GetAllOrders;
using CleanArc.WebFramework.Attributes;
using CleanArc.WebFramework.BaseController;
using Mediator;
namespace CleanArc.Web.Api.Controllers.V1.Admin
{
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/OrderManagement")]
[Display(Description= "Managing Users related Orders")]
[Authorize(ConstantPolicies.DynamicPermission)]
public class OrderManagementController : BaseController
{
private readonly ISender _sender;
public OrderManagementController(ISender sender)
{
_sender = sender;
}
[HttpGet("OrderList")]
[ProducesOkApiResponseType<List<GetAllOrdersQueryResult>>]
public async Task<IActionResult> GetOrders()
{
var queryResult = await _sender.Send(new GetAllOrdersQuery());
return base.OperationResult(queryResult);
}
}
}
@@ -0,0 +1,67 @@
using System.ComponentModel.DataAnnotations;
using Asp.Versioning;
using CleanArc.Application.Features.Role.Commands.AddRoleCommand;
using CleanArc.Application.Features.Role.Commands.UpdateRoleClaimsCommand;
using CleanArc.Application.Features.Role.Queries.GetAllRolesQuery;
using CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery;
using CleanArc.Infrastructure.Identity.Identity.PermissionManager;
using CleanArc.WebFramework.Attributes;
using CleanArc.WebFramework.BaseController;
using Mediator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace CleanArc.Web.Api.Controllers.V1.Admin
{
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/RoleManager")]
[Authorize(ConstantPolicies.DynamicPermission)]
[Display(Description = "Managing Related Roles for the System")]
public class RoleManagerController(ISender sender) : BaseController
{
[HttpGet("Roles")]
[ProducesOkApiResponseType<List<GetAllRolesQueryResponse>>]
public async Task<IActionResult> GetRoles()
{
var queryResult = await sender.Send(new GetAllRolesQuery());
return base.OperationResult(queryResult);
}
[HttpGet("AuthRoutes")]
[ProducesOkApiResponseType<List<GetAuthorizableRoutesQueryResponse>>]
public async Task<IActionResult> GetAuthRoutes()
{
var queryModel = await sender.Send(new GetAuthorizableRoutesQuery());
return base.OperationResult(queryModel);
}
/// <summary>
/// Update a role permissions (claims) based on RouteKey received in AuthRoutes API
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPut("UpdateRolePermissions")]
[ProducesOkApiResponseType]
public async Task<IActionResult> UpdateRolePermissions(UpdateRoleClaimsCommand model)
{
var commandResult =
await sender.Send(new UpdateRoleClaimsCommand(model.RoleId, model.RoleClaimValue));
return base.OperationResult(commandResult);
}
[HttpPost("NewRole")]
[ProducesOkApiResponseType]
public async Task<IActionResult> AddRole(AddRoleCommand model)
{
var commandResult = await sender.Send(model);
return base.OperationResult(commandResult);
}
}
}
@@ -0,0 +1,36 @@
using CleanArc.Infrastructure.Identity.Identity.PermissionManager;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using Asp.Versioning;
using CleanArc.Application.Features.Users.Queries.GetUsers;
using CleanArc.WebFramework.Attributes;
using CleanArc.WebFramework.BaseController;
using Mediator;
namespace CleanArc.Web.Api.Controllers.V1.Admin
{
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/UserManagement")]
[Display(Description = "Managing API Users")]
[Authorize(ConstantPolicies.DynamicPermission)]
public class UserManagementController : BaseController
{
private readonly ISender _sender;
public UserManagementController(ISender sender)
{
_sender = sender;
}
[HttpGet("CurrentUsers")]
[ProducesOkApiResponseType<List<GetUsersQueryResponse>>]
public async Task<IActionResult> GetAllUsers()
{
var queryResult = await _sender.Send(new GetUsersQuery());
return base.OperationResult(queryResult);
}
}
}
@@ -0,0 +1,52 @@
using Asp.Versioning;
using CleanArc.Application.Features.Order.Commands;
using CleanArc.Application.Features.Order.Queries.GetUserOrders;
using CleanArc.WebFramework.Attributes;
using CleanArc.WebFramework.BaseController;
using Mediator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace CleanArc.Web.Api.Controllers.V1.Order;
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/User")]
[Authorize]
public class OrderController(ISender sender) : BaseController
{
[HttpPost("CreateNewOrder")]
[ProducesOkApiResponseType]
public async Task<IActionResult> CreateNewOrder(AddOrderCommand model)
{
model.UserId = base.UserId;
var command = await sender.Send(model);
return base.OperationResult(command);
}
[HttpGet("GetUserOrders")]
[ProducesOkApiResponseType<List<GetUsersQueryResultModel>>]
public async Task<IActionResult> GetUserOrders()
{
var query = await sender.Send(new GetUserOrdersQueryModel(UserId));
return base.OperationResult(query);
}
[HttpPut("UpdateOrder")]
[ProducesOkApiResponseType]
public async Task<IActionResult> UpdateOrder(UpdateUserOrderCommand model)
{
model.UserId=base.UserId;
var command = await sender.Send(model);
return base.OperationResult(command);
}
[HttpDelete("DeleteAllUserOrders")]
[ProducesOkApiResponseType]
public async Task<IActionResult> DeleteAllUserOrders()
=> base.OperationResult(await sender.Send(new DeleteUserOrdersCommand(base.UserId)));
}
@@ -0,0 +1,88 @@
using Asp.Versioning;
using CleanArc.Application.Features.Users.Commands.Create;
using CleanArc.Application.Features.Users.Commands.RefreshUserTokenCommand;
using CleanArc.Application.Features.Users.Commands.RequestLogout;
using CleanArc.Application.Features.Users.Queries.GenerateUserToken;
using CleanArc.Application.Features.Users.Queries.TokenRequest;
using CleanArc.Application.Models.Jwt;
using CleanArc.WebFramework.Attributes;
using CleanArc.WebFramework.BaseController;
using CleanArc.WebFramework.Swagger;
using Mediator;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace CleanArc.Web.Api.Controllers.V1.UserManagement;
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/User")]
public class UserController : BaseController
{
private readonly IMediator _mediator;
public UserController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost("Register")]
[ProducesOkApiResponseType<UserCreateCommandResult>]
public async Task<IActionResult> CreateUser(UserCreateCommand model)
{
var command = await _mediator.Send(model);
return base.OperationResult(command);
}
[HttpPost("TokenRequest")]
[ProducesOkApiResponseType<UserTokenRequestQueryResponse>]
public async Task<IActionResult> TokenRequest(UserTokenRequestQuery model)
{
var query = await _mediator.Send(model);
return base.OperationResult(query);
}
[HttpPost("LoginConfirmation")]
[ProducesOkApiResponseType<AccessToken>]
public async Task<IActionResult> ValidateUser(GenerateUserTokenQuery model)
{
var result = await _mediator.Send(model);
return base.OperationResult(result);
}
[HttpPost("RefreshSignIn")]
[RequireTokenWithoutAuthorization]
[ProducesOkApiResponseType<AccessToken>]
public async Task<IActionResult> RefreshUserToken(RefreshUserTokenCommand model)
{
var checkCurrentAccessTokenValidity =await HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
if (checkCurrentAccessTokenValidity.Succeeded)
return BadRequest("Current access token is valid. No need to refresh");
var newTokenResult = await _mediator.Send(model);
return base.OperationResult(newTokenResult);
}
[HttpPost("Logout")]
[Authorize]
[ProducesOkApiResponseType]
public async Task<IActionResult> RequestLogout()
{
var commandResult = await _mediator.Send(new RequestLogoutCommand(base.UserId));
return base.OperationResult(commandResult);
}
[HttpPost("PasswordTokenRequest")]
[ProducesOkApiResponseType<AccessToken>]
public async Task<IActionResult> PasswordTokenRequest(PasswordUserTokenRequestQuery model)
=> base.OperationResult(await _mediator.Send(model));
}
+118
View File
@@ -0,0 +1,118 @@
using System.Diagnostics;
using CleanArc.Application.Features.Users.Commands.Create;
using CleanArc.Application.Models.ApiResult;
using CleanArc.Application.Models.Identity;
using CleanArc.Application.ServiceConfiguration;
using CleanArc.Domain.Entities.User;
using CleanArc.Infrastructure.CrossCutting.Logging;
using CleanArc.Infrastructure.Identity.Identity.Dtos;
using CleanArc.Infrastructure.Identity.Jwt;
using CleanArc.Infrastructure.Identity.ServiceConfiguration;
using CleanArc.Infrastructure.Monitoring.Configurations;
using CleanArc.Infrastructure.Persistence.ServiceConfiguration;
using CleanArc.SharedKernel.Extensions;
using CleanArc.Web.Api.Controllers.V1.UserManagement;
using CleanArc.Web.Plugins.Grpc;
using CleanArc.WebFramework.Filters;
using CleanArc.WebFramework.Middlewares;
using CleanArc.WebFramework.ServiceConfiguration;
using CleanArc.WebFramework.Swagger;
using Mapster;
using Microsoft.AspNetCore.Mvc;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog(LoggingConfiguration.ConfigureLogger);
var configuration = builder.Configuration;
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
builder
.ConfigureHealthChecks()
.SetupOpenTelemetry();
builder.Services.Configure<IdentitySettings>(configuration.GetSection(nameof(IdentitySettings)));
var identitySettings = configuration.GetSection(nameof(IdentitySettings)).Get<IdentitySettings>();
builder.Services.AddControllers(options =>
{
options.Filters.Add(typeof(OkResultAttribute));
options.Filters.Add(typeof(NotFoundResultAttribute));
options.Filters.Add(typeof(ContentResultFilterAttribute));
options.Filters.Add(typeof(ModelStateValidationAttribute));
options.Filters.Add(typeof(BadRequestResultFilterAttribute));
options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult<Dictionary<string, List<string>>>),
StatusCodes.Status400BadRequest));
options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult),
StatusCodes.Status401Unauthorized));
options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult),
StatusCodes.Status403Forbidden));
options.Filters.Add(new ProducesResponseTypeAttribute(typeof(ApiResult),
StatusCodes.Status500InternalServerError));
}).ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true;
options.SuppressMapClientErrors = true;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwagger("v1","v1.1");
builder.Services.AddApplicationServices()
.RegisterIdentityServices(identitySettings)
.AddPersistenceServices(configuration)
.AddWebFrameworkServices();
builder.Services.RegisterValidatorsAsServices();
builder.Services.AddExceptionHandler<ExceptionHandler>();
builder.Services.AddMapster();
TypeAdapterConfig.GlobalSettings.Scan(typeof(UserCreateCommand).Assembly,
typeof(GetRolesDto).Assembly);
#region Plugin Services Configuration
builder.Services.ConfigureGrpcPluginServices();
#endregion
var app = builder.Build();
await app.ApplyMigrationsAsync();
await app.SeedDefaultUsersAsync();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
app.UseExceptionHandler(_=>{});
app.UseSwaggerAndUi();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.UseMetrics()
.UseHealthChecks();
app.ConfigureGrpcPipeline();
await app.RunAsync();
@@ -0,0 +1,31 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:8746",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Web.Api": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5002",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,12 @@
{
"ConnectionStrings": {
"SqlServer": "Data Source=localhost;Initial Catalog=CleanArc_DB_8_0;Integrated Security=true;Encrypt=False"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
@@ -0,0 +1,20 @@
{
"ConnectionStrings": {
"SqlServer": "Server=sql_server2022;Database=CleanArc_DB_Docker;User Id=SA;Password=A&VeryComplex123Password;MultipleActiveResultSets=true;encrypt=false",
"logDb": "Server=sql_server2022;Database=CleanArc_Log_DB_Docker;User Id=SA;Password=A&VeryComplex123Password;MultipleActiveResultSets=true;encrypt=false"
},
"IdentitySettings": {
"SecretKey": "ShouldBe-LongerThan-16Char-SecretKey",
"Encryptkey": "16CharEncryptKey",
"Issuer": "MyWebsite",
"Audience": "MyWebsite",
"NotBeforeMinutes": "0",
"ExpirationMinutes": "10000"
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}
@@ -0,0 +1,39 @@
using CleanArc.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace CleanArc.WebFramework.Attributes;
/// <summary>
/// Documents the 200 OK response type of endpoint as ApiResult
/// </summary>
/// <typeparam name="TResponse">API response type that will be in data JSON property</typeparam>
public class ProducesOkApiResponseType<TResponse>:ProducesResponseTypeAttribute
{
private ProducesOkApiResponseType(int statusCode) : base(statusCode)
{
}
public ProducesOkApiResponseType() : base(typeof(ApiResult<TResponse>), StatusCodes.Status200OK)
{
}
private ProducesOkApiResponseType(Type type, int statusCode, string contentType, params string[] additionalContentTypes) : base(type, statusCode, contentType, additionalContentTypes)
{
}
}
public class ProducesOkApiResponseType:ProducesResponseTypeAttribute
{
private ProducesOkApiResponseType(int statusCode) : base(statusCode)
{
}
public ProducesOkApiResponseType() : base(typeof(ApiResult), StatusCodes.Status200OK)
{
}
private ProducesOkApiResponseType(Type type, int statusCode, string contentType, params string[] additionalContentTypes) : base(type, statusCode, contentType, additionalContentTypes)
{
}
}
@@ -0,0 +1,52 @@
using System.Security.Claims;
using CleanArc.Application.Models.Common;
using CleanArc.SharedKernel.Extensions;
using CleanArc.WebFramework.Filters;
using Microsoft.AspNetCore.Mvc;
namespace CleanArc.WebFramework.BaseController;
public class BaseController : ControllerBase
{
protected string UserName => User.Identity?.Name;
protected int UserId => int.Parse(User.Identity.GetUserId());
protected string UserEmail => User.Identity.FindFirstValue(ClaimTypes.Email);
protected string UserRole => User.Identity.FindFirstValue(ClaimTypes.Role);
protected string UserKey => User.FindFirstValue(ClaimTypes.UserData);
protected IActionResult OperationResult<TModel>(OperationResult<TModel> result)
{
if (result is null)
return new ServerErrorResult("Server Error");
if (result.IsSuccess)
return result.Result is bool ? Ok() : Ok(result.Result);
if (result.IsNotFound)
{
AddErrors(result);
var notFoundErrors = new ValidationProblemDetails(ModelState);
return NotFound(notFoundErrors.Errors);
}
AddErrors(result);
var badRequestErrors = new ValidationProblemDetails(ModelState);
return BadRequest(badRequestErrors.Errors);
}
private void AddErrors<TModel>(OperationResult<TModel> result)
{
foreach (var error in result.ErrorMessages)
{
ModelState.AddModelError(error.Key,error.Value);
}
}
}
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NSwag.AspNetCore" />
<PackageReference Include="NuGet.Packaging" />
<PackageReference Include="Asp.Versioning.Http" />
<PackageReference Include="Asp.Versioning.Mvc" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\CleanArc.Application\CleanArc.Application.csproj" />
<ProjectReference Include="..\..\Infrastructure\CleanArc.Infrastructure.CrossCutting\CleanArc.Infrastructure.CrossCutting.csproj" />
<ProjectReference Include="..\..\Infrastructure\CleanArc.Infrastructure.Identity\CleanArc.Infrastructure.Identity.csproj" />
<ProjectReference Include="..\..\Infrastructure\CleanArc.Infrastructure.Persistence\CleanArc.Infrastructure.Persistence.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,28 @@
using CleanArc.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
namespace CleanArc.WebFramework.EndpointFilters;
public class BadRequestResultEndpointFilter:IEndpointFilter
{
public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var result = await next(context);
if (result is not IStatusCodeHttpResult statusCodeResult)
{
return result;
}
if (statusCodeResult.StatusCode != StatusCodes.Status400BadRequest)
return result;
if (result is IValueHttpResult valueHttp)
{
return Results.BadRequest(new ApiResult<object>(false, ApiResultStatusCode.BadRequest, valueHttp.Value));
}
return Results.BadRequest(new ApiResult(false, ApiResultStatusCode.BadRequest));
}
}
@@ -0,0 +1,46 @@
using FluentValidation;
using Microsoft.AspNetCore.Http;
namespace CleanArc.WebFramework.EndpointFilters;
public class ModelStateValidationEndpointFilter:IEndpointFilter
{
public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var validationSummery = new Dictionary<string, List<string>>();
foreach (var contextArgument in context.Arguments)
{
if (contextArgument is null)
continue;
var validator =
context.HttpContext.RequestServices.GetService(
typeof(IValidator<>).MakeGenericType(contextArgument.GetType())) as IValidator;
if (validator is null)
continue;
var validationResult = await validator.ValidateAsync(new ValidationContext<object>(contextArgument));
if (validationResult.IsValid) continue;
foreach (var validationResultError in validationResult.Errors)
{
if (validationSummery.TryGetValue(validationResultError.PropertyName, out var value))
{
value.Add(validationResultError.ErrorMessage);
continue;
}
validationSummery.Add(validationResultError.PropertyName, new (){validationResultError.ErrorMessage});
}
}
if (validationSummery.Count == 0)
return await next(context);
return Results.BadRequest(validationSummery);
}
}
@@ -0,0 +1,28 @@
using CleanArc.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
namespace CleanArc.WebFramework.EndpointFilters;
public class NotFoundResultEndpointFilter:IEndpointFilter
{
public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var result = await next(context);
if (result is not IStatusCodeHttpResult statusCodeResult)
{
return result;
}
if (statusCodeResult.StatusCode != StatusCodes.Status404NotFound)
return result;
if (result is IValueHttpResult valueHttp)
{
return Results.BadRequest(new ApiResult<object>(false, ApiResultStatusCode.NotFound, valueHttp.Value));
}
return Results.BadRequest(new ApiResult(false, ApiResultStatusCode.NotFound));
}
}
@@ -0,0 +1,28 @@
using CleanArc.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
namespace CleanArc.WebFramework.EndpointFilters;
public class OkResultEndpointFilter:IEndpointFilter
{
public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var result=await next(context);
if (result is not IStatusCodeHttpResult statusCodeResult)
{
return result;
}
if(statusCodeResult.StatusCode !=StatusCodes.Status200OK)
return result;
if (result is IValueHttpResult valueHttp)
{
return Results.Ok(new ApiResult<object>(true, ApiResultStatusCode.Success, valueHttp.Value));
}
return Results.Ok(new ApiResult(true, ApiResultStatusCode.Success));
}
}
@@ -0,0 +1,65 @@
using CleanArc.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace CleanArc.WebFramework.Filters;
[Obsolete(message:"Separated filters added")]
public class ApiResultFilterAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is OkObjectResult okObjectResult)
{
var apiResult = new ApiResult<object>(true, ApiResultStatusCode.Success, okObjectResult.Value);
context.Result = new JsonResult(apiResult) { StatusCode = okObjectResult.StatusCode };
}
else if (context.Result is OkResult okResult)
{
var apiResult = new ApiResult(true, ApiResultStatusCode.Success);
context.Result = new JsonResult(apiResult) { StatusCode = okResult.StatusCode };
}
else if (context.Result is BadRequestResult badRequestResult)
{
var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest);
context.Result = new JsonResult(apiResult) { StatusCode = badRequestResult.StatusCode };
context.HttpContext.Response.StatusCode = 400;
}
else if (context.Result is BadRequestObjectResult badRequestObjectResult)
{
var message = badRequestObjectResult.Value.ToString();
if (badRequestObjectResult.Value is SerializableError errors)
{
var errorMessages = errors.SelectMany(p => (string[])p.Value).Distinct();
message = string.Join(" | ", errorMessages);
}
var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest, message);
context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode };
context.HttpContext.Response.StatusCode = 400;
}
else if (context.Result is ContentResult contentResult)
{
var apiResult = new ApiResult(true, ApiResultStatusCode.Success, contentResult.Content);
context.Result = new JsonResult(apiResult) { StatusCode = contentResult.StatusCode };
}
else if (context.Result is NotFoundResultAttribute notFoundResult)
{
var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound);
context.Result = new JsonResult(apiResult) { StatusCode = StatusCodes.Status404NotFound };
}
else if (context.Result is NotFoundObjectResult notFoundObjectResult)
{
var apiResult = new ApiResult<object>(false, ApiResultStatusCode.NotFound, notFoundObjectResult.Value);
context.Result = new JsonResult(apiResult) { StatusCode = notFoundObjectResult.StatusCode };
}
else if (context.Result is ObjectResult objectResult && objectResult.StatusCode == null
&& !(objectResult.Value is ApiResult))
{
var apiResult = new ApiResult<object>(true, ApiResultStatusCode.Success, objectResult.Value);
context.Result = new JsonResult(apiResult) { StatusCode = objectResult.StatusCode };
}
base.OnResultExecuting(context);
}
}
@@ -0,0 +1,37 @@
using CleanArc.Application.Models.ApiResult;
using CleanArc.SharedKernel.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace CleanArc.WebFramework.Filters;
public class BadRequestResultFilterAttribute : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.Result is BadRequestObjectResult badRequestObjectResult)) return;
var modelState = context.ModelState;
if (!modelState.IsValid)
{
var errors = new ValidationProblemDetails(modelState);
var message = ApiResultStatusCode.BadRequest.ToDisplay();
var apiResult = new ApiResult<IDictionary<string, string[]>>(false, ApiResultStatusCode.BadRequest, errors.Errors, message);
context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode };
context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
else
{
var apiResult = new ApiResult<object>(false, ApiResultStatusCode.BadRequest,badRequestObjectResult.Value,ApiResultStatusCode.BadRequest.ToDisplay());
context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode };
context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}
}
}
@@ -0,0 +1,15 @@
using CleanArc.Application.Models.ApiResult;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace CleanArc.WebFramework.Filters;
public class ContentResultFilterAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.Result is ContentResult contentResult)) return;
var apiResult = new ApiResult(true, ApiResultStatusCode.Success, contentResult.Content);
context.Result = new JsonResult(apiResult) { StatusCode = contentResult.StatusCode };
}
}
@@ -0,0 +1,62 @@
using CleanArc.Application.Models.ApiResult;
using CleanArc.SharedKernel.Extensions;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using StatusCodes = Microsoft.AspNetCore.Http.StatusCodes;
namespace CleanArc.WebFramework.Filters;
public class ModelStateValidationAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
foreach (var contextActionArgument in context.ActionArguments.Values)
{
var viewModelValidator =
context.HttpContext.RequestServices.GetService(
typeof(IValidator<>).MakeGenericType(contextActionArgument.GetType()));
if (viewModelValidator is IValidator validator)
{
var validationResult =await validator.ValidateAsync(new ValidationContext<object>(contextActionArgument));
if (!validationResult.IsValid)
{
foreach (var validationResultError in validationResult.Errors)
{
context.ModelState.AddModelError(validationResultError.PropertyName, validationResultError.ErrorMessage);
}
}
}
}
var modelState = context.ModelState;
if (!modelState.IsValid)
{
var model = context.ActionArguments.FirstOrDefault().Value;
if (model != null)
{
var errors = new ValidationProblemDetails(modelState);
var message = ApiResultStatusCode.BadRequest.ToDisplay();
var apiResult = new ApiResult<IDictionary<string, string[]>>(false, ApiResultStatusCode.BadRequest, errors.Errors, message);
context.Result = new JsonResult(apiResult) { StatusCode = StatusCodes.Status400BadRequest };
context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}
else
{
var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest);
context.Result = new JsonResult(apiResult) { StatusCode = 400 };
context.HttpContext.Response.StatusCode = 400;
}
}
await base.OnActionExecutionAsync(context, next);
}
}
@@ -0,0 +1,24 @@
using CleanArc.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace CleanArc.WebFramework.Filters;
public class NotFoundResultAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if ((context.Result is NotFoundObjectResult notFoundObjectResult))
{
var apiResult = new ApiResult<object>(false, ApiResultStatusCode.NotFound, notFoundObjectResult.Value);
context.Result = new JsonResult(apiResult) { StatusCode = notFoundObjectResult.StatusCode };
}
else if(context.Result is NotFoundResult)
{
var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound);
context.Result = new JsonResult(apiResult) { StatusCode =StatusCodes.Status404NotFound };
}
}
}
@@ -0,0 +1,28 @@
using CleanArc.Application.Models.ApiResult;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace CleanArc.WebFramework.Filters;
public class OkResultAttribute:ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
switch (context.Result)
{
case OkObjectResult okObjectResult:
{
var apiResult = new ApiResult<object>(true, ApiResultStatusCode.Success, okObjectResult.Value);
context.Result = new JsonResult(apiResult) { StatusCode = okObjectResult.StatusCode };
break;
}
case OkResult okResult:
{
var apiResult = new ApiResult(true, ApiResultStatusCode.Success);
context.Result = new JsonResult(apiResult) { StatusCode = okResult.StatusCode };
break;
}
default:return;
}
}
}
@@ -0,0 +1,22 @@
using CleanArc.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace CleanArc.WebFramework.Filters;
public class ServerErrorResult:IActionResult
{
public string Message { get;}
public ServerErrorResult(string message)
{
Message = message;
}
public async Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
var response = new ApiResult(false, ApiResultStatusCode.ServerError, Message);
await context.HttpContext.Response.WriteAsJsonAsync(response);
}
}
@@ -0,0 +1,63 @@
using CleanArc.Application.Models.ApiResult;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using CleanArc.SharedKernel.Extensions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace CleanArc.WebFramework.Middlewares;
public class ExceptionHandler(ILogger<ExceptionHandler> logger,IWebHostEnvironment environment) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken cancellationToken)
{
if (environment.IsDevelopment())
return false;
if (exception is FluentValidation.ValidationException validationException)
{
context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity;
var errors = new Dictionary<string, List<string>>();
foreach (var validationExceptionError in validationException.Errors)
{
if (!errors.ContainsKey(validationExceptionError.PropertyName))
errors.Add(validationExceptionError.PropertyName, new List<string>() { validationExceptionError.ErrorMessage });
else
errors[validationExceptionError.PropertyName].Add(validationExceptionError.ErrorMessage);
}
var apiResult = new ApiResult<IDictionary<string, List<string>>>(false, ApiResultStatusCode.EntityProcessError, errors, ApiResultStatusCode.EntityProcessError.ToDisplay());
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(apiResult, cancellationToken: cancellationToken);
return true;
}
var exceptionFeature = context.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionFeature is not null)
logger.LogError(exceptionFeature.Error,
"Unhandled exception occured. Path: {exceptionUrlPath} ."
, exceptionFeature.Path
);
else
logger.LogError(exception, "Error captured in global exception handler");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/problem+json";
var response = new ApiResult(false,
ApiResultStatusCode.ServerError, "Internal Server Error");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(response, cancellationToken: cancellationToken);
return true;
}
}
@@ -0,0 +1,45 @@
using Asp.Versioning;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArc.WebFramework.ServiceConfiguration;
public static class ServiceCollectionExtension
{
public static IServiceCollection AddWebFrameworkServices(this IServiceCollection services)
{
services.AddApiVersioning(options =>
{
//url segment => {version}
options.AssumeDefaultVersionWhenUnspecified = true; //default => false;
options.DefaultApiVersion = new ApiVersion(1, 0); //v1.0 == v1
options.ReportApiVersions = true;
//ApiVersion.TryParse("1.0", out var version10);
//ApiVersion.TryParse("1", out var version1);
//var a = version10 == version1;
//options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
// api/posts?api-version=1
//options.ApiVersionReader = new UrlSegmentApiVersionReader();
// api/v1/posts
//options.ApiVersionReader = new HeaderApiVersionReader(new[] { "Api-Version" });
// header => Api-Version : 1
//options.ApiVersionReader = new MediaTypeApiVersionReader()
//options.ApiVersionReader = ApiVersionReader.Combine(new QueryStringApiVersionReader("api-version"), new UrlSegmentApiVersionReader())
// combine of [querystring] & [urlsegment]
}).AddMvc()
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'V";
options.SubstituteApiVersionInUrl = true;
});;
return services;
}
}
@@ -0,0 +1,25 @@
using CleanArc.SharedKernel.Extensions;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
namespace CleanArc.WebFramework.Swagger;
public class ApiVersionDocumentProcessor: IDocumentProcessor
{
public void Process(DocumentProcessorContext context)
{
// Filter out operations that do not match the current document version
var version = context.Document.Info.Version; // e.g., "v1"
var pathsToRemove = context.Document.Paths
.Where(pathItem => !RegExHelpers.MatchesApiVersion( version,pathItem.Key))
.Select(path => path.Key)
.ToList();
foreach (var path in pathsToRemove)
{
context.Document.Paths.Remove(path);
}
}
}
@@ -0,0 +1,122 @@
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
using Pluralize.NET;
namespace CleanArc.WebFramework.Swagger;
public class ApplySummariesOperationFilter : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
if (context.ControllerType is null)
return true;
var actionName = context.MethodInfo.Name;
var controllerName = context.ControllerType.Name.Replace("Controller", "");
var pluralizer = new Pluralizer();
var singularizeName =pluralizer.Singularize(controllerName);
var pluralizeName = pluralizer.Pluralize(singularizeName);
var parameterCount = context.OperationDescription.Operation.Parameters
.Count(p => p.Name != "version" && p.Name != "api-version");
if (IsGetAllAction(actionName, pluralizeName, singularizeName, parameterCount))
{
if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary))
{
context.OperationDescription.Operation.Summary = $"Returns all {pluralizeName}";
}
}
else if (IsActionName(actionName, singularizeName, "Post", "Create"))
{
if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary))
{
context.OperationDescription.Operation.Summary = $"Creates a {singularizeName}";
}
if (context.OperationDescription.Operation.Parameters.Count > 0 &&
string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description))
{
context.OperationDescription.Operation.Parameters[0].Description = $"A {singularizeName} representation";
}
}
else if (IsActionName(actionName, singularizeName, "Read", "Get"))
{
if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary))
{
context.OperationDescription.Operation.Summary = $"Retrieves a {singularizeName} by unique id";
}
if (context.OperationDescription.Operation.Parameters.Count > 0 &&
string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description))
{
context.OperationDescription.Operation.Parameters[0].Description = $"A unique id for the {singularizeName}";
}
}
else if (IsActionName(actionName, singularizeName, "Put", "Edit", "Update"))
{
if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary))
{
context.OperationDescription.Operation.Summary = $"Updates a {singularizeName} by unique id";
}
if (context.OperationDescription.Operation.Parameters.Count > 0 &&
string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description))
{
context.OperationDescription.Operation.Parameters[0].Description = $"A {singularizeName} representation";
}
}
else if (IsActionName(actionName, singularizeName, "Delete", "Remove"))
{
if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary))
{
context.OperationDescription.Operation.Summary = $"Deletes a {singularizeName} by unique id";
}
if (context.OperationDescription.Operation.Parameters.Count > 0 &&
string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description))
{
context.OperationDescription.Operation.Parameters[0].Description = $"A unique id for the {singularizeName}";
}
}
return true;
}
private bool IsActionName(string actionName, string singularizeName, params string[] names)
{
foreach (var name in names)
{
if (actionName.Equals(name, StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}ById", StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}{singularizeName}", StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}{singularizeName}ById", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private bool IsGetAllAction(string actionName, string pluralizeName, string singularizeName, int parameterCount)
{
var actionNames = new[] { "Get", "Read", "Select" };
foreach (var name in actionNames)
{
if ((actionName.Equals(name, StringComparison.OrdinalIgnoreCase) && parameterCount == 0) ||
actionName.Equals($"{name}All", StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}{pluralizeName}", StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}All{singularizeName}", StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}All{pluralizeName}", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}
@@ -0,0 +1,36 @@
using NSwag;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
namespace CleanArc.WebFramework.Swagger;
public class CustomTokenRequiredOperationFilter : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
var hasAttribute = context.MethodInfo
.GetCustomAttributes(typeof(RequireTokenWithoutAuthorizationAttribute), false).Any();
if (hasAttribute)
{
// Add security requirements to the operation
var securityScheme = new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = OpenApiSecurityApiKeyLocation.Header,
Name = "Authorization"
};
var securityRequirement = new OpenApiSecurityRequirement { { securityScheme.Scheme, new List<string>() } };
context.OperationDescription.Operation.Security=[securityRequirement];
}
return true;
}
}
@@ -0,0 +1,11 @@
namespace CleanArc.WebFramework.Swagger;
/// <summary>
/// Marker Attribute for Custom Actions or controllers that need token but without authorization check
/// </summary>
[AttributeUsage(AttributeTargets.Class| AttributeTargets.Method)]
public class RequireTokenWithoutAuthorizationAttribute : Attribute
{
};
@@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NSwag;
namespace CleanArc.WebFramework.Swagger;
public static class SwaggerConfigurationExtensions
{
public static void AddSwagger(this IServiceCollection services,
params string[] versions)
{
ArgumentNullException.ThrowIfNull(services, nameof(services));
foreach (var version in versions)
{
services.AddOpenApiDocument(options =>
{
options.Title = "Clean Architecture OpenAPI docs";
options.Version = version;
options.DocumentName = version;
options.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme()
{
Description = "Enter JWT Token ONLY",
In = OpenApiSecurityApiKeyLocation.Header,
Name = "Authorization",
Type = OpenApiSecuritySchemeType.Http,
Scheme = JwtBearerDefaults.AuthenticationScheme,
});
options.DocumentProcessors.Add(new ApiVersionDocumentProcessor());
options.OperationProcessors.Add(new ApplySummariesOperationFilter());
options.OperationProcessors.Add(new CustomTokenRequiredOperationFilter());
options.OperationProcessors.Add(
new NSwag.Generation.Processors.Security.AspNetCoreOperationSecurityScopeProcessor("Bearer"));
});
}
}
public static void UseSwaggerAndUi(this WebApplication app)
{
ArgumentNullException.ThrowIfNull(app, nameof(app));
app.UseOpenApi();
app.UseSwaggerUi(options =>
{
options.PersistAuthorization = true;
options.EnableTryItOut = true;
options.Path = "/swagger";
});
app.UseReDoc(settings =>
{
settings.Path = "/api-docs/{documentName}";
settings.DocumentTitle = "Clean Architecture API doc sample";
});
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" />
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Core\CleanArc.Application\CleanArc.Application.csproj" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="ProtoModels\UserGrpcServiceModels.proto" GrpcServices="Server" />
<Protobuf Include="ProtoModels\OrderGrpcServiceModels.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using CleanArc.Web.Plugins.Grpc.Services;
namespace CleanArc.Web.Plugins.Grpc;
public static class GrpcPluginStartup
{
public static IServiceCollection ConfigureGrpcPluginServices(this IServiceCollection services)
{
services.AddGrpc();
services.AddGrpcReflection();
return services;
}
public static void ConfigureGrpcPipeline(this WebApplication app)
{
app.MapGrpcService<UserGrpcServices>();
app.MapGrpcService<OrderGrpcServices>();
app.MapGrpcReflectionService();
app.MapGet("/GrpcUser", async context =>
{
await context.Response.WriteAsync(
"Communication with this gRPC endpoint must be made through a gRPC client.");
});
app.MapGet("/GrpcUserOrder", async context =>
{
await context.Response.WriteAsync(
"Communication with this gRPC endpoint must be made through a gRPC client.");
});
}
}
@@ -0,0 +1,17 @@
syntax = "proto3";
option csharp_namespace = "CleanArc.Web.Plugins.Grpc.ProtoModels";
import "google/protobuf/empty.proto";
package GrpcOrderController;
service OrderServices {
rpc GetUserOrders(google.protobuf.Empty) returns (stream GetUserOrdersModel);
}
message GetUserOrdersModel{
int32 OrderId=1;
string OrderName=2;
}
@@ -0,0 +1,44 @@
syntax = "proto3";
option csharp_namespace = "CleanArc.Web.Plugins.Grpc.ProtoModels";
package GrpcUserController;
service UserServices {
rpc TokenRequest(UserTokenRequest) returns (TokenRequestResult);
rpc GetUserToken(GetUserTokenRequestModel) returns (GetUserTokenRequestResult);
}
message UserTokenRequest{
string PhoneNumber=1;
}
message TokenRequestResult{
string Message=1;
bool IsSuccess=2;
TokenRequestResultModel UserTokenRequestResult=3;
}
message TokenRequestResultModel{
string UserKey=1;
}
message GetUserTokenRequestModel
{
string UserKey=1;
string Code=2;
}
message GetUserTokenRequestResult{
string Message=1;
bool IsSuccess=2;
UserToken Token=3;
}
message UserToken{
string AccessToken=1;
string RefreshToken=2;
string TokenType=3;
int32 ExpiresIn=4;
}
@@ -0,0 +1,46 @@
using CleanArc.Application.Features.Order.Queries.GetUserOrders;
using CleanArc.SharedKernel.Extensions;
using CleanArc.Web.Plugins.Grpc.ProtoModels;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Mediator;
using Microsoft.AspNetCore.Authorization;
namespace CleanArc.Web.Plugins.Grpc.Services
{
[Authorize]
public class OrderGrpcServices:OrderServices.OrderServicesBase
{
private readonly ISender _sender;
public OrderGrpcServices(ISender sender)
{
_sender = sender;
}
public override async Task GetUserOrders(Empty request, IServerStreamWriter<GetUserOrdersModel> responseStream, ServerCallContext context)
{
var userId = int.Parse(context.GetHttpContext().User.Identity.GetUserId());
var query = await _sender.Send(new GetUserOrdersQueryModel(userId));
if (!query.IsSuccess)
{
context.Status = new Status(StatusCode.InvalidArgument, query.GetErrorMessage());
return;
}
foreach (var getUsersQueryResultModel in query.Result)
{
await responseStream.WriteAsync(new GetUserOrdersModel()
{ OrderId = getUsersQueryResultModel.OrderId, OrderName = getUsersQueryResultModel.OrderName });
await Task.Delay(400);
}
}
}
}
@@ -0,0 +1,83 @@
using CleanArc.Application.Features.Users.Queries.GenerateUserToken;
using CleanArc.Application.Features.Users.Queries.TokenRequest;
using CleanArc.Web.Plugins.Grpc.ProtoModels;
using Grpc.Core;
using Mediator;
namespace CleanArc.Web.Plugins.Grpc.Services;
public class UserGrpcServices : UserServices.UserServicesBase
{
private readonly IMediator _mediator;
public UserGrpcServices(IMediator mediator)
{
_mediator = mediator;
}
public override async Task<TokenRequestResult> TokenRequest(UserTokenRequest request, ServerCallContext context)
{
if (request is null)
{
context.Status = new Status(StatusCode.InvalidArgument, "Required Arguments Not Found");
return new TokenRequestResult() { IsSuccess = false, Message = "Input Model Not Found" };
}
var tokenQuery = await _mediator.Send(new UserTokenRequestQuery(request.PhoneNumber));
if (!tokenQuery.IsSuccess)
{
context.Status = new Status(StatusCode.InvalidArgument, "User not found");
return new TokenRequestResult()
{ IsSuccess = false, Message = tokenQuery.GetErrorMessage(), UserTokenRequestResult = null };
}
if (tokenQuery.IsNotFound)
{
context.Status = new Status(StatusCode.NotFound, "User Not Found");
return new TokenRequestResult()
{ IsSuccess = false, Message = tokenQuery.GetErrorMessage(), UserTokenRequestResult = null };
}
return new TokenRequestResult()
{
IsSuccess = true,
Message = string.Empty,
UserTokenRequestResult = new TokenRequestResultModel() { UserKey = tokenQuery.Result.UserKey }
};
}
public override async Task<GetUserTokenRequestResult> GetUserToken(GetUserTokenRequestModel request, ServerCallContext context)
{
if (request is null)
{
context.Status = new Status(StatusCode.InvalidArgument, "Required Arguments Not Found");
return new GetUserTokenRequestResult() { IsSuccess = false, Message = "Input Model Not Found" };
}
var tokenQuery = await _mediator.Send(new GenerateUserTokenQuery(request.UserKey, request.Code));
if (!tokenQuery.IsSuccess)
{
context.Status = new Status(StatusCode.InvalidArgument, tokenQuery.GetErrorMessage());
return new GetUserTokenRequestResult() { IsSuccess = false, Message = tokenQuery.GetErrorMessage() };
}
return new GetUserTokenRequestResult()
{
IsSuccess = true, Message = string.Empty,
Token = new UserToken()
{
AccessToken = tokenQuery.Result.access_token, ExpiresIn = tokenQuery.Result.expires_in,
RefreshToken = tokenQuery.Result.refresh_token, TokenType = tokenQuery.Result.token_type
}
};
}
}
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Common\ValidationBase\**" />
<EmbeddedResource Remove="Common\ValidationBase\**" />
<None Remove="Common\ValidationBase\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArc.Domain\CleanArc.Domain.csproj" />
<PackageReference Include="Mediator.SourceGenerator">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.Abstractions" />
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
</Project>
@@ -0,0 +1,34 @@
using CleanArc.Application.Models.Common;
using Mediator;
using Microsoft.Extensions.Logging;
namespace CleanArc.Application.Common;
public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TResponse : class
where TRequest : IRequest<TResponse>
{
public async ValueTask<TResponse> Handle(TRequest message, MessageHandlerDelegate<TRequest, TResponse> next, CancellationToken cancellationToken)
{
try
{
var response = await next(message,cancellationToken);
return response;
}
catch (Exception e)
{
logger.LogError(e, e.Message);
if (typeof(TResponse).GetGenericTypeDefinition() == typeof(OperationResult<>))
{
var response = new OperationResult<TResponse> { IsException = true };
return response as TResponse;
}
return default;
}
}
}
@@ -0,0 +1,33 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Mediator;
namespace CleanArc.Application.Common;
public class MetricsBehaviour<TRequest, TResponse> :
IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly Histogram<long> _requestResponseDurationHistogram;
public MetricsBehaviour(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("mediator_meter");
_requestResponseDurationHistogram = meter.CreateHistogram<long>(
"Request_Response_Duration", "ms"
, "Determines the total request response durations");
}
public async ValueTask<TResponse> Handle(TRequest message, MessageHandlerDelegate<TRequest, TResponse> next, CancellationToken cancellationToken)
{
var stopWatch = Stopwatch.StartNew();
var response = await next(message,cancellationToken);
stopWatch.Stop();
_requestResponseDurationHistogram.Record(stopWatch.ElapsedMilliseconds,new []{new KeyValuePair<string, object>("Request",message.GetType().Name)});
return response;
}
}
@@ -0,0 +1,40 @@
using CleanArc.Application.Models.Common;
using FluentValidation;
using FluentValidation.Results;
using Mediator;
namespace CleanArc.Application.Common;
public class ValidateCommandBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TResponse : IOperationResult, new()
where TRequest : IRequest<TResponse>
{
public async ValueTask<TResponse> Handle(TRequest message, MessageHandlerDelegate<TRequest, TResponse> next, CancellationToken cancellationToken)
{
var errors = new List<ValidationFailure>();
foreach (var validator in validators)
{
var validationResult =
await validator.ValidateAsync(new ValidationContext<TRequest>(message), cancellationToken);
if (!validationResult.IsValid)
errors.AddRange(validationResult.Errors);
}
if (errors.Any())
{
return new TResponse()
{
ErrorMessages = errors.Select(c => new KeyValuePair<string, string>(c.PropertyName, c.ErrorMessage))
.ToList()
};
}
return await next(message, cancellationToken);
}
}
@@ -0,0 +1,13 @@
using System.Security.Claims;
using CleanArc.Application.Models.Jwt;
using CleanArc.Domain.Entities.User;
namespace CleanArc.Application.Contracts;
public interface IJwtService
{
Task<AccessToken> GenerateAsync(User user);
Task<ClaimsPrincipal> GetPrincipalFromExpiredToken(string token);
Task<AccessToken> GenerateByPhoneNumberAsync(string phoneNumber);
Task<AccessToken> RefreshToken(Guid refreshTokenId);
}
@@ -0,0 +1,31 @@
using CleanArc.Domain.Entities.User;
using Microsoft.AspNetCore.Identity;
namespace CleanArc.Application.Contracts.Identity;
public interface IAppUserManager
{
Task<IdentityResult> CreateUser(User user);
Task<IdentityResult> CreateUser(User user,string password);
Task<bool> IsExistUser(string phoneNumber);
Task<bool> IsExistUserName(string userName);
Task<string> GeneratePhoneNumberConfirmationToken(User user, string phoneNumber);
Task<User> GetUserByCode(string code);
Task<IdentityResult> ChangePhoneNumber(User user, string phoneNumber, string code);
Task<IdentityResult> VerifyUserCode(User user,string code);
Task<string> GenerateOtpCode(User user);
Task<User> GetUserByPhoneNumber(string phoneNumber);
Task<User> GetByUserName(string userName);
Task<User> GetUserByIdAsync(int userId);
Task<List<User>> GetAllUsersAsync();
Task<IdentityResult> CreateUserWithPasswordAsync(User user,string password);
Task<IdentityResult> AddUserToRoleAsync(User user, Role role);
Task<IdentityResult> IncrementAccessFailedCountAsync(User user);
Task<bool> IsUserLockedOutAsync(User user);
Task ResetUserLockoutAsync(User user);
Task UpdateUserAsync(User user);
Task UpdateSecurityStampAsync(User user);
Task<bool> IsPasswordValidAsync(User user, string password);
Task<string[]> GetRoleAsync(User user);
}
@@ -0,0 +1,16 @@
using CleanArc.Application.Models.Identity;
using CleanArc.Domain.Entities.User;
using Microsoft.AspNetCore.Identity;
namespace CleanArc.Application.Contracts.Identity;
public interface IRoleManagerService
{
Task<List<GetRolesDto>> GetRolesAsync();
Task<IdentityResult> CreateRoleAsync(CreateRoleDto model);
Task<bool> DeleteRoleAsync(int roleId);
Task<List<ActionDescriptionDto>> GetPermissionActionsAsync();
Task<RolePermissionDto> GetRolePermissionsAsync(int roleId);
Task<bool> ChangeRolePermissionsAsync(EditRolePermissionsDto model);
Task<Role> GetRoleByIdAsync(int roleId);
}
@@ -0,0 +1,12 @@
using CleanArc.Domain.Entities.Order;
namespace CleanArc.Application.Contracts.Persistence;
public interface IOrderRepository
{
Task AddOrderAsync(Order order);
Task<List<Order>> GetAllUserOrdersAsync(int userId);
Task<List<Order>> GetAllOrdersWithRelatedUserAsync();
Task<Order> GetUserOrderByIdAndUserIdAsync(int userId,int orderId,bool trackEntity);
Task DeleteUserOrdersAsync(int userId);
}
@@ -0,0 +1,9 @@
namespace CleanArc.Application.Contracts.Persistence;
public interface IUnitOfWork
{
public IUserRefreshTokenRepository UserRefreshTokenRepository { get; }
public IOrderRepository OrderRepository { get; }
Task CommitAsync();
ValueTask RollBackAsync();
}
@@ -0,0 +1,11 @@
using CleanArc.Domain.Entities.User;
namespace CleanArc.Application.Contracts.Persistence;
public interface IUserRefreshTokenRepository
{
Task<Guid> CreateToken(int userId);
Task<UserRefreshToken> GetTokenWithInvalidation(Guid id);
Task<User> GetUserByRefreshToken(Guid tokenId);
Task RemoveUserOldTokens(int userId, CancellationToken cancellationToken);
}
@@ -0,0 +1,44 @@
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using CleanArc.Domain.Entities.User;
using CleanArc.SharedKernel.Extensions;
using Mediator;
namespace CleanArc.Application.Features.Admin.Commands.AddAdminCommand
{
internal class AddAdminCommandHandler:IRequestHandler<AddAdminCommand,OperationResult<bool>>
{
private readonly IAppUserManager _userManager;
private readonly IRoleManagerService _roleManagerService;
public AddAdminCommandHandler(IAppUserManager userManager, IRoleManagerService roleManagerService)
{
_userManager = userManager;
_roleManagerService = roleManagerService;
}
public async ValueTask<OperationResult<bool>> Handle(AddAdminCommand request, CancellationToken cancellationToken)
{
var role = await _roleManagerService.GetRoleByIdAsync(request.RoleId);
if(role is null)
return OperationResult<bool>.NotFoundResult("Specified role not found");
var newAdmin = new User { UserName = request.UserName, Email = request.Email };
var adminCreateResult =
await _userManager.CreateUserWithPasswordAsync(
newAdmin, request.Password);
if(!adminCreateResult.Succeeded)
return OperationResult<bool>.FailureResult(adminCreateResult.Errors.StringifyIdentityResultErrors());
var addAdminToRoleResult = await _userManager.AddUserToRoleAsync(newAdmin, role);
if(addAdminToRoleResult.Succeeded)
return OperationResult<bool>.SuccessResult(true);
return OperationResult<bool>.FailureResult(addAdminToRoleResult.Errors.StringifyIdentityResultErrors());
}
}
}
@@ -0,0 +1,31 @@
using CleanArc.Application.Models.Common;
using CleanArc.SharedKernel.ValidationBase;
using CleanArc.SharedKernel.ValidationBase.Contracts;
using FluentValidation;
using Mediator;
namespace CleanArc.Application.Features.Admin.Commands.AddAdminCommand;
public record AddAdminCommand
(string UserName, string Email, string Password, int RoleId) : IRequest<OperationResult<bool>>,
IValidatableModel<AddAdminCommand>
{
public IValidator<AddAdminCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<AddAdminCommand> validator)
{
validator.RuleFor(c => c.Email)
.EmailAddress()
.WithMessage("Please enter an valid email");
validator.RuleFor(c => c.UserName)
.NotEmpty()
.NotNull()
.WithMessage("Please specify a valid username");
validator
.RuleFor(c => c.RoleId)
.GreaterThan(0)
.WithMessage("Please select a valid role");
return validator;
}
};
@@ -0,0 +1,47 @@
using CleanArc.Application.Contracts;
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using CleanArc.Application.Models.Jwt;
using Mediator;
namespace CleanArc.Application.Features.Admin.Queries.GetToken;
public class AdminGetTokenQueryHandler:IRequestHandler<AdminGetTokenQuery,OperationResult<AdminGetTokenQueryResult>>
{
private readonly IAppUserManager _userManager;
private readonly IJwtService _jwtService;
public AdminGetTokenQueryHandler(IAppUserManager userManager, IJwtService jwtService)
{
_userManager = userManager;
_jwtService = jwtService;
}
public async ValueTask<OperationResult<AdminGetTokenQueryResult>> Handle(AdminGetTokenQuery request, CancellationToken cancellationToken)
{
var user = await _userManager.GetByUserName(request.UserName);
if(user is null)
return OperationResult<AdminGetTokenQueryResult>.FailureResult("User not found");
var isUserLockedOut = await _userManager.IsUserLockedOutAsync(user);
if(isUserLockedOut)
if (user.LockoutEnd != null)
return OperationResult<AdminGetTokenQueryResult>.FailureResult(
$"User is locked out. Try in {(user.LockoutEnd-DateTimeOffset.Now).Value.Minutes} Minutes");
var userRoles = await _userManager.GetRoleAsync(user);
if(!userRoles.Any())
return OperationResult<AdminGetTokenQueryResult>.FailureResult("This user does not have any role assigned");
if(!await _userManager.IsPasswordValidAsync(user, request.Password))
return OperationResult<AdminGetTokenQueryResult>.NotFoundResult("User not found");
var token= await _jwtService.GenerateAsync(user);
return OperationResult<AdminGetTokenQueryResult>.SuccessResult(new(token,userRoles));
}
}
@@ -0,0 +1,5 @@
using CleanArc.Application.Models.Jwt;
namespace CleanArc.Application.Features.Admin.Queries.GetToken;
public record AdminGetTokenQueryResult(AccessToken Token,string[] Roles);
@@ -0,0 +1,27 @@
using CleanArc.Application.Models.Common;
using CleanArc.Application.Models.Jwt;
using CleanArc.SharedKernel.ValidationBase;
using CleanArc.SharedKernel.ValidationBase.Contracts;
using FluentValidation;
using Mediator;
namespace CleanArc.Application.Features.Admin.Queries.GetToken;
public record AdminGetTokenQuery(string UserName, string Password) : IRequest<OperationResult<AdminGetTokenQueryResult>>,
IValidatableModel<AdminGetTokenQuery>
{
public IValidator<AdminGetTokenQuery> ValidateApplicationModel(ApplicationBaseValidationModelProvider<AdminGetTokenQuery> validator)
{
validator.RuleFor(c => c.UserName)
.NotEmpty()
.NotNull()
.WithMessage("Please enter admin username");
validator.RuleFor(c => c.Password)
.NotEmpty()
.NotNull()
.WithMessage("Please enter admin password");
return validator;
}
};
@@ -0,0 +1,25 @@
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Contracts.Persistence;
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Order.Commands;
internal class AddOrderCommandHandler(IUnitOfWork unitOfWork, IAppUserManager userManager)
: IRequestHandler<AddOrderCommand, OperationResult<bool>>
{
public async ValueTask<OperationResult<bool>> Handle(AddOrderCommand request, CancellationToken cancellationToken)
{
var user = await userManager.GetUserByIdAsync(request.UserId);
if(user==null)
return OperationResult<bool>.FailureResult("User Not Found");
await unitOfWork.OrderRepository.AddOrderAsync(new Domain.Entities.Order.Order()
{ UserId = user.Id, OrderName = request.OrderName });
await unitOfWork.CommitAsync();
return OperationResult<bool>.SuccessResult(true);
}
}
@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
using CleanArc.Application.Models.Common;
using CleanArc.SharedKernel.ValidationBase;
using CleanArc.SharedKernel.ValidationBase.Contracts;
using FluentValidation;
using Mediator;
namespace CleanArc.Application.Features.Order.Commands;
public record AddOrderCommand( string OrderName) : IRequest<OperationResult<bool>>,
IValidatableModel<AddOrderCommand>
{
[JsonIgnore]
public int UserId { get; set; }
public IValidator<AddOrderCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<AddOrderCommand> validator)
{
validator.RuleFor(c => c.OrderName)
.NotEmpty()
.NotNull()
.WithMessage("Please enter your role name");
return validator;
}
}
@@ -0,0 +1,15 @@
using CleanArc.Application.Contracts.Persistence;
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Order.Commands;
public class DeleteUserOrdersCommandHandler(IUnitOfWork unitOfWork) : IRequestHandler<DeleteUserOrdersCommand,OperationResult<bool>>
{
public async ValueTask<OperationResult<bool>> Handle(DeleteUserOrdersCommand request, CancellationToken cancellationToken)
{
await unitOfWork.OrderRepository.DeleteUserOrdersAsync(request.UserId);
return OperationResult<bool>.SuccessResult(true);
}
}
@@ -0,0 +1,6 @@
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Order.Commands;
public record DeleteUserOrdersCommand(int UserId):IRequest<OperationResult<bool>>;
@@ -0,0 +1,25 @@
using CleanArc.Application.Contracts.Persistence;
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Order.Commands;
public class UpdateUserOrderCommandHandler(IUnitOfWork unitOfWork) : IRequestHandler<UpdateUserOrderCommand,OperationResult<bool>>
{
public async ValueTask<OperationResult<bool>> Handle(UpdateUserOrderCommand request, CancellationToken cancellationToken)
{
var order = await unitOfWork.OrderRepository.GetUserOrderByIdAndUserIdAsync(request.UserId, request.OrderId,
true);
if(order is null)
return OperationResult<bool>.NotFoundResult("Specified Order not found");
order.OrderName=request.OrderName;
await unitOfWork.CommitAsync();
return OperationResult<bool>.SuccessResult(true);
}
}
@@ -0,0 +1,23 @@
using System.Text.Json.Serialization;
using CleanArc.Application.Models.Common;
using CleanArc.SharedKernel.ValidationBase;
using CleanArc.SharedKernel.ValidationBase.Contracts;
using FluentValidation;
using Mediator;
namespace CleanArc.Application.Features.Order.Commands;
public record UpdateUserOrderCommand
(int OrderId, string OrderName) : IRequest<OperationResult<bool>>,IValidatableModel<UpdateUserOrderCommand>
{
[JsonIgnore]
public int UserId { get; set; }
public IValidator<UpdateUserOrderCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<UpdateUserOrderCommand> validator)
{
validator.RuleFor(c => c.OrderId).NotEmpty().GreaterThan(0);
validator.RuleFor(c => c.OrderName).NotEmpty().NotNull();
return validator;
}
};
@@ -0,0 +1,6 @@
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Order.Queries.GetAllOrders;
public record GetAllOrdersQuery():IRequest<OperationResult<List<GetAllOrdersQueryResult>>>;
@@ -0,0 +1,20 @@
using CleanArc.Application.Contracts.Persistence;
using CleanArc.Application.Models.Common;
using MapsterMapper;
using Mediator;
namespace CleanArc.Application.Features.Order.Queries.GetAllOrders
{
internal class GetAllOrdersQueryHandler(IUnitOfWork unitOfWork, IMapper mapper)
: IRequestHandler<GetAllOrdersQuery, OperationResult<List<GetAllOrdersQueryResult>>>
{
public async ValueTask<OperationResult<List<GetAllOrdersQueryResult>>> Handle(GetAllOrdersQuery request, CancellationToken cancellationToken)
{
var orders = await unitOfWork.OrderRepository.GetAllOrdersWithRelatedUserAsync();
var result = orders.Select(mapper.Map<Domain.Entities.Order.Order,GetAllOrdersQueryResult>).ToList();
return OperationResult<List<GetAllOrdersQueryResult>>.SuccessResult(result);
}
}
}
@@ -0,0 +1,19 @@
using Mapster;
namespace CleanArc.Application.Features.Order.Queries.GetAllOrders;
public record GetAllOrdersQueryResult(int OrderId, string OrderName, int OrderOwnerId, string OrderOwnerUserName);
class GetAllOrdersQueryResultMapping : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<Domain.Entities.Order.Order, GetAllOrdersQueryResult>()
.Map(dest => dest.OrderId, src => src.Id)
.Map(dest => dest.OrderName, src => src.OrderName)
.Map(dest => dest.OrderOwnerId, src => src.User.Id)
.Map(dest => dest.OrderOwnerUserName, src => src.User.UserName);
}
}
@@ -0,0 +1,21 @@
using CleanArc.Application.Contracts.Persistence;
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Order.Queries.GetUserOrders;
internal class GetUserOrdersQueryHandler(IUnitOfWork unitOfWork)
: IRequestHandler<GetUserOrdersQueryModel, OperationResult<List<GetUsersQueryResultModel>>>
{
public async ValueTask<OperationResult<List<GetUsersQueryResultModel>>> Handle(GetUserOrdersQueryModel request, CancellationToken cancellationToken)
{
var orders = await unitOfWork.OrderRepository.GetAllUserOrdersAsync(request.UserId);
if(!orders.Any())
return OperationResult<List<GetUsersQueryResultModel>>.NotFoundResult("You Don't Have Any Orders");
var result = orders.Select(c => new GetUsersQueryResultModel(c.Id, c.OrderName));
return OperationResult<List<GetUsersQueryResultModel>>.SuccessResult(result.ToList());
}
}
@@ -0,0 +1,6 @@
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Order.Queries.GetUserOrders;
public record GetUserOrdersQueryModel(int UserId) : IRequest<OperationResult<List<GetUsersQueryResultModel>>>;
@@ -0,0 +1,3 @@
namespace CleanArc.Application.Features.Order.Queries.GetUserOrders;
public record GetUsersQueryResultModel(int OrderId, string OrderName);
@@ -0,0 +1,24 @@
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using CleanArc.Application.Models.Identity;
using Mediator;
namespace CleanArc.Application.Features.Role.Commands.AddRoleCommand
{
internal class AddRoleCommandHandler(IRoleManagerService roleManagerService)
: IRequestHandler<AddRoleCommand, OperationResult<bool>>
{
public async ValueTask<OperationResult<bool>> Handle(AddRoleCommand request, CancellationToken cancellationToken)
{
var addRoleResult =
await roleManagerService.CreateRoleAsync(new CreateRoleDto() { RoleName = request.RoleName });
if (addRoleResult.Succeeded)
return OperationResult<bool>.SuccessResult(true);
var errors = string.Join("\n", addRoleResult.Errors.Select(c => c.Description));
return OperationResult<bool>.FailureResult(errors);
}
}
}
@@ -0,0 +1,22 @@
using CleanArc.Application.Models.Common;
using CleanArc.SharedKernel.ValidationBase;
using CleanArc.SharedKernel.ValidationBase.Contracts;
using FluentValidation;
using Mediator;
namespace CleanArc.Application.Features.Role.Commands.AddRoleCommand;
public record AddRoleCommand(string RoleName) : IRequest<OperationResult<bool>>,
IValidatableModel<AddRoleCommand>
{
public IValidator<AddRoleCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<AddRoleCommand> validator)
{
validator
.RuleFor(c => c.RoleName)
.NotEmpty()
.NotNull()
.WithMessage("Please enter role name");
return validator;
}
};
@@ -0,0 +1,21 @@
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using CleanArc.Application.Models.Identity;
using Mediator;
namespace CleanArc.Application.Features.Role.Commands.UpdateRoleClaimsCommand
{
internal class UpdateRoleClaimsCommandHandler(IRoleManagerService roleManagerService)
: IRequestHandler<UpdateRoleClaimsCommand, OperationResult<bool>>
{
public async ValueTask<OperationResult<bool>> Handle(UpdateRoleClaimsCommand request, CancellationToken cancellationToken)
{
var updateRoleResult = await roleManagerService.ChangeRolePermissionsAsync(new EditRolePermissionsDto()
{ RoleId = request.RoleId, Permissions = request.RoleClaimValue });
return updateRoleResult
? OperationResult<bool>.SuccessResult(true)
: OperationResult<bool>.FailureResult("Could Not Update Claims for given Role");
}
}
}
@@ -0,0 +1,7 @@
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Role.Commands.UpdateRoleClaimsCommand;
public record UpdateRoleClaimsCommand( int RoleId, List<string> RoleClaimValue):IRequest<OperationResult<bool>>;
@@ -0,0 +1,22 @@
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Role.Queries.GetAllRolesQuery
{
internal class GetAllRolesQueryHandler(IRoleManagerService roleManagerService)
: IRequestHandler<GetAllRolesQuery, OperationResult<List<GetAllRolesQueryResponse>>>
{
public async ValueTask<OperationResult<List<GetAllRolesQueryResponse>>> Handle(GetAllRolesQuery request, CancellationToken cancellationToken)
{
var roles = await roleManagerService.GetRolesAsync();
if(!roles.Any())
return OperationResult<List<GetAllRolesQueryResponse>>.NotFoundResult("No Roles Found");
var result = roles.Select(c => new GetAllRolesQueryResponse(int.Parse(c.Id), c.Name)).ToList();
return OperationResult<List<GetAllRolesQueryResponse>>.SuccessResult(result);
}
}
}
@@ -0,0 +1,3 @@
namespace CleanArc.Application.Features.Role.Queries.GetAllRolesQuery;
public record GetAllRolesQueryResponse(int RoleId,string RoleName);
@@ -0,0 +1,6 @@
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Role.Queries.GetAllRolesQuery;
public record GetAllRolesQuery():IRequest<OperationResult<List<GetAllRolesQueryResponse>>>;
@@ -0,0 +1,24 @@
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery
{
internal class GetAuthorizableRoutesQueryHandler(IRoleManagerService roleManagerService)
: IRequestHandler<GetAuthorizableRoutesQuery, OperationResult<List<GetAuthorizableRoutesQueryResponse>>>
{
public async ValueTask<OperationResult<List<GetAuthorizableRoutesQueryResponse>>> Handle(GetAuthorizableRoutesQuery request, CancellationToken cancellationToken)
{
var authRoutes = await roleManagerService.GetPermissionActionsAsync();
if(!authRoutes.Any())
return OperationResult<List<GetAuthorizableRoutesQueryResponse>>.NotFoundResult("No Special auth route found");
var result = authRoutes.Select(c =>
new GetAuthorizableRoutesQueryResponse(c.Key, c.AreaName, c.ControllerName, c.ActionName,c.ControllerDescription))
.ToList();
return OperationResult<List<GetAuthorizableRoutesQueryResponse>>.SuccessResult(result);
}
}
}
@@ -0,0 +1,3 @@
namespace CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery;
public record GetAuthorizableRoutesQueryResponse(string RouteKey,string AreaName,string ControllerName,string ActionName,string ControllerDescription);
@@ -0,0 +1,6 @@
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery;
public record GetAuthorizableRoutesQuery():IRequest<OperationResult<List<GetAuthorizableRoutesQueryResponse>>>;
@@ -0,0 +1,54 @@
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using CleanArc.Domain.Entities.User;
using MapsterMapper;
using Mediator;
using Microsoft.Extensions.Logging;
namespace CleanArc.Application.Features.Users.Commands.Create;
internal class UserCreateCommandHandler(
IAppUserManager userManager,
ILogger<UserCreateCommandHandler> logger,
IMapper mapper)
: IRequestHandler<UserCreateCommand, OperationResult<UserCreateCommandResult>>
{
public async ValueTask<OperationResult<UserCreateCommandResult>> Handle(UserCreateCommand request,
CancellationToken cancellationToken)
{
var userNameExist = await userManager.IsExistUser(request.PhoneNumber);
if (userNameExist)
return OperationResult<UserCreateCommandResult>.FailureResult("Phone number already exists");
var phoneNumberExist = await userManager.IsExistUserName(request.UserName);
if (phoneNumberExist)
return OperationResult<UserCreateCommandResult>.FailureResult("Username already exists");
//var user = new User { UserName = request.UserName, Name = request.FirstName, FamilyName = request.LastName, PhoneNumber = request.PhoneNumber };
var user = mapper.Map<User>(request);
var createResult =string.IsNullOrEmpty(request.Password)?
await userManager.CreateUser(user)
:await userManager.CreateUser(user, request.Password);
if (!createResult.Succeeded)
{
return OperationResult<UserCreateCommandResult>.FailureResult(string.Join(",",
createResult.Errors.Select(c => c.Description)));
}
var code = await userManager.GeneratePhoneNumberConfirmationToken(user, user.PhoneNumber);
logger.LogWarning($"Generated Code for User ID {user.Id} is {code}");
//TODO Send Code Via Sms Provider
return OperationResult<UserCreateCommandResult>.SuccessResult(new UserCreateCommandResult
{ UserGeneratedKey = user.GeneratedCode });
}
}
@@ -0,0 +1,6 @@
namespace CleanArc.Application.Features.Users.Commands.Create;
public class UserCreateCommandResult
{
public string UserGeneratedKey { get; set; }
}
@@ -0,0 +1,50 @@
using System.Text.RegularExpressions;
using CleanArc.Application.Models.Common;
using CleanArc.Domain.Entities.User;
using CleanArc.SharedKernel.ValidationBase;
using CleanArc.SharedKernel.ValidationBase.Contracts;
using FluentValidation;
using Mediator;
namespace CleanArc.Application.Features.Users.Commands.Create;
public record UserCreateCommand
(string UserName, string Name, string FamilyName, string PhoneNumber,string Password,string RepeatPassword)
: IRequest<OperationResult<UserCreateCommandResult>>
,IValidatableModel<UserCreateCommand>
{
public IValidator<UserCreateCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<UserCreateCommand> validator)
{
validator
.RuleFor(c => c.Name)
.NotEmpty()
.NotNull()
.WithMessage("User must have first name");
validator.RuleFor(c => c.UserName)
.NotEmpty()
.NotNull()
.WithMessage("Please enter your username");
validator
.RuleFor(c => c.FamilyName)
.NotEmpty()
.NotNull()
.WithMessage("User must have last name");
validator.RuleFor(c => c.PhoneNumber).NotEmpty()
.NotNull().WithMessage("Phone Number is required.")
.MinimumLength(10).WithMessage("PhoneNumber must not be less than 10 characters.")
.MaximumLength(20).WithMessage("PhoneNumber must not exceed 50 characters.")
.Matches(new Regex(@"^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$")).WithMessage("Phone number is not valid");
validator.RuleFor(c => c.Password)
.Matches(c => c.RepeatPassword)
.WithMessage("passwords do not match");
return validator;
}
}
@@ -0,0 +1,21 @@
using CleanArc.Application.Contracts;
using CleanArc.Application.Models.Common;
using CleanArc.Application.Models.Jwt;
using Mediator;
namespace CleanArc.Application.Features.Users.Commands.RefreshUserTokenCommand
{
internal class RefreshUserTokenCommandHandler(IJwtService jwtService)
: IRequestHandler<RefreshUserTokenCommand, OperationResult<AccessToken>>
{
public async ValueTask<OperationResult<AccessToken>> Handle(RefreshUserTokenCommand request, CancellationToken cancellationToken)
{
var newToken = await jwtService.RefreshToken(request.RefreshToken);
if(newToken is null)
return OperationResult<AccessToken>.FailureResult("Invalid refresh token");
return OperationResult<AccessToken>.SuccessResult(newToken);
}
}
}
@@ -0,0 +1,22 @@
using CleanArc.Application.Models.Common;
using CleanArc.Application.Models.Jwt;
using CleanArc.SharedKernel.ValidationBase;
using CleanArc.SharedKernel.ValidationBase.Contracts;
using FluentValidation;
using Mediator;
namespace CleanArc.Application.Features.Users.Commands.RefreshUserTokenCommand;
public record RefreshUserTokenCommand(Guid RefreshToken) : IRequest<OperationResult<AccessToken>>,
IValidatableModel<RefreshUserTokenCommand>
{
public IValidator<RefreshUserTokenCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<RefreshUserTokenCommand> validator)
{
validator.RuleFor(c => c.RefreshToken)
.NotEmpty()
.NotNull()
.WithMessage("Please enter valid user refresh token");
return validator;
}
};
@@ -0,0 +1,22 @@
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Users.Commands.RequestLogout
{
internal class RequestLogoutCommandHandler(IAppUserManager userManager)
: IRequestHandler<RequestLogoutCommand, OperationResult<bool>>
{
public async ValueTask<OperationResult<bool>> Handle(RequestLogoutCommand request, CancellationToken cancellationToken)
{
var user = await userManager.GetUserByIdAsync(request.UserId);
if (user == null)
return OperationResult<bool>.FailureResult("User not found");
await userManager.UpdateSecurityStampAsync(user);
return OperationResult<bool>.SuccessResult(true);
}
}
}
@@ -0,0 +1,6 @@
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Users.Commands.RequestLogout;
public record RequestLogoutCommand(int UserId):IRequest<OperationResult<bool>>;
@@ -0,0 +1,33 @@
using CleanArc.Application.Contracts;
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using CleanArc.Application.Models.Jwt;
using CleanArc.SharedKernel.Extensions;
using Mediator;
namespace CleanArc.Application.Features.Users.Queries.GenerateUserToken;
internal class GenerateUserTokenQueryHandler(IJwtService jwtService, IAppUserManager userManager)
: IRequestHandler<GenerateUserTokenQuery, OperationResult<AccessToken>>
{
public async ValueTask<OperationResult<AccessToken>> Handle(GenerateUserTokenQuery request, CancellationToken cancellationToken)
{
var user = await userManager.GetUserByCode(request.UserKey);
if (user is null)
return OperationResult<AccessToken>.FailureResult("User Not found");
var result = user.PhoneNumberConfirmed? await userManager.VerifyUserCode(
user, request.Code):await userManager.ChangePhoneNumber(user,user.PhoneNumber,request.Code);
if (!result.Succeeded)
return OperationResult<AccessToken>.FailureResult(result.Errors.StringifyIdentityResultErrors());
await userManager.UpdateUserAsync(user);
var token = await jwtService.GenerateAsync(user);
return OperationResult<AccessToken>.SuccessResult(token);
}
}
@@ -0,0 +1,28 @@
using CleanArc.Application.Models.Common;
using CleanArc.Application.Models.Jwt;
using CleanArc.SharedKernel.ValidationBase;
using CleanArc.SharedKernel.ValidationBase.Contracts;
using FluentValidation;
using Mediator;
namespace CleanArc.Application.Features.Users.Queries.GenerateUserToken;
public record GenerateUserTokenQuery(string UserKey, string Code) : IRequest<OperationResult<AccessToken>>,
IValidatableModel<GenerateUserTokenQuery>
{
public IValidator<GenerateUserTokenQuery> ValidateApplicationModel(ApplicationBaseValidationModelProvider<GenerateUserTokenQuery> validator)
{
validator.RuleFor(c => c.Code)
.NotEmpty()
.NotNull()
.Length(6)
.WithMessage("User code is not valid");
validator.RuleFor(c => c.UserKey)
.NotEmpty()
.NotNull()
.WithMessage("Invalid user key");
return validator;
}
};
@@ -0,0 +1,22 @@
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using CleanArc.Domain.Entities.User;
using MapsterMapper;
using Mediator;
namespace CleanArc.Application.Features.Users.Queries.GetUsers;
internal class GetUsersQueryHandler(IAppUserManager userManager, IMapper mapper)
: IRequestHandler<GetUsersQuery, OperationResult<List<GetUsersQueryResponse>>>
{
public async ValueTask<OperationResult<List<GetUsersQueryResponse>>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
{
var usersModel =
(await userManager.GetAllUsersAsync()).Select(mapper.Map<User, GetUsersQueryResponse>).ToList();
if(!usersModel.Any())
return OperationResult<List<GetUsersQueryResponse>>.NotFoundResult("No Users Found!");
return OperationResult<List<GetUsersQueryResponse>>.SuccessResult(usersModel);
}
}
@@ -0,0 +1,11 @@
using CleanArc.Domain.Entities.User;
namespace CleanArc.Application.Features.Users.Queries.GetUsers;
public record GetUsersQueryResponse
{
public string UserName { get; set; }
public string Email { get; set; }
public int UserId { get; set; }
}
@@ -0,0 +1,6 @@
using CleanArc.Application.Models.Common;
using Mediator;
namespace CleanArc.Application.Features.Users.Queries.GetUsers;
public record GetUsersQuery : IRequest<OperationResult<List<GetUsersQueryResponse>>>;
@@ -0,0 +1,27 @@
using CleanArc.Application.Contracts;
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using CleanArc.Application.Models.Jwt;
using Mediator;
namespace CleanArc.Application.Features.Users.Queries.TokenRequest;
public class PasswordUserTokenRequestQueryResult
(IAppUserManager userManager,IJwtService jwtService)
:IRequestHandler<PasswordUserTokenRequestQuery,OperationResult<AccessToken>>
{
public async ValueTask<OperationResult<AccessToken>> Handle(PasswordUserTokenRequestQuery request, CancellationToken cancellationToken)
{
var user = await userManager.GetByUserName(request.UserName);
if(user is null)
return OperationResult<AccessToken>.NotFoundResult("User not found");
if(!await userManager.IsPasswordValidAsync(user,request.Password))
return OperationResult<AccessToken>.NotFoundResult("User not found");
var token = await jwtService.GenerateAsync(user);
return OperationResult<AccessToken>.SuccessResult(token);
}
}
@@ -0,0 +1,24 @@
using CleanArc.Application.Models.Common;
using CleanArc.Application.Models.Jwt;
using CleanArc.SharedKernel.ValidationBase;
using CleanArc.SharedKernel.ValidationBase.Contracts;
using FluentValidation;
using Mediator;
namespace CleanArc.Application.Features.Users.Queries.TokenRequest;
public record PasswordUserTokenRequestQuery
(string UserName,string Password)
:IValidatableModel<PasswordUserTokenRequestQuery>,IRequest<OperationResult<AccessToken>>
{
public IValidator<PasswordUserTokenRequestQuery> ValidateApplicationModel(ApplicationBaseValidationModelProvider<PasswordUserTokenRequestQuery> validator)
{
validator.RuleFor(c => c.UserName)
.NotEmpty();
validator.RuleFor(c => c.Password)
.NotEmpty();
return validator;
}
}
@@ -0,0 +1,30 @@
using CleanArc.Application.Contracts.Identity;
using CleanArc.Application.Models.Common;
using Mediator;
using Microsoft.Extensions.Logging;
namespace CleanArc.Application.Features.Users.Queries.TokenRequest;
public class UserTokenRequestQueryHandler(
IAppUserManager userManager,
ILogger<UserTokenRequestQueryHandler> logger)
: IRequestHandler<UserTokenRequestQuery, OperationResult<UserTokenRequestQueryResponse>>
{
public async ValueTask<OperationResult<UserTokenRequestQueryResponse>> Handle(UserTokenRequestQuery request, CancellationToken cancellationToken)
{
var user = await userManager.GetUserByPhoneNumber(request.UserPhoneNumber);
if(user is null)
return OperationResult<UserTokenRequestQueryResponse>.NotFoundResult("User Not found");
var code = user.PhoneNumberConfirmed? await userManager.GenerateOtpCode(user) : await userManager.GeneratePhoneNumberConfirmationToken(user,user.PhoneNumber);
logger.LogWarning($"Generated Code for user Id {user.Id} is {code}");
//TODO Send Code Via Sms Provider
return OperationResult<UserTokenRequestQueryResponse>.SuccessResult(new UserTokenRequestQueryResponse {UserKey = user.GeneratedCode});
}
}
@@ -0,0 +1,6 @@
namespace CleanArc.Application.Features.Users.Queries.TokenRequest;
public class UserTokenRequestQueryResponse
{
public string UserKey { get; set; }
}

Some files were not shown because too many files have changed in this diff Show More