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:
+17
@@ -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;
|
||||
}
|
||||
+78
@@ -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;
|
||||
}
|
||||
}
|
||||
+28
@@ -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);
|
||||
};
|
||||
}
|
||||
+16
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user