From b7d65d7d5c192710536315d97d19a8283e2586eb Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 14 Jul 2026 16:36:41 +0200 Subject: [PATCH] refactor(application): Remove IUnitOfWork, add generic IRepository with AutoMapper - Remove IUnitOfWork pattern (not needed with auto-save repositories) - Add generic IRepository with CreateAsync, UpdateSingleAsync, DeleteSingleAsync - Add UpdateAsync/DeleteAsync for bulk operations - Add ICommandPublisher interface for RabbitMQ integration - Add service interfaces: IEmailService, IPdfProcessingService, IDmsService, IEncryptionService, IEmailQueue - Configure Application DI with MediatR, AutoMapper, FluentValidation --- .../Common/Interfaces/ICommandPublisher.cs | 19 ++++++ .../DependencyInjection.cs | 30 +++++++++ .../Interfaces/Repositories/IRepository.cs | 63 +++++++++++++++---- .../Interfaces/Repositories/IUnitOfWork.cs | 33 ---------- 4 files changed, 101 insertions(+), 44 deletions(-) create mode 100644 src/DigitalData.EmailProfiler.Application/Common/Interfaces/ICommandPublisher.cs create mode 100644 src/DigitalData.EmailProfiler.Application/DependencyInjection.cs delete mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IUnitOfWork.cs diff --git a/src/DigitalData.EmailProfiler.Application/Common/Interfaces/ICommandPublisher.cs b/src/DigitalData.EmailProfiler.Application/Common/Interfaces/ICommandPublisher.cs new file mode 100644 index 0000000..e63c4e3 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Common/Interfaces/ICommandPublisher.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace DigitalData.EmailProfiler.Application.Common.Interfaces; + +/// +/// Interface for publishing commands to a message broker (e.g., RabbitMQ) +/// +public interface ICommandPublisher +{ + /// + /// Publishes a command to the message broker for asynchronous processing + /// + /// The command type (must implement IRequest) + /// The command to publish + /// Cancellation token + /// Task representing the publish operation + Task PublishAsync(TCommand command, CancellationToken cancellationToken = default) + where TCommand : IRequest; +} diff --git a/src/DigitalData.EmailProfiler.Application/DependencyInjection.cs b/src/DigitalData.EmailProfiler.Application/DependencyInjection.cs new file mode 100644 index 0000000..52d918d --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/DependencyInjection.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; + +namespace DigitalData.EmailProfiler.Application; + +/// +/// Dependency injection configuration for Application layer. +/// +public static class DependencyInjection +{ + public static IServiceCollection AddApplicationServices(this IServiceCollection services) + { + var assembly = Assembly.GetExecutingAssembly(); + + // MediatR - Register all handlers + services.AddMediatR(config => + { + config.RegisterServicesFromAssembly(assembly); + }); + + // AutoMapper - Register all profiles + services.AddAutoMapper(assembly); + + // FluentValidation - Register all validators + services.AddValidatorsFromAssembly(assembly); + + return services; + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IRepository.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IRepository.cs index 32ab791..7c77631 100644 --- a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IRepository.cs +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IRepository.cs @@ -3,17 +3,58 @@ using System.Linq.Expressions; namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories; /// -/// Base repository interface for common CRUD operations. +/// Base repository interface for common CRUD operations with AutoMapper support. +/// Changes are automatically saved after each operation - no explicit SaveChanges needed. /// -/// Entity type -public interface IRepository where T : class +/// Entity type +public interface IRepository where TEntity : class { - Task GetByIdAsync(int id, CancellationToken cancellationToken = default); - Task> GetAllAsync(CancellationToken cancellationToken = default); - Task> FindAsync(Expression> predicate, CancellationToken cancellationToken = default); - Task AddAsync(T entity, CancellationToken cancellationToken = default); - Task UpdateAsync(T entity, CancellationToken cancellationToken = default); - Task DeleteAsync(T entity, CancellationToken cancellationToken = default); - Task ExistsAsync(Expression> predicate, CancellationToken cancellationToken = default); - Task CountAsync(Expression>? predicate = null, CancellationToken cancellationToken = default); + // ==================== QUERY OPERATIONS ==================== + + Task GetByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task> FindAsync(Expression> predicate, CancellationToken cancellationToken = default); + Task FirstOrDefaultAsync(Expression> predicate, CancellationToken cancellationToken = default); + Task ExistsAsync(Expression> predicate, CancellationToken cancellationToken = default); + Task CountAsync(Expression>? predicate = null, CancellationToken cancellationToken = default); + + // ==================== CREATE OPERATIONS ==================== + + /// + /// Create entity from DTO using AutoMapper and save immediately. + /// + /// Created entity with ID populated + Task CreateAsync(TDto dto, CancellationToken cancellationToken = default) where TDto : class; + + // ==================== UPDATE OPERATIONS ==================== + + /// + /// Update ALL entities matching expression using DTO via AutoMapper. + /// WARNING: This can update multiple records. Use UpdateSingleAsync for single-record updates. + /// + /// Number of entities updated + Task UpdateAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default) where TDto : class; + + /// + /// Update SINGLE entity matching expression using DTO via AutoMapper. + /// SAFETY: Throws exception if zero or multiple entities match the predicate. + /// + /// Thrown when zero or multiple entities match + Task UpdateSingleAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default) where TDto : class; + + // ==================== DELETE OPERATIONS ==================== + + /// + /// Delete ALL entities matching expression. + /// WARNING: This can delete multiple records. Use DeleteSingleAsync for single-record deletes. + /// + /// Number of entities deleted + Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default); + + /// + /// Delete SINGLE entity matching expression. + /// SAFETY: Throws exception if zero or multiple entities match the predicate. + /// + /// Thrown when zero or multiple entities match + Task DeleteSingleAsync(Expression> predicate, CancellationToken cancellationToken = default); } diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IUnitOfWork.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IUnitOfWork.cs deleted file mode 100644 index 58d9c9d..0000000 --- a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IUnitOfWork.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories; - -/// -/// Unit of Work pattern for transaction management. -/// -public interface IUnitOfWork : IDisposable -{ - IEmailAccountRepository EmailAccounts { get; } - IEmailProfileRepository EmailProfiles { get; } - IEmailProcessRepository EmailProcesses { get; } - IEmailHistoryRepository EmailHistories { get; } - IEmailOutboxRepository EmailOutbox { get; } - - /// - /// Save all changes to the database. - /// - Task SaveChangesAsync(CancellationToken cancellationToken = default); - - /// - /// Begin a database transaction. - /// - Task BeginTransactionAsync(CancellationToken cancellationToken = default); - - /// - /// Commit the current transaction. - /// - Task CommitTransactionAsync(CancellationToken cancellationToken = default); - - /// - /// Rollback the current transaction. - /// - Task RollbackTransactionAsync(CancellationToken cancellationToken = default); -}