Refactor project files for improved readability and structure
- Cleaned up whitespace in Directory.Build.props and Directory.Packages.props for consistency. - Reformatted project file references in PnvPanel.Api.csproj for better clarity. - Enhanced code readability in various endpoint files by adjusting line breaks and indentation. - Standardized method signatures and improved formatting in ResultExtensions and multiple endpoint classes for better maintainability.
This commit is contained in:
@@ -3,11 +3,16 @@ using PnvPanel.Application.Common.Messaging;
|
||||
|
||||
namespace PnvPanel.Application.Common.Behaviors;
|
||||
|
||||
public sealed class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
|
||||
: IPipelineBehavior<TRequest, TResponse>
|
||||
public sealed class LoggingBehavior<TRequest, TResponse>(
|
||||
ILogger<LoggingBehavior<TRequest, TResponse>> logger
|
||||
) : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
{
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
logger.LogInformation("Handling {RequestName}", requestName);
|
||||
|
||||
@@ -10,12 +10,18 @@ namespace PnvPanel.Application.Common.Behaviors;
|
||||
/// вместо разбросанных if(!profile.IsActivated) по хендлерам. Применяется только к запросам с этим
|
||||
/// маркером (generic-ограничение), остальные проходят мимо.
|
||||
/// </summary>
|
||||
public sealed class RequireActivationBehavior<TRequest, TResponse>(ICurrentUser currentUser, IIdentityService identityService)
|
||||
: IPipelineBehavior<TRequest, TResponse>
|
||||
public sealed class RequireActivationBehavior<TRequest, TResponse>(
|
||||
ICurrentUser currentUser,
|
||||
IIdentityService identityService
|
||||
) : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequiresActivation
|
||||
where TResponse : Result
|
||||
{
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (currentUser.UserId is not { } userId)
|
||||
return ResultFailureFactory.Create<TResponse>(AuthErrors.Unauthorized);
|
||||
|
||||
@@ -5,7 +5,8 @@ namespace PnvPanel.Application.Common.Behaviors;
|
||||
/// <summary>Строит Result/Result<T> failure-ответ через reflection — общий хелпер для generic pipeline behaviors.</summary>
|
||||
internal static class ResultFailureFactory
|
||||
{
|
||||
public static TResponse Create<TResponse>(Error error) where TResponse : Result
|
||||
public static TResponse Create<TResponse>(Error error)
|
||||
where TResponse : Result
|
||||
{
|
||||
if (typeof(TResponse) == typeof(Result))
|
||||
return (TResponse)(object)Result.Failure(error);
|
||||
|
||||
@@ -12,7 +12,11 @@ public sealed class UnitOfWorkBehavior<TRequest, TResponse>(IAppDbContext dbCont
|
||||
: IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : ICommand<TResponse>
|
||||
{
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var response = await next();
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -4,12 +4,17 @@ using PnvPanel.Application.Common.Models;
|
||||
|
||||
namespace PnvPanel.Application.Common.Behaviors;
|
||||
|
||||
public sealed class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
|
||||
: IPipelineBehavior<TRequest, TResponse>
|
||||
public sealed class ValidationBehavior<TRequest, TResponse>(
|
||||
IEnumerable<IValidator<TRequest>> validators
|
||||
) : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
where TResponse : Result
|
||||
{
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (!validators.Any())
|
||||
return await next();
|
||||
@@ -25,7 +30,8 @@ public sealed class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidat
|
||||
|
||||
var error = Error.Validation(
|
||||
"Validation.Failed",
|
||||
string.Join("; ", failures.Select(f => f.ErrorMessage)));
|
||||
string.Join("; ", failures.Select(f => f.ErrorMessage))
|
||||
);
|
||||
|
||||
return ResultFailureFactory.Create<TResponse>(error);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,25 @@ namespace PnvPanel.Application.Common.Interfaces;
|
||||
public sealed record AuthenticatedUser(Guid Id, string UserName, string Role);
|
||||
|
||||
public sealed record CurrentUserProfile(
|
||||
Guid Id, string UserName, Guid RoleId, string Role, bool IsActivated, bool IsBlocked, int MaxConfigs, int MaxIpLimit,
|
||||
string SubscriptionToken);
|
||||
Guid Id,
|
||||
string UserName,
|
||||
Guid RoleId,
|
||||
string Role,
|
||||
bool IsActivated,
|
||||
bool IsBlocked,
|
||||
int MaxConfigs,
|
||||
int MaxIpLimit,
|
||||
string SubscriptionToken
|
||||
);
|
||||
|
||||
public sealed record UserSummaryDto(Guid Id, string UserName, string Role, bool IsActivated, bool IsBlocked, DateTimeOffset? ActivatedAt);
|
||||
public sealed record UserSummaryDto(
|
||||
Guid Id,
|
||||
string UserName,
|
||||
string Role,
|
||||
bool IsActivated,
|
||||
bool IsBlocked,
|
||||
DateTimeOffset? ActivatedAt
|
||||
);
|
||||
|
||||
public sealed record UserStatsDto(int Total, int Activated);
|
||||
|
||||
@@ -16,30 +31,57 @@ public sealed record TelegramLinkInfo(bool IsLinked, long? TelegramUserId, strin
|
||||
|
||||
public interface IIdentityService
|
||||
{
|
||||
Task<Result<Guid>> CreateUserAsync(string userName, string password, CancellationToken cancellationToken);
|
||||
Task<Result<Guid>> CreateUserAsync(
|
||||
string userName,
|
||||
string password,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<Result<AuthenticatedUser>> ValidateCredentialsAsync(string userName, string password, CancellationToken cancellationToken);
|
||||
Task<Result<AuthenticatedUser>> ValidateCredentialsAsync(
|
||||
string userName,
|
||||
string password,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<CurrentUserProfile?> GetProfileAsync(Guid userId, CancellationToken cancellationToken);
|
||||
|
||||
Task<Result> ChangePasswordAsync(Guid userId, string currentPassword, string newPassword, CancellationToken cancellationToken);
|
||||
Task<Result> ChangePasswordAsync(
|
||||
Guid userId,
|
||||
string currentPassword,
|
||||
string newPassword,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<Result> ChangeUserNameAsync(Guid userId, string newUserName, CancellationToken cancellationToken);
|
||||
Task<Result> ChangeUserNameAsync(
|
||||
Guid userId,
|
||||
string newUserName,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Помечает пользователя активированным. Изменение не коммитится немедленно (в отличие от
|
||||
/// CreateUserAsync/ChangePasswordAsync) — оно попадает в трекер того же DbContext и сохраняется
|
||||
/// вместе с изменением ActivationRequest одной транзакцией через UnitOfWorkBehavior.
|
||||
/// </summary>
|
||||
Task<Result> ActivateUserAsync(Guid userId, Guid activatedBy, CancellationToken cancellationToken);
|
||||
Task<Result> ActivateUserAsync(
|
||||
Guid userId,
|
||||
Guid activatedBy,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<IReadOnlyDictionary<Guid, string>> GetUserNamesAsync(IReadOnlyCollection<Guid> userIds, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyDictionary<Guid, string>> GetUserNamesAsync(
|
||||
IReadOnlyCollection<Guid> userIds,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Удаляет аккаунт (самоудаление). Конфиги должны быть отозваны заранее вызывающей стороной.</summary>
|
||||
Task<Result> DeleteUserAsync(Guid userId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Для агрегированной подписки /sub/{userToken} (все активные конфиги пользователя).</summary>
|
||||
Task<Guid?> FindUserIdBySubscriptionTokenAsync(string token, CancellationToken cancellationToken);
|
||||
Task<Guid?> FindUserIdBySubscriptionTokenAsync(
|
||||
string token,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Блокировка: вход запрещён (см. ValidateCredentialsAsync). Конфиги гасит вызывающая сторона.</summary>
|
||||
Task<Result> BlockUserAsync(Guid userId, CancellationToken cancellationToken);
|
||||
@@ -47,21 +89,43 @@ public interface IIdentityService
|
||||
Task<Result> UnblockUserAsync(Guid userId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Сброс пароля админом — для пользователей без привязанного Telegram (M7).</summary>
|
||||
Task<Result> ResetPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken);
|
||||
Task<Result> ResetPasswordAsync(
|
||||
Guid userId,
|
||||
string newPassword,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<PagedList<UserSummaryDto>> ListUsersAsync(int page, int pageSize, string? search, CancellationToken cancellationToken);
|
||||
Task<PagedList<UserSummaryDto>> ListUsersAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
string? search,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<UserStatsDto> GetUserStatsAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task<Result> LinkTelegramAsync(Guid userId, long telegramUserId, string? telegramUsername, CancellationToken cancellationToken);
|
||||
Task<Result> LinkTelegramAsync(
|
||||
Guid userId,
|
||||
long telegramUserId,
|
||||
string? telegramUsername,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<Result> UnlinkTelegramAsync(Guid userId, CancellationToken cancellationToken);
|
||||
|
||||
Task<Guid?> FindUserIdByTelegramUserIdAsync(long telegramUserId, CancellationToken cancellationToken);
|
||||
Task<Guid?> FindUserIdByTelegramUserIdAsync(
|
||||
long telegramUserId,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<TelegramLinkInfo> GetTelegramLinkInfoAsync(Guid userId, CancellationToken cancellationToken);
|
||||
Task<TelegramLinkInfo> GetTelegramLinkInfoAsync(
|
||||
Guid userId,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Telegram ID активных (активированных, не заблокированных) пользователей с привязкой —
|
||||
/// для рассылки уведомлений вроде публикации новости.</summary>
|
||||
Task<IReadOnlyCollection<long>> GetActivatedLinkedTelegramUserIdsAsync(CancellationToken cancellationToken);
|
||||
Task<IReadOnlyCollection<long>> GetActivatedLinkedTelegramUserIdsAsync(
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,21 +12,53 @@ namespace PnvPanel.Application.Common.Interfaces;
|
||||
public interface IRealtimeNotifier
|
||||
{
|
||||
Task NotifyConfigTrafficUpdatedAsync(
|
||||
Guid userId, Guid configId, long usedUpBytes, long usedDownBytes, CancellationToken cancellationToken);
|
||||
Guid userId,
|
||||
Guid configId,
|
||||
long usedUpBytes,
|
||||
long usedDownBytes,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task NotifyConfigStatusChangedAsync(Guid userId, Guid configId, ConfigStatus status, CancellationToken cancellationToken);
|
||||
Task NotifyConfigStatusChangedAsync(
|
||||
Guid userId,
|
||||
Guid configId,
|
||||
ConfigStatus status,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task NotifyNodeStatusChangedAsync(Guid nodeId, NodeStatus status, DateTimeOffset? lastSyncAt, CancellationToken cancellationToken);
|
||||
Task NotifyNodeStatusChangedAsync(
|
||||
Guid nodeId,
|
||||
NodeStatus status,
|
||||
DateTimeOffset? lastSyncAt,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task NotifyActivationRequestedAsync(
|
||||
Guid requestId, Guid userId, string userName, string? comment, DateTimeOffset createdAt, CancellationToken cancellationToken);
|
||||
Guid requestId,
|
||||
Guid userId,
|
||||
string userName,
|
||||
string? comment,
|
||||
DateTimeOffset createdAt,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task NotifyUserActivatedAsync(Guid userId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Единственное широковещательное событие (всем подключенным клиентам), а не по группе.</summary>
|
||||
Task NotifyNewsPublishedAsync(Guid postId, string title, DateTimeOffset createdAt, CancellationToken cancellationToken);
|
||||
Task NotifyNewsPublishedAsync(
|
||||
Guid postId,
|
||||
string title,
|
||||
DateTimeOffset createdAt,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task NotifyTicketCreatedAsync(Guid ticketId, Guid userId, string userName, TicketType type, CancellationToken cancellationToken);
|
||||
Task NotifyTicketCreatedAsync(
|
||||
Guid ticketId,
|
||||
Guid userId,
|
||||
string userName,
|
||||
TicketType type,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Новый комментарий или смена статуса — пушится автору тикета (не всем участникам треда).</summary>
|
||||
Task NotifyTicketUpdatedAsync(Guid ticketId, Guid userId, CancellationToken cancellationToken);
|
||||
|
||||
@@ -10,7 +10,10 @@ public interface IRefreshTokenService
|
||||
{
|
||||
Task<IssuedRefreshToken> IssueAsync(Guid userId, CancellationToken cancellationToken);
|
||||
|
||||
Task<Result<RotatedRefreshToken>> RotateAsync(string rawToken, CancellationToken cancellationToken);
|
||||
Task<Result<RotatedRefreshToken>> RotateAsync(
|
||||
string rawToken,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task RevokeAsync(string rawToken, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,19 @@ public sealed record RoleDto(Guid Id, string Name, int MaxConfigs, int MaxIpLimi
|
||||
|
||||
public interface IRoleService
|
||||
{
|
||||
Task<Result<RoleDto>> CreateRoleAsync(string name, int maxConfigs, int maxIpLimit, CancellationToken cancellationToken);
|
||||
Task<Result<RoleDto>> CreateRoleAsync(
|
||||
string name,
|
||||
int maxConfigs,
|
||||
int maxIpLimit,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<Result<RoleDto>> UpdateRoleAsync(Guid roleId, int maxConfigs, int maxIpLimit, CancellationToken cancellationToken);
|
||||
Task<Result<RoleDto>> UpdateRoleAsync(
|
||||
Guid roleId,
|
||||
int maxConfigs,
|
||||
int maxIpLimit,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<Result> DeleteRoleAsync(Guid roleId, CancellationToken cancellationToken);
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ namespace PnvPanel.Application.Common.Interfaces;
|
||||
public interface ITelegramNotifier
|
||||
{
|
||||
Task NotifyAdminsActivationRequestedAsync(
|
||||
Guid requestId, string userName, string? comment, CancellationToken cancellationToken);
|
||||
Guid requestId,
|
||||
string userName,
|
||||
string? comment,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Личное сообщение пользователю, если у него привязан Telegram (иначе no-op).
|
||||
/// Если задан PublicSiteUrl — добавляет кнопку-ссылку на сайт.</summary>
|
||||
@@ -18,11 +22,21 @@ public interface ITelegramNotifier
|
||||
|
||||
/// <summary>Баг-репорт/предложение — только кнопка-ссылка на сайт (переписка и картинки — там),
|
||||
/// без инлайн-действий.</summary>
|
||||
Task NotifyAdminsBugReportCreatedAsync(Guid ticketId, string userName, string message, CancellationToken cancellationToken);
|
||||
Task NotifyAdminsBugReportCreatedAsync(
|
||||
Guid ticketId,
|
||||
string userName,
|
||||
string message,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Заявка на роль — инлайн-кнопки «Одобрить/Отклонить», решается полностью в Telegram.</summary>
|
||||
Task NotifyAdminsRoleRequestCreatedAsync(
|
||||
Guid ticketId, string userName, string roleDescription, string justification, CancellationToken cancellationToken);
|
||||
Guid ticketId,
|
||||
string userName,
|
||||
string roleDescription,
|
||||
string justification,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Рассылка о публикации новости всем активированным пользователям с привязанным Telegram
|
||||
/// (кнопка-ссылка на сайт, где новость показана целиком).</summary>
|
||||
@@ -30,5 +44,10 @@ public interface ITelegramNotifier
|
||||
|
||||
/// <summary>Пользователь переоткрыл решённый тикет — только кнопка-ссылка на сайт, без
|
||||
/// инлайн-действий (аналогично баг-репортам).</summary>
|
||||
Task NotifyAdminsTicketReopenedAsync(Guid ticketId, string userName, TicketType type, CancellationToken cancellationToken);
|
||||
Task NotifyAdminsTicketReopenedAsync(
|
||||
Guid ticketId,
|
||||
string userName,
|
||||
TicketType type,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@ using PnvPanel.Domain.Nodes;
|
||||
|
||||
namespace PnvPanel.Application.Common.Interfaces;
|
||||
|
||||
public sealed record RemoteInboundInfo(string RemoteInboundId, VpnProtocol Protocol, string Remark, int Port);
|
||||
public sealed record RemoteInboundInfo(
|
||||
string RemoteInboundId,
|
||||
VpnProtocol Protocol,
|
||||
string Remark,
|
||||
int Port
|
||||
);
|
||||
|
||||
public sealed record NodeProbeResult(bool IsReachable, string? ErrorMessage);
|
||||
|
||||
@@ -20,7 +25,10 @@ public interface IXuiPanelGateway
|
||||
|
||||
Task<NodeProbeResult> ProbeAsync(Node node, CancellationToken cancellationToken);
|
||||
|
||||
Task<Result<IReadOnlyList<RemoteInboundInfo>>> ListInboundsAsync(Node node, CancellationToken cancellationToken);
|
||||
Task<Result<IReadOnlyList<RemoteInboundInfo>>> ListInboundsAsync(
|
||||
Node node,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
void InvalidateClient(Guid nodeId);
|
||||
|
||||
@@ -30,25 +38,49 @@ public interface IXuiPanelGateway
|
||||
/// -1 (RoleQuota.Unlimited) означает без лимита — гейтвей сам переводит его в нативное значение 3x-ui.
|
||||
/// </summary>
|
||||
Task<Result<string>> AddClientAsync(
|
||||
Node node, string inboundRemoteId, VpnProtocol protocol, string clientEmail, string clientName, int limitIp,
|
||||
CancellationToken cancellationToken);
|
||||
Node node,
|
||||
string inboundRemoteId,
|
||||
VpnProtocol protocol,
|
||||
string clientEmail,
|
||||
string clientName,
|
||||
int limitIp,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<Result> RemoveClientAsync(
|
||||
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol,
|
||||
CancellationToken cancellationToken);
|
||||
Node node,
|
||||
string inboundRemoteId,
|
||||
string clientExternalId,
|
||||
VpnProtocol protocol,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<Result> UpdateClientAsync(
|
||||
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol,
|
||||
string name, bool enable, CancellationToken cancellationToken);
|
||||
Node node,
|
||||
string inboundRemoteId,
|
||||
string clientExternalId,
|
||||
VpnProtocol protocol,
|
||||
string name,
|
||||
bool enable,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task<Result<string>> BuildConnectionStringAsync(
|
||||
Node node, Inbound inbound, string clientExternalId, string clientName, string publicHost,
|
||||
CancellationToken cancellationToken);
|
||||
Node node,
|
||||
Inbound inbound,
|
||||
string clientExternalId,
|
||||
string clientName,
|
||||
string publicHost,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Трафик по клиентам инбаунда, ключ — ClientEmail. ThreeXui.Net не даёт типизированного метода
|
||||
/// для этого — извлекается из сырого clientStats[] в RawInboundJson (стандартное поле 3x-ui API).
|
||||
/// </summary>
|
||||
Task<Result<IReadOnlyDictionary<string, ClientTrafficInfo>>> GetClientTrafficAsync(
|
||||
Node node, string inboundRemoteId, CancellationToken cancellationToken);
|
||||
Node node,
|
||||
string inboundRemoteId,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
namespace PnvPanel.Application.Common.Messaging;
|
||||
|
||||
public interface ICommandHandler<in TCommand, TResponse> where TCommand : ICommand<TResponse>
|
||||
public interface ICommandHandler<in TCommand, TResponse>
|
||||
where TCommand : ICommand<TResponse>
|
||||
{
|
||||
Task<TResponse> Handle(TCommand command, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -4,5 +4,9 @@ public delegate Task<TResponse> RequestHandlerDelegate<TResponse>();
|
||||
|
||||
public interface IPipelineBehavior<TRequest, TResponse>
|
||||
{
|
||||
Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken);
|
||||
Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
namespace PnvPanel.Application.Common.Messaging;
|
||||
|
||||
public interface IQueryHandler<in TQuery, TResponse> where TQuery : IQuery<TResponse>
|
||||
public interface IQueryHandler<in TQuery, TResponse>
|
||||
where TQuery : IQuery<TResponse>
|
||||
{
|
||||
Task<TResponse> Handle(TQuery query, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ namespace PnvPanel.Application.Common.Messaging;
|
||||
/// <summary>Собственный тонкий CQRS-диспетчер (без MediatR).</summary>
|
||||
public interface ISender
|
||||
{
|
||||
Task<TResponse> Send<TResponse>(ICommand<TResponse> command, CancellationToken cancellationToken = default);
|
||||
Task<TResponse> Send<TResponse>(IQuery<TResponse> query, CancellationToken cancellationToken = default);
|
||||
Task<TResponse> Send<TResponse>(
|
||||
ICommand<TResponse> command,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
Task<TResponse> Send<TResponse>(
|
||||
IQuery<TResponse> query,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,22 +4,34 @@ namespace PnvPanel.Application.Common.Messaging;
|
||||
|
||||
internal sealed class Sender(IServiceProvider serviceProvider) : ISender
|
||||
{
|
||||
public Task<TResponse> Send<TResponse>(ICommand<TResponse> command, CancellationToken cancellationToken = default)
|
||||
=> Dispatch<TResponse>(command, typeof(ICommandHandler<,>), cancellationToken);
|
||||
public Task<TResponse> Send<TResponse>(
|
||||
ICommand<TResponse> command,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Dispatch<TResponse>(command, typeof(ICommandHandler<,>), cancellationToken);
|
||||
|
||||
public Task<TResponse> Send<TResponse>(IQuery<TResponse> query, CancellationToken cancellationToken = default)
|
||||
=> Dispatch<TResponse>(query, typeof(IQueryHandler<,>), cancellationToken);
|
||||
public Task<TResponse> Send<TResponse>(
|
||||
IQuery<TResponse> query,
|
||||
CancellationToken cancellationToken = default
|
||||
) => Dispatch<TResponse>(query, typeof(IQueryHandler<,>), cancellationToken);
|
||||
|
||||
private Task<TResponse> Dispatch<TResponse>(object request, Type handlerOpenType, CancellationToken cancellationToken)
|
||||
private Task<TResponse> Dispatch<TResponse>(
|
||||
object request,
|
||||
Type handlerOpenType,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var requestType = request.GetType();
|
||||
var handlerType = handlerOpenType.MakeGenericType(requestType, typeof(TResponse));
|
||||
var behaviorType = typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse));
|
||||
var behaviorType = typeof(IPipelineBehavior<,>).MakeGenericType(
|
||||
requestType,
|
||||
typeof(TResponse)
|
||||
);
|
||||
|
||||
dynamic handler = serviceProvider.GetRequiredService(handlerType);
|
||||
var behaviors = ((IEnumerable<object>)serviceProvider.GetServices(behaviorType)).Reverse();
|
||||
|
||||
RequestHandlerDelegate<TResponse> pipeline = () => handler.Handle((dynamic)request, cancellationToken);
|
||||
RequestHandlerDelegate<TResponse> pipeline = () =>
|
||||
handler.Handle((dynamic)request, cancellationToken);
|
||||
|
||||
foreach (dynamic behavior in behaviors)
|
||||
{
|
||||
|
||||
@@ -14,10 +14,21 @@ public sealed record Error(string Code, string Message, ErrorType Type = ErrorTy
|
||||
{
|
||||
public static readonly Error None = new(string.Empty, string.Empty);
|
||||
|
||||
public static Error Validation(string code, string message) => new(code, message, ErrorType.Validation);
|
||||
public static Error NotFound(string code, string message) => new(code, message, ErrorType.NotFound);
|
||||
public static Error Conflict(string code, string message) => new(code, message, ErrorType.Conflict);
|
||||
public static Error Unauthorized(string code, string message) => new(code, message, ErrorType.Unauthorized);
|
||||
public static Error Forbidden(string code, string message) => new(code, message, ErrorType.Forbidden);
|
||||
public static Error Failure(string code, string message) => new(code, message, ErrorType.Failure);
|
||||
public static Error Validation(string code, string message) =>
|
||||
new(code, message, ErrorType.Validation);
|
||||
|
||||
public static Error NotFound(string code, string message) =>
|
||||
new(code, message, ErrorType.NotFound);
|
||||
|
||||
public static Error Conflict(string code, string message) =>
|
||||
new(code, message, ErrorType.Conflict);
|
||||
|
||||
public static Error Unauthorized(string code, string message) =>
|
||||
new(code, message, ErrorType.Unauthorized);
|
||||
|
||||
public static Error Forbidden(string code, string message) =>
|
||||
new(code, message, ErrorType.Forbidden);
|
||||
|
||||
public static Error Failure(string code, string message) =>
|
||||
new(code, message, ErrorType.Failure);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,17 @@ namespace PnvPanel.Application.Common.Models;
|
||||
public static class PagedListExtensions
|
||||
{
|
||||
public static async Task<PagedList<T>> ToPagedListAsync<T>(
|
||||
this IQueryable<T> query, int page, int pageSize, CancellationToken cancellationToken)
|
||||
this IQueryable<T> query,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);
|
||||
var items = await query
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
return new PagedList<T>(items, total, page, pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@ public class Result
|
||||
}
|
||||
|
||||
public static Result Success() => new(true, Error.None);
|
||||
|
||||
public static Result Failure(Error error) => new(false, error);
|
||||
|
||||
public static Result<T> Success<T>(T value) => new(value, true, Error.None);
|
||||
|
||||
public static Result<T> Failure<T>(Error error) => new(default, false, error);
|
||||
}
|
||||
|
||||
@@ -27,11 +29,15 @@ public class Result<T> : Result
|
||||
{
|
||||
private readonly T? _value;
|
||||
|
||||
internal Result(T? value, bool isSuccess, Error error) : base(isSuccess, error) => _value = value;
|
||||
internal Result(T? value, bool isSuccess, Error error)
|
||||
: base(isSuccess, error) => _value = value;
|
||||
|
||||
public T Value => IsSuccess
|
||||
? _value!
|
||||
: throw new InvalidOperationException("Нельзя получить значение неуспешного результата.");
|
||||
public T Value =>
|
||||
IsSuccess
|
||||
? _value!
|
||||
: throw new InvalidOperationException(
|
||||
"Нельзя получить значение неуспешного результата."
|
||||
);
|
||||
|
||||
public static implicit operator Result<T>(T value) => Success(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user