zplCore.comzplCore.com

11Massendaten-Bindung

label.For<T>() liefert eine typisierte, streamende LabelBinding<T>: funktioniert mit Listen, LINQ, EF Core (IQueryable oder AsAsyncEnumerable()), LiteDB, DataRow und Dictionaries – Millionen Datensätze, ohne sie im Speicher zu halten.

var binding = label.For<OrderRec>()
    .Map("Total", o => o.Price * o.Quantity)    // berechnetes Feld (kompilierter Lambda)
    .Copies(o => o.Quantity)                    // ^PQ pro Datensatz
    .UseStoredFormat();                         // ^DF einmal, danach nur ^XF + ^FN-Werte

// DataField = "Customer.Address.City" wird per PropertyPath aufgelöst
string zpl = binding.ToZpl(orders);
foreach (string chunk in binding.Generate(orders)) { /* lazy, Datensatz für Datensatz */ }
foreach (string batch in binding.Batches(orders, labelsPerBatch: 500)) { /* Druckjobs */ }

await using var file = new StreamWriter("job.zpl");
await binding.WriteZplAsync(stream, file);                       // IAsyncEnumerable

int sent = await binding.PrintAsync(new ZplPrinterClient("192.168.1.50"), stream, labelsPerBatch: 500);
IEnumerable<byte[]> pngs = binding.Render(orders.Take(3));
byte[] pdf = binding.RenderPdf(orders.Take(100));
MethodeZweck
Map(field, o => …) / MapItem(name, …)Feld oder benanntes Element an kompilierten Lambda binden
Copies(o => n)Anzahl Kopien pro Datensatz (^PQ)
UseStoredFormat(name)Vorlage einmal per ^DF, danach nur ^XF/^FN-Werte – typisch 80–95 % weniger Daten. Druckt exakt wie die normalen Formate: Elemente, deren Layout vom Wert abhängt (Einpassen, vertikale Zentrierung, kurzer Bindestrich, Überlauf, eingepasste Barcodes, Richtext, Tabellen, Grafiken), gehen vollständig mit jedem Datensatz
Across(columns, gap)mehrere Datensätze nebeneinander (mehrbahnige Rollen)
ToZpl / WriteZpl / WriteZplAsynckomplett als String oder in einen TextWriter
Generate / GenerateAsynclazy, ein ZPL-Format pro Datensatz
Batches(records, n)Druckjobs zu je n Etiketten
PrintAsync(printer, records, n)direkt an ZplPrinterClient senden, liefert Anzahl
Render / RenderPdfVorschau als Bilder bzw. PDF
WithParallelism(n)Threads, die die Formate eines Jobs erzeugen (parallele Erzeugung)
PropertyPath: DataField = "Customer.Address.City" navigiert durch Properties, Felder, Dictionaries, DataRow, JsonElement und anonyme Typen. Eigene Wrapper-Typen (z. B. BsonValue) entpackt PropertyPath.RegisterUnwrapper<T>().

EF Core + SQLite

Aus dem Test DatabaseBindingTests: 2000 Sendungen in SQLite, Navigation-Properties, asynchrones Streaming mit gespeichertem Format – der letzte Barcode wird gerendert und mit ZXing dekodiert.

var label = new Label(LabelUnit.Mm, 100, 60)
    .Add(new TextItem { X = 3, Y = 3, DataField = "Customer.Name", FontSize = 14 })
    .Add(new TextItem { X = 3, Y = 12, DataField = "Customer.City" })
    .Add(new TextItem { X = 60, Y = 12, DataField = "Weight", DataFieldFormat = "0.0 kg" })
    .Add(new BarcodeItem { X = 3, Y = 22, DataField = "TrackingNo", BarHeight = 15 })
    .Add(new TextItem { X = 3, Y = 50, Expression = "\"Paket \" + LABEL_NUMBER" });

var options = new DbContextOptionsBuilder<ShippingDb>().UseSqlite("Data Source=shipping.db").Options;
await using var db = new ShippingDb(options);

// Streaming über alle offenen Sendungen – nie alles im Speicher
var query = db.Shipments.Include(s => s.Customer)
    .Where(s => !s.Printed && s.Customer.City == "Wien")
    .OrderBy(s => s.TrackingNo)
    .AsNoTracking()
    .AsAsyncEnumerable();

await using var output = new StreamWriter("versand.zpl");
await label.For<Shipment>()
    .Copies(s => s.Parcels)
    .UseStoredFormat()
    .WriteZplAsync(query, output);

