Files
DigitalData.MessagingService/IMPLEMENTATION_GUIDE.md

955 lines
29 KiB
Markdown

# MessagingService - Implementation Guide for AI Agents
## Overview
This guide provides step-by-step instructions for AI agents to continue the implementation of the MessagingService 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.MessagingService/
├── 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.MessagingService.Application/Interfaces/Repositories/`
Create these files:
**IEmailProfileRepository.cs**:
```csharp
using DigitalData.MessagingService.Domain.Entities;
namespace DigitalData.MessagingService.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.MessagingService.Domain.Entities;
namespace DigitalData.MessagingService.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.MessagingService.Domain.Entities;
namespace DigitalData.MessagingService.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.MessagingService.Application/Interfaces/Services/`
**IEmailService.cs**:
```csharp
namespace DigitalData.MessagingService.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.MessagingService.Application/EmailProfiles/Commands/`
**CreateEmailProfileCommand.cs**:
```csharp
using MediatR;
namespace DigitalData.MessagingService.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.MessagingService.Application/EmailProfiles/Queries/`
**GetEmailProfilesQuery.cs**:
```csharp
using MediatR;
using AutoMapper;
using DigitalData.MessagingService.Application.Common.Dtos;
namespace DigitalData.MessagingService.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.MessagingService.Application/EmailProfiles/Validators/`
**CreateEmailProfileCommandValidator.cs**:
```csharp
using FluentValidation;
using DigitalData.MessagingService.Application.EmailProfiles.Commands;
namespace DigitalData.MessagingService.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.MessagingService.Application/Common/Mappings/`
**MappingProfile.cs**:
```csharp
using AutoMapper;
using DigitalData.MessagingService.Domain.Entities;
using DigitalData.MessagingService.Application.Common.Dtos;
namespace DigitalData.MessagingService.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.MessagingService.Application/DependencyInjection.cs`
```csharp
using Microsoft.Extensions.DependencyInjection;
using FluentValidation;
using System.Reflection;
namespace DigitalData.MessagingService.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.MessagingService.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.MessagingService.Infrastructure/Persistence/MessagingServiceDbContext.cs`
```csharp
using Microsoft.EntityFrameworkCore;
using DigitalData.MessagingService.Domain.Entities;
using System.Reflection;
namespace DigitalData.MessagingService.Infrastructure.Persistence;
public class MessagingServiceDbContext : DbContext
{
public MessagingServiceDbContext(DbContextOptions<MessagingServiceDbContext> 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.MessagingService.Infrastructure/Persistence/Repositories/`
**EmailProfileRepository.cs**:
```csharp
using Microsoft.EntityFrameworkCore;
using DigitalData.MessagingService.Domain.Entities;
using DigitalData.MessagingService.Application.Interfaces.Repositories;
namespace DigitalData.MessagingService.Infrastructure.Persistence.Repositories;
public class EmailProfileRepository : IEmailProfileRepository
{
private readonly MessagingServiceDbContext _context;
public EmailProfileRepository(MessagingServiceDbContext 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.MessagingService.Infrastructure/DependencyInjection.cs`
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using DigitalData.MessagingService.Infrastructure.Persistence;
using DigitalData.MessagingService.Application.Interfaces.Repositories;
using DigitalData.MessagingService.Infrastructure.Persistence.Repositories;
namespace DigitalData.MessagingService.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// DbContext
services.AddDbContext<MessagingServiceDbContext>(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.MessagingService.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.MessagingService.API/Program.cs`
```csharp
using DigitalData.MessagingService.API;
using DigitalData.MessagingService.Application;
using DigitalData.MessagingService.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 MessagingService API");
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application start-up failed");
}
finally
{
Log.CloseAndFlush();
}
```
#### 4.3. Create Controllers
**Location**: `src/DigitalData.MessagingService.API/Controllers/`
**EmailProfilesController.cs**:
```csharp
using Microsoft.AspNetCore.Mvc;
using MediatR;
using DigitalData.MessagingService.Application.EmailProfiles.Commands;
using DigitalData.MessagingService.Application.EmailProfiles.Queries;
namespace DigitalData.MessagingService.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.MessagingService.API/Workers/`
**EmailPollingWorker.cs** and **EmailSenderWorker.cs** (See agents.md for implementation examples)
#### 4.5. Update appsettings.json
**Location**: `src/DigitalData.MessagingService.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.MessagingService.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.MessagingService.Tests/Unit/Domain/`
**MessageIdGeneratorTests.cs**:
```csharp
using Xunit;
using FluentAssertions;
using DigitalData.MessagingService.Domain.Services;
namespace DigitalData.MessagingService.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 MessagingService
## Ü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.MessagingService.API
dotnet run
# Create migration
cd src/DigitalData.MessagingService.Infrastructure
dotnet ef migrations add InitialCreate --startup-project ../DigitalData.MessagingService.API
# Update database
dotnet ef database update --startup-project ../DigitalData.MessagingService.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