Add local blob-triggered CSV validation with Azurite
This commit is contained in:
1 parent
b72aa278a5
commit
2075bcb2c3
7 files changed
+153
-27
No files matched your search
@@ -6,4 +6,7 @@
|
|||||||
<Folder Name="/src/ImportAssist.Functions/">
|
<Folder Name="/src/ImportAssist.Functions/">
|
||||||
<Project Path="src/ImportAssist.Functions/ImportAssist_Functions.csproj" />
|
<Project Path="src/ImportAssist.Functions/ImportAssist_Functions.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/tools/">
|
||||||
|
<Project Path="tools/ImportAssist.Upload/ImportAssist.Upload.csproj" />
|
||||||
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
@@ -17,39 +17,19 @@ if (!File.Exists(path))
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var lines = File.ReadAllLines(path);
|
IReadOnlyList<InvoiceRow> rows;
|
||||||
|
|
||||||
if (lines.Length == 0 ||
|
try
|
||||||
lines[0] != "InvoiceId;CustomerId;Amount;Currency")
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
rows = InvoiceCsvParser.Parse(File.ReadAllText(path));
|
||||||
"Die Datei ist leer oder die Kopfzeile stimmt nicht.");
|
}
|
||||||
|
catch (FormatException exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(exception.Message);
|
||||||
Environment.ExitCode = 1;
|
Environment.ExitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows = new List<InvoiceRow>();
|
|
||||||
|
|
||||||
for (var index = 1; index < lines.Length; index++)
|
|
||||||
{
|
|
||||||
var line = lines[index];
|
|
||||||
var columns = line.Split(';');
|
|
||||||
|
|
||||||
if (line.Contains('"') || columns.Length != 4)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"Zeile {index + 1}: Erwartet werden vier Spalten ohne Anführungszeichen.");
|
|
||||||
Environment.ExitCode = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.Add(new InvoiceRow(
|
|
||||||
columns[0],
|
|
||||||
columns[1],
|
|
||||||
columns[2],
|
|
||||||
columns[3]));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var row in rows)
|
foreach (var row in rows)
|
||||||
{
|
{
|
||||||
var errors = InvoiceValidator.Validate(row);
|
var errors = InvoiceValidator.Validate(row);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
namespace ImportAssist.Core;
|
||||||
|
|
||||||
|
public static class InvoiceCsvParser
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<InvoiceRow> Parse(string csv)
|
||||||
|
{
|
||||||
|
using var reader = new StringReader(csv);
|
||||||
|
|
||||||
|
var header = reader.ReadLine()?.TrimStart('\uFEFF');
|
||||||
|
|
||||||
|
if (header != "InvoiceId;CustomerId;Amount;Currency")
|
||||||
|
{
|
||||||
|
throw new FormatException(
|
||||||
|
"Die Datei ist leer oder die Kopfzeile stimmt nicht.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = new List<InvoiceRow>();
|
||||||
|
var lineNumber = 1;
|
||||||
|
|
||||||
|
while (reader.ReadLine() is { } line)
|
||||||
|
{
|
||||||
|
lineNumber++;
|
||||||
|
|
||||||
|
var columns = line.Split(';');
|
||||||
|
|
||||||
|
if (line.Contains('"') || columns.Length != 4)
|
||||||
|
{
|
||||||
|
throw new FormatException(
|
||||||
|
$"Zeile {lineNumber}: Erwartet werden vier Spalten ohne Anführungszeichen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.Add(new InvoiceRow(
|
||||||
|
columns[0],
|
||||||
|
columns[1],
|
||||||
|
columns[2],
|
||||||
|
columns[3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
|
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
|
||||||
|
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs" Version="6.8.2" />
|
||||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.OpenTelemetry" Version="1.2.0" />
|
<PackageReference Include="Microsoft.Azure.Functions.Worker.OpenTelemetry" Version="1.2.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
|
||||||
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.7.0" />
|
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.7.0" />
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using ImportAssist.Core;
|
||||||
|
using Microsoft.Azure.Functions.Worker;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ImportAssist_Functions;
|
||||||
|
|
||||||
|
public sealed class InvoiceBlobTrigger(
|
||||||
|
ILogger<InvoiceBlobTrigger> logger)
|
||||||
|
{
|
||||||
|
[Function("InvoiceBlobTrigger")]
|
||||||
|
public void Run(
|
||||||
|
[BlobTrigger(
|
||||||
|
"imports/{name}",
|
||||||
|
Connection = "AzureWebJobsStorage")]
|
||||||
|
string content,
|
||||||
|
string name)
|
||||||
|
{
|
||||||
|
IReadOnlyList<InvoiceRow> rows;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
rows = InvoiceCsvParser.Parse(content);
|
||||||
|
}
|
||||||
|
catch (FormatException exception)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"Datei {BlobName} wegen CSV-Formatfehler abgewiesen: {Reason}",
|
||||||
|
name,
|
||||||
|
exception.Message);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validCount = 0;
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var errors = InvoiceValidator.Validate(row);
|
||||||
|
|
||||||
|
if (errors.Count == 0)
|
||||||
|
{
|
||||||
|
validCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invoiceId = string.IsNullOrWhiteSpace(row.InvoiceId)
|
||||||
|
? "(ohne Rechnungsnummer)"
|
||||||
|
: row.InvoiceId;
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"{BlobName}: {InvoiceId}: {Result}",
|
||||||
|
name,
|
||||||
|
invoiceId,
|
||||||
|
errors.Count == 0
|
||||||
|
? "Gültig"
|
||||||
|
: string.Join("; ", errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Dateiprüfung abgeschlossen: {BlobName}, " +
|
||||||
|
"{TotalCount} Rechnungen, {ValidCount} gültig, {InvalidCount} fehlerhaft.",
|
||||||
|
name,
|
||||||
|
rows.Count,
|
||||||
|
validCount,
|
||||||
|
rows.Count - validCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.29.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Azure.Storage.Blobs;
|
||||||
|
|
||||||
|
if (args.Length != 1 || !File.Exists(args[0]))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Bitte eine vorhandene Datei angeben.");
|
||||||
|
Environment.ExitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var container = new BlobContainerClient(
|
||||||
|
"UseDevelopmentStorage=true",
|
||||||
|
"imports");
|
||||||
|
|
||||||
|
await container.CreateIfNotExistsAsync();
|
||||||
|
|
||||||
|
var blobName = $"{Guid.NewGuid():N}-{Path.GetFileName(args[0])}";
|
||||||
|
var blob = container.GetBlobClient(blobName);
|
||||||
|
|
||||||
|
await blob.UploadAsync(args[0]);
|
||||||
|
|
||||||
|
Console.WriteLine($"Hochgeladen: imports/{blobName}");
|
||||||
Reference in new issue
Block a user