- ARCHITECTURE.md: reflect the implemented DI service layer, CAMTParser, OpenTelemetry/observability, the ported report engine, and CAMT+MT940 banking; mark the resolved observations. - copilot-instructions.md: add Services/DI, dual-format banking, observability and testing sections; add an Instruction-Sync banner. - CLAUDE.md (new): Claude Code project instructions mirroring the shared rules, plus build/test workflow notes. Both files state they must stay in sync. - USER_GUIDE.md (new, Fuchs/Docs): end-user process guide (login, invoices, reminders, requests, banking incl. MT940/CAMT upload, DATEV, reports). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
24 KiB
Fuchs Intranet — Solution Architecture
Auto-generated architecture analysis
.NET 10 · ASP.NET Core MVC · SQL Server
1. Solution Overview
The Fuchs Intranet solution is a line-of-business web application for Sebastian Fuchs Bad und Heizung GmbH & Co. KG (a plumbing/heating company in Düsseldorf). It manages invoices, reminders, service requests, banking transactions, reports, and user authentication — all exposed through a single-page intranet front-end.
| Project | Type | Purpose |
|---|---|---|
| Fuchs | ASP.NET Core Web (MVC) | Main web application — the intranet |
| Fuchs_DataService | Console / Windows Service (Topshelf) | Background data sync service (MFR ERP polling) |
| MFR_RESTClient | Class Library | REST/OData client for the MFR ERP system |
| OCORE | Class Library (shared) | Core utilities: SQL, crypto, email, IO, logging |
| OCORE_web | Class Library (shared) | Web utilities: MVC helpers, middleware, auth, captcha |
| OCORE_web_pdf | Class Library (shared) | PDF generation (MigraDoc/PDFsharp, HTML→PDF) |
| OCORE_Charting | Class Library (shared) | Data visualization / charting (ported System.Windows.Forms.DataVisualization) |
| MT940Parser | Class Library (external) | SWIFT MT940/MT942 bank statement parser |
| CAMTParser | Class Library (in-repo) | ISO 20022 CAMT (camt.052/053/054) bank statement parser |
All projects target net10.0.
2. Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────┐
│ CLIENTS (Browser) │
│ SPA-like JS front-end (js/intranet/) │
└──────────────────────────────┬──────────────────────────────────────────┘
│ HTTP (GET / POST)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Fuchs (ASP.NET Core MVC) │
│ │
│ Program.cs ─── ConfigureServices / ConfigureApp │
│ │ │
│ ├── Cookie Authentication (scheme: "fuchs_intranet") │
│ ├── Distributed Memory Cache │
│ └── DI Registrations: │
│ • Fuchs_intranet (singleton — config + auth + DB helper) │
│ • IFdsMfr → FdsMfr (singleton — ERP sync) │
│ │
│ Routes: │
│ /{fn?}/{id?}/{code?} → IntranetController.Index (SPA shell) │
│ /do/{fn?}/{id?}/{code?} → IntranetController.Do (API) │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ IntranetController (partial class) │ │
│ │ │ │
│ │ .cs — core: Index, Do dispatcher, Auth, Login/Logout │ │
│ │ .Invoices.cs — invoice CRUD, DATEV export, PDF │ │
│ │ .Invoices2.cs— invoice sub-handlers (MFR refresh, get, list) │ │
│ │ .Reminder.cs — payment reminder CRUD + PDF │ │
│ │ .Requests.cs — service request management │ │
│ │ .Banking.cs — MT940 upload, transaction queries │ │
│ │ .Reports.cs — report catalog, execution │ │
│ └──────────┬──────────────────────────────────────────────────────┘ │
│ │ calls │
│ ┌──────────▼──────────────────────────────────────────────────────┐ │
│ │ code/ (Business Logic) │ │
│ │ │ │
│ │ FuchsIntranet.cs — Fuchs_intranet singleton (config, auth, │ │
│ │ DB connections, debug logging) │ │
│ │ FdsInvoiceData.cs — Invoice data model + PDF generation │ │
│ │ FdsReminderData.cs — Reminder data model + PDF generation │ │
│ │ FuchsPdf.cs — PDF layout/rendering (MigraDoc) │ │
│ │ FuchsWidgets.cs — Dashboard widget data (SQL-driven) │ │
│ │ FuchsReports.cs — Report dispatch │ │
│ │ FuchsFdsEmail.cs — Email sending + DB logging │ │
│ │ Banking.cs — MT940 parsing to DataTable │ │
│ │ MigraDocExtensions — MigraDoc helper extensions │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Logging/FuchsLoggerProvider.cs — custom ILoggerProvider │
└────────┬────────────────┬──────────────────┬───────────────────────┬────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌────────────────┐ ┌──────────────────┐
│ OCORE │ │ OCORE_web │ │ OCORE_web_pdf │ │ MT940Parser │
│ │ │ │ │ │ │ │
│ • SQL helper │ │ • MVC helper │ │ • MigraDoc/ │ │ • SWIFT MT940/ │
│ (ADO.NET) │ │ (JSON,File)│ │ PDFsharp │ │ MT942 parser │
│ • Email │ │ • Cookie Auth│ │ • HTML→PDF │ │ │
│ • IO/Files │ │ • Middleware │ │ • Font resolver │ │ │
│ • Crypto │ │ • Captcha │ │ │ │ │
│ • Logging │ │ • Background │ │ │ │ │
│ • CSV/XML │ │ services │ │ │ │ │
│ • DateTime │ │ • Security │ │ │ │ │
└──────────────┘ └──────────────┘ └────────────────┘ └──────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Fuchs_DataService (Windows Service / Console) │
│ │
│ FdsMain.cs — Topshelf host, job definitions │
│ PeriodicHostedService — BackgroundService with PeriodicTimer │
│ FdsMfr.cs (IFdsMfr) — MFR sync orchestration │
│ FdsMfrClient.cs — MFR REST client wrapper │
│ FdsShared.cs — FdsConfig (appsettings.json reader) │
│ FdsZip.cs — 7-Zip archive handling (DATEV export) │
│ FdsDebug.cs — Debug/file logging │
│ │
│ Jobs: MfrSync (every N min) │
│ → UpdateIfNecessary_async (entity table sync) │
│ → UpdateRequested_async (on-demand entity refresh) │
│ → GetInvoiceFiles_async (invoice PDF download) │
└────────────────────────┬────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ MFR_RESTClient │
│ │
│ MFRClient.cs — RestSharp-based REST/OData client │
│ MFRClientModels.cs — Config, credentials, entity types │
│ ODataEnvelope.cs — OData response wrapper │
│ Entities/MfrGeneric.cs— Generic entity helpers │
└────────────────────────┬────────────────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ MFR ERP System │
│ (External REST) │
└─────────────────────┘
┌─────────────────────┐
All projects ──────►│ SQL Server │
│ (fuchs_fds DB) │
│ Stored procedures │
│ Symmetric key enc. │
└─────────────────────┘
3. Dependency Graph
Fuchs (Web)
├── OCORE
├── OCORE_web ──► OCORE
├── OCORE_web_pdf
├── MT940Parser
├── MFR_RESTClient
└── Fuchs_DataService
├── OCORE
├── OCORE_web
└── MFR_RESTClient
OCORE_Charting (standalone — referenced by solution but no direct project reference)
4. Key Architectural Patterns
4.1 Partial Controller Pattern
IntranetController is split across 7 partial-class files, each handling a business domain (invoices, reminders, banking, etc.). The main Do() action uses a switch expression to dispatch to domain-specific methods.
4.2 Singleton Configuration Object
Fuchs_intranet is a manually-managed singleton (via FuchsOcmsIntranet) initialized at startup with IConfiguration. It holds connection strings, app settings, auth helpers, and DB connection factory methods.
4.3 Service Layer (Dependency Injection)
Business logic lives in DI-registered services under Fuchs/Services/ behind interfaces, injected into IntranetController:
IComService, IPdfService, IInvoiceService, IReminderService, IReportService, IWidgetService, IBankingService, IMfrClientFactory.
Stateless services (IPdfService, IBankingService, IMfrClientFactory) are singletons; DB/request-scoped services are scoped (see Program.cs).
FdsInvoiceData / FdsReminderData are now pure data holders (parse + properties); loading, persistence and PDF generation live in the services (fully async — no Task.Run(...).Wait()).
FuchsPdf / FuchsVisualization remain as static rendering libraries used by the services. The earlier static, controller-coupled helpers (FuchsWidgets, FuchsReports, Banking, FuchsFdsEmail) have been removed.
4.4 SQL-First Data Access
There is no ORM (no EF Core). All data access uses ADO.NET via OCORE SQL helpers (getSQLDatatable_async, getSQLDataSet_async, setSQLValue_async) calling stored procedures and inline SQL. DataTable/DataRow is the primary data transfer mechanism.
4.5 Background Service
Fuchs_DataService runs as a Windows Service (Topshelf) with a PeriodicHostedService that polls the MFR ERP system on a timer, syncing entities and downloading invoice files.
4.6 Authentication
Cookie-based authentication (CookieAuthenticationDefaults) with custom claims (FuchsUserIdentity). SQL-based user/password verification.
5. Service Layer (implemented)
Status: DONE. The services below are implemented and DI-registered in
Program.cs. The original extraction rationale is retained for reference / history.FuchsFdsEmail→IComService(ProcessWeb Mailer API, inline base64 attachments),FuchsWidgets→IWidgetService,FuchsPdf→IPdfService,Banking→IBankingService(now MT940 and CAMT),FdsInvoiceData/FdsReminderData→IInvoiceService/IReminderService(data classes are now pure POCOs),FuchsReports→IReportService(backed by the portedFuchsVisualizationengine),FdsMfrClient→IMfrClientFactory.
5.1 FuchsFdsEmail → IEmailService
Current state: Static class using System.Configuration.ConfigurationManager (legacy!) and receiving Fuchs_intranet as a parameter.
Problem: Uses cfg.AppSettings["FDS_EmailSettings"] — violates the project's own rule to not use System.Configuration.ConfigurationManager. Untestable. Static state (_settings cache).
Proposed service:
public interface IEmailService
{
Task<bool> SendEmailAsync(string reference, string subject, string html,
string email, string name, Dictionary<string, byte[]>? attachments);
}
public class FuchsEmailService : IEmailService
{
private readonly EmailServerSettings _settings;
private readonly Fuchs_intranet _intranet;
// Inject IConfiguration, Fuchs_intranet, ILogger<FuchsEmailService>
}
Registration: builder.Services.AddSingleton<IEmailService, FuchsEmailService>();
Files affected: Fuchs\code\FuchsFdsEmail.cs, all callers in controller partials.
5.2 FuchsPdf → IPdfService
Current state: Large static class with static helper methods, hardcoded license key, company-specific constants.
Problem: Not injectable, not testable, mixes configuration (colors, company data) with PDF rendering logic.
Proposed service:
public interface IPdfService
{
Document CreateInvoicePdf(FdsInvoiceData invoice);
Document CreateReminderPdf(FdsReminderData reminder);
Task<PdfImageCollection> DocToImageCollectionAsync(Document doc);
}
public class FuchsPdfService : IPdfService
{
// Inject ILogger<FuchsPdfService>
// Company data could come from IOptions<FuchsPdfOptions>
}
Registration: builder.Services.AddSingleton<IPdfService, FuchsPdfService>();
Files affected: Fuchs\code\FuchsPdf.cs, FdsInvoiceData.cs, FdsReminderData.cs, controller partials.
5.3 FuchsWidgets → IWidgetService
Current state: Static class that receives the entire IntranetController as a parameter to access _intranet, UserAccountID, DbSec, etc.
Problem: Tight coupling to controller — passes the whole controller instance. Cannot be unit tested independently.
Proposed service:
public interface IWidgetService
{
Task<object> GetUserWidgetsAsync(string userAccountId, DatabaseSecurity dbSec);
Task<object> GetWidgetDataAsync(string widgetId, string userAccountId, DatabaseSecurity dbSec);
Task<object> GetSingleWidgetAsync(string shortName, string userAccountId, DatabaseSecurity dbSec);
}
public class FuchsWidgetService : IWidgetService
{
private readonly Fuchs_intranet _intranet;
// Inject Fuchs_intranet, ILogger<FuchsWidgetService>
}
Registration: builder.Services.AddScoped<IWidgetService, FuchsWidgetService>();
Files affected: Fuchs\code\FuchsWidgets.cs, IntranetController.cs (Do method).
5.4 Banking → IBankingService
Current state: Static class with MT940 parsing logic.
Problem: Minor — already fairly stateless, but takes ILogger as parameter instead of injection.
Proposed service:
public interface IBankingService
{
DataTable ParseMT940(Stream stream, DataTable? schema = null);
}
public class BankingService : IBankingService
{
private readonly ILogger<BankingService> _logger;
// Inject ILogger
}
Registration: builder.Services.AddSingleton<IBankingService, BankingService>();
Files affected: Fuchs\code\Banking.cs, IntranetController.Banking.cs.
5.5 FdsInvoiceData / FdsReminderData → Factory Services
Current state: Data model classes that directly call stored procedures and PDF generation in their constructors/methods. They receive IntranetController as a parameter for DB access.
Problem: Business objects doing their own persistence (Active Record anti-pattern). Tightly coupled to controller.
Proposed services:
public interface IInvoiceService
{
Task<FdsInvoiceData> LoadInvoiceAsync(string id, string userAccountId);
Task<string> RegisterInvoiceAsync(FdsInvoiceData invoice, bool change);
Task<Document> GenerateInvoicePdfAsync(FdsInvoiceData invoice);
}
public interface IReminderService
{
Task<FdsReminderData> LoadReminderAsync(string id, string userAccountId);
Task<string> RegisterReminderAsync(FdsReminderData reminder, bool change);
Task<Document> GenerateReminderPdfAsync(FdsReminderData reminder);
}
Registration: builder.Services.AddScoped<IInvoiceService, InvoiceService>();
Files affected: FdsInvoiceData.cs, FdsReminderData.cs, all controller partials that create these objects.
5.6 FuchsReports → IReportService
Current state: Static class with a single dispatch method, receives controller.
Proposed service:
public interface IReportService
{
Task<IActionResult> ProcessRequestAsync(string action, string id,
string userAccountId, DatabaseSecurity dbSec);
}
5.7 FdsMfrClient → Injectable MFR Client
Current state: Created with new FdsMfrClient() directly in controller code (e.g., IntranetController.Invoices2.cs line 26). Uses static FdsConfig for credentials.
Problem: Cannot be mocked for testing. Credentials hardwired to static config.
Proposed service:
public interface IMfrClientFactory
{
FdsMfrClient Create();
}
public class MfrClientFactory : IMfrClientFactory, IDisposable
{
private readonly ILoggerFactory _loggerFactory;
private readonly MFRClientCredentials _credentials;
// Inject ILoggerFactory, IOptions<MfrSettings>
}
Registration: builder.Services.AddSingleton<IMfrClientFactory, MfrClientFactory>();
Files affected: FdsMfrClient.cs, IntranetController.Invoices2.cs, any code doing new FdsMfrClient().
5.8 Fuchs_intranet — Decompose the God Object
Current state: Single class handling configuration, DB connections, authentication, module auth queries, debug logging, and PDF licensing.
Recommended split:
| Responsibility | Proposed Service | Lifetime |
|---|---|---|
| Configuration (conn strings, app settings) | IOptions<FuchsSettings> |
Singleton |
| DB connection factory | IDbConnectionFactory |
Singleton |
| User authentication | IAuthenticationService |
Scoped |
| Module authorization | IAuthorizationService (custom) |
Scoped |
| Debug/error logging | Use built-in ILogger<T> |
— |
6. Priority Ranking
| Priority | Candidate | Impact | Effort |
|---|---|---|---|
| 🔴 1 | FuchsFdsEmail → IEmailService |
Fixes ConfigurationManager violation, high testability gain |
Low |
| 🔴 2 | FuchsWidgets → IWidgetService |
Removes controller coupling | Low |
| 🟡 3 | FdsMfrClient → IMfrClientFactory |
Removes new in controllers, enables mocking |
Medium |
| 🟡 4 | Banking → IBankingService |
Clean DI pattern, minor effort | Low |
| 🟡 5 | FuchsPdf → IPdfService |
Large file, significant but high-value refactor | Medium |
| 🟠 6 | FdsInvoiceData/FdsReminderData → Services |
Major architectural improvement, most effort | High |
| 🟠 7 | Fuchs_intranet decomposition |
God-object split, foundational but risky | High |
| 🟢 8 | FuchsReports → IReportService |
Minimal current logic, prep for future | Low |
7. Additional Observations
- ✅ Resolved — email/SMS moved off
ConfigurationManagerintoIComService(ProcessWeb Mailer API). - ✅ Resolved —
FdsInvoiceData/FdsReminderDataare now pure data holders; DB + PDF logic moved toIInvoiceService/IReminderService. - ✅ Resolved —
FdsMfrClientis created viaIMfrClientFactory(nonewin controllers). - ✅ Resolved —
OCORE_Chartingis now used (transitively, viaOCORE_web's chart engine) by the report renderer (FuchsVisualization). - ⏳ Open — Topshelf in
Fuchs_DataServicecould be replaced with nativedotnetWorker Service hosting for .NET 10 alignment.
8. Observability (OpenTelemetry)
- Instrumentation is centralised in
Fuchs/Observability/FuchsTelemetry.cs: oneActivitySourceand oneMeter(Fuchs.Intranet). - Metrics — counters (
fuchs.invoices.rendered,fuchs.reminders.rendered,fuchs.reports.rendered,fuchs.emails.sent/.failed,fuchs.sms.sent,fuchs.banking.mt940.rows,fuchs.mfr.calls) and duration histograms (fuchs.pdf.render.duration,fuchs.report.render.duration,fuchs.email.send.duration). - Tracing — ASP.NET Core, HttpClient and SqlClient instrumentation plus the app
ActivitySource; services start spans for their key operations. - Configured in
Program.cs. Always collected in-process; OTLP export is opt-in viaFuchs:Telemetry:OtlpEndpoint(and can be disabled withFuchs:Telemetry:Enabled=false), so a missing collector never affects the app. - All services + handlers log entry/result/timing/errors via
ILogger<T>with structured placeholders.
9. Bank Statement Parsing (MT940 + CAMT)
BankingService(IBankingService) accepts both MT940 (SWIFT text, via the externalMT940Parser) and CAMT (ISO 20022 camt.052/053/054 XML, via the in-repoCAMTParser).ParseToDatatableauto-detects the format from content (XML → CAMT, else MT940) and maps either into thefds__tt__bankingtransactionsschema; thebam/uphandler and the frontend upload accept both.CAMTParsermatches elements by local name (namespace-agnostic) so it works across every camt schema version. When the banking schema changes, keep the MT940 and CAMT column mappings inBankingServicealigned.