Updated the `Take` property in `ReadRecentReceiverSuggestionsQuery` to be nullable, allowing for unlimited results when not specified. Refactored the `Handle` method in `ReadRecentReceiverSuggestionsQueryHandler` to conditionally apply the `Take` limit only when provided. Renamed the cancellation token parameter for consistency. Improved code clarity and flexibility by removing redundant operations and dynamically applying the `Take` logic.
99 lines
3.8 KiB
C#
99 lines
3.8 KiB
C#
using DigitalData.Core.Abstraction.Application.Repository;
|
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|
using EnvelopeGenerator.Domain.Entities;
|
|
using MediatR;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
|
|
|
/// <summary>
|
|
/// Reads recent receiver suggestions for envelope creation.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Sender scope is assigned via chain method to avoid exposing username mutation from API query binding.
|
|
/// </remarks>
|
|
public record ReadRecentReceiverSuggestionsQuery : IRequest<IReadOnlyList<RecentReceiverSuggestionDto>>
|
|
{
|
|
/// <summary>
|
|
/// Sender username used for scoping suggestion results.
|
|
/// </summary>
|
|
internal string? Username { get; private init; }
|
|
|
|
/// <summary>
|
|
/// Desired number of results.
|
|
/// </summary>
|
|
public int? Take { get; init; } = null;
|
|
|
|
/// <summary>
|
|
/// Returns a new query instance scoped to the specified sender username.
|
|
/// </summary>
|
|
/// <param name="username">Sender username from authenticated claims.</param>
|
|
/// <returns>Scoped query instance.</returns>
|
|
public ReadRecentReceiverSuggestionsQuery ForSender(string username)
|
|
{
|
|
return this with { Username = username };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles <see cref="ReadRecentReceiverSuggestionsQuery"/>.
|
|
/// </summary>
|
|
public class ReadRecentReceiverSuggestionsQueryHandler(IRepository<EnvelopeReceiver> repository)
|
|
: IRequestHandler<ReadRecentReceiverSuggestionsQuery, IReadOnlyList<RecentReceiverSuggestionDto>>
|
|
{
|
|
/// <summary>
|
|
/// Retrieves distinct recent receiver suggestions grouped by receiver email.
|
|
/// </summary>
|
|
/// <param name="request">Suggestion query.</param>
|
|
/// <param name="cancel">Cancellation token.</param>
|
|
/// <returns>Ordered list of recent receiver suggestions.</returns>
|
|
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> Handle(ReadRecentReceiverSuggestionsQuery request, CancellationToken cancel)
|
|
{
|
|
var query = repository.Query
|
|
.AsNoTracking()
|
|
.Where(er => er.Receiver != null);
|
|
|
|
if (request.Username is not null)
|
|
query = query.Where(er => er.Envelope != null && er.Envelope.User.Username == request.Username);
|
|
|
|
// 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 = 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);
|
|
|
|
return request.Take is int take
|
|
? await suggestions.Take(take).ToListAsync(cancel)
|
|
: await suggestions.ToListAsync(cancel);
|
|
}
|
|
}
|