LiteDB

using var db = new LiteDatabase("produkte.db");
PropertyPath.RegisterUnwrapper<BsonValue>(v => v.RawValue);   // BsonValue → .NET-Wert

var label = new Label(LabelUnit.Mm, 60, 40)
    .Add(new TextItem { X = 2, Y = 2, DataField = "Name" })
    .Add(new TextItem { X = 2, Y = 9, DataField = "Supplier.Name" })
    .Add(new TextItem { X = 40, Y = 9, DataField = "Price", DataFieldFormat = "0.00" })
    .Add(new BarcodeItem { X = 2, Y = 16, DataField = "Sku", BarHeight = 12 });

// typisiert mit LINQ
var typed = db.GetCollection<LiteProduct>("products").Query()
    .Where(p => p.Price > 400 && p.Supplier.Name == "ACME")
    .OrderBy(p => p.Price)
    .ToEnumerable();
string zpl = label.For<LiteProduct>().ToZpl(typed);

// untypisiert: BsonDocument, verschachtelte Pfade funktionieren genauso
var docs = db.GetCollection("products").Find(Query.EQ("Supplier.Name", "Contoso"));
string zpl2 = label.For<BsonDocument>().ToZpl(docs);

Mehrbahnige Etiketten

var small = new Label(LabelUnit.Mm, 30, 20)
    .Add(new TextItem { X = 2, Y = 2, DataField = "Sku", FontSize = 9 })
    .Add(new BarcodeItem { X = 2, Y = 7, DataField = "Sku", Symbology = Symbology.DataMatrix });

// 3 Etiketten nebeneinander auf einem 96 mm breiten Trägerband, 3 mm Abstand
string zpl = small.For<Line>().Across(3, gap: 3).ToZpl(lines);
Across(3, 3): drei Datensätze nebeneinander

Datenquellen

Die Bindung arbeitet mit jedem IEnumerable<T>, IQueryable<T> oder IAsyncEnumerable<T>: Filtern und Sortieren laufen in der Datenbank, die Labels werden gestreamt. Damit funktioniert jeder EF-Core-Provider und jeder Datenbanktreiber mit LINQ-Unterstützung, zum Beispiel:

  • Microsoft SQL Server, Azure SQL, PostgreSQL, MySQL, MariaDB, Oracle, SQLite, Firebird, SAP HANA, IBM Db2 / Informix
  • MongoDB, Azure Cosmos DB, Couchbase, RavenDB, Amazon DynamoDB, Elasticsearch, OpenSearch, LiteDB
  • Dapper / ADO.NET (DataRow), Entity Framework Core, C#-Objekte / LINQ, JSON (JsonElement) und dynamische Datensätze

JSON- & dynamische Records, Datensatzanzahl, Stream/PipeWriter, Ausdrücke

Ohne eigene Klasse binden: JsonElement, JsonNode, ExpandoObject, Dictionaries, DataRow und LiteDB-BsonDocument (Pfade wie order.no). Ist die Anzahl bekannt (WithRecordCount, CountQueryableWith oder recordCount:), stehen TOTAL_PAGES und PrintJobInfo.RecordCount schon ab dem ersten Etikett zur Verfügung. WriteZplAsync schreibt UTF-8 direkt in einen Stream oder PipeWriter (ASP.NET Core). Variablen: LABEL_INDEX, LABEL_NUMBER, LABEL_WIDTH, LABEL_HEIGHT, RECORD_INDEX, PAGE_NUMBER, TOTAL_PAGES plus eigene. Funktionen u. a. Upper Lower Left Right Mid Substring Trim PadLeft Replace Contains If Coalesce IsEmpty IsNumeric Format ToNumber ToDate Today Now AddDays Round Min Max Mod10 Mod11 Mod43 Luhn Gs1CheckDigit VoicePickCode GS1. Zahlen und Datumswerte nutzen Expressions.Culture (Standard: invariant); Format/ToNumber nehmen optional einen Kulturnamen.

Ausprobieren: Die Live-Demo bindet eine JSON-Vorlage an JSON-Datensätze (label.For<JsonElement>()) und zeigt ZPL, Vorschau und Zeiten je Phase.

