From 56274835899891d0dc0376b18d79039c4aba452f Mon Sep 17 00:00:00 2001 From: TekH Date: Mon, 5 Oct 2026 16:43:34 +0200 Subject: [PATCH] Make RecentReceiverSuggestionDto immutable; refactor query Updated RecentReceiverSuggestionDto to use `init` properties, adding immutability and a new `LastUsedAt` field. Refactored ReadRecentReceiverSuggestionsQueryHandler to simplify query logic, remove `IMapper` dependency, and improve determinism in results. Introduced tie-breaking for duplicate timestamps and optimized ordering by `LastUsedAt` and `EmailAddress`. Removed redundant `Include` and mapping steps for clarity. --- .../Receiver/RecentReceiverSuggestionDto.cs | 6 ++- .../ReadRecentReceiverSuggestionsQuery.cs | 50 ++++++++++++++----- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/EnvelopeGenerator.Application/Common/Dto/Receiver/RecentReceiverSuggestionDto.cs b/EnvelopeGenerator.Application/Common/Dto/Receiver/RecentReceiverSuggestionDto.cs index 1928f56e..37a167e0 100644 --- a/EnvelopeGenerator.Application/Common/Dto/Receiver/RecentReceiverSuggestionDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/Receiver/RecentReceiverSuggestionDto.cs @@ -2,7 +2,9 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver; public class RecentReceiverSuggestionDto { - public required string FullName { get; set; } + public required string FullName { get; init; } - public required string EmailAddress { get; set; } + public required string EmailAddress { get; init; } + + public required DateTime LastUsedAt { get; init; } } diff --git a/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadRecentReceiverSuggestionsQuery.cs b/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadRecentReceiverSuggestionsQuery.cs index c9017cd6..f6cef12f 100644 --- a/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadRecentReceiverSuggestionsQuery.cs +++ b/EnvelopeGenerator.Application/EnvelopeReceivers/Queries/ReadRecentReceiverSuggestionsQuery.cs @@ -1,4 +1,3 @@ -using AutoMapper; using DigitalData.Core.Abstraction.Application.Repository; using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Domain.Entities; @@ -21,7 +20,7 @@ public record ReadRecentReceiverSuggestionsQuery : IRequest - /// Desired number of results. Effective range is clamped to 3..5. + /// Desired number of results. Default value is 5. /// public int Take { get; init; } = 5; @@ -39,7 +38,7 @@ public record ReadRecentReceiverSuggestionsQuery : IRequest /// Handles . /// -public class ReadRecentReceiverSuggestionsQueryHandler(IRepository repository, IMapper mapper) +public class ReadRecentReceiverSuggestionsQueryHandler(IRepository repository) : IRequestHandler> { /// @@ -50,23 +49,50 @@ public class ReadRecentReceiverSuggestionsQueryHandler(IRepositoryOrdered list of recent receiver suggestions. public async Task> Handle(ReadRecentReceiverSuggestionsQuery request, CancellationToken cancellationToken) { - var take = Math.Clamp(request.Take, 3, 5); - var query = repository.Query .AsNoTracking() - .Include(er => er.Receiver) .Where(er => er.Receiver != null); if (request.Username is not null) query = query.Where(er => er.Envelope != null && er.Envelope.User.Username == request.Username); - var rows = await query - .GroupBy(er => er.Receiver!.EmailAddress.ToUpperInvariant()) - .Select(g => g.OrderByDescending(er => er.ChangedWhen ?? er.AddedWhen).First()) - .OrderByDescending(er => er.ChangedWhen ?? er.AddedWhen) - .Take(take) + // 1) For each email, compute the latest usage timestamp. + var latestPerEmail = query + .GroupBy(er => er.Receiver!.EmailAddress) + .Select(g => new + { + EmailAddress = g.Key, + LastUsedAt = g.Max(er => er.AddedWhen), + }); + + // 2) Join back to rows that match the latest timestamp per email. + var latestRows = + from er in query + join latest in latestPerEmail + on new { EmailAddress = er.Receiver!.EmailAddress, LastUsedAt = er.AddedWhen } + equals new { latest.EmailAddress, latest.LastUsedAt } + select new + { + EmailAddress = er.Receiver!.EmailAddress, + FullName = er.Name, + LastUsedAt = er.AddedWhen, + }; + + // 3) Tie-break: if multiple rows share the same latest timestamp for an email, + // pick a deterministic name via Max(FullName), then order by most recent usage. + var suggestions = await latestRows + .GroupBy(x => new { x.EmailAddress, x.LastUsedAt }) + .Select(g => new RecentReceiverSuggestionDto + { + EmailAddress = g.Key.EmailAddress, + FullName = g.Max(x => x.FullName) ?? string.Empty, + LastUsedAt = g.Key.LastUsedAt, + }) + .OrderByDescending(x => x.LastUsedAt) + .ThenBy(x => x.EmailAddress) + .Take(request.Take) .ToListAsync(cancellationToken); - return mapper.Map>(rows); + return suggestions; } }