Refactor email repository and query handling

Replaced `IMailRepository` with `IReceivedEmailRepository` to improve modularity and functionality. Updated `IReceivedEmailRepository` to make the `EmailAccount` parameter optional in the `FindAsync` method.

Added `ReceivedEmailRepository` with advanced filtering capabilities, including account, flags, text, UID, date, and recipient filters. Implemented deferred materialization for recipient filtering due to EF Core limitations.

Registered `IReceivedEmailRepository` in dependency injection. Updated `ReadEmailQueryHandler` to use the new repository. Improved query performance by applying filters conditionally.
This commit is contained in:
2026-08-13 13:28:19 +02:00
parent 7f04c4b09f
commit 7207c5b4d9
4 changed files with 87 additions and 2 deletions

View File

@@ -6,6 +6,6 @@ namespace DigitalData.MessagingService.Application.Common.Interfaces.Repositorie
public interface IReceivedEmailRepository : IRepository<ReceivedEmail>
{
public Task<IEnumerable<ReceivedEmail>> FindAsync(MailSearchFilter mailSearchFilter, EmailAccount accountQuery, CancellationToken cancellationToken = default);
public Task<IEnumerable<ReceivedEmail>> FindAsync(MailSearchFilter mailSearchFilter, EmailAccount? accountQuery = null, CancellationToken cancellationToken = default);
}
#endif

View File

@@ -28,7 +28,7 @@ public record ReadEmailQuery : IRequest<IEnumerable<ReceivedEmailDto>>
public MailSearchFilter Mail { get; init; } = new();
}
public class ReadEmailQueryHandler(IMapper Mapper, ILogger<ReadEmailQueryHandler> Logger, IRepository<EmailAccount> EmailAccountRepo, IMailRepository MailRepo) : IRequestHandler<ReadEmailQuery, IEnumerable<ReceivedEmailDto>>
public class ReadEmailQueryHandler(IMapper Mapper, ILogger<ReadEmailQueryHandler> Logger, IRepository<EmailAccount> EmailAccountRepo, IReceivedEmailRepository MailRepo) : IRequestHandler<ReadEmailQuery, IEnumerable<ReceivedEmailDto>>
{
public async Task<IEnumerable<ReceivedEmailDto>> Handle(ReadEmailQuery request, CancellationToken cancellationToken)
{

View File

@@ -66,6 +66,8 @@ public static class DependencyInjection
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddScoped<IReceivedEmailRepository, ReceivedEmailRepository>();
// AutoMapper - Register entity self-mappings (T -> T) for generic repository
services.AddAutoMapper(config => config.AddMaps(typeof(EntitySelfMappingProfile).Assembly));

View File

@@ -0,0 +1,83 @@
using AutoMapper;
using DigitalData.MessagingService.Application.Common.Dto.MailSearch;
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
using DigitalData.MessagingService.Domain.Entities;
using DigitalData.MessagingService.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
namespace DigitalData.MessagingService.Infrastructure.Repositories;
public class ReceivedEmailRepository(MessagingServiceDbContext Context, IMapper Mapper) : Repository<ReceivedEmail>(Context, Mapper), IReceivedEmailRepository
{
public async Task<IEnumerable<ReceivedEmail>> FindAsync(MailSearchFilter mailSearchFilter, EmailAccount? accountQuery = null, CancellationToken cancellationToken = default)
{
IQueryable<ReceivedEmail> query = DbSet;
// ── Account filter ─────────────────────────────────────────────────────
if (accountQuery is not null)
query = query.Where(x => x.AccountId == accountQuery.Id);
// ── Flag filters ───────────────────────────────────────────────────────
if (mailSearchFilter.UnseenOnly)
query = query.Where(x => !x.IsSeen);
// ── Text filters ───────────────────────────────────────────────────────
if (!string.IsNullOrWhiteSpace(mailSearchFilter.SubjectContains))
query = query.Where(x => x.Subject.Contains(mailSearchFilter.SubjectContains));
if (!string.IsNullOrWhiteSpace(mailSearchFilter.SenderContains))
query = query.Where(x => x.From.Contains(mailSearchFilter.SenderContains));
if (!string.IsNullOrWhiteSpace(mailSearchFilter.BodyContains))
query = query.Where(x => x.TextBody.Contains(mailSearchFilter.BodyContains)
|| x.HtmlBody.Contains(mailSearchFilter.BodyContains));
// ── UID filter ─────────────────────────────────────────────────────────
if (mailSearchFilter.Uid is { } uid)
{
if (uid.Absolute.HasValue)
query = query.Where(x => x.Uid == uid.Absolute.Value);
else
{
if (uid.Min.HasValue)
query = query.Where(x => x.Uid >= uid.Min.Value);
if (uid.Max.HasValue)
query = query.Where(x => x.Uid <= uid.Max.Value);
}
}
// ── Date filter ────────────────────────────────────────────────────────
if (mailSearchFilter.Date is { } date)
{
if (date.After.HasValue)
query = query.Where(x => x.Date >= date.After.Value);
if (date.Before.HasValue)
query = query.Where(x => x.Date <= date.Before.Value);
}
// ── Attachments ────────────────────────────────────────────────────────
if (mailSearchFilter.WithAttachments)
query = query.Include(x => x.Attachments);
// ── Sort ───────────────────────────────────────────────────────────────
query = mailSearchFilter.SortOrder == MailSortOrder.OldestFirst
? query.OrderBy(x => x.Date)
: query.OrderByDescending(x => x.Date);
// ── Limit ──────────────────────────────────────────────────────────────
if (mailSearchFilter.MaxCount.HasValue)
query = query.Take(mailSearchFilter.MaxCount.Value);
// ── RecipientContains: To/Cc are IEnumerable<string> (nvarchar(max)) ──
// EF Core cannot translate collection predicates on these columns to SQL.
// Materialization is deferred until after other DB-side filters narrow the set.
var results = await query.ToListAsync(cancellationToken);
if (!string.IsNullOrWhiteSpace(mailSearchFilter.RecipientContains))
results = [.. results
.Where(x => x.To.Any(t => t.Contains(mailSearchFilter.RecipientContains, StringComparison.OrdinalIgnoreCase))
|| x.Cc.Any(c => c.Contains(mailSearchFilter.RecipientContains, StringComparison.OrdinalIgnoreCase)))];
return results;
}
}