// Records ohne eigene Klasse: JsonElement, JsonNode, ExpandoObject, Dictionaries, DataRow, BsonDocument
using var doc = JsonDocument.Parse("""[{"name":"Anna","order":{"no":"A-1"}},{"name":"Ben","order":{"no":"A-2"}}]""");
string zpl = label.For<JsonElement>().ToZpl(doc.RootElement.EnumerateArray());

dynamic d = new ExpandoObject(); d.name = "Cleo";
string one = label.For<ExpandoObject>().ToZpl([(ExpandoObject)d]);

// Bekannte Anzahl → TOTAL_PAGES/RecordCount schon beim ersten Etikett; UTF-8 direkt in Stream oder PipeWriter
var binding = label.For<JsonElement>().WithRecordCount(2);
await binding.WriteZplAsync(rows, response, dpi: 203, recordCount: 2);
await binding.WriteZplAsync(rows, pipe);

// Ausdrücke: Funktionen, Variablen und Kultur
label.Expressions.Culture = System.Globalization.CultureInfo.GetCultureInfo("de-DE");
label.Add(new TextItem { X = 5, Y = 50,
    Expression = "\"Seite \" + PAGE_NUMBER + \"/\" + TOTAL_PAGES + \" · \" + Format(Today(), \"d\") + \" · \" + Upper(Left(name, 3))" });

Parallele Job-Erzeugung

Die Formate eines Jobs entstehen auf mehreren Threads gleichzeitig, in Blöcken von Datensätzen, die 1, 2, 4 … 512 groß werden – die ersten Etiketten streamen also weiterhin sofort. Datensätze werden der Reihe nach gelesen, nummeriert, gezählt und kopiert: Das ZPL ist Byte für Byte dasselbe wie bei sequenzieller Erzeugung – einschließlich Zählern, Kopien, PrintJobInfo, OnProgress, Start- und Endetikett, gespeicherten Formaten und mehrbahnigem Druck. Standard: alle Prozessoren (Environment.ProcessorCount) für Datensätze, deren Felder ohne Ihren Code gelesen werden – Dictionaries, JsonElement, JsonNode, DataRow, Strings und primitive Typen. Standardmäßig sequenziell sind Klassen (POCOs), die über ihre Property-Getter gelesen werden und dabei Daten nachladen können (z. B. Entity-Framework-Proxys mit einem gemeinsamen DbContext), und Jobs, die während der Erzeugung Aufrufercode ausführen: Map, eigene Ausdrucksfunktionen, AddTypeReference oder eine eigene Expressions.Clock. WithParallelism(n) legt die Zahl der Threads in jedem Fall fest – mehr für Klassen mit threadsicheren Gettern, 1 für streng sequenzielle Erzeugung.

// Dictionaries, JsonElement, JsonNode, DataRow: Formate entstehen standardmäßig auf allen Kernen
string zpl = label.For<IReadOnlyDictionary<string, object?>>().ToZpl(rows);

// Klassen (Getter können nachladen, z. B. EF-Core-Proxys) und Jobs mit Map, eigenen Funktionen oder Clock:
// standardmäßig sequenziell – parallel nur auf ausdrücklichen Wunsch
string fast = label.For<OrderRec>().WithParallelism(Environment.ProcessorCount).ToZpl(orders);

string sequential = label.For<IReadOnlyDictionary<string, object?>>().WithParallelism(1).ToZpl(rows);
// zpl == sequential: gleiche Reihenfolge, Zähler, Kopien und PrintJobInfo, Byte für Byte

Gemessen mit samples/ParallelBench: die Vorlage samples/templates/shipping.json (100 × 150 mm, 33 Elemente), gebunden an 100.000 Dictionary-Datensätze, ein Job mit 135 MB ZPL, Median aus 11 Läufen, .NET 10 Release, 16 Kerne (AMD EPYC 9454P, virtuelle Maschine, Windows Server 2025). Sequenziell (WithParallelism(1)) 9,7 s (Workstation-GC) / 9,4 s (Server-GC); Standard 2,4 s (4,0-mal so schnell) mit dem Workstation-GC und 1,4 s (6,8-mal so schnell) mit dem Server-GC. Parallele Erzeugung alloziert auf allen Threads zugleich, und der Workstation-GC räumt auf einem Heap auf – für maximale Skalierung den Server-GC in der Anwendung einschalten (<ServerGarbageCollection>true</ServerGarbageCollection> in der Projektdatei, Standard bei ASP.NET-Core-Apps, oder DOTNET_gcServer=1).

dotnet run -c Release --project samples/ParallelBench -- samples/templates/shipping.json 100000 11