feat: Add NetworkMembershipCQ - Phase 4 Application Layer
- Implemented 3 Commands with handlers and validators:
* JoinNetworkCommand: Add user to binary network tree
- Validates parent exists and is in network
- Validates leg position is empty
- Records history with NetworkMembershipAction.Join
* MoveInNetworkCommand: Move user to different position
- Validates new parent and leg availability
- Prevents circular dependencies (IsDescendant check)
- Records old/new parent and leg in history
* RemoveFromNetworkCommand: Remove user from network
- Validates no children exist (must move/remove first)
- Soft delete (sets NetworkParentId to null)
- Idempotent design
- Implemented 3 Queries with handlers, validators, and DTOs:
* GetNetworkTreeQuery: Binary tree visualization
- Recursive tree building with MaxDepth limit (1-10)
- Returns nested structure with Left/Right children
* GetUserNetworkPositionQuery: User position details
- Parent info, leg position, children counts
- Left/Right child counts for balance view
* GetNetworkMembershipHistoryQuery: Complete audit trail
- Filter by UserId, pagination support
- Shows Join/Move/Remove actions with full details
- All operations include complete history tracking
- Binary tree validation (parent-child relationships)
- Circular dependency prevention in MoveInNetwork
- 21 new files, ~850 lines of code
- Build successful with 0 errors
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.JoinNetwork;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای افزودن کاربر به شبکه دوتایی (Binary Network)
|
||||||
|
/// </summary>
|
||||||
|
public record JoinNetworkCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر که میخواهد به شبکه بپیوندد
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه والد در شبکه (Sponsor/Parent)
|
||||||
|
/// </summary>
|
||||||
|
public long ParentId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// موقعیت در شبکه (Left یا Right)
|
||||||
|
/// </summary>
|
||||||
|
public NetworkLeg LegPosition { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دلیل/یادداشت (اختیاری)
|
||||||
|
/// </summary>
|
||||||
|
public string? Reason { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.JoinNetwork;
|
||||||
|
|
||||||
|
public class JoinNetworkCommandHandler : IRequestHandler<JoinNetworkCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public JoinNetworkCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(JoinNetworkCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// بررسی وجود کاربر
|
||||||
|
var user = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException(nameof(User), request.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی اینکه کاربر قبلاً در شبکه نباشد
|
||||||
|
if (user.NetworkParentId.HasValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"کاربر با شناسه {request.UserId} قبلاً در شبکه عضو شده است");
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی وجود والد
|
||||||
|
var parent = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.ParentId, cancellationToken);
|
||||||
|
|
||||||
|
if (parent == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException(nameof(User), $"Parent with Id {request.ParentId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی والد خودش در شبکه باشد (یا Root باشد)
|
||||||
|
if (!parent.NetworkParentId.HasValue && parent.Id != 1) // فرض: UserId=1 همیشه Root
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"والد با شناسه {request.ParentId} خودش در شبکه عضو نیست");
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی خالی بودن Leg موردنظر
|
||||||
|
var legOccupied = await _context.Users
|
||||||
|
.AnyAsync(x => x.NetworkParentId == request.ParentId && x.LegPosition == request.LegPosition,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (legOccupied)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"موقعیت {request.LegPosition} زیر والد {request.ParentId} قبلاً پر شده است");
|
||||||
|
}
|
||||||
|
|
||||||
|
// افزودن به شبکه
|
||||||
|
user.NetworkParentId = request.ParentId;
|
||||||
|
user.LegPosition = request.LegPosition;
|
||||||
|
|
||||||
|
_context.Users.Update(user);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// ثبت تاریخچه
|
||||||
|
var history = new NetworkMembershipHistory
|
||||||
|
{
|
||||||
|
UserId = request.UserId,
|
||||||
|
OldParentId = null,
|
||||||
|
NewParentId = request.ParentId,
|
||||||
|
OldLegPosition = null,
|
||||||
|
NewLegPosition = request.LegPosition,
|
||||||
|
Action = NetworkMembershipAction.Join,
|
||||||
|
Reason = request.Reason ?? "عضویت در شبکه",
|
||||||
|
PerformedBy = "System" // TODO: باید از Current User گرفته شود
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.NetworkMembershipHistories.AddAsync(history, cancellationToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.JoinNetwork;
|
||||||
|
|
||||||
|
public class JoinNetworkCommandValidator : AbstractValidator<JoinNetworkCommand>
|
||||||
|
{
|
||||||
|
public JoinNetworkCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.UserId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("شناسه کاربر معتبر نیست");
|
||||||
|
|
||||||
|
RuleFor(x => x.ParentId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("شناسه والد معتبر نیست");
|
||||||
|
|
||||||
|
RuleFor(x => x.LegPosition)
|
||||||
|
.IsInEnum()
|
||||||
|
.WithMessage("موقعیت شبکه باید Left یا Right باشد");
|
||||||
|
|
||||||
|
RuleFor(x => x.Reason)
|
||||||
|
.MaximumLength(500)
|
||||||
|
.WithMessage("طول دلیل نباید بیشتر از 500 کاراکتر باشد")
|
||||||
|
.When(x => !string.IsNullOrEmpty(x.Reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
{
|
||||||
|
var result = await ValidateAsync(
|
||||||
|
ValidationContext<JoinNetworkCommand>.CreateWithOptions(
|
||||||
|
(JoinNetworkCommand)model,
|
||||||
|
x => x.IncludeProperties(propertyName)));
|
||||||
|
|
||||||
|
if (result.IsValid)
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.MoveInNetwork;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای جابجایی کاربر در شبکه دوتایی
|
||||||
|
/// </summary>
|
||||||
|
public record MoveInNetworkCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر که میخواهد جابجا شود
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه والد جدید در شبکه
|
||||||
|
/// </summary>
|
||||||
|
public long NewParentId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// موقعیت جدید در شبکه (Left یا Right)
|
||||||
|
/// </summary>
|
||||||
|
public NetworkLeg NewLegPosition { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دلیل جابجایی
|
||||||
|
/// </summary>
|
||||||
|
public string? Reason { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.MoveInNetwork;
|
||||||
|
|
||||||
|
public class MoveInNetworkCommandHandler : IRequestHandler<MoveInNetworkCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public MoveInNetworkCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(MoveInNetworkCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// بررسی وجود کاربر
|
||||||
|
var user = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException(nameof(User), request.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی اینکه کاربر در شبکه باشد
|
||||||
|
if (!user.NetworkParentId.HasValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"کاربر با شناسه {request.UserId} در شبکه عضو نیست");
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی وجود والد جدید
|
||||||
|
var newParent = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.NewParentId, cancellationToken);
|
||||||
|
|
||||||
|
if (newParent == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException(nameof(User), $"New Parent with Id {request.NewParentId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی اینکه والد جدید خود کاربر یا فرزندان او نباشد (جلوگیری از Loop)
|
||||||
|
if (await IsDescendant(request.NewParentId, request.UserId, cancellationToken))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("نمیتوان کاربر را زیر فرزندان خودش جابجا کرد (ایجاد حلقه)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی خالی بودن Leg جدید
|
||||||
|
var legOccupied = await _context.Users
|
||||||
|
.AnyAsync(x => x.NetworkParentId == request.NewParentId &&
|
||||||
|
x.LegPosition == request.NewLegPosition &&
|
||||||
|
x.Id != request.UserId,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (legOccupied)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"موقعیت {request.NewLegPosition} زیر والد {request.NewParentId} قبلاً پر شده است");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ذخیره مقادیر قبلی برای History
|
||||||
|
var oldParentId = user.NetworkParentId;
|
||||||
|
var oldLegPosition = user.LegPosition;
|
||||||
|
|
||||||
|
// جابجایی
|
||||||
|
user.NetworkParentId = request.NewParentId;
|
||||||
|
user.LegPosition = request.NewLegPosition;
|
||||||
|
|
||||||
|
_context.Users.Update(user);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// ثبت تاریخچه
|
||||||
|
var history = new NetworkMembershipHistory
|
||||||
|
{
|
||||||
|
UserId = request.UserId,
|
||||||
|
OldParentId = oldParentId,
|
||||||
|
NewParentId = request.NewParentId,
|
||||||
|
OldLegPosition = oldLegPosition,
|
||||||
|
NewLegPosition = request.NewLegPosition,
|
||||||
|
Action = NetworkMembershipAction.Move,
|
||||||
|
Reason = request.Reason ?? "جابجایی در شبکه",
|
||||||
|
PerformedBy = "System" // TODO: باید از Current User گرفته شود
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.NetworkMembershipHistories.AddAsync(history, cancellationToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بررسی میکند که آیا potentialDescendant فرزند (مستقیم یا غیرمستقیم) userId هست یا خیر
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> IsDescendant(long potentialDescendantId, long userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var current = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == potentialDescendantId, cancellationToken);
|
||||||
|
|
||||||
|
while (current?.NetworkParentId != null)
|
||||||
|
{
|
||||||
|
if (current.NetworkParentId == userId)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == current.NetworkParentId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.MoveInNetwork;
|
||||||
|
|
||||||
|
public class MoveInNetworkCommandValidator : AbstractValidator<MoveInNetworkCommand>
|
||||||
|
{
|
||||||
|
public MoveInNetworkCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.UserId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("شناسه کاربر معتبر نیست");
|
||||||
|
|
||||||
|
RuleFor(x => x.NewParentId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("شناسه والد جدید معتبر نیست");
|
||||||
|
|
||||||
|
RuleFor(x => x.NewLegPosition)
|
||||||
|
.IsInEnum()
|
||||||
|
.WithMessage("موقعیت شبکه باید Left یا Right باشد");
|
||||||
|
|
||||||
|
RuleFor(x => x.Reason)
|
||||||
|
.MaximumLength(500)
|
||||||
|
.WithMessage("طول دلیل نباید بیشتر از 500 کاراکتر باشد")
|
||||||
|
.When(x => !string.IsNullOrEmpty(x.Reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
{
|
||||||
|
var result = await ValidateAsync(
|
||||||
|
ValidationContext<MoveInNetworkCommand>.CreateWithOptions(
|
||||||
|
(MoveInNetworkCommand)model,
|
||||||
|
x => x.IncludeProperties(propertyName)));
|
||||||
|
|
||||||
|
if (result.IsValid)
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.RemoveFromNetwork;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای حذف کاربر از شبکه دوتایی
|
||||||
|
/// </summary>
|
||||||
|
public record RemoveFromNetworkCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر که باید از شبکه حذف شود
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دلیل حذف
|
||||||
|
/// </summary>
|
||||||
|
public string? Reason { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.RemoveFromNetwork;
|
||||||
|
|
||||||
|
public class RemoveFromNetworkCommandHandler : IRequestHandler<RemoveFromNetworkCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public RemoveFromNetworkCommandHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(RemoveFromNetworkCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// بررسی وجود کاربر
|
||||||
|
var user = await _context.Users
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException(nameof(User), request.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی اینکه کاربر در شبکه باشد
|
||||||
|
if (!user.NetworkParentId.HasValue)
|
||||||
|
{
|
||||||
|
// اگر قبلاً حذف شده، هیچ کاری نکن (Idempotent)
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی وجود فرزندان
|
||||||
|
var hasChildren = await _context.Users
|
||||||
|
.AnyAsync(x => x.NetworkParentId == request.UserId, cancellationToken);
|
||||||
|
|
||||||
|
if (hasChildren)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"کاربر با شناسه {request.UserId} دارای فرزند در شبکه است. ابتدا باید فرزندان جابجا یا حذف شوند");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ذخیره مقادیر قبلی برای History
|
||||||
|
var oldParentId = user.NetworkParentId;
|
||||||
|
var oldLegPosition = user.LegPosition;
|
||||||
|
|
||||||
|
// حذف از شبکه (Soft Delete)
|
||||||
|
user.NetworkParentId = null;
|
||||||
|
user.LegPosition = null;
|
||||||
|
|
||||||
|
_context.Users.Update(user);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// ثبت تاریخچه
|
||||||
|
var history = new NetworkMembershipHistory
|
||||||
|
{
|
||||||
|
UserId = request.UserId,
|
||||||
|
OldParentId = oldParentId,
|
||||||
|
NewParentId = null,
|
||||||
|
OldLegPosition = oldLegPosition,
|
||||||
|
NewLegPosition = null,
|
||||||
|
Action = NetworkMembershipAction.Remove,
|
||||||
|
Reason = request.Reason ?? "حذف از شبکه",
|
||||||
|
PerformedBy = "System" // TODO: باید از Current User گرفته شود
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.NetworkMembershipHistories.AddAsync(history, cancellationToken);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Commands.RemoveFromNetwork;
|
||||||
|
|
||||||
|
public class RemoveFromNetworkCommandValidator : AbstractValidator<RemoveFromNetworkCommand>
|
||||||
|
{
|
||||||
|
public RemoveFromNetworkCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.UserId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("شناسه کاربر معتبر نیست");
|
||||||
|
|
||||||
|
RuleFor(x => x.Reason)
|
||||||
|
.MaximumLength(500)
|
||||||
|
.WithMessage("طول دلیل نباید بیشتر از 500 کاراکتر باشد")
|
||||||
|
.When(x => !string.IsNullOrEmpty(x.Reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
{
|
||||||
|
var result = await ValidateAsync(
|
||||||
|
ValidationContext<RemoveFromNetworkCommand>.CreateWithOptions(
|
||||||
|
(RemoveFromNetworkCommand)model,
|
||||||
|
x => x.IncludeProperties(propertyName)));
|
||||||
|
|
||||||
|
if (result.IsValid)
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkMembershipHistory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت تاریخچه تغییرات شبکه یک کاربر
|
||||||
|
/// </summary>
|
||||||
|
public record GetNetworkMembershipHistoryQuery : IRequest<GetNetworkMembershipHistoryResponseDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر (اختیاری)
|
||||||
|
/// </summary>
|
||||||
|
public long? UserId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مرتبسازی (پیشفرض: -Created)
|
||||||
|
/// </summary>
|
||||||
|
public string? SortBy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pagination
|
||||||
|
/// </summary>
|
||||||
|
public PaginationState? PaginationState { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkMembershipHistory;
|
||||||
|
|
||||||
|
public class GetNetworkMembershipHistoryQueryHandler : IRequestHandler<GetNetworkMembershipHistoryQuery, GetNetworkMembershipHistoryResponseDto>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetNetworkMembershipHistoryQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GetNetworkMembershipHistoryResponseDto> Handle(GetNetworkMembershipHistoryQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var query = _context.NetworkMembershipHistories
|
||||||
|
.AsNoTracking()
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (request.UserId.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.UserId == request.UserId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.ApplyOrder(sortBy: request.SortBy ?? "-Created");
|
||||||
|
|
||||||
|
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||||
|
|
||||||
|
var models = await query
|
||||||
|
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||||
|
.Select(x => new GetNetworkMembershipHistoryResponseModel
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
UserId = x.UserId,
|
||||||
|
OldParentId = x.OldParentId,
|
||||||
|
NewParentId = x.NewParentId,
|
||||||
|
OldLegPosition = x.OldLegPosition,
|
||||||
|
NewLegPosition = x.NewLegPosition,
|
||||||
|
Action = x.Action,
|
||||||
|
Reason = x.Reason,
|
||||||
|
PerformedBy = x.PerformedBy,
|
||||||
|
Created = x.Created
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new GetNetworkMembershipHistoryResponseDto
|
||||||
|
{
|
||||||
|
MetaData = meta,
|
||||||
|
Models = models
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkMembershipHistory;
|
||||||
|
|
||||||
|
public class GetNetworkMembershipHistoryQueryValidator : AbstractValidator<GetNetworkMembershipHistoryQuery>
|
||||||
|
{
|
||||||
|
public GetNetworkMembershipHistoryQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.UserId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("شناسه کاربر معتبر نیست")
|
||||||
|
.When(x => x.UserId.HasValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
{
|
||||||
|
var result = await ValidateAsync(
|
||||||
|
ValidationContext<GetNetworkMembershipHistoryQuery>.CreateWithOptions(
|
||||||
|
(GetNetworkMembershipHistoryQuery)model,
|
||||||
|
x => x.IncludeProperties(propertyName)));
|
||||||
|
|
||||||
|
if (result.IsValid)
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkMembershipHistory;
|
||||||
|
|
||||||
|
public class GetNetworkMembershipHistoryResponseDto
|
||||||
|
{
|
||||||
|
public MetaData MetaData { get; set; }
|
||||||
|
public List<GetNetworkMembershipHistoryResponseModel> Models { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GetNetworkMembershipHistoryResponseModel
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public long? OldParentId { get; set; }
|
||||||
|
public long? NewParentId { get; set; }
|
||||||
|
public NetworkLeg? OldLegPosition { get; set; }
|
||||||
|
public NetworkLeg? NewLegPosition { get; set; }
|
||||||
|
public NetworkMembershipAction Action { get; set; }
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
public string? PerformedBy { get; set; }
|
||||||
|
public DateTimeOffset Created { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت درخت شبکه از یک کاربر (Binary Tree)
|
||||||
|
/// </summary>
|
||||||
|
public record GetNetworkTreeQuery : IRequest<NetworkTreeDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر که میخواهیم درخت زیرمجموعه او را ببینیم
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تعداد سطوح (Depth) که میخواهیم نمایش دهیم (پیشفرض: 3)
|
||||||
|
/// </summary>
|
||||||
|
public int MaxDepth { get; init; } = 3;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||||
|
|
||||||
|
public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, NetworkTreeDto?>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetNetworkTreeQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<NetworkTreeDto?> Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var rootUser = await _context.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken);
|
||||||
|
|
||||||
|
if (rootUser == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tree = await BuildTree(rootUser.Id, request.MaxDepth, 0, cancellationToken);
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<NetworkTreeDto> BuildTree(long userId, int maxDepth, int currentDepth, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var user = await _context.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException(nameof(User), userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var node = new NetworkTreeDto
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Mobile = user.Mobile,
|
||||||
|
FirstName = user.FirstName,
|
||||||
|
LastName = user.LastName,
|
||||||
|
LegPosition = user.LegPosition,
|
||||||
|
CurrentDepth = currentDepth
|
||||||
|
};
|
||||||
|
|
||||||
|
// اگر به حداکثر عمق رسیدیم، دیگر فرزندان را نمیخوانیم
|
||||||
|
if (currentDepth >= maxDepth)
|
||||||
|
{
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// پیدا کردن فرزند چپ
|
||||||
|
var leftChild = await _context.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Left,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (leftChild != null)
|
||||||
|
{
|
||||||
|
node.LeftChild = await BuildTree(leftChild.Id, maxDepth, currentDepth + 1, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// پیدا کردن فرزند راست
|
||||||
|
var rightChild = await _context.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == NetworkLeg.Right,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (rightChild != null)
|
||||||
|
{
|
||||||
|
node.RightChild = await BuildTree(rightChild.Id, maxDepth, currentDepth + 1, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||||
|
|
||||||
|
public class GetNetworkTreeQueryValidator : AbstractValidator<GetNetworkTreeQuery>
|
||||||
|
{
|
||||||
|
public GetNetworkTreeQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.UserId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("شناسه کاربر معتبر نیست");
|
||||||
|
|
||||||
|
RuleFor(x => x.MaxDepth)
|
||||||
|
.InclusiveBetween(1, 10)
|
||||||
|
.WithMessage("عمق درخت باید بین 1 تا 10 باشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
{
|
||||||
|
var result = await ValidateAsync(
|
||||||
|
ValidationContext<GetNetworkTreeQuery>.CreateWithOptions(
|
||||||
|
(GetNetworkTreeQuery)model,
|
||||||
|
x => x.IncludeProperties(propertyName)));
|
||||||
|
|
||||||
|
if (result.IsValid)
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO برای نمایش درخت دوتایی شبکه
|
||||||
|
/// </summary>
|
||||||
|
public class NetworkTreeDto
|
||||||
|
{
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public string? Mobile { get; set; }
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
public NetworkLeg? LegPosition { get; set; }
|
||||||
|
public int CurrentDepth { get; set; }
|
||||||
|
public NetworkTreeDto? LeftChild { get; set; }
|
||||||
|
public NetworkTreeDto? RightChild { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetUserNetworkPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت موقعیت کاربر در شبکه
|
||||||
|
/// </summary>
|
||||||
|
public record GetUserNetworkPositionQuery : IRequest<UserNetworkPositionDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetUserNetworkPosition;
|
||||||
|
|
||||||
|
public class GetUserNetworkPositionQueryHandler : IRequestHandler<GetUserNetworkPositionQuery, UserNetworkPositionDto?>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetUserNetworkPositionQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserNetworkPositionDto?> Handle(GetUserNetworkPositionQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var user = await _context.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.Id == request.UserId)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.Mobile,
|
||||||
|
x.FirstName,
|
||||||
|
x.LastName,
|
||||||
|
x.NetworkParentId,
|
||||||
|
x.LegPosition
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// شمارش فرزندان
|
||||||
|
var childrenCount = await _context.Users
|
||||||
|
.CountAsync(x => x.NetworkParentId == request.UserId, cancellationToken);
|
||||||
|
|
||||||
|
var leftChildCount = await _context.Users
|
||||||
|
.CountAsync(x => x.NetworkParentId == request.UserId && x.LegPosition == NetworkLeg.Left,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var rightChildCount = await _context.Users
|
||||||
|
.CountAsync(x => x.NetworkParentId == request.UserId && x.LegPosition == NetworkLeg.Right,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
// اطلاعات والد
|
||||||
|
string? parentMobile = null;
|
||||||
|
if (user.NetworkParentId.HasValue)
|
||||||
|
{
|
||||||
|
parentMobile = await _context.Users
|
||||||
|
.Where(x => x.Id == user.NetworkParentId)
|
||||||
|
.Select(x => x.Mobile)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserNetworkPositionDto
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Mobile = user.Mobile,
|
||||||
|
FirstName = user.FirstName,
|
||||||
|
LastName = user.LastName,
|
||||||
|
NetworkParentId = user.NetworkParentId,
|
||||||
|
ParentMobile = parentMobile,
|
||||||
|
LegPosition = user.LegPosition,
|
||||||
|
TotalChildren = childrenCount,
|
||||||
|
LeftChildCount = leftChildCount,
|
||||||
|
RightChildCount = rightChildCount,
|
||||||
|
IsInNetwork = user.NetworkParentId.HasValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetUserNetworkPosition;
|
||||||
|
|
||||||
|
public class GetUserNetworkPositionQueryValidator : AbstractValidator<GetUserNetworkPositionQuery>
|
||||||
|
{
|
||||||
|
public GetUserNetworkPositionQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.UserId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("شناسه کاربر معتبر نیست");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
{
|
||||||
|
var result = await ValidateAsync(
|
||||||
|
ValidationContext<GetUserNetworkPositionQuery>.CreateWithOptions(
|
||||||
|
(GetUserNetworkPositionQuery)model,
|
||||||
|
x => x.IncludeProperties(propertyName)));
|
||||||
|
|
||||||
|
if (result.IsValid)
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
return result.Errors.Select(e => e.ErrorMessage);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetUserNetworkPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO برای نمایش موقعیت کاربر در شبکه
|
||||||
|
/// </summary>
|
||||||
|
public class UserNetworkPositionDto
|
||||||
|
{
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public string? Mobile { get; set; }
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
public long? NetworkParentId { get; set; }
|
||||||
|
public string? ParentMobile { get; set; }
|
||||||
|
public NetworkLeg? LegPosition { get; set; }
|
||||||
|
public int TotalChildren { get; set; }
|
||||||
|
public int LeftChildCount { get; set; }
|
||||||
|
public int RightChildCount { get; set; }
|
||||||
|
public bool IsInNetwork { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user