init
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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>
|
||||
+28
@@ -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));
|
||||
}
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
@@ -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.");
|
||||
});
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
|
||||
+44
@@ -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();
|
||||
}
|
||||
+11
@@ -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);
|
||||
}
|
||||
+44
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
};
|
||||
+47
@@ -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));
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
|
||||
namespace CleanArc.Application.Features.Admin.Queries.GetToken;
|
||||
|
||||
public record AdminGetTokenQueryResult(AccessToken Token,string[] Roles);
|
||||
+27
@@ -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;
|
||||
}
|
||||
};
|
||||
+25
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
@@ -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);
|
||||
}
|
||||
}
|
||||
+6
@@ -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>>;
|
||||
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
};
|
||||
+6
@@ -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>>>;
|
||||
+20
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -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);
|
||||
}
|
||||
}
|
||||
+21
@@ -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());
|
||||
}
|
||||
}
|
||||
+6
@@ -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>>>;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
namespace CleanArc.Application.Features.Order.Queries.GetUserOrders;
|
||||
|
||||
public record GetUsersQueryResultModel(int OrderId, string OrderName);
|
||||
+24
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
};
|
||||
+21
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -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>>;
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
namespace CleanArc.Application.Features.Role.Queries.GetAllRolesQuery;
|
||||
|
||||
public record GetAllRolesQueryResponse(int RoleId,string RoleName);
|
||||
+6
@@ -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>>>;
|
||||
+24
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
namespace CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery;
|
||||
|
||||
public record GetAuthorizableRoutesQueryResponse(string RouteKey,string AreaName,string ControllerName,string ActionName,string ControllerDescription);
|
||||
+6
@@ -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>>>;
|
||||
+54
@@ -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 });
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace CleanArc.Application.Features.Users.Commands.Create;
|
||||
|
||||
public class UserCreateCommandResult
|
||||
{
|
||||
public string UserGeneratedKey { get; set; }
|
||||
}
|
||||
+50
@@ -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;
|
||||
}
|
||||
}
|
||||
+21
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
};
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -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>>;
|
||||
+33
@@ -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);
|
||||
}
|
||||
}
|
||||
+28
@@ -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;
|
||||
}
|
||||
};
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -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>>>;
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
}
|
||||
+30
@@ -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});
|
||||
}
|
||||
}
|
||||
+6
@@ -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
Reference in New Issue
Block a user