Step-by-step guide covering all remaining phases: - Phase 2: Application Layer (Repositories, Services, Commands, Queries, Validators) - Phase 3: Infrastructure Layer (DbContext, Repositories, External Services) - Phase 4: API Layer (Controllers, Workers, Middleware) - Phase 5: Configuration (appsettings, Serilog, Scalar) - Phase 6: Testing (Unit tests, Integration tests) - Phase 7: Documentation (README.md in German) - Phase 8: Build and deployment Includes complete code examples, best practices, and verification steps. Future AI agents can follow this guide to continue development systematically.
955 lines
29 KiB
Markdown
955 lines
29 KiB
Markdown
# EmailProfiler - Implementation Guide for AI Agents
|
|
|
|
## Overview
|
|
|
|
This guide provides step-by-step instructions for AI agents to continue the implementation of the EmailProfiler application. The project is a modern .NET 8.0 rewrite of a legacy VB.NET email automation system.
|
|
|
|
---
|
|
|
|
## Current Status (2026-07-07)
|
|
|
|
✅ **COMPLETED**:
|
|
- Domain Layer (100%)
|
|
- All entities with proper `[Table]` and `[Column]` attributes
|
|
- Value Objects (MessageId, EmailAddress)
|
|
- Enums (ErrorCode, ProcessType, etc.)
|
|
- Domain Services (MessageIdGenerator)
|
|
- Domain Events (EmailProcessedEvent)
|
|
- Exceptions (DomainException, ValidationException, AttachmentProcessingException)
|
|
- agents.md documentation
|
|
- Project builds successfully
|
|
|
|
🚧 **IN PROGRESS**:
|
|
- Application Layer (5% - only DTOs created)
|
|
|
|
❌ **PENDING**:
|
|
- Application Layer (95%)
|
|
- Infrastructure Layer (0%)
|
|
- API Layer (minimal structure only)
|
|
- Testing (0%)
|
|
- README.md documentation (0%)
|
|
|
|
---
|
|
|
|
## Architecture Overview
|
|
|
|
```
|
|
DigitalData.EmailProfiler/
|
|
├── src/
|
|
│ ├── Domain/ ✅ COMPLETE
|
|
│ ├── Application/ 🚧 IN PROGRESS (5%)
|
|
│ ├── Infrastructure/ ❌ TODO
|
|
│ └── API/ ❌ TODO (minimal structure exists)
|
|
├── tests/
|
|
│ └── Tests/ ❌ TODO
|
|
├── legacy/ 📖 Reference only
|
|
├── agents.md ✅ COMPLETE
|
|
├── README.md ❌ TODO
|
|
└── IMPLEMENTATION_GUIDE.md 📄 This file
|
|
```
|
|
|
|
---
|
|
|
|
## Phase-by-Phase Implementation Plan
|
|
|
|
### PHASE 2: Application Layer (Current Focus)
|
|
|
|
#### 2.1. Create Repository Interfaces
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/`
|
|
|
|
Create these files:
|
|
|
|
**IEmailProfileRepository.cs**:
|
|
```csharp
|
|
using DigitalData.EmailProfiler.Domain.Entities;
|
|
|
|
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
|
|
|
public interface IEmailProfileRepository
|
|
{
|
|
Task<EmailProfile?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
|
Task<List<EmailProfile>> GetAllAsync(CancellationToken cancellationToken = default);
|
|
Task<List<EmailProfile>> GetActiveProfilesAsync(CancellationToken cancellationToken = default);
|
|
Task<List<EmailProfile>> GetProfilesDueForPollingAsync(CancellationToken cancellationToken = default);
|
|
Task<int> AddAsync(EmailProfile profile, CancellationToken cancellationToken = default);
|
|
Task UpdateAsync(EmailProfile profile, CancellationToken cancellationToken = default);
|
|
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
|
|
}
|
|
```
|
|
|
|
**IEmailAccountRepository.cs**:
|
|
```csharp
|
|
using DigitalData.EmailProfiler.Domain.Entities;
|
|
|
|
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
|
|
|
public interface IEmailAccountRepository
|
|
{
|
|
Task<EmailAccount?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
|
Task<List<EmailAccount>> GetAllAsync(CancellationToken cancellationToken = default);
|
|
Task<List<EmailAccount>> GetActiveAccountsAsync(CancellationToken cancellationToken = default);
|
|
Task<int> AddAsync(EmailAccount account, CancellationToken cancellationToken = default);
|
|
Task UpdateAsync(EmailAccount account, CancellationToken cancellationToken = default);
|
|
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
|
|
}
|
|
```
|
|
|
|
**IEmailHistoryRepository.cs**:
|
|
```csharp
|
|
using DigitalData.EmailProfiler.Domain.Entities;
|
|
|
|
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
|
|
|
public interface IEmailHistoryRepository
|
|
{
|
|
Task<EmailHistory?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
|
Task<EmailHistory?> GetByMessageIdHashAsync(string hash, CancellationToken cancellationToken = default);
|
|
Task<bool> ExistsAsync(string messageIdHash, CancellationToken cancellationToken = default);
|
|
Task<List<EmailHistory>> GetByProfileIdAsync(int profileId, DateTime? from, DateTime? to, CancellationToken cancellationToken = default);
|
|
Task<int> AddAsync(EmailHistory history, CancellationToken cancellationToken = default);
|
|
Task UpdateAsync(EmailHistory history, CancellationToken cancellationToken = default);
|
|
}
|
|
```
|
|
|
|
**IEmailProcessRepository.cs**, **IEmailOutboxRepository.cs** - Similar patterns.
|
|
|
|
#### 2.2. Create Service Interfaces
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.Application/Interfaces/Services/`
|
|
|
|
**IEmailService.cs**:
|
|
```csharp
|
|
namespace DigitalData.EmailProfiler.Application.Interfaces.Services;
|
|
|
|
public interface IEmailService
|
|
{
|
|
Task<List<EmailMessage>> FetchUnreadEmailsAsync(
|
|
EmailAccount account,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<bool> TestConnectionAsync(
|
|
EmailAccount account,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task SendEmailAsync(
|
|
EmailAccount account,
|
|
string recipient,
|
|
string subject,
|
|
string body,
|
|
bool isHtml = true,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task DeleteEmailAsync(EmailAccount account, int imapUid, CancellationToken cancellationToken = default);
|
|
Task MoveEmailAsync(EmailAccount account, int imapUid, string folderName, CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public class EmailMessage
|
|
{
|
|
public int ImapUid { get; set; }
|
|
public string MessageId { get; set; } = string.Empty;
|
|
public string From { get; set; } = string.Empty;
|
|
public string Subject { get; set; } = string.Empty;
|
|
public DateTime Date { get; set; }
|
|
public string BodyHtml { get; set; } = string.Empty;
|
|
public string BodyText { get; set; } = string.Empty;
|
|
public List<EmailAttachmentData> Attachments { get; set; } = new();
|
|
public byte[] RawEmailData { get; set; } = Array.Empty<byte>();
|
|
}
|
|
|
|
public class EmailAttachmentData
|
|
{
|
|
public string FileName { get; set; } = string.Empty;
|
|
public string ContentType { get; set; } = string.Empty;
|
|
public byte[] Data { get; set; } = Array.Empty<byte>();
|
|
}
|
|
```
|
|
|
|
**IPdfProcessingService.cs**, **IDmsService.cs**, **IEncryptionService.cs**, **IEmailQueue.cs** - See agents.md for examples.
|
|
|
|
#### 2.3. Create MediatR Commands
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.Application/EmailProfiles/Commands/`
|
|
|
|
**CreateEmailProfileCommand.cs**:
|
|
```csharp
|
|
using MediatR;
|
|
|
|
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
|
|
|
|
public record CreateEmailProfileCommand(
|
|
string ProfileName,
|
|
int EmailAccountId,
|
|
int? ProcessId,
|
|
int PollIntervalMinutes) : IRequest<int>;
|
|
|
|
public class CreateEmailProfileCommandHandler : IRequestHandler<CreateEmailProfileCommand, int>
|
|
{
|
|
private readonly IEmailProfileRepository _repository;
|
|
|
|
public CreateEmailProfileCommandHandler(IEmailProfileRepository repository)
|
|
{
|
|
_repository = repository;
|
|
}
|
|
|
|
public async Task<int> Handle(CreateEmailProfileCommand request, CancellationToken cancellationToken)
|
|
{
|
|
var profile = new EmailProfile
|
|
{
|
|
ProfileName = request.ProfileName,
|
|
EmailAccountId = request.EmailAccountId,
|
|
ProcessId = request.ProcessId,
|
|
PollIntervalMinutes = request.PollIntervalMinutes,
|
|
IsActive = true,
|
|
AddedWhen = DateTime.UtcNow
|
|
};
|
|
|
|
return await _repository.AddAsync(profile, cancellationToken);
|
|
}
|
|
}
|
|
```
|
|
|
|
**UpdateEmailProfileCommand.cs**, **DeleteEmailProfileCommand.cs**, **ActivateProfileCommand.cs** - Similar patterns.
|
|
|
|
#### 2.4. Create MediatR Queries
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.Application/EmailProfiles/Queries/`
|
|
|
|
**GetEmailProfilesQuery.cs**:
|
|
```csharp
|
|
using MediatR;
|
|
using AutoMapper;
|
|
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
|
|
|
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Queries;
|
|
|
|
public record GetEmailProfilesQuery : IRequest<List<EmailProfileDto>>;
|
|
|
|
public class GetEmailProfilesQueryHandler : IRequestHandler<GetEmailProfilesQuery, List<EmailProfileDto>>
|
|
{
|
|
private readonly IEmailProfileRepository _repository;
|
|
private readonly IMapper _mapper;
|
|
|
|
public GetEmailProfilesQueryHandler(IEmailProfileRepository repository, IMapper mapper)
|
|
{
|
|
_repository = repository;
|
|
_mapper = mapper;
|
|
}
|
|
|
|
public async Task<List<EmailProfileDto>> Handle(GetEmailProfilesQuery request, CancellationToken cancellationToken)
|
|
{
|
|
var profiles = await _repository.GetAllAsync(cancellationToken);
|
|
return _mapper.Map<List<EmailProfileDto>>(profiles);
|
|
}
|
|
}
|
|
```
|
|
|
|
**GetEmailProfileByIdQuery.cs**, **GetActiveProfilesQuery.cs**, **GetProfilesDueForPollingQuery.cs** - Similar patterns.
|
|
|
|
#### 2.5. Create Validators
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.Application/EmailProfiles/Validators/`
|
|
|
|
**CreateEmailProfileCommandValidator.cs**:
|
|
```csharp
|
|
using FluentValidation;
|
|
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
|
|
|
|
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Validators;
|
|
|
|
public class CreateEmailProfileCommandValidator : AbstractValidator<CreateEmailProfileCommand>
|
|
{
|
|
public CreateEmailProfileCommandValidator()
|
|
{
|
|
RuleFor(x => x.ProfileName)
|
|
.NotEmpty().WithMessage("Profile name is required")
|
|
.MaximumLength(100).WithMessage("Profile name must not exceed 100 characters");
|
|
|
|
RuleFor(x => x.EmailAccountId)
|
|
.GreaterThan(0).WithMessage("Email account ID must be greater than 0");
|
|
|
|
RuleFor(x => x.PollIntervalMinutes)
|
|
.GreaterThan(0).WithMessage("Poll interval must be greater than 0")
|
|
.LessThanOrEqualTo(1440).WithMessage("Poll interval must not exceed 1440 minutes (24 hours)");
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2.6. Create AutoMapper Profiles
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.Application/Common/Mappings/`
|
|
|
|
**MappingProfile.cs**:
|
|
```csharp
|
|
using AutoMapper;
|
|
using DigitalData.EmailProfiler.Domain.Entities;
|
|
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
|
|
|
namespace DigitalData.EmailProfiler.Application.Common.Mappings;
|
|
|
|
public class MappingProfile : Profile
|
|
{
|
|
public MappingProfile()
|
|
{
|
|
// EmailProfile mappings
|
|
CreateMap<EmailProfile, EmailProfileDto>()
|
|
.ForMember(d => d.EmailAccountName, opt => opt.MapFrom(s => s.EmailAccount != null ? s.EmailAccount.AccountName : null))
|
|
.ForMember(d => d.ProcessName, opt => opt.MapFrom(s => s.EmailProcess != null ? s.EmailProcess.ProcessName : null));
|
|
|
|
// EmailAccount mappings
|
|
CreateMap<EmailAccount, EmailAccountDto>();
|
|
|
|
// EmailHistory mappings
|
|
CreateMap<EmailHistory, EmailHistoryDto>()
|
|
.ForMember(d => d.ProfileName, opt => opt.MapFrom(s => s.Profile != null ? s.Profile.ProfileName : null))
|
|
.ForMember(d => d.Attachments, opt => opt.MapFrom(s => s.Attachments));
|
|
|
|
// EmailAttachment mappings
|
|
CreateMap<EmailAttachment, EmailAttachmentDto>();
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2.7. Create DependencyInjection.cs
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.Application/DependencyInjection.cs`
|
|
|
|
```csharp
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using FluentValidation;
|
|
using System.Reflection;
|
|
|
|
namespace DigitalData.EmailProfiler.Application;
|
|
|
|
public static class DependencyInjection
|
|
{
|
|
public static IServiceCollection AddApplication(this IServiceCollection services)
|
|
{
|
|
var assembly = Assembly.GetExecutingAssembly();
|
|
|
|
// MediatR
|
|
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly));
|
|
|
|
// AutoMapper
|
|
services.AddAutoMapper(assembly);
|
|
|
|
// FluentValidation
|
|
services.AddValidatorsFromAssembly(assembly);
|
|
|
|
return services;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### PHASE 3: Infrastructure Layer
|
|
|
|
#### 3.1. Add NuGet Packages
|
|
|
|
```bash
|
|
cd src/DigitalData.EmailProfiler.Infrastructure
|
|
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
|
|
dotnet add package Microsoft.EntityFrameworkCore.Tools
|
|
dotnet add package MailKit
|
|
dotnet add package MimeKit
|
|
dotnet add package PdfSharp
|
|
dotnet add package Microsoft.Identity.Client
|
|
dotnet add package Microsoft.AspNetCore.DataProtection
|
|
```
|
|
|
|
#### 3.2. Create DbContext
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.Infrastructure/Persistence/EmailProfilerDbContext.cs`
|
|
|
|
```csharp
|
|
using Microsoft.EntityFrameworkCore;
|
|
using DigitalData.EmailProfiler.Domain.Entities;
|
|
using System.Reflection;
|
|
|
|
namespace DigitalData.EmailProfiler.Infrastructure.Persistence;
|
|
|
|
public class EmailProfilerDbContext : DbContext
|
|
{
|
|
public EmailProfilerDbContext(DbContextOptions<EmailProfilerDbContext> options) : base(options) { }
|
|
|
|
public DbSet<EmailAccount> EmailAccounts { get; set; }
|
|
public DbSet<EmailProfile> EmailProfiles { get; set; }
|
|
public DbSet<EmailProcess> EmailProcesses { get; set; }
|
|
public DbSet<ProcessStep> ProcessSteps { get; set; }
|
|
public DbSet<IndexingStep> IndexingSteps { get; set; }
|
|
public DbSet<EmailHistory> EmailHistories { get; set; }
|
|
public DbSet<EmailAttachment> EmailAttachments { get; set; }
|
|
public DbSet<EmailOutbox> EmailOutbox { get; set; }
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
base.OnModelCreating(modelBuilder);
|
|
|
|
// Apply configurations from assembly (if you create IEntityTypeConfiguration classes)
|
|
// modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
|
|
|
// Note: All entity configurations are already done via attributes in Domain entities
|
|
// This is important - DO NOT modify database schema here!
|
|
}
|
|
|
|
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
// Auto-populate audit fields
|
|
var entries = ChangeTracker.Entries<BaseEntity>();
|
|
|
|
foreach (var entry in entries)
|
|
{
|
|
if (entry.State == EntityState.Added)
|
|
{
|
|
entry.Entity.CreatedDate = DateTime.UtcNow;
|
|
entry.Entity.CreatedBy = "System"; // TODO: Get from current user context
|
|
}
|
|
|
|
if (entry.State == EntityState.Modified)
|
|
{
|
|
entry.Entity.ModifiedDate = DateTime.UtcNow;
|
|
entry.Entity.ModifiedBy = "System"; // TODO: Get from current user context
|
|
}
|
|
}
|
|
|
|
return base.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3.3. Create Repositories
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.Infrastructure/Persistence/Repositories/`
|
|
|
|
**EmailProfileRepository.cs**:
|
|
```csharp
|
|
using Microsoft.EntityFrameworkCore;
|
|
using DigitalData.EmailProfiler.Domain.Entities;
|
|
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
|
|
|
namespace DigitalData.EmailProfiler.Infrastructure.Persistence.Repositories;
|
|
|
|
public class EmailProfileRepository : IEmailProfileRepository
|
|
{
|
|
private readonly EmailProfilerDbContext _context;
|
|
|
|
public EmailProfileRepository(EmailProfilerDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
public async Task<EmailProfile?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.EmailProfiles
|
|
.Include(p => p.EmailAccount)
|
|
.Include(p => p.EmailProcess)
|
|
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
|
}
|
|
|
|
public async Task<List<EmailProfile>> GetAllAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.EmailProfiles
|
|
.Include(p => p.EmailAccount)
|
|
.Include(p => p.EmailProcess)
|
|
.OrderBy(p => p.Sequence)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<List<EmailProfile>> GetActiveProfilesAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
return await _context.EmailProfiles
|
|
.Include(p => p.EmailAccount)
|
|
.Include(p => p.EmailProcess)
|
|
.Where(p => p.IsActive && p.EmailAccount!.IsActive)
|
|
.OrderBy(p => p.Sequence)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<List<EmailProfile>> GetProfilesDueForPollingAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
|
|
return await _context.EmailProfiles
|
|
.Include(p => p.EmailAccount)
|
|
.Include(p => p.EmailProcess)
|
|
.Where(p => p.IsActive
|
|
&& p.EmailAccount!.IsActive
|
|
&& (!p.LastPollTime.HasValue ||
|
|
EF.Functions.DateDiffMinute(p.LastPollTime.Value, now) >= p.PollIntervalMinutes))
|
|
.OrderBy(p => p.Sequence)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<int> AddAsync(EmailProfile profile, CancellationToken cancellationToken = default)
|
|
{
|
|
_context.EmailProfiles.Add(profile);
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
return profile.Id;
|
|
}
|
|
|
|
public async Task UpdateAsync(EmailProfile profile, CancellationToken cancellationToken = default)
|
|
{
|
|
_context.EmailProfiles.Update(profile);
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task DeleteAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
var profile = await GetByIdAsync(id, cancellationToken);
|
|
if (profile != null)
|
|
{
|
|
_context.EmailProfiles.Remove(profile);
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Create similar repositories for **EmailAccountRepository**, **EmailHistoryRepository**, etc.
|
|
|
|
#### 3.4. Create External Services
|
|
|
|
**MailKitEmailService.cs**, **PdfSharpProcessingService.cs**, **WindreamDmsService.cs**, **EncryptionService.cs**, **InMemoryEmailQueue.cs**
|
|
|
|
(See agents.md for examples - these are complex services)
|
|
|
|
#### 3.5. Create DependencyInjection.cs
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.Infrastructure/DependencyInjection.cs`
|
|
|
|
```csharp
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using DigitalData.EmailProfiler.Infrastructure.Persistence;
|
|
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
|
using DigitalData.EmailProfiler.Infrastructure.Persistence.Repositories;
|
|
|
|
namespace DigitalData.EmailProfiler.Infrastructure;
|
|
|
|
public static class DependencyInjection
|
|
{
|
|
public static IServiceCollection AddInfrastructure(
|
|
this IServiceCollection services,
|
|
IConfiguration configuration)
|
|
{
|
|
// DbContext
|
|
services.AddDbContext<EmailProfilerDbContext>(options =>
|
|
options.UseSqlServer(
|
|
configuration.GetConnectionString("DefaultConnection"),
|
|
sqlOptions =>
|
|
{
|
|
sqlOptions.EnableRetryOnFailure(
|
|
maxRetryCount: 5,
|
|
maxRetryDelay: TimeSpan.FromSeconds(30),
|
|
errorNumbersToAdd: null);
|
|
sqlOptions.CommandTimeout(60);
|
|
}));
|
|
|
|
// Repositories
|
|
services.AddScoped<IEmailProfileRepository, EmailProfileRepository>();
|
|
services.AddScoped<IEmailAccountRepository, EmailAccountRepository>();
|
|
services.AddScoped<IEmailHistoryRepository, EmailHistoryRepository>();
|
|
// ... add other repositories
|
|
|
|
// External Services
|
|
// services.AddScoped<IEmailService, MailKitEmailService>();
|
|
// services.AddScoped<IPdfProcessingService, PdfSharpProcessingService>();
|
|
// services.AddScoped<IDmsService, WindreamDmsService>();
|
|
// services.AddScoped<IEncryptionService, DataProtectionEncryptionService>();
|
|
// services.AddSingleton<IEmailQueue, InMemoryEmailQueue>();
|
|
|
|
return services;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### PHASE 4: API Layer
|
|
|
|
#### 4.1. Add NuGet Packages
|
|
|
|
```bash
|
|
cd src/DigitalData.EmailProfiler.API
|
|
dotnet add package Serilog.AspNetCore
|
|
dotnet add package Serilog.Sinks.File
|
|
dotnet add package Serilog.Sinks.MSSqlServer
|
|
dotnet add package Scalar.AspNetCore
|
|
```
|
|
|
|
#### 4.2. Update Program.cs
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.API/Program.cs`
|
|
|
|
```csharp
|
|
using DigitalData.EmailProfiler.API;
|
|
using DigitalData.EmailProfiler.Application;
|
|
using DigitalData.EmailProfiler.Infrastructure;
|
|
using Serilog;
|
|
using Scalar.AspNetCore;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Configure Serilog
|
|
Log.Logger = new LoggerConfiguration()
|
|
.ReadFrom.Configuration(builder.Configuration)
|
|
.Enrich.FromLogContext()
|
|
.WriteTo.Console()
|
|
.WriteTo.File("logs/emailprofiler-.log", rollingInterval: RollingInterval.Day)
|
|
.CreateLogger();
|
|
|
|
builder.Host.UseSerilog();
|
|
|
|
// Check for Windows Service mode
|
|
if (builder.Configuration["Hosting:Mode"] == "WindowsService")
|
|
{
|
|
builder.Host.UseWindowsService();
|
|
}
|
|
|
|
// Add services
|
|
builder.Services.AddControllers();
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen();
|
|
|
|
// Add Application and Infrastructure layers
|
|
builder.Services.AddApplication();
|
|
builder.Services.AddInfrastructure(builder.Configuration);
|
|
|
|
// Add Background Workers
|
|
// builder.Services.AddHostedService<EmailPollingWorker>();
|
|
// builder.Services.AddHostedService<EmailSenderWorker>();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Configure the HTTP request pipeline
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
|
|
// Add Scalar
|
|
app.MapScalarApiReference();
|
|
}
|
|
|
|
app.UseHttpsRedirection();
|
|
app.UseAuthorization();
|
|
app.MapControllers();
|
|
|
|
try
|
|
{
|
|
Log.Information("Starting EmailProfiler API");
|
|
app.Run();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Fatal(ex, "Application start-up failed");
|
|
}
|
|
finally
|
|
{
|
|
Log.CloseAndFlush();
|
|
}
|
|
```
|
|
|
|
#### 4.3. Create Controllers
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.API/Controllers/`
|
|
|
|
**EmailProfilesController.cs**:
|
|
```csharp
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using MediatR;
|
|
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
|
|
using DigitalData.EmailProfiler.Application.EmailProfiles.Queries;
|
|
|
|
namespace DigitalData.EmailProfiler.API.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class EmailProfilesController : ControllerBase
|
|
{
|
|
private readonly IMediator _mediator;
|
|
private readonly ILogger<EmailProfilesController> _logger;
|
|
|
|
public EmailProfilesController(IMediator mediator, ILogger<EmailProfilesController> logger)
|
|
{
|
|
_mediator = mediator;
|
|
_logger = logger;
|
|
}
|
|
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetAll(CancellationToken cancellationToken)
|
|
{
|
|
var query = new GetEmailProfilesQuery();
|
|
var result = await _mediator.Send(query, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpGet("{id}")]
|
|
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
|
|
{
|
|
var query = new GetEmailProfileByIdQuery(id);
|
|
var result = await _mediator.Send(query, cancellationToken);
|
|
|
|
if (result == null)
|
|
return NotFound();
|
|
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> Create(CreateEmailProfileCommand command, CancellationToken cancellationToken)
|
|
{
|
|
var id = await _mediator.Send(command, cancellationToken);
|
|
return CreatedAtAction(nameof(GetById), new { id }, id);
|
|
}
|
|
|
|
// Add Update, Delete, Activate, Deactivate endpoints
|
|
}
|
|
```
|
|
|
|
Create similar controllers for **EmailAccountsController**, **EmailHistoryController**, **DashboardController**.
|
|
|
|
#### 4.4. Create Background Workers
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.API/Workers/`
|
|
|
|
**EmailPollingWorker.cs** and **EmailSenderWorker.cs** (See agents.md for implementation examples)
|
|
|
|
#### 4.5. Update appsettings.json
|
|
|
|
**Location**: `src/DigitalData.EmailProfiler.API/appsettings.json`
|
|
|
|
```json
|
|
{
|
|
"ConnectionStrings": {
|
|
"DefaultConnection": "Server=(local);Database=DD_ECM;Integrated Security=true;TrustServerCertificate=true"
|
|
},
|
|
"Serilog": {
|
|
"MinimumLevel": {
|
|
"Default": "Information",
|
|
"Override": {
|
|
"Microsoft": "Warning",
|
|
"System": "Warning"
|
|
}
|
|
}
|
|
},
|
|
"Workers": {
|
|
"EmailPolling": {
|
|
"Enabled": true,
|
|
"IntervalSeconds": 60
|
|
},
|
|
"EmailSender": {
|
|
"Enabled": true,
|
|
"IntervalSeconds": 5
|
|
}
|
|
},
|
|
"Hosting": {
|
|
"Mode": "IIS"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### PHASE 5: Testing
|
|
|
|
#### 5.1. Add NuGet Packages
|
|
|
|
```bash
|
|
cd tests/DigitalData.EmailProfiler.Tests
|
|
dotnet add package FakeItEasy
|
|
dotnet add package Bogus
|
|
dotnet add package FluentAssertions
|
|
dotnet add package Microsoft.AspNetCore.Mvc.Testing
|
|
dotnet add package Testcontainers.MsSql
|
|
```
|
|
|
|
#### 5.2. Create Unit Tests
|
|
|
|
**Location**: `tests/DigitalData.EmailProfiler.Tests/Unit/Domain/`
|
|
|
|
**MessageIdGeneratorTests.cs**:
|
|
```csharp
|
|
using Xunit;
|
|
using FluentAssertions;
|
|
using DigitalData.EmailProfiler.Domain.Services;
|
|
|
|
namespace DigitalData.EmailProfiler.Tests.Unit.Domain;
|
|
|
|
public class MessageIdGeneratorTests
|
|
{
|
|
[Fact]
|
|
public void Generate_ShouldCreateValidMessageId()
|
|
{
|
|
// Arrange
|
|
var generator = new MessageIdGenerator();
|
|
var original = "test-msg-123";
|
|
var sender = "sender@example.com";
|
|
var date = new DateTime(2026, 1, 1, 12, 0, 0);
|
|
var subject = "Test Subject";
|
|
|
|
// Act
|
|
var messageId = generator.Generate(original, sender, date, subject);
|
|
|
|
// Assert
|
|
messageId.Should().NotBeNull();
|
|
messageId.Hash.Should().NotBeNullOrEmpty();
|
|
messageId.Value.Should().Contain(original);
|
|
messageId.Value.Should().Contain(sender);
|
|
}
|
|
|
|
[Fact]
|
|
public void Generate_SameInput_ShouldProduceSameHash()
|
|
{
|
|
// Arrange
|
|
var generator = new MessageIdGenerator();
|
|
var original = "test-msg-123";
|
|
var sender = "sender@example.com";
|
|
var date = new DateTime(2026, 1, 1, 12, 0, 0);
|
|
var subject = "Test Subject";
|
|
|
|
// Act
|
|
var messageId1 = generator.Generate(original, sender, date, subject);
|
|
var messageId2 = generator.Generate(original, sender, date, subject);
|
|
|
|
// Assert
|
|
messageId1.Hash.Should().Be(messageId2.Hash);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 5.3. Create Integration Tests
|
|
|
|
Use Testcontainers for database integration tests.
|
|
|
|
---
|
|
|
|
### PHASE 6: Documentation
|
|
|
|
#### 6.1. Create README.md (in German)
|
|
|
|
**Location**: `README.md`
|
|
|
|
The README should include (in German):
|
|
- Application overview
|
|
- Architecture diagram
|
|
- API endpoints documentation
|
|
- Worker processes description
|
|
- Database tables documentation
|
|
- Configuration guide (appsettings.json)
|
|
- Deployment instructions (IIS and Windows Service)
|
|
- Troubleshooting guide
|
|
|
|
**Template structure**:
|
|
```markdown
|
|
# DigitalData EmailProfiler
|
|
|
|
## Übersicht
|
|
[Application overview in German]
|
|
|
|
## Architektur
|
|
[Architecture description]
|
|
|
|
## API Endpunkte
|
|
|
|
### Email Profile Management
|
|
- GET /api/emailprofiles - Alle Profile abrufen
|
|
- GET /api/emailprofiles/{id} - Profil nach ID abrufen
|
|
- POST /api/emailprofiles - Neues Profil erstellen
|
|
- PUT /api/emailprofiles/{id} - Profil aktualisieren
|
|
- DELETE /api/emailprofiles/{id} - Profil löschen
|
|
|
|
[... continue for all controllers]
|
|
|
|
## Background Workers
|
|
|
|
### EmailPollingWorker
|
|
Überwacht E-Mail-Konten und verarbeitet eingehende E-Mails.
|
|
|
|
**Konfiguration**:
|
|
```json
|
|
"Workers": {
|
|
"EmailPolling": {
|
|
"Enabled": true,
|
|
"IntervalSeconds": 60
|
|
}
|
|
}
|
|
```
|
|
|
|
[... continue for all workers]
|
|
|
|
## Datenbank Tabellen
|
|
|
|
### TBDD_EMAIL_ACCOUNT
|
|
[Table description]
|
|
|
|
[... continue for all tables]
|
|
|
|
## Konfiguration
|
|
|
|
[Detailed configuration guide]
|
|
|
|
## Deployment
|
|
|
|
### IIS Deployment
|
|
[Step-by-step guide]
|
|
|
|
### Windows Service Deployment
|
|
[Step-by-step guide]
|
|
```
|
|
|
|
---
|
|
|
|
## Build and Test Commands
|
|
|
|
```bash
|
|
# Build solution
|
|
dotnet build
|
|
|
|
# Run tests
|
|
dotnet test
|
|
|
|
# Run API
|
|
cd src/DigitalData.EmailProfiler.API
|
|
dotnet run
|
|
|
|
# Create migration
|
|
cd src/DigitalData.EmailProfiler.Infrastructure
|
|
dotnet ef migrations add InitialCreate --startup-project ../DigitalData.EmailProfiler.API
|
|
|
|
# Update database
|
|
dotnet ef database update --startup-project ../DigitalData.EmailProfiler.API
|
|
```
|
|
|
|
---
|
|
|
|
## Important Reminders for AI Agents
|
|
|
|
1. **NEVER modify database schema** - use `[Table]` and `[Column]` attributes
|
|
2. **NEVER commit to git** - wait for user instruction
|
|
3. **All code and comments in English** - except README.md (German)
|
|
4. **Use Serilog for logging** - structured logging
|
|
5. **Worker intervals configurable** - via appsettings.json
|
|
6. **Support IIS and Windows Service** - via configuration
|
|
7. **Check for database triggers** - add to DbContext if they exist
|
|
8. **RabbitMQ is future enhancement** - currently use InMemoryEmailQueue
|
|
|
|
---
|
|
|
|
## Next Steps for Continuation
|
|
|
|
1. Complete Application Layer (Commands, Queries, Validators)
|
|
2. Complete Infrastructure Layer (DbContext, Repositories, Services)
|
|
3. Complete API Layer (Controllers, Workers, Middleware)
|
|
4. Create comprehensive tests
|
|
5. Write README.md in German
|
|
6. Build and test the complete application
|
|
|
|
---
|
|
|
|
**Document Version**: 1.0
|
|
**Last Updated**: 2026-07-07
|
|
**Status**: Phase 1 Complete, Phase 2-6 Pending
|