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));
| Methode | Zweck |
|---|---|
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 / WriteZplAsync | komplett als String oder in einen TextWriter |
Generate / GenerateAsync | lazy, ein ZPL-Format pro Datensatz |
Batches(records, n) | Druckjobs zu je n Etiketten |
PrintAsync(printer, records, n) | direkt an ZplPrinterClient senden, liefert Anzahl |
Render / RenderPdf | Vorschau als Bilder bzw. PDF |
WithParallelism(n) | Threads, die die Formate eines Jobs erzeugen (parallele Erzeugung) |
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);

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

