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:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user