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.
This commit is contained in:
2026-10-05 16:43:34 +02:00
parent bf23e88cf8
commit 5627483589
2 changed files with 42 additions and 14 deletions

View File

@@ -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; }
}

View File

@@ -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<IReadOnlyList<Recent
internal string? Username { get; private init; }
/// <summary>
/// Desired number of results. Effective range is clamped to 3..5.
/// Desired number of results. Default value is 5.
/// </summary>
public int Take { get; init; } = 5;
@@ -39,7 +38,7 @@ public record ReadRecentReceiverSuggestionsQuery : IRequest<IReadOnlyList<Recent
/// <summary>
/// Handles <see cref="ReadRecentReceiverSuggestionsQuery"/>.
/// </summary>
public class ReadRecentReceiverSuggestionsQueryHandler(IRepository<EnvelopeReceiver> repository, IMapper mapper)
public class ReadRecentReceiverSuggestionsQueryHandler(IRepository<EnvelopeReceiver> repository)
: IRequestHandler<ReadRecentReceiverSuggestionsQuery, IReadOnlyList<RecentReceiverSuggestionDto>>
{
/// <summary>
@@ -50,23 +49,50 @@ public class ReadRecentReceiverSuggestionsQueryHandler(IRepository<EnvelopeRecei
/// <returns>Ordered list of recent receiver suggestions.</returns>
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> 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<List<RecentReceiverSuggestionDto>>(rows);
return suggestions;
}
}