docs: add comprehensive implementation guide for AI agents
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.
This commit is contained in:
954
IMPLEMENTATION_GUIDE.md
Normal file
954
IMPLEMENTATION_GUIDE.md
Normal file
@@ -0,0 +1,954 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user