Letztes Jahr hat mich ein Maschinenbauer aus dem Schwarzwald engagiert. 180 Mitarbeiter, Eigenentwicklung für Auftragsabwicklung, vier Entwickler im Team — drei davon seit über acht Jahren dabei. Das System lief. Irgendwie. Die Bugs aber, die immer wieder reinkamen, hatten fast alle dasselbe Muster: irgendwo wurde eine int-ID für eine CustomerId als ArticleId übergeben. Kein Compile-Fehler. Kein Runtime-Fehler beim ersten Durchlauf. Nur falsche Daten in der Datenbank, drei Tage später entdeckt, ein halber Tag Debugging.
Das ist kein Einzelfall. Das ist die Regeldiagnose bei Systemen, die über Jahre gewachsen sind und in denen niemand je aufgeräumt hat, weil "es ja läuft".
Khalid Abuhakmeh beschreibt in seinem Artikel das Problem präzise: Primitive Typen wie int, string oder DateTime tragen keine semantische Bedeutung in sich. Eine DateTime ist eine DateTime — ob sie ein Geburtsdatum, ein Lieferdatum oder ein Zahlungsziel darstellt, weiß der Compiler nicht. Das führt zu Bugs, die der Compiler hätte verhindern können, es aber nicht tut. Value Objects lösen das: statt string customerId gibt es einen Typ CustomerId, der genau das repräsentiert und nichts anderes. Vogen ist eine NuGet-Bibliothek, die per Source Generator den Boilerplate-Code für solche Value Objects generiert — Vergleichsoperatoren, Validierung, Serialisierung. Das Ergebnis ist kompakterer, typsicherer Code mit wenig Handarbeit. Khalid zeigt das am Pac-Man-Beispiel mit den Geister-Namen, und das Beispiel ist didaktisch gut gewählt: verständlich, prägnant, ohne zu viel Rauschen.
Was er nicht schreibt — weil er es vermutlich nicht schreiben muss, sein Publikum sitzt in anderen Kontexten — ist die Frage, wie das in einem KMU mit vier Entwicklern und einer Legacy-Codebase aus 2013 landet.
Ich beantworte die Frage direkt: Das Pattern lohnt sich. Die Bibliothek Vogen lohnt sich — mit Einschränkungen.
Was sich lohnt:
Der Kern der Idee ist universell richtig. Primitive Obsession ist ein echtes Problem, nicht akademisches Geplänkel. Wenn in deinem System int sowohl Kunden-IDs als auch Artikel-IDs als auch Bestellpositionen repräsentiert, hast du ein strukturelles Problem, das mit der Zeit teuer wird. Value Objects sind die richtige Antwort darauf. Und Vogen macht die Umsetzung erheblich günstiger als handgeschriebene Record-Klassen mit manuell implementierten Validierungen.
Konkret empfehle ich in KMU-Projekten Value Objects für genau drei Kategorien:
- Geschäftliche IDs —
CustomerId,OrderId,ArticleId. Hier passieren die meisten Mix-up-Bugs. Hier ist der ROI am höchsten. - Geldbeträge — niemals rohe
decimal-Werte ohne Typ, der auch die Währung trägt. Ich habe Systeme gesehen, in denen Euro und Cent gemischt wurden, weil beidedecimalwaren. - Validierte Eingaben an Systemgrenzen — E-Mail-Adressen, IBAN, Postleitzahlen. Einmal validiert beim Erstellen des Value Objects, nie wieder.
Für alles andere — interne Berechnungsvariablen, temporäre Aggregatswerte, UI-Binding-Hilfsobjekte — ist der Overhead nicht gerechtfertigt.
Was nicht funktioniert:
Vogen setzt Source Generators voraus. In Projekten mit älteren Build-Pipelines, halbherzig gepflegten CI/CD-Setups oder Mischung aus .NET Framework und .NET 8 (beides gleichzeitig, ja, das existiert) kann das zu Überraschungen führen. Ich rate dazu, die Library in einem separaten Branch auszuprobieren und den Build komplett durchlaufen zu lassen, bevor man sie in den Haupt-Branch mergt.
Außerdem: Vogen generiert Code, den dein Team nicht selbst geschrieben hat und den nicht jeder sofort lesen kann. Vier Entwickler, drei davon seit acht Jahren im gleichen System — die reagieren auf "da ist jetzt ein Source Generator dabei" unterschiedlich. Einen kurzen Brown-Bag-Session-Slot à 30 Minuten, in dem ihr gemeinsam durch ein Beispiel geht, halte ich für Pflicht, nicht optional.
Das fehlt im Original-Artikel: die menschliche Seite der Einführung. Khalid schreibt für Leute, die das Pattern sofort verstehen und selbst anwenden können. Im Mittelstand hast du oft jemanden im Team, der seit Jahren das gleiche System pflegt, sehr gut darin ist — aber bei "Source Generators" und "partial struct" kurz schluckt. Das ist kein Qualitätsproblem, das ist Realität. Der Einführungspfad muss das berücksichtigen.
Meine konkrete Empfehlung:
Wenn du ein Team von zwei bis zehn Entwicklern hast und eine gewachsene Codebase mit primitiven IDs und Werttypen überall:
Fang mit dem schmerzhaftesten Punkt an. Such in deinem Code nach Methoden, die zwei oder mehr int-Parameter nebeneinander haben — GetOrder(int customerId, int orderId) zum Beispiel. Das sind deine Hochrisikostellen. Erstell dort zuerst Value Objects, mit oder ohne Vogen. Ohne Vogen kannst du mit simplen Records anfangen:
public record CustomerId(int Value);
public record OrderId(int Value);
Das kostet zehn Minuten und macht den Compiler zum Verbündeten. Vogen kommt, wenn ihr das Pattern für wertvoll haltet und den Source-Generator-Overhead in eurer Infrastruktur validiert habt.
Was ich nicht empfehle: sofort die ganze Codebase umschreiben. Das überfordert das Team, bricht laufende Features, und der Business-Owner sieht keinen Unterschied im Produkt. Wähle einen Bereich, mach ihn fertig, zeig was es gebracht hat — dann entscheidet ihr gemeinsam, ob ihr weitermacht.
Die Kernfrage für KMU ist nie "ist das Pattern gut?" — oft ist es gut. Die Frage ist "bringt uns das jetzt, in diesem System, mit diesem Team mehr als es kostet?" Bei IDs und Geldbeträgen: ja, fast immer. Bei allem anderen: prüfen.
Der vollständige Original-Beitrag von Khalid Abuhakmeh: Vogen and Value Objects with C# and .NET
Wenn du gerade vor einer ähnlichen Entscheidung stehst — Legacy-System, gemischtes Team, und du willst nicht erst drei Monate experimentieren, bis du weißt wo du stehst — dann lass uns das in 30 Minuten durchgehen.
— Bernhard