diff --git a/docs/model.ndm2 b/docs/model.ndm2
index d21861a..27f010a 100644
--- a/docs/model.ndm2
+++ b/docs/model.ndm2
@@ -12045,6 +12045,503 @@
"filestreamPartitionScheme": "",
"dataCompressions": []
}
+ },
+ {
+ "objectType": "Table_MSSQL",
+ "name": "OtpService",
+ "comment": "",
+ "owner": "",
+ "isChangeTracking": false,
+ "isTrackColumnsUpdated": false,
+ "oldName": "",
+ "isSystemTable": false,
+ "createTime": "",
+ "modifyTime": "",
+ "objectID": 0,
+ "numberOfRows": 0,
+ "identityCurrent": 0,
+ "dataLength": 0,
+ "indexLength": 0,
+ "fields": [
+ {
+ "objectType": "TableField_MSSQL",
+ "name": "SendOtp",
+ "type": "Command",
+ "size": 0,
+ "isNullable": "Yes",
+ "scale": 0,
+ "comment": "ارسال رمز پویا",
+ "computedExpression": "",
+ "defaultValue": "",
+ "defaultValueType": "Others",
+ "schema": "",
+ "userDefinedType": "",
+ "collate": "",
+ "isWithValues": false,
+ "isFilestream": false,
+ "isColumnSet": false,
+ "isPersisted": false,
+ "isSparse": false,
+ "isRowGUIDColumn": false,
+ "oldName": "SendOtp",
+ "computedBaseType": "",
+ "isDefaultConstraint": false,
+ "defaultConstraint": "",
+ "isIdentity": false,
+ "isExistingField": false,
+ "identitySeed": 0,
+ "identityIncrement": 0,
+ "identityIsNotForReplication": false
+ },
+ {
+ "objectType": "TableField_MSSQL",
+ "name": "VerifyOtpCode",
+ "type": "Command",
+ "size": 0,
+ "isNullable": "Yes",
+ "scale": 0,
+ "comment": "فعال سازی رمز پویا",
+ "computedExpression": "",
+ "defaultValue": "",
+ "defaultValueType": "Others",
+ "schema": "",
+ "userDefinedType": "",
+ "collate": "",
+ "isWithValues": false,
+ "isFilestream": false,
+ "isColumnSet": false,
+ "isPersisted": false,
+ "isSparse": false,
+ "isRowGUIDColumn": false,
+ "oldName": "VerifyOtpCode",
+ "computedBaseType": "",
+ "isDefaultConstraint": false,
+ "defaultConstraint": "",
+ "isIdentity": false,
+ "isExistingField": false,
+ "identitySeed": 0,
+ "identityIncrement": 0,
+ "identityIsNotForReplication": false
+ }
+ ],
+ "indexes": [],
+ "foreignKeys": [
+ {
+ "objectType": "ForeignKey_MSSQL",
+ "name": "fk_OtpService_VerifyOtpCodeRequest_1",
+ "fields": [
+ "VerifyOtpCode"
+ ],
+ "referencedSchema": "BackOffice",
+ "referencedTable": "VerifyOtpCodeRequest",
+ "referencedFields": [
+ "Mobile"
+ ],
+ "onDelete": "",
+ "onUpdate": "",
+ "isNotForReplication": false,
+ "isEnabled": true,
+ "comment": "",
+ "sourceCardinality": "NoneRelationship",
+ "targetCardinality": "NoneRelationship",
+ "oldName": ""
+ },
+ {
+ "objectType": "ForeignKey_MSSQL",
+ "name": "fk_OtpService_VerifyOtpCodeResponse_1",
+ "fields": [
+ "VerifyOtpCode"
+ ],
+ "referencedSchema": "BackOffice",
+ "referencedTable": "VerifyOtpCodeResponse",
+ "referencedFields": [
+ "Token"
+ ],
+ "onDelete": "",
+ "onUpdate": "",
+ "isNotForReplication": false,
+ "isEnabled": true,
+ "comment": "",
+ "sourceCardinality": "NoneRelationship",
+ "targetCardinality": "NoneRelationship",
+ "oldName": ""
+ },
+ {
+ "objectType": "ForeignKey_MSSQL",
+ "name": "fk_OtpService_SendOtpRequest_1",
+ "fields": [
+ "SendOtp"
+ ],
+ "referencedSchema": "BackOffice",
+ "referencedTable": "SendOtpRequest",
+ "referencedFields": [
+ "Mobile"
+ ],
+ "onDelete": "",
+ "onUpdate": "",
+ "isNotForReplication": false,
+ "isEnabled": true,
+ "comment": "",
+ "sourceCardinality": "NoneRelationship",
+ "targetCardinality": "NoneRelationship",
+ "oldName": ""
+ }
+ ],
+ "uniques": [],
+ "checks": [],
+ "triggers": [],
+ "storage": {
+ "objectType": "Storage_MSSQL",
+ "name": "",
+ "oldName": "",
+ "storageType": "Default",
+ "filegroup": "",
+ "textImageFilegroup": "",
+ "filestreamFilegroup": "",
+ "partitionScheme": "",
+ "partitionColumn": "",
+ "filestreamPartitionScheme": "",
+ "dataCompressions": []
+ }
+ },
+ {
+ "objectType": "Table_MSSQL",
+ "name": "VerifyOtpCodeRequest",
+ "comment": "ورودی فعال سازی رمز پویا",
+ "owner": "",
+ "isChangeTracking": false,
+ "isTrackColumnsUpdated": false,
+ "oldName": "",
+ "isSystemTable": false,
+ "createTime": "",
+ "modifyTime": "",
+ "objectID": 9803,
+ "numberOfRows": 0,
+ "identityCurrent": 0,
+ "dataLength": 0,
+ "indexLength": 0,
+ "fields": [
+ {
+ "objectType": "TableField_MSSQL",
+ "name": "Mobile",
+ "type": "nvarchar",
+ "size": -2147483648,
+ "isNullable": "No",
+ "scale": -2147483648,
+ "comment": "شماره موبایل",
+ "computedExpression": "",
+ "defaultValue": "",
+ "defaultValueType": "Others",
+ "schema": "",
+ "userDefinedType": "",
+ "collate": "",
+ "isWithValues": false,
+ "isFilestream": false,
+ "isColumnSet": false,
+ "isPersisted": false,
+ "isSparse": false,
+ "isRowGUIDColumn": false,
+ "oldName": "Mobile",
+ "computedBaseType": "",
+ "isDefaultConstraint": false,
+ "defaultConstraint": "",
+ "isIdentity": false,
+ "isExistingField": false,
+ "identitySeed": 0,
+ "identityIncrement": 0,
+ "identityIsNotForReplication": false
+ },
+ {
+ "objectType": "TableField_MSSQL",
+ "name": "Code",
+ "type": "nvarchar",
+ "size": -2147483648,
+ "isNullable": "No",
+ "scale": -2147483648,
+ "comment": "رمز پویا",
+ "computedExpression": "",
+ "defaultValue": "",
+ "defaultValueType": "Others",
+ "schema": "",
+ "userDefinedType": "",
+ "collate": "",
+ "isWithValues": false,
+ "isFilestream": false,
+ "isColumnSet": false,
+ "isPersisted": false,
+ "isSparse": false,
+ "isRowGUIDColumn": false,
+ "oldName": "Code",
+ "computedBaseType": "",
+ "isDefaultConstraint": false,
+ "defaultConstraint": "",
+ "isIdentity": false,
+ "isExistingField": false,
+ "identitySeed": 0,
+ "identityIncrement": 0,
+ "identityIsNotForReplication": false
+ },
+ {
+ "objectType": "TableField_MSSQL",
+ "name": "ParentId",
+ "type": "bigint",
+ "size": -2147483648,
+ "isNullable": "Yes",
+ "scale": -2147483648,
+ "comment": "شناسه پدر",
+ "computedExpression": "",
+ "defaultValue": "",
+ "defaultValueType": "Others",
+ "schema": "",
+ "userDefinedType": "",
+ "collate": "",
+ "isWithValues": false,
+ "isFilestream": false,
+ "isColumnSet": false,
+ "isPersisted": false,
+ "isSparse": false,
+ "isRowGUIDColumn": false,
+ "oldName": "ParentId",
+ "computedBaseType": "",
+ "isDefaultConstraint": false,
+ "defaultConstraint": "",
+ "isIdentity": false,
+ "isExistingField": false,
+ "identitySeed": 0,
+ "identityIncrement": 0,
+ "identityIsNotForReplication": false
+ }
+ ],
+ "indexes": [],
+ "primaryKey": {
+ "objectType": "PrimaryKey_MSSQL",
+ "name": "_copy_10_copy_1_copy_1",
+ "fields": [],
+ "fillFactor": 0,
+ "oldName": "",
+ "isClustered": false,
+ "isPadded": false,
+ "noRecomputeStatistics": false,
+ "ignoreDuplicatedKeyValues": false,
+ "allowRowLocks": false,
+ "allowPageLocks": false,
+ "storage": {
+ "objectType": "Storage_MSSQL",
+ "name": "",
+ "oldName": "",
+ "storageType": "Default",
+ "filegroup": "",
+ "textImageFilegroup": "",
+ "filestreamFilegroup": "",
+ "partitionScheme": "",
+ "partitionColumn": "",
+ "filestreamPartitionScheme": "",
+ "dataCompressions": []
+ }
+ },
+ "foreignKeys": [],
+ "uniques": [],
+ "checks": [],
+ "triggers": [],
+ "storage": {
+ "objectType": "Storage_MSSQL",
+ "name": "",
+ "oldName": "",
+ "storageType": "Default",
+ "filegroup": "",
+ "textImageFilegroup": "",
+ "filestreamFilegroup": "",
+ "partitionScheme": "",
+ "partitionColumn": "",
+ "filestreamPartitionScheme": "",
+ "dataCompressions": []
+ }
+ },
+ {
+ "objectType": "Table_MSSQL",
+ "name": "SendOtpRequest",
+ "comment": "ورودی ارسال رمز پویا",
+ "owner": "",
+ "isChangeTracking": false,
+ "isTrackColumnsUpdated": false,
+ "oldName": "",
+ "isSystemTable": false,
+ "createTime": "",
+ "modifyTime": "",
+ "objectID": 9803,
+ "numberOfRows": 0,
+ "identityCurrent": 0,
+ "dataLength": 0,
+ "indexLength": 0,
+ "fields": [
+ {
+ "objectType": "TableField_MSSQL",
+ "name": "Mobile",
+ "type": "nvarchar",
+ "size": -2147483648,
+ "isNullable": "No",
+ "scale": -2147483648,
+ "comment": "شماره موبایل",
+ "computedExpression": "",
+ "defaultValue": "",
+ "defaultValueType": "Others",
+ "schema": "",
+ "userDefinedType": "",
+ "collate": "",
+ "isWithValues": false,
+ "isFilestream": false,
+ "isColumnSet": false,
+ "isPersisted": false,
+ "isSparse": false,
+ "isRowGUIDColumn": false,
+ "oldName": "Mobile",
+ "computedBaseType": "",
+ "isDefaultConstraint": false,
+ "defaultConstraint": "",
+ "isIdentity": false,
+ "isExistingField": false,
+ "identitySeed": 0,
+ "identityIncrement": 0,
+ "identityIsNotForReplication": false
+ }
+ ],
+ "indexes": [],
+ "primaryKey": {
+ "objectType": "PrimaryKey_MSSQL",
+ "name": "_copy_10_copy_1",
+ "fields": [],
+ "fillFactor": 0,
+ "oldName": "",
+ "isClustered": false,
+ "isPadded": false,
+ "noRecomputeStatistics": false,
+ "ignoreDuplicatedKeyValues": false,
+ "allowRowLocks": false,
+ "allowPageLocks": false,
+ "storage": {
+ "objectType": "Storage_MSSQL",
+ "name": "",
+ "oldName": "",
+ "storageType": "Default",
+ "filegroup": "",
+ "textImageFilegroup": "",
+ "filestreamFilegroup": "",
+ "partitionScheme": "",
+ "partitionColumn": "",
+ "filestreamPartitionScheme": "",
+ "dataCompressions": []
+ }
+ },
+ "foreignKeys": [],
+ "uniques": [],
+ "checks": [],
+ "triggers": [],
+ "storage": {
+ "objectType": "Storage_MSSQL",
+ "name": "",
+ "oldName": "",
+ "storageType": "Default",
+ "filegroup": "",
+ "textImageFilegroup": "",
+ "filestreamFilegroup": "",
+ "partitionScheme": "",
+ "partitionColumn": "",
+ "filestreamPartitionScheme": "",
+ "dataCompressions": []
+ }
+ },
+ {
+ "objectType": "Table_MSSQL",
+ "name": "VerifyOtpCodeResponse",
+ "comment": "خروجی فعال سازی رمز پویا",
+ "owner": "",
+ "isChangeTracking": false,
+ "isTrackColumnsUpdated": false,
+ "oldName": "",
+ "isSystemTable": false,
+ "createTime": "",
+ "modifyTime": "",
+ "objectID": 9803,
+ "numberOfRows": 0,
+ "identityCurrent": 0,
+ "dataLength": 0,
+ "indexLength": 0,
+ "fields": [
+ {
+ "objectType": "TableField_MSSQL",
+ "name": "Token",
+ "type": "nvarchar",
+ "size": -2147483648,
+ "isNullable": "No",
+ "scale": -2147483648,
+ "comment": "توکن",
+ "computedExpression": "",
+ "defaultValue": "",
+ "defaultValueType": "Others",
+ "schema": "",
+ "userDefinedType": "",
+ "collate": "",
+ "isWithValues": false,
+ "isFilestream": false,
+ "isColumnSet": false,
+ "isPersisted": false,
+ "isSparse": false,
+ "isRowGUIDColumn": false,
+ "oldName": "Token",
+ "computedBaseType": "",
+ "isDefaultConstraint": false,
+ "defaultConstraint": "",
+ "isIdentity": false,
+ "isExistingField": false,
+ "identitySeed": 0,
+ "identityIncrement": 0,
+ "identityIsNotForReplication": false
+ }
+ ],
+ "indexes": [],
+ "primaryKey": {
+ "objectType": "PrimaryKey_MSSQL",
+ "name": "_copy_10_copy_1_copy_1_copy_1",
+ "fields": [],
+ "fillFactor": 0,
+ "oldName": "",
+ "isClustered": false,
+ "isPadded": false,
+ "noRecomputeStatistics": false,
+ "ignoreDuplicatedKeyValues": false,
+ "allowRowLocks": false,
+ "allowPageLocks": false,
+ "storage": {
+ "objectType": "Storage_MSSQL",
+ "name": "",
+ "oldName": "",
+ "storageType": "Default",
+ "filegroup": "",
+ "textImageFilegroup": "",
+ "filestreamFilegroup": "",
+ "partitionScheme": "",
+ "partitionColumn": "",
+ "filestreamPartitionScheme": "",
+ "dataCompressions": []
+ }
+ },
+ "foreignKeys": [],
+ "uniques": [],
+ "checks": [],
+ "triggers": [],
+ "storage": {
+ "objectType": "Storage_MSSQL",
+ "name": "",
+ "oldName": "",
+ "storageType": "Default",
+ "filegroup": "",
+ "textImageFilegroup": "",
+ "filestreamFilegroup": "",
+ "partitionScheme": "",
+ "partitionColumn": "",
+ "filestreamPartitionScheme": "",
+ "dataCompressions": []
+ }
}
],
"views": []
@@ -13138,6 +13635,70 @@
"b": 147,
"a": 1
}
+ },
+ {
+ "type": "table",
+ "schemaName": "BackOffice",
+ "tableName": "OtpService",
+ "x": 5740,
+ "y": 20,
+ "width": 310,
+ "height": 120,
+ "isBold": false,
+ "titleColor": {
+ "r": 0,
+ "g": 0,
+ "b": 0,
+ "a": 1
+ }
+ },
+ {
+ "type": "table",
+ "schemaName": "BackOffice",
+ "tableName": "VerifyOtpCodeRequest",
+ "x": 6100,
+ "y": 20,
+ "width": 310,
+ "height": 130,
+ "isBold": false,
+ "titleColor": {
+ "r": 255,
+ "g": 214,
+ "b": 147,
+ "a": 1
+ }
+ },
+ {
+ "type": "table",
+ "schemaName": "BackOffice",
+ "tableName": "SendOtpRequest",
+ "x": 5380,
+ "y": 30,
+ "width": 310,
+ "height": 100,
+ "isBold": false,
+ "titleColor": {
+ "r": 255,
+ "g": 214,
+ "b": 147,
+ "a": 1
+ }
+ },
+ {
+ "type": "table",
+ "schemaName": "BackOffice",
+ "tableName": "VerifyOtpCodeResponse",
+ "x": 5740,
+ "y": 190,
+ "width": 310,
+ "height": 100,
+ "isBold": false,
+ "titleColor": {
+ "r": 200,
+ "g": 255,
+ "b": 160,
+ "a": 1
+ }
}
],
"layers": [],
@@ -15417,6 +15978,108 @@
"isFontItalic": false,
"isVisible": false
}
+ },
+ {
+ "name": "fk_OtpService_VerifyOtpCodeRequest_1",
+ "sourceTableName": "OtpService",
+ "sourceSchemaName": "BackOffice",
+ "lineWidth": 1,
+ "visible": true,
+ "vertices": [
+ {
+ "x": 6065,
+ "y": 80
+ },
+ {
+ "x": 6085,
+ "y": 80
+ }
+ ],
+ "label": {
+ "x": 6061,
+ "y": 52,
+ "width": 270,
+ "height": 30,
+ "fontName": "Arial Unicode MS",
+ "fontSize": 14,
+ "fontColor": {
+ "r": 204,
+ "g": 204,
+ "b": 204,
+ "a": 1
+ },
+ "isFontBold": false,
+ "isFontItalic": false,
+ "isVisible": false
+ }
+ },
+ {
+ "name": "fk_OtpService_VerifyOtpCodeResponse_1",
+ "sourceTableName": "OtpService",
+ "sourceSchemaName": "BackOffice",
+ "lineWidth": 1,
+ "visible": true,
+ "vertices": [
+ {
+ "x": 5880,
+ "y": 155
+ },
+ {
+ "x": 5880,
+ "y": 175
+ }
+ ],
+ "label": {
+ "x": 5882,
+ "y": 105,
+ "width": 280,
+ "height": 30,
+ "fontName": "Arial Unicode MS",
+ "fontSize": 14,
+ "fontColor": {
+ "r": 204,
+ "g": 204,
+ "b": 204,
+ "a": 1
+ },
+ "isFontBold": false,
+ "isFontItalic": false,
+ "isVisible": false
+ }
+ },
+ {
+ "name": "fk_OtpService_SendOtpRequest_1",
+ "sourceTableName": "OtpService",
+ "sourceSchemaName": "BackOffice",
+ "lineWidth": 1,
+ "visible": true,
+ "vertices": [
+ {
+ "x": 5725,
+ "y": 80
+ },
+ {
+ "x": 5705,
+ "y": 80
+ }
+ ],
+ "label": {
+ "x": 5735,
+ "y": 42,
+ "width": 231,
+ "height": 30,
+ "fontName": "Arial Unicode MS",
+ "fontSize": 14,
+ "fontColor": {
+ "r": 204,
+ "g": 204,
+ "b": 204,
+ "a": 1
+ },
+ "isFontBold": false,
+ "isFontItalic": false,
+ "isVisible": false
+ }
}
],
"viewRelations": []
diff --git a/src/BackOffice.BFF.Application/BackOffice.BFF.Application.csproj b/src/BackOffice.BFF.Application/BackOffice.BFF.Application.csproj
new file mode 100644
index 0000000..44fbade
--- /dev/null
+++ b/src/BackOffice.BFF.Application/BackOffice.BFF.Application.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net7.0
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BackOffice.BFF.Application/Common/Behaviours/LoggingBehaviour.cs b/src/BackOffice.BFF.Application/Common/Behaviours/LoggingBehaviour.cs
new file mode 100644
index 0000000..2bcc372
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Behaviours/LoggingBehaviour.cs
@@ -0,0 +1,24 @@
+using MediatR.Pipeline;
+using Microsoft.Extensions.Logging;
+
+namespace BackOffice.BFF.Application.Common.Behaviours;
+
+public class LoggingBehaviour : IRequestPreProcessor where TRequest : notnull
+{
+ private readonly ILogger _logger;
+ private readonly ICurrentUserService _currentUserService;
+
+ public LoggingBehaviour(ILogger logger, ICurrentUserService currentUserService)
+ {
+ _logger = logger;
+ _currentUserService = currentUserService;
+ }
+
+ public async Task Process(TRequest request, CancellationToken cancellationToken)
+ {
+ var requestName = typeof(TRequest).Name;
+ var userId = _currentUserService.UserId ?? string.Empty;
+ _logger.LogInformation("Request: {Name} {@UserId} {@Request}",
+ requestName, userId, request);
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Behaviours/PerformanceBehaviour.cs b/src/BackOffice.BFF.Application/Common/Behaviours/PerformanceBehaviour.cs
new file mode 100644
index 0000000..84a3dcf
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Behaviours/PerformanceBehaviour.cs
@@ -0,0 +1,43 @@
+using System.Diagnostics;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace BackOffice.BFF.Application.Common.Behaviours;
+
+public class PerformanceBehaviour : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly Stopwatch _timer;
+ private readonly ILogger _logger;
+ private readonly ICurrentUserService _currentUserService;
+
+ public PerformanceBehaviour(ILogger logger, ICurrentUserService currentUserService)
+ {
+ _timer = new Stopwatch();
+ _logger = logger;
+ _currentUserService = currentUserService;
+ }
+
+ public async Task Handle(TRequest request, RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ _timer.Start();
+
+ var response = await next();
+
+ _timer.Stop();
+
+ var elapsedMilliseconds = _timer.ElapsedMilliseconds;
+
+ if (elapsedMilliseconds > 500)
+ {
+ var requestName = typeof(TRequest).Name;
+ var userId = _currentUserService.UserId ?? string.Empty;
+
+ _logger.LogWarning("Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@Request}",
+ requestName, elapsedMilliseconds, userId, request);
+ }
+
+ return response;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Behaviours/UnhandledExceptionBehaviour.cs b/src/BackOffice.BFF.Application/Common/Behaviours/UnhandledExceptionBehaviour.cs
new file mode 100644
index 0000000..a17309c
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Behaviours/UnhandledExceptionBehaviour.cs
@@ -0,0 +1,31 @@
+using Microsoft.Extensions.Logging;
+
+namespace BackOffice.BFF.Application.Common.Behaviours;
+
+public class UnhandledExceptionBehaviour : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly ILogger _logger;
+
+ public UnhandledExceptionBehaviour(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public async Task Handle(TRequest request, RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ return await next();
+ }
+ catch (Exception ex)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ _logger.LogError(ex, "Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
+
+ throw;
+ }
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Behaviours/ValidationBehaviour.cs b/src/BackOffice.BFF.Application/Common/Behaviours/ValidationBehaviour.cs
new file mode 100644
index 0000000..60456a5
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Behaviours/ValidationBehaviour.cs
@@ -0,0 +1,37 @@
+using ValidationException = BackOffice.BFF.Application.Common.Exceptions.ValidationException;
+
+namespace BackOffice.BFF.Application.Common.Behaviours;
+
+public class ValidationBehaviour : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly IEnumerable> _validators;
+
+ public ValidationBehaviour(IEnumerable> validators)
+ {
+ _validators = validators;
+ }
+
+ public async Task Handle(TRequest request, RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ if (_validators.Any())
+ {
+ var context = new ValidationContext(request);
+
+ var validationResults = await Task.WhenAll(
+ _validators.Select(v =>
+ v.ValidateAsync(context, cancellationToken)));
+
+ var failures = validationResults
+ .Where(r => r.Errors.Any())
+ .SelectMany(r => r.Errors)
+ .ToList();
+
+ if (failures.Any())
+ throw new ValidationException(failures);
+ }
+
+ return await next();
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Exceptions/DuplicateException.cs b/src/BackOffice.BFF.Application/Common/Exceptions/DuplicateException.cs
new file mode 100644
index 0000000..506696f
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Exceptions/DuplicateException.cs
@@ -0,0 +1,29 @@
+namespace BackOffice.BFF.Application.Common.Exceptions;
+
+public class DuplicateException : Exception
+{
+ public DuplicateException()
+ : base()
+ {
+ }
+
+ public DuplicateException(string message)
+ : base(message)
+ {
+ }
+
+ public DuplicateException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ public DuplicateException(string name, object key)
+ : base($"Entity \"{name}\" ({key}) already exists.")
+ {
+ }
+
+ public DuplicateException(string name, string field, object? key)
+ : base($"Entity \"{name}\" field \"{field}\" ({key}) already exists.")
+ {
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Exceptions/ForbiddenAccessException.cs b/src/BackOffice.BFF.Application/Common/Exceptions/ForbiddenAccessException.cs
new file mode 100644
index 0000000..c06479f
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Exceptions/ForbiddenAccessException.cs
@@ -0,0 +1,8 @@
+namespace BackOffice.BFF.Application.Common.Exceptions;
+
+public class ForbiddenAccessException : Exception
+{
+ public ForbiddenAccessException() : base()
+ {
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Exceptions/NotFoundException.cs b/src/BackOffice.BFF.Application/Common/Exceptions/NotFoundException.cs
new file mode 100644
index 0000000..aa97cbd
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Exceptions/NotFoundException.cs
@@ -0,0 +1,24 @@
+namespace BackOffice.BFF.Application.Common.Exceptions;
+
+public class NotFoundException : Exception
+{
+ public NotFoundException()
+ : base()
+ {
+ }
+
+ public NotFoundException(string message)
+ : base(message)
+ {
+ }
+
+ public NotFoundException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ public NotFoundException(string name, object key)
+ : base($"Entity \"{name}\" ({key}) was not found.")
+ {
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Exceptions/ValidationException.cs b/src/BackOffice.BFF.Application/Common/Exceptions/ValidationException.cs
new file mode 100644
index 0000000..8b117cb
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Exceptions/ValidationException.cs
@@ -0,0 +1,22 @@
+using FluentValidation.Results;
+
+namespace BackOffice.BFF.Application.Common.Exceptions;
+
+public class ValidationException : Exception
+{
+ public ValidationException()
+ : base("One or more validation failures have occurred.")
+ {
+ Errors = new Dictionary();
+ }
+
+ public ValidationException(IEnumerable failures)
+ : this()
+ {
+ Errors = failures
+ .GroupBy(e => e.PropertyName, e => e.ErrorMessage)
+ .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
+ }
+
+ public IDictionary Errors { get; }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs b/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs
new file mode 100644
index 0000000..7ef9830
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs
@@ -0,0 +1,17 @@
+using CMSMicroservice.Protobuf.Protos.Package;
+using FMSMicroservice.Protobuf.Protos.FileInfo;
+
+namespace BackOffice.BFF.Application.Common.Interfaces;
+
+public interface IApplicationContractContext
+{
+ #region FM
+
+ FileInfoContract.FileInfoContractClient FileInfos { get; }
+
+ #endregion
+ #region CMS
+ PackageContract.PackageContractClient Packages { get; }
+
+ #endregion
+}
diff --git a/src/BackOffice.BFF.Application/Common/Interfaces/ICurrentUserService.cs b/src/BackOffice.BFF.Application/Common/Interfaces/ICurrentUserService.cs
new file mode 100644
index 0000000..f2e6270
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Interfaces/ICurrentUserService.cs
@@ -0,0 +1,6 @@
+namespace BackOffice.BFF.Application.Common.Interfaces;
+
+public interface ICurrentUserService
+{
+ string? UserId { get; }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Interfaces/ITokenProvider.cs b/src/BackOffice.BFF.Application/Common/Interfaces/ITokenProvider.cs
new file mode 100644
index 0000000..c1107c2
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Interfaces/ITokenProvider.cs
@@ -0,0 +1,6 @@
+namespace BackOffice.BFF.Application.Common.Interfaces;
+
+public interface ITokenProvider
+{
+ Task GetTokenAsync();
+}
diff --git a/src/BackOffice.BFF.Application/Common/Mappings/GeneralMapping.cs b/src/BackOffice.BFF.Application/Common/Mappings/GeneralMapping.cs
new file mode 100644
index 0000000..c7e33fe
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Mappings/GeneralMapping.cs
@@ -0,0 +1,64 @@
+using System.Globalization;
+namespace BackOffice.BFF.Application.Common.Mappings;
+
+public class GeneralMapping : IRegister
+{
+ void IRegister.Register(TypeAdapterConfig config)
+ {
+ config.NewConfig()
+ .MapWith(src => decimal.Parse(src));
+
+ config.NewConfig()
+ .MapWith(src => src.ToString("R", new CultureInfo("en-us")));
+
+ config.NewConfig()
+ .MapWith(src => src == null ? string.Empty : src.Value.ToString("R", new CultureInfo("en-us")));
+
+ config.NewConfig()
+ .MapWith(src => string.IsNullOrEmpty(src) ? null : decimal.Parse(src));
+
+ config.NewConfig()
+ .MapWith(src => src == Guid.Empty ? string.Empty : src.ToString());
+
+ config.NewConfig()
+ .MapWith(src => string.IsNullOrEmpty(src) ? Guid.Empty : Guid.Parse(src));
+
+ config.NewConfig()
+ .MapWith(src => string.IsNullOrEmpty(src) ? null : Guid.Parse(src));
+
+ config.NewConfig()
+ .MapWith(src => src.ToDateTime());
+
+ config.NewConfig()
+ .MapWith(src => src == null ? null : src.ToDateTime());
+
+ config.NewConfig()
+ .MapWith(src => Timestamp.FromDateTime(DateTime.SpecifyKind(src, DateTimeKind.Utc)));
+
+ config.NewConfig()
+ .MapWith(src => src.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(src.Value, DateTimeKind.Utc)) : null);
+
+ config.NewConfig()
+ .MapWith(src => src.ToTimeSpan());
+
+ config.NewConfig()
+ .MapWith(src => src == null ? null : src.ToTimeSpan());
+
+ config.NewConfig()
+ .MapWith(src => Duration.FromTimeSpan(src));
+
+ config.NewConfig()
+ .MapWith(src => src.HasValue ? Duration.FromTimeSpan(src.Value) : null);
+
+ config.Default
+ .UseDestinationValue(member => member.SetterModifier == AccessModifier.None &&
+ member.Type.IsGenericType &&
+ member.Type.GetGenericTypeDefinition() == typeof(Google.Protobuf.Collections.RepeatedField<>));
+
+ config.NewConfig()
+ .MapWith(src => src.ToByteArray());
+
+ config.NewConfig()
+ .MapWith(src => Google.Protobuf.ByteString.CopyFrom(src));
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Mappings/PackageProfile.cs b/src/BackOffice.BFF.Application/Common/Mappings/PackageProfile.cs
new file mode 100644
index 0000000..d33887d
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Mappings/PackageProfile.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.Common.Mappings;
+
+public class PackageProfile : IRegister
+{
+ void IRegister.Register(TypeAdapterConfig config)
+ {
+ //config.NewConfig()
+ // .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Mappings/RoleProfile.cs b/src/BackOffice.BFF.Application/Common/Mappings/RoleProfile.cs
new file mode 100644
index 0000000..5067769
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Mappings/RoleProfile.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.Common.Mappings;
+
+public class RoleProfile : IRegister
+{
+ void IRegister.Register(TypeAdapterConfig config)
+ {
+ //config.NewConfig()
+ // .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Mappings/UserAddressProfile.cs b/src/BackOffice.BFF.Application/Common/Mappings/UserAddressProfile.cs
new file mode 100644
index 0000000..c7b19c7
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Mappings/UserAddressProfile.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.Common.Mappings;
+
+public class UserAddressProfile : IRegister
+{
+ void IRegister.Register(TypeAdapterConfig config)
+ {
+ //config.NewConfig()
+ // .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Mappings/UserOrderProfile.cs b/src/BackOffice.BFF.Application/Common/Mappings/UserOrderProfile.cs
new file mode 100644
index 0000000..f32ec6f
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Mappings/UserOrderProfile.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.Common.Mappings;
+
+public class UserOrderProfile : IRegister
+{
+ void IRegister.Register(TypeAdapterConfig config)
+ {
+ //config.NewConfig()
+ // .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Mappings/UserProfile.cs b/src/BackOffice.BFF.Application/Common/Mappings/UserProfile.cs
new file mode 100644
index 0000000..2fc57f2
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Mappings/UserProfile.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.Common.Mappings;
+
+public class UserProfile : IRegister
+{
+ void IRegister.Register(TypeAdapterConfig config)
+ {
+ //config.NewConfig()
+ // .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Mappings/UserRoleProfile.cs b/src/BackOffice.BFF.Application/Common/Mappings/UserRoleProfile.cs
new file mode 100644
index 0000000..d8c531d
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Mappings/UserRoleProfile.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.Common.Mappings;
+
+public class UserRoleProfile : IRegister
+{
+ void IRegister.Register(TypeAdapterConfig config)
+ {
+ //config.NewConfig()
+ // .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Models/MetaData.cs b/src/BackOffice.BFF.Application/Common/Models/MetaData.cs
new file mode 100644
index 0000000..afae90a
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Models/MetaData.cs
@@ -0,0 +1,22 @@
+namespace BackOffice.BFF.Application.Common.Models;
+
+public class MetaData
+{
+ //صفحه جاری
+ public long CurrentPage { get; set; }
+
+ //تعداد کل صفحات
+ public long TotalPage { get; set; }
+
+ //تعداد در هر صفحه
+ public long PageSize { get; set; }
+
+ //تعداد کل آیتمها
+ public long TotalCount { get; set; }
+
+ //قبلی دارد؟
+ public bool HasPrevious { get; set; }
+
+ //بعدی دارد؟
+ public bool HasNext { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Models/PaginationState.cs b/src/BackOffice.BFF.Application/Common/Models/PaginationState.cs
new file mode 100644
index 0000000..95cc1b0
--- /dev/null
+++ b/src/BackOffice.BFF.Application/Common/Models/PaginationState.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.Common.Models;
+
+public class PaginationState
+{
+ //شماره صفحه
+ public int PageNumber { get; set; }
+
+ //اندازه صفحه
+ public int PageSize { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/ConfigureServices.cs b/src/BackOffice.BFF.Application/ConfigureServices.cs
new file mode 100644
index 0000000..937fde5
--- /dev/null
+++ b/src/BackOffice.BFF.Application/ConfigureServices.cs
@@ -0,0 +1,32 @@
+using BackOffice.BFF.Application.Common.Behaviours;
+using MapsterMapper;
+using System.Reflection;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+public static class ConfigureServices
+{
+ public static IServiceCollection AddApplicationServices(this IServiceCollection services)
+ {
+ services.AddMapping();
+ services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
+ services.AddMediatR(AppDomain.CurrentDomain.GetAssemblies());
+
+ services.AddTransient(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>));
+ services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
+ services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>));
+
+ return services;
+ }
+ private static IServiceCollection AddMapping(this IServiceCollection services)
+ {
+ var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
+ // scans the assembly and gets the IRegister, adding the registration to the TypeAdapterConfig
+ typeAdapterConfig.Scan(Assembly.GetExecutingAssembly());
+ // register the mapper as Singleton service for my application
+ var mapperConfig = new Mapper(typeAdapterConfig);
+ services.AddSingleton(mapperConfig);
+ return services;
+ }
+}
+
diff --git a/src/BackOffice.BFF.Application/GlobalUsings.cs b/src/BackOffice.BFF.Application/GlobalUsings.cs
new file mode 100644
index 0000000..fe7cf40
--- /dev/null
+++ b/src/BackOffice.BFF.Application/GlobalUsings.cs
@@ -0,0 +1,12 @@
+global using MediatR;
+global using FluentValidation;
+global using Mapster;
+global using System.Threading.Tasks;
+global using System.Threading;
+global using System.Collections.Generic;
+global using System;
+global using System.Linq;
+global using Google.Protobuf.WellKnownTypes;
+global using BackOffice.BFF.Application.Common.Interfaces;
+global using BackOffice.BFF.Application.Common.Models;
+global using BackOffice.BFF.Application.Common.Exceptions;
diff --git a/src/BackOffice.BFF.Application/PackageCQ/Commands/CreateNewPackage/CreateNewPackageCommand.cs b/src/BackOffice.BFF.Application/PackageCQ/Commands/CreateNewPackage/CreateNewPackageCommand.cs
new file mode 100644
index 0000000..91f7697
--- /dev/null
+++ b/src/BackOffice.BFF.Application/PackageCQ/Commands/CreateNewPackage/CreateNewPackageCommand.cs
@@ -0,0 +1,21 @@
+namespace BackOffice.BFF.Application.PackageCQ.Commands.CreateNewPackage;
+public record CreateNewPackageCommand : IRequest
+{
+ //عنوان
+ public string Title { get; init; }
+ //توضیحات
+ public string Description { get; init; }
+ //فایل تصویر
+ public BoostCardFileModel ImageFile { get; init; }
+ //قیمت
+ public long Price { get; init; }
+
+}public class BoostCardFileModel
+{
+ //فایل
+ public byte[] File { get; set; }
+ //نام
+ public string FileName { get; set; }
+ //نوع فایل
+ public string Mime { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/PackageCQ/Commands/CreateNewPackage/CreateNewPackageCommandHandler.cs b/src/BackOffice.BFF.Application/PackageCQ/Commands/CreateNewPackage/CreateNewPackageCommandHandler.cs
new file mode 100644
index 0000000..dc1105d
--- /dev/null
+++ b/src/BackOffice.BFF.Application/PackageCQ/Commands/CreateNewPackage/CreateNewPackageCommandHandler.cs
@@ -0,0 +1,37 @@
+using BackOffice.BFF.Application.UserCQ.Commands.CreateNewUser;
+using CMSMicroservice.Protobuf.Protos.Package;
+using Google.Protobuf;
+
+namespace BackOffice.BFF.Application.PackageCQ.Commands.CreateNewPackage;
+public class CreateNewPackageCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public CreateNewPackageCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(CreateNewPackageCommand request, CancellationToken cancellationToken)
+ {
+ var createNewPackageRequest = request.Adapt();
+ if (request.ImageFile != null && request.ImageFile.File != null && request.ImageFile.File.Length > 0)
+ {
+ var createNewFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
+ {
+ Directory = "Images/Package",
+ IsBase64 = false,
+ MIME = request.ImageFile.Mime,
+ FileName = request.ImageFile.FileName,
+ File = ByteString.CopyFrom(request.ImageFile.File)
+ }, cancellationToken: cancellationToken);
+
+ if (createNewFileInfo != null && !string.IsNullOrWhiteSpace(createNewFileInfo.File))
+ createNewPackageRequest.ImagePath = createNewFileInfo.File;
+ }
+
+ var response = await _context.Packages.CreateNewPackageAsync(createNewPackageRequest, cancellationToken: cancellationToken);
+
+ return response.Adapt();
+ }
+}
diff --git a/src/BackOffice.BFF.Application/PackageCQ/Commands/CreateNewPackage/CreateNewPackageCommandValidator.cs b/src/BackOffice.BFF.Application/PackageCQ/Commands/CreateNewPackage/CreateNewPackageCommandValidator.cs
new file mode 100644
index 0000000..b9c8ed3
--- /dev/null
+++ b/src/BackOffice.BFF.Application/PackageCQ/Commands/CreateNewPackage/CreateNewPackageCommandValidator.cs
@@ -0,0 +1,22 @@
+namespace BackOffice.BFF.Application.PackageCQ.Commands.CreateNewPackage;
+public class CreateNewPackageCommandValidator : AbstractValidator
+{
+ public CreateNewPackageCommandValidator()
+ {
+ RuleFor(model => model.Title)
+ .NotEmpty();
+ RuleFor(model => model.Description)
+ .NotEmpty();
+ RuleFor(model => model.ImageFile)
+ .NotNull();
+ RuleFor(model => model.Price)
+ .NotNull();
+ }
+ public Func