using System.Data; using System.Data.Common; using Microsoft.EntityFrameworkCore; namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree; public class GetNetworkTreeQueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly ILogger _logger; public GetNetworkTreeQueryHandler( IApplicationDbContext context, ILogger logger) { _context = context; _logger = logger; } public async Task Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken) { // UserId must be provided - customer endpoints resolve from JWT before calling var userId = request.UserId; if (userId == 0) { throw new ArgumentException("UserId is required for network tree query"); } // Create a new request with the resolved userId var resolvedRequest = request with { UserId = userId }; try { // دریافت نتایج flat از Stored Procedure var flatNodes = await ExecuteStoredProcedureAsync(resolvedRequest, cancellationToken); if (flatNodes == null || !flatNodes.Any()) { return null; } // تبدیل نتایج flat به ساختار درختی var tree = BuildTreeFromFlatNodes(flatNodes); return tree; } catch (Exception ex) { _logger.LogError(ex, "Error executing GetNetworkTree for UserId: {UserId}", request.UserId); throw; } } /// /// اجرای Stored Procedure و دریافت نتایج /// private async Task> ExecuteStoredProcedureAsync( GetNetworkTreeQuery request, CancellationToken cancellationToken) { var results = new List(); var connection = _context.Database.GetDbConnection(); try { if (connection.State != ConnectionState.Open) { await connection.OpenAsync(cancellationToken); } using var command = connection.CreateCommand(); command.CommandText = "[CMS].[GetNetworkTree]"; command.CommandType = CommandType.StoredProcedure; command.CommandTimeout = 120; // 2 minutes timeout for large trees // Parameters - use DbParameter for provider-agnostic code var rootUserIdParam = command.CreateParameter(); rootUserIdParam.ParameterName = "@RootUserId"; rootUserIdParam.DbType = DbType.Int64; rootUserIdParam.Value = request.UserId; command.Parameters.Add(rootUserIdParam); var maxDepthParam = command.CreateParameter(); maxDepthParam.ParameterName = "@MaxDepth"; maxDepthParam.DbType = DbType.Int32; maxDepthParam.Value = request.MaxDepth > 0 ? request.MaxDepth : 100; command.Parameters.Add(maxDepthParam); var isClubActiveParam = command.CreateParameter(); isClubActiveParam.ParameterName = "@IsClubActive"; isClubActiveParam.DbType = DbType.Boolean; isClubActiveParam.Value = request.IsClubActive.HasValue ? request.IsClubActive.Value : DBNull.Value; command.Parameters.Add(isClubActiveParam); var weekParam = command.CreateParameter(); weekParam.ParameterName = "@ActivationWeekDefinitionId"; weekParam.DbType = DbType.Int64; weekParam.Value = request.ActivationWeekDefinitionId.HasValue ? request.ActivationWeekDefinitionId.Value : DBNull.Value; command.Parameters.Add(weekParam); using var reader = await command.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { var node = new NetworkTreeNodeDto { UserId = reader.GetInt64(reader.GetOrdinal("UserId")), Mobile = reader.IsDBNull(reader.GetOrdinal("Mobile")) ? null : reader.GetString(reader.GetOrdinal("Mobile")), FirstName = reader.IsDBNull(reader.GetOrdinal("FirstName")) ? null : reader.GetString(reader.GetOrdinal("FirstName")), LastName = reader.IsDBNull(reader.GetOrdinal("LastName")) ? null : reader.GetString(reader.GetOrdinal("LastName")), ReferralCode = reader.IsDBNull(reader.GetOrdinal("ReferralCode")) ? null : reader.GetString(reader.GetOrdinal("ReferralCode")), LegPosition = reader.IsDBNull(reader.GetOrdinal("LegPosition")) ? null : reader.GetInt32(reader.GetOrdinal("LegPosition")), ParentId = reader.IsDBNull(reader.GetOrdinal("ParentId")) ? null : reader.GetInt64(reader.GetOrdinal("ParentId")), NetworkLevel = reader.GetInt32(reader.GetOrdinal("NetworkLevel")), ClubActivatedAt = reader.IsDBNull(reader.GetOrdinal("ClubActivatedAt")) ? null : reader.GetDateTime(reader.GetOrdinal("ClubActivatedAt")), IsClubActive = Convert.ToBoolean(reader.GetValue(reader.GetOrdinal("IsClubActive"))), ActivationWeekDefinitionId = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDefinitionId")) ? null : reader.GetInt64(reader.GetOrdinal("ActivationWeekDefinitionId")), ActivationWeekDisplayName = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDisplayName")) ? null : reader.GetString(reader.GetOrdinal("ActivationWeekDisplayName")), IsActivatedInTargetWeek = Convert.ToBoolean(reader.GetValue(reader.GetOrdinal("IsActivatedInTargetWeek"))), UserCreated = new DateTimeOffset(reader.GetDateTime(reader.GetOrdinal("UserCreated"))), IsNewActivation = reader.IsDBNull(reader.GetOrdinal("IsNewActivation")) ? null : (bool?)(reader.GetInt32(reader.GetOrdinal("IsNewActivation")) == 1), PackageName = reader.IsDBNull(reader.GetOrdinal("PackageName")) ? null : reader.GetString(reader.GetOrdinal("PackageName")), PackageId = reader.IsDBNull(reader.GetOrdinal("PackageId")) ? null : reader.GetInt64(reader.GetOrdinal("PackageId")) }; results.Add(node); } } finally { // Connection is managed by DbContext, don't close it here } _logger.LogInformation("GetNetworkTree SP returned {Count} nodes for UserId: {UserId}", results.Count, request.UserId); return results; } /// /// تبدیل لیست flat به ساختار درختی /// private NetworkTreeDto? BuildTreeFromFlatNodes(List flatNodes) { if (!flatNodes.Any()) return null; // Dictionary برای دسترسی سریع به نودها var nodeDict = new Dictionary(); // ایجاد همه نودها foreach (var flatNode in flatNodes) { nodeDict[flatNode.UserId] = new NetworkTreeDto { UserId = flatNode.UserId, Mobile = flatNode.Mobile, FirstName = flatNode.FirstName, LastName = flatNode.LastName, ReferralCode = flatNode.ReferralCode, LegPosition = flatNode.LegPosition.HasValue ? (NetworkLeg)flatNode.LegPosition.Value : null, CurrentDepth = flatNode.NetworkLevel, ClubActivatedAt = flatNode.ClubActivatedAt, IsClubActive = flatNode.IsClubActive, ActivationWeekDefinitionId = flatNode.ActivationWeekDefinitionId, ActivationWeekDisplayName = flatNode.ActivationWeekDisplayName, IsActivatedInTargetWeek = flatNode.IsActivatedInTargetWeek, UserCreated = flatNode.UserCreated, IsNewActivation = flatNode.IsNewActivation, PackageName = flatNode.PackageName, PackageId = flatNode.PackageId }; } // برقراری ارتباط Parent-Child foreach (var flatNode in flatNodes) { if (flatNode.ParentId.HasValue && nodeDict.ContainsKey(flatNode.ParentId.Value)) { var parent = nodeDict[flatNode.ParentId.Value]; var child = nodeDict[flatNode.UserId]; // تعیین موقعیت چپ یا راست if (flatNode.LegPosition == (int)NetworkLeg.Left) { parent.LeftChild = child; } else if (flatNode.LegPosition == (int)NetworkLeg.Right) { parent.RightChild = child; } } } // پیدا کردن ریشه (اولین نود با Level=0) var rootNode = flatNodes.FirstOrDefault(n => n.NetworkLevel == 0); return rootNode != null ? nodeDict[rootNode.UserId] : null; } }