docs: Deutsche Methoden- und Codekommentare ergänzen

This commit is contained in:
admin committed 2026-09-07 16:37:10 +02:00
1 parent bb9b20cc25
commit 11e7f68304
9 files changed
+97

No files matched your search

+3
View File
@@ -1,5 +1,6 @@
using ImportAssist.Core;
// Einstiegspunkt für die lokale Prüfung: args[0] enthält den Pfad zur CSV-Datei.
if (args.Length != 1)
{
Console.Error.WriteLine(
@@ -25,11 +26,13 @@ try
}
catch (FormatException exception)
{
// Ein fehlerhafter Dateiaufbau verhindert die Prüfung aller enthaltenen Rechnungen.
Console.Error.WriteLine(exception.Message);
Environment.ExitCode = 1;
return;
}
// Fachliche Fehler werden je Rechnung ausgegeben; sie ändern den Prozess-Exitcode nicht.
foreach (var row in rows)
{
var errors = InvoiceValidator.Validate(row);
+21
View File
@@ -1,11 +1,29 @@
namespace ImportAssist.Core;
/// <summary>
/// Ordnet das fachliche Prüfergebnis einer Rechnung ihrer ursprünglichen CSV-Zeile zu.
/// </summary>
/// <param name="LineNumber">Zeilennummer in der CSV-Datei; die erste Datenzeile hat die Nummer 2.</param>
/// <param name="InvoiceId">Unverändert übernommene Rechnungsnummer, gegebenenfalls leer.</param>
/// <param name="IsValid">Gibt an, ob die fachliche Prüfung keine Fehler gefunden hat.</param>
/// <param name="Errors">Fachliche Fehlermeldungen für diese Rechnung; bei gültigen Rechnungen leer.</param>
public sealed record InvoiceCheckResult(
int LineNumber,
string InvoiceId,
bool IsValid,
IReadOnlyList<string> Errors);
/// <summary>
/// Fasst die Prüfung eines CSV-Blobs mit Ergebnissen je Rechnung oder einem Dateiformatfehler zusammen.
/// </summary>
/// <param name="SourceBlob">Name des geprüften Blobs im Importcontainer.</param>
/// <param name="CheckedAtUtc">Zeitpunkt der Berichtserstellung in UTC.</param>
/// <param name="Status">
/// Completed bei abgeschlossener Prüfung, auch mit ungültigen Rechnungen;
/// Rejected bei einem nicht verarbeitbaren CSV-Format.
/// </param>
/// <param name="Rows">Ergebnisse je Rechnung; bei einem Dateiformatfehler leer.</param>
/// <param name="FileErrors">Fehler des CSV-Dateiformats, getrennt von fachlichen Rechnungsfehlern.</param>
public sealed record ImportReport(
string SourceBlob,
DateTimeOffset CheckedAtUtc,
@@ -13,7 +31,10 @@ public sealed record ImportReport(
IReadOnlyList<InvoiceCheckResult> Rows,
IReadOnlyList<string> FileErrors)
{
/// <summary>Gesamtzahl der im Bericht enthaltenen Rechnungsprüfungen.</summary>
public int TotalCount => Rows.Count;
/// <summary>Anzahl der Rechnungen ohne fachliche Fehler.</summary>
public int ValidCount => Rows.Count(row => row.IsValid);
/// <summary>Anzahl der Rechnungen mit fachlichen Fehlern; Dateiformatfehler zählen nicht mit.</summary>
public int InvalidCount => TotalCount - ValidCount;
}
+16
View File
@@ -1,11 +1,25 @@
namespace ImportAssist.Core;
/// <summary>
/// Liest Rechnungen aus dem vereinfachten, mit Semikolon getrennten Importformat.
/// </summary>
public static class InvoiceCsvParser
{
/// <summary>
/// Prüft den Aufbau der CSV-Datei und übernimmt die vier Spalten jeder Datenzeile als Text.
/// Die fachliche Prüfung der Rechnungen erfolgt separat durch <see cref="InvoiceValidator"/>.
/// </summary>
/// <param name="csv">Vollständiger Dateiinhalt mit der Kopfzeile InvoiceId;CustomerId;Amount;Currency.</param>
/// <returns>Rechnungszeilen in Dateireihenfolge; bei einer Datei nur mit Kopfzeile eine leere Liste.</returns>
/// <exception cref="FormatException">
/// Die Kopfzeile fehlt oder weicht ab, oder eine Datenzeile enthält Anführungszeichen
/// beziehungsweise nicht genau vier Spalten.
/// </exception>
public static IReadOnlyList<InvoiceRow> Parse(string csv)
{
using var reader = new StringReader(csv);
// Ein optionales Unicode-BOM am Dateianfang gehört nicht zum Namen der ersten Spalte.
var header = reader.ReadLine()?.TrimStart('\uFEFF');
if (header != "InvoiceId;CustomerId;Amount;Currency")
@@ -21,6 +35,8 @@ public static class InvoiceCsvParser
{
lineNumber++;
// Das Importformat unterstützt keine in Anführungszeichen eingeschlossenen Felder.
// Auch Leerzeilen werden als Datenzeilen geprüft und nicht übersprungen.
var columns = line.Split(';');
if (line.Contains('"') || columns.Length != 4)
+8
View File
@@ -1,5 +1,13 @@
namespace ImportAssist.Core;
/// <summary>
/// Enthält die ungeprüften Rechnungsfelder. Der Betrag bleibt zunächst Text,
/// damit auch ungültige Zahlen als fachlicher Prüfungsfehler gemeldet werden können.
/// </summary>
/// <param name="InvoiceId">Rechnungsnummer; darf bei der fachlichen Prüfung nicht leer sein.</param>
/// <param name="CustomerId">Kundennummer; darf bei der fachlichen Prüfung nicht leer sein.</param>
/// <param name="Amount">Betrag als Text, für die Prüfung mit Dezimalpunkt und ohne Tausendertrennzeichen.</param>
/// <param name="Currency">Übernommene Währungsangabe; wird derzeit nicht validiert.</param>
public sealed record InvoiceRow(
string InvoiceId,
string CustomerId,
+11
View File
@@ -2,8 +2,17 @@ using System.Globalization;
namespace ImportAssist.Core;
/// <summary>
/// Bündelt die fachlichen Rechnungsprüfungen für CLI, HTTP-Aufrufe und Blob-Importe.
/// </summary>
public static class InvoiceValidator
{
/// <summary>
/// Prüft Rechnungsnummer, Kundennummer und einen positiven, lesbaren Betrag.
/// Die Währung wird derzeit nicht geprüft.
/// </summary>
/// <param name="row">Zu prüfende Rechnung mit den ursprünglichen Feldwerten als Text.</param>
/// <returns>Alle gefundenen Fehlermeldungen; eine leere Liste kennzeichnet eine gültige Rechnung.</returns>
public static IReadOnlyList<string> Validate(InvoiceRow row)
{
var errors = new List<string>();
@@ -18,6 +27,8 @@ public static class InvoiceValidator
errors.Add("Kundennummer fehlt.");
}
// Das Zahlenformat ist unabhängig von der Serversprache: Dezimalpunkt und Vorzeichen
// sind erlaubt, Tausendertrennzeichen und umgebende Leerzeichen dagegen nicht.
var allowedStyles =
NumberStyles.AllowLeadingSign |
NumberStyles.AllowDecimalPoint;
@@ -5,6 +5,10 @@ using Microsoft.Extensions.Logging;
namespace ImportAssist_Functions;
/// <summary>
/// Prüft CSV-Dateien aus dem Blobcontainer imports und erstellt JSON-Berichte im Container results.
/// </summary>
/// <param name="logger">Vom Functions-Host bereitgestellter Logger für die Zusammenfassung der Prüfung.</param>
public sealed class InvoiceBlobTrigger(
ILogger<InvoiceBlobTrigger> logger)
{
@@ -14,6 +18,12 @@ public sealed class InvoiceBlobTrigger(
WriteIndented = true
};
/// <summary>
/// Liest und validiert einen importierten CSV-Blob und liefert den serialisierten Prüfbericht.
/// </summary>
/// <param name="content">Vom Blob-Trigger als Text geladener Dateiinhalt.</param>
/// <param name="name">Über den Binding-Platzhalter name ermittelter Blobname im Importcontainer.</param>
/// <returns>JSON-Bericht, den das Output-Binding unter results/{name}.json speichert.</returns>
[Function("InvoiceBlobTrigger")]
[BlobOutput(
"results/{name}.json",
@@ -37,6 +47,7 @@ public sealed class InvoiceBlobTrigger(
var row = rows[index];
var errors = InvoiceValidator.Validate(row);
// Die Kopfzeile belegt Zeile 1; der nullbasierte Index beginnt deshalb bei CSV-Zeile 2.
results.Add(new InvoiceCheckResult(
LineNumber: index + 2,
InvoiceId: row.InvoiceId,
@@ -44,6 +55,7 @@ public sealed class InvoiceBlobTrigger(
Errors: errors));
}
// Completed bezeichnet die abgeschlossene Prüfung, auch wenn einzelne Rechnungen ungültig sind.
report = new ImportReport(
SourceBlob: name,
CheckedAtUtc: DateTimeOffset.UtcNow,
@@ -53,6 +65,7 @@ public sealed class InvoiceBlobTrigger(
}
catch (FormatException exception)
{
// Bei einem Formatfehler wird die gesamte Datei abgelehnt; es gibt keine Teilergebnisse.
report = new ImportReport(
SourceBlob: name,
CheckedAtUtc: DateTimeOffset.UtcNow,
@@ -71,6 +84,7 @@ public sealed class InvoiceBlobTrigger(
report.ValidCount,
report.InvalidCount);
// Das Output-Binding übernimmt das Speichern; dafür ist hier kein eigener Storage-Client nötig.
return JsonSerializer.Serialize(report, JsonOptions);
}
}
+3
View File
@@ -6,10 +6,13 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenTelemetry;
// Erstellt den Functions-Worker; args enthält die Startargumente des Prozesses.
var builder = FunctionsApplication.CreateBuilder(args);
// Aktiviert die ASP.NET-Core-Anbindung für HTTP-Trigger mit HttpRequest und IActionResult.
builder.ConfigureFunctionsWebApplication();
// Telemetrie wird nur bei konfigurierter Application-Insights-Verbindung nach Azure exportiert.
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING")))
{
builder.Services.AddOpenTelemetry()
@@ -7,11 +7,23 @@ using Microsoft.Extensions.Logging;
namespace ImportAssist_Functions;
/// <summary>
/// Stellt die fachliche Prüfung einer einzelnen Rechnung über einen HTTP-POST-Endpunkt bereit.
/// </summary>
/// <param name="logger">Vom Functions-Host bereitgestellter Logger für das Ergebnis der Prüfung.</param>
public sealed class ValidateInvoice(ILogger<ValidateInvoice> logger)
{
private static readonly JsonSerializerOptions JsonOptions =
new(JsonSerializerDefaults.Web);
/// <summary>
/// Liest eine Rechnung aus dem JSON-Request und prüft sie mit den gemeinsamen fachlichen Regeln.
/// </summary>
/// <param name="request">HTTP-Anfrage mit einem Rechnungsobjekt aus Textfeldern im Request-Body.</param>
/// <returns>
/// HTTP 400 bei ungültigem JSON beziehungsweise einem nicht lesbaren oder fehlenden Rechnungsobjekt;
/// andernfalls HTTP 200 mit Rechnungsnummer, Gültigkeitskennzeichen und fachlichen Fehlermeldungen.
/// </returns>
[Function("ValidateInvoice")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest request)
@@ -20,6 +32,7 @@ public sealed class ValidateInvoice(ILogger<ValidateInvoice> logger)
try
{
// Ein abgebrochener HTTP-Aufruf soll auch das Einlesen des Request-Bodys abbrechen.
row = await JsonSerializer.DeserializeAsync<InvoiceRow>(
request.Body,
JsonOptions,
@@ -33,6 +46,7 @@ public sealed class ValidateInvoice(ILogger<ValidateInvoice> logger)
});
}
// Der JSON-Wert null lässt sich deserialisieren, enthält aber keine prüfbare Rechnung.
if (row is null)
{
return new BadRequestObjectResult(new
@@ -47,6 +61,7 @@ public sealed class ValidateInvoice(ILogger<ValidateInvoice> logger)
"Rechnungsprüfung abgeschlossen: {ErrorCount} Fehler.",
errors.Count);
// Fachliche Fehler gehören zum regulären Prüfergebnis und werden deshalb mit HTTP 200 geliefert.
return new OkObjectResult(new
{
invoiceId = row.InvoiceId,
+6
View File
@@ -1,12 +1,16 @@
using Azure.Storage.Blobs;
// Lokales Hilfsprogramm für Azurite (UseDevelopmentStorage=true):
// <Dateipfad> lädt einen Import hoch; result <Blobname> liest dessen JSON-Prüfbericht.
if (args.Length == 2 && args[0] == "result")
{
// args[1] ist der beim Upload ausgegebene Blobname einschließlich der ursprünglichen Dateiendung.
var resultBlob = new BlobClient(
"UseDevelopmentStorage=true",
"results",
$"{args[1]}.json");
// Die Function verarbeitet Uploads asynchron; der Bericht kann daher noch fehlen.
if (!(await resultBlob.ExistsAsync()).Value)
{
Console.Error.WriteLine(
@@ -31,6 +35,7 @@ var container = new BlobContainerClient(
"UseDevelopmentStorage=true",
"imports");
// Beide Container werden auch bei einer frisch gestarteten lokalen Storage-Umgebung angelegt.
await container.CreateIfNotExistsAsync();
var resultsContainer = new BlobContainerClient(
@@ -39,6 +44,7 @@ var resultsContainer = new BlobContainerClient(
await resultsContainer.CreateIfNotExistsAsync();
// Die zufällige Kennung verhindert Namenskollisionen bei wiederholtem Upload derselben Datei.
var blobName = $"{Guid.NewGuid():N}-{Path.GetFileName(args[0])}";
var blob = container.GetBlobClient(blobName);