955 lines
29 KiB
Markdown
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
|