Complete DI migration: wire all business services end-to-end

Move the intranet off the static-helper / Active-Record pattern onto
constructor-injected services, removing controller coupling and the
sync-over-async (Task.Run().Wait()) hot spots in the data classes.

Services now registered and consumed via DI:
- IBankingService, IPdfService, IMfrClientFactory (singletons)
- IWidgetService, IReportService, IInvoiceService, IReminderService (scoped)

Key changes:
- FuchsWidgetService: real widget logic (sql_table/indicator/html +
  rendering_options) ported from the static class, which is deleted.
- FuchsReportService + FuchsVisualization: report engine decoupled from
  IntranetController (takes connStr/dbSec/userAccountId); static
  FuchsReports deleted.
- InvoiceService / ReminderService: implement load/register/render/store
  (previously NotImplementedException stubs). FdsInvoiceData /
  FdsReminderData are now pure data holders — all DB + PDF work moved into
  the services, async throughout (no Task.Run().Wait()).
- Controllers inject and call the services; all `new FdsMfrClient()` calls
  go through IMfrClientFactory.
- Deleted dead code: static Banking, FuchsWidgets, FuchsReports, and the
  unused IDbConnectionFactory.
- InternalsVisibleTo("Fuchs.Tests") for testing internal mapping logic.

Tests: 127 passing (Banking tests moved to the service; added data-holder
tests for FdsInvoiceData/FdsReminderData). Full solution builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 12:57:59 +02:00
parent c81619fa53
commit 8dee630abb
25 changed files with 711 additions and 947 deletions
@@ -29,7 +29,7 @@ public partial class IntranetController
_intranet.Intranet__SQLConnectionString,
Security: DbSec, options: SqlOpt(fn, id, code))).DataTable;
var tbl = Banking.ParseToDatatable(stream, schemaDt);
var tbl = _banking.ParseToDatatable(stream, schemaDt);
var tmptbl = "bs_" + Guid.NewGuid().ToString().Replace("-", "");
var dtwa = new DatatableWriterAsync(tbl, _intranet.Intranet__SQLConnectionString)
@@ -146,7 +146,7 @@ public partial class IntranetController
return BadRequest400();
}
_logger.LogInformation("mfrrel: resetting MFR relation for invoice {InvoiceId}, user={User}", relId, UserAccountID);
using (var mfr = new fds.FdsMfrClient())
using (var mfr = _mfrFactory.Create())
await mfr.Update__entitytable(EntityTypes.Invoice,
fds.FdsMfr.UpdateNeed.Reset, new[] { relId });
return Ok();
@@ -22,7 +22,7 @@ public partial class IntranetController
if (!long.TryParse(Form("id"), out long tgtid)) { _logger.LogWarning("HandleInvoicePget: invalid 'id' value='{Value}' user={User}", Form("id"), UserAccountID); return BadRequest400(); }
_logger.LogDebug("HandleInvoicePget tgtid={TgtId} user={User}", tgtid, UserAccountID);
using (var mfr = new fds.FdsMfrClient())
using (var mfr = _mfrFactory.Create())
{
_logger.LogDebug("HandleInvoicePget resetting invoice entity tgtid={TgtId}", tgtid);
await mfr.Update__entitytable(EntityTypes.Invoice,
@@ -50,7 +50,7 @@ public partial class IntranetController
}
}
_logger.LogDebug("HandleInvoicePget resetting {InvCount} invoices and {SrqCount} service requests", invIds.Count, srqIds.Count);
using var mfr2 = new fds.FdsMfrClient();
using var mfr2 = _mfrFactory.Create();
foreach (var iid in invIds)
await mfr2.Update__entitytable(EntityTypes.Invoice, fds.FdsMfr.UpdateNeed.Reset, new[] { iid });
foreach (var iid in srqIds)
@@ -413,9 +413,9 @@ public partial class IntranetController
return ldic;
}
private static async Task<string[]> BuildPdfImageArray(byte[] content)
private async Task<string[]> BuildPdfImageArray(byte[] content)
{
var imgcol = await FuchsPdf.BytesToImageCollection(content);
var imgcol = await _pdf.BytesToImageCollectionAsync(content);
return imgcol.ImgB64Array;
}
}
@@ -38,11 +38,11 @@ public partial class IntranetController
{
if (!HasForm("remc")) return BadRequest400();
var ctd = JsonConvert.DeserializeObject(Form("remc"))!;
var fdRem = new FdsReminderData(ctd);
fdRem.RegisterReminder(this, change: false, remId: "");
var fdRem = await _reminders.RegisterReminderAsync(
new FdsReminderData(ctd), change: false, remId: "", UserAccountID, DbSec);
if (!string.IsNullOrEmpty(fdRem.Id))
{
var imgcol = await FuchsPdf.DocToImageCollection(fdRem.ReminderPDF(this));
var imgcol = await _pdf.DocToImageCollectionAsync(_reminders.GenerateReminderPdf(fdRem, fdRem.IsDraft));
return await JSONAsync(new { id = fdRem.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
}
return StatusCode(500, new { error = "Erinnerung wurde nicht registriert" });
@@ -64,12 +64,11 @@ public partial class IntranetController
case "rdoc":
{
if (!HasForm("id")) return BadRequest400();
byte[]? fc = null;
var file = FdsReminderData.GetStoredFile(ref fc, Form("id"), this);
if (file == null) return StatusCode(404, new { error = "Dokument wurde nicht gefunden" });
var (file, fc) = await _reminders.GetStoredFileAsync(Form("id"), UserAccountID, DbSec);
if (file == null || fc == null) return StatusCode(404, new { error = "Dokument wurde nicht gefunden" });
return Form("typ") != "img"
? await FileContentResultAsync(fc!, file.MimeType(), file.Name)
: await JSONAsync(new { id = Form("id"), img = await BuildPdfImageArray(fc!) });
? await FileContentResultAsync(fc, file.MimeType(), file.Name)
: await JSONAsync(new { id = Form("id"), img = await BuildPdfImageArray(fc) });
}
case "idoc": return await HandleReminderIdoc(fn, id, code);
@@ -107,8 +106,8 @@ public partial class IntranetController
if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true)
{
string remId = frdic["Id"]?.ToString() ?? "";
var fdRem = new FdsReminderData(remId, this);
byte[] filebyte = await fdRem.StoreReminderDocumentFile(this);
var fdRem = await _reminders.LoadReminderAsync(remId, UserAccountID, DbSec);
byte[] filebyte = await _reminders.StoreReminderDocumentFileAsync(fdRem, fdRem.IsDraft, UserAccountID, DbSec);
string email = frdic.nz("SendToEmail", "");
if (!string.IsNullOrEmpty(email) && filebyte.Length > 0)
{
@@ -140,17 +139,17 @@ public partial class IntranetController
private async Task<IActionResult> HandleReminderIdoc(string fn, string id, string code)
{
if (!HasForm("id") || string.IsNullOrEmpty(Form("id"))) return StatusCode(404);
var fdRem = new FdsReminderData(Form("id"), this);
var fdRem = await _reminders.LoadReminderAsync(Form("id"), UserAccountID, DbSec);
if (string.IsNullOrEmpty(fdRem.Id)) return StatusCode(404, new { error = "Erinnerung wurde nicht gefunden" });
string filename = fdRem.ReminderRegistration.nz("DocumentName").ne($"Zahlungserinnerung_{fdRem.Id}.pdf");
string filename = fdRem.ReminderRegistration!.nz("DocumentName").ne($"Zahlungserinnerung_{fdRem.Id}.pdf");
if (Form("typ") != "img")
{
byte[] ct = Form("create", "0") != "1"
? (await fdRem.GetReminderFile(this)) is { Length: > 0 } f1 ? f1 : await fdRem.StoreReminderDocumentFile(this)
: FuchsPdf.DocToPdfBytes(fdRem.ReminderPDF(this));
? (await _reminders.GetReminderFileAsync(fdRem, fdRem.IsDraft, _mfr, UserAccountID, DbSec)) is { Length: > 0 } f1 ? f1 : await _reminders.StoreReminderDocumentFileAsync(fdRem, fdRem.IsDraft, UserAccountID, DbSec)
: _pdf.DocToPdfBytes(_reminders.GenerateReminderPdf(fdRem, fdRem.IsDraft));
return await FileContentResultAsync(ct, "application/pdf", filename, inline: true);
}
var imgcol = await FuchsPdf.DocToImageCollection(fdRem.ReminderPDF(this));
var imgcol = await _pdf.DocToImageCollectionAsync(_reminders.GenerateReminderPdf(fdRem, fdRem.IsDraft));
return await JSONAsync(new { id = fdRem.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
}
@@ -46,7 +46,7 @@ public partial class IntranetController
}
default:
return await FuchsReports.ProcessFdsRequest(this, id.ToLower(), code);
return await _reports.ProcessRequestAsync(id.ToLower(), code, UserAccountID, DbSec, RequestParamsDict());
}
}
}
@@ -45,8 +45,9 @@ public partial class IntranetController
case "save":
{
if (!HasForm("invc")) return BadRequest400();
var fdInv = new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!);
fdInv.RegisterInvoice(this, change: !string.IsNullOrEmpty(Form("id")), invId: Form("id"));
var fdInv = await _invoices.RegisterInvoiceAsync(
new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!),
change: !string.IsNullOrEmpty(Form("id")), invId: Form("id"), UserAccountID, DbSec);
return !string.IsNullOrEmpty(fdInv.Id)
? await JSONAsync(new { id = fdInv.Id })
: StatusCode(500, new { error = "Rechnung wurde nicht gespeichert" });
@@ -55,11 +56,12 @@ public partial class IntranetController
case "sprep":
{
if (!HasForm("invc")) return BadRequest400();
var fdInv = new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!);
fdInv.RegisterInvoice(this, change: false, invId: "");
var fdInv = await _invoices.RegisterInvoiceAsync(
new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!),
change: false, invId: "", UserAccountID, DbSec);
if (!string.IsNullOrEmpty(fdInv.Id))
{
var imgcol = await FuchsPdf.DocToImageCollection(fdInv.InvoicePDF(this));
var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
}
return StatusCode(500, new { error = "Rechnung wurde nicht registriert" });
@@ -68,11 +70,12 @@ public partial class IntranetController
case "sedit":
{
if (!HasForm("id", "invc")) return BadRequest400();
var fdInv = new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!);
fdInv.RegisterInvoice(this, change: true, invId: Form("id"));
var fdInv = await _invoices.RegisterInvoiceAsync(
new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!),
change: true, invId: Form("id"), UserAccountID, DbSec);
if (!string.IsNullOrEmpty(fdInv.Id))
{
var imgcol = await FuchsPdf.DocToImageCollection(fdInv.InvoicePDF(this));
var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
}
return StatusCode(500, new { error = "Rechnung wurde nicht registriert" });
@@ -171,7 +174,7 @@ public partial class IntranetController
[EntityHelper.EntityName(EntityTypes.ServiceRequest)] =
new fds.FdsMfrClient.DatabaseSchema(EntityTypes.ServiceRequest)
};
using var mfr = new fds.FdsMfrClient();
using var mfr = _mfrFactory.Create();
await mfr.Update__entitytable(EntityTypes.ServiceRequest,
fds.FdsMfr.UpdateNeed.Reset, ids.ToArray(), schemaDic: schemaDic);
return Ok();
@@ -258,8 +261,8 @@ public partial class IntranetController
if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true)
{
string invId = frdic["Id"]?.ToString() ?? "";
var fdInv = new FdsInvoiceData(invId, this);
byte[] filebyte = await fdInv.StoreInvoiceDocumentFile(this);
var fdInv = await _invoices.LoadInvoiceAsync(invId, UserAccountID, DbSec);
byte[] filebyte = await _invoices.StoreInvoiceDocumentFileAsync(fdInv, fdInv.IsDraft, UserAccountID, DbSec);
var dtset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getInvoice] @Id, @authuser;",
_intranet.Intranet__SQLConnectionString,
@@ -293,19 +296,19 @@ public partial class IntranetController
private async Task<IActionResult> HandleRequestIdoc(string fn, string id, string code)
{
if (!HasForm("id") || string.IsNullOrEmpty(Form("id"))) return StatusCode(404);
var fdInv = new FdsInvoiceData(Form("id"), this);
var fdInv = await _invoices.LoadInvoiceAsync(Form("id"), UserAccountID, DbSec);
if (string.IsNullOrEmpty(fdInv.Id)) return StatusCode(404, new { error = "Rechnung wurde nicht gefunden" });
string filename = fdInv.InvoiceRegistration.nz("DocumentName").ne($"Rechnung_{fdInv.Id}.pdf");
string filename = fdInv.InvoiceRegistration!.nz("DocumentName").ne($"Rechnung_{fdInv.Id}.pdf");
if (Form("typ") != "img")
{
byte[]? ct = Form("create", "0") != "1"
? await fdInv.GetInvoiceFile(this) is { Length: > 0 } f1 ? f1 : await fdInv.StoreInvoiceDocumentFile(this)
: FuchsPdf.DocToPdfBytes(fdInv.InvoicePDF(this));
? await _invoices.GetInvoiceFileAsync(fdInv, fdInv.IsDraft, _mfr) is { Length: > 0 } f1 ? f1 : await _invoices.StoreInvoiceDocumentFileAsync(fdInv, fdInv.IsDraft, UserAccountID, DbSec)
: _pdf.DocToPdfBytes(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
return ct != null
? await FileContentResultAsync(ct, "application/pdf", filename, inline: true)
: StatusCode(500, new { error = "Rechnungs-PDF konnte nicht erstellt werden" });
}
var imgcol = await FuchsPdf.DocToImageCollection(fdInv.InvoicePDF(this));
var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
}
@@ -322,8 +325,8 @@ public partial class IntranetController
if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true)
{
string invId = frdic["Id"]?.ToString() ?? "";
var fdInv = new FdsInvoiceData(invId, this);
byte[] filebyte = FuchsPdf.DocToPdfBytes(fdInv.InvoicePDF(this));
var fdInv = await _invoices.LoadInvoiceAsync(invId, UserAccountID, DbSec);
byte[] filebyte = await _invoices.RenderInvoicePdfBytesAsync(fdInv, fdInv.IsDraft);
string email = frdic.nz("SendToEmail", "");
if (!string.IsNullOrEmpty(email) && filebyte.Length > 0)
{
+41 -6
View File
@@ -26,6 +26,13 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
internal readonly fds.IFdsMfr _mfr;
private readonly ILogger<IntranetController> _logger;
private readonly IComService _comService;
private readonly IBankingService _banking;
private readonly IPdfService _pdf;
private readonly IMfrClientFactory _mfrFactory;
private readonly IWidgetService _widgets;
private readonly IReportService _reports;
private readonly IInvoiceService _invoices;
private readonly IReminderService _reminders;
private readonly List<string> _allowedNonAuth = new() { "spwc", "spw" };
private readonly List<string> _allowedGet = new()
{
@@ -41,12 +48,40 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
public string UserAccountID => UserIdent.UserAccountId;
public string AuthAccount => UserIdent.Email;
public IntranetController(Fuchs_intranet intranet, fds.IFdsMfr mfr, ILogger<IntranetController> logger, IComService comService)
public IntranetController(
Fuchs_intranet intranet,
fds.IFdsMfr mfr,
ILogger<IntranetController> logger,
IComService comService,
IBankingService banking,
IPdfService pdf,
IMfrClientFactory mfrFactory,
IWidgetService widgets,
IReportService reports,
IInvoiceService invoices,
IReminderService reminders)
{
_intranet = intranet;
_mfr = mfr;
_logger = logger;
_comService = comService;
_banking = banking;
_pdf = pdf;
_mfrFactory = mfrFactory;
_widgets = widgets;
_reports = reports;
_invoices = invoices;
_reminders = reminders;
}
/// <summary>Merged query-string + form parameters (form wins) for report processing.</summary>
internal Dictionary<string, string> RequestParamsDict()
{
var prms = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var kv in Request.Query) prms[kv.Key] = kv.Value.ToString();
if (Request.HasFormContentType)
foreach (var kv in Request.Form) prms[kv.Key] = kv.Value.ToString();
return prms;
}
// ── Standard param list (pre-populates @authuser) ────────────────────────
@@ -106,7 +141,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
IActionResult? result = fn.ToLower() switch
{
"ping" => Ok(),
"wdg" => await FuchsWidgets.IntranetWdg(this, id),
"wdg" => await _widgets.GetWidgetAsync(id, UserAccountID, DbSec, Request),
"todos" => new PhysicalFileResult(
Path.Combine(Directory.GetCurrentDirectory(), "Data", "ProjectToDos.html"),
"text/html"),
@@ -374,7 +409,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
if (string.IsNullOrEmpty(id))
{
// Empty id → return the OData EDMX schema ($metadata), matching legacy fds.getSchema()
using var mfrSchema = new fds.FdsMfrClient();
using var mfrSchema = _mfrFactory.Create();
string schema = await mfrSchema.ReadAnything(
mfrSchema.ClientConfig.BaseUrl + "$metadata", throwErrorIfNotOk: false);
return Content(schema, "text/xml", System.Text.Encoding.UTF8);
@@ -383,7 +418,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
{
string path = id + (!string.IsNullOrEmpty(code) ? "/" + code : HttpUtility.UrlDecode(Request.QueryString.Value ?? ""));
_logger.LogDebug("HandleMfr reading OData path={Path} user={User}", path, UserAccountID);
using var mfrRead = new fds.FdsMfrClient();
using var mfrRead = _mfrFactory.Create();
var result = await mfrRead.ReadOData(path, throwErrorIfNotOk: false);
_logger.LogDebug("HandleMfr OData read complete for path={Path} user={User}", path, UserAccountID);
return Content(JsonConvert.SerializeObject(result), "application/json");
@@ -402,7 +437,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
if (et != EntityTypes.none && string.IsNullOrEmpty(Request.Form["need"]))
{
_logger.LogInformation("MfrUpdate entity={EntityType} need=Short user={User}", et, UserAccountID);
using var mfrSingle = new fds.FdsMfrClient();
using var mfrSingle = _mfrFactory.Create();
await mfrSingle.Update__entitytable(et, fds.FdsMfr.UpdateNeed.Short);
_logger.LogDebug("MfrUpdate Short completed for entity={EntityType}", et);
return Ok();
@@ -411,7 +446,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
{
var need = fds.FdsMfr.UpdateNeedValue(needParam);
_logger.LogInformation("MfrUpdate entity={EntityType} need={Need} user={User}", et, need, UserAccountID);
using var mfr = new fds.FdsMfrClient();
using var mfr = _mfrFactory.Create();
await mfr.Update__entitytable(et, updateNeed: need, debugDetails: false);
_logger.LogDebug("MfrUpdate completed for entity={EntityType} need={Need}", et, need);
return Ok();