Die Evolution von .NET: Integration von KI und mobilen Funktionen in modernen Webanwendungen
Milan Jovanović hat kürzlich ein starkes Argument gegen vorzeitige API-Versionierung veröffentlicht. Der wesentliche Punkt: Die meisten Teams greifen zu früh zu v2, weil ihnen eine Strategie zur Vertragsevolution fehlt. Versionierung ist ein Kompatibilitätswerkzeug, keine Designstrategie.
Das Argument resoniert mit unserem Ingenieurteam bei Iron Software. Wir liefern .NET-Bibliotheken, was bedeutet, dass die öffentliche Oberfläche unserer Produkte ist eine API. Jede Methodensignatur, jede Eigenschaft, jedes Standardverhalten ist ein Vertrag, der in Tausenden von Kunden-Codebasen sitzt. Ein großes Versions-Update ist keine Veröffentlichung. Es ist ein Migrationsprojekt für alle nachgelagerten.
Was folgt, ist ein Entwicklerstandpunkt zu Milans Artikel aus der Sicht eines Bibliotheksautors und wie dieselben Kompatibilitätsregeln gelten, egal ob Sie eine REST-API oder ein NuGet-Paket liefern.
Zusammenfassung
- Versionierung ist keine Designstrategie. Es ist der Notausgang, wenn das Nebeneinander scheitert.
- Breaking Changes verstecken sich im Verhalten, nicht nur in URLs oder Schemata.
- Die vier Kompatibilitätsregeln: nicht entfernen, Verarbeitung nicht ändern, Validierung nicht verschärfen, Zusätze optional halten.
- Eine neue Operation ist fast immer günstiger als eine neue Version.
- Echte Veralterung erfordert Laufzeitsignale und Telemetrie, nicht nur Dokumentationsaktualisierungen.
Die HTTP-Regeln gelten auch für Bibliotheks-APIs
Milan rahmt die Diskussion um eine REST-API für /orders ein, aber die gleichen Regeln gelten, wenn Ihre API eine öffentliche C#-Klasse ist, die in einem NuGet-Paket ausgeliefert wird. Die Zuordnung ist direkt:
| REST-API-Änderung | NuGet-Bibliotheksäquivalent |
|---|---|
| Umbenennen eines JSON-Feldes | Umbenennen einer öffentlichen Eigenschaft |
| Entfernen eines Endpunkts | Entfernen einer öffentlichen Methode |
| Verschärfen der Anforderungsvalidierung | Hinzufügen eines nicht-nullbaren Parameters |
| Änderung des Betriebsverhaltens | Änderung dessen, was eine Methode im Hintergrund tut |
| Hinzufügen eines erforderlichen Feldes | Hinzufügen eines erforderlichen Konstruktorsparameters |
Wenn Sie jemals eine Hauptversion einer beliebten .NET-Bibliothek bezogen und einen halben Tag damit verbracht haben, umbenannte APIs zu beheben, dann waren Sie am empfangenden Ende einer v2 Entscheidung, die wahrscheinlich ergänzend hätte behandelt werden können.
Was Verbraucher tatsächlich bricht
Milans Liste ist präzise:
- Entfernen oder Umbenennen von Feldern
- Ändern der Bedeutung vorhandener Daten
- Verschärfen der Anforderungsvalidierung
- Ändern von Seitierungs- oder Fehlerformaten
- Annehmen, dass enum-ähnliche Werte für immer geschlossen sind
Der zweite Punkt ist der, der Teams am häufigsten überrascht: das Ändern der Bedeutung vorhandener Daten, ohne ihre Form zu ändern. Das JSON sieht gleich aus. Die C#-Signatur sieht gleich aus. Alles kompiliert. Nichts wirft zur Laufzeit. Aber das Feld bedeutet jetzt etwas anderes, und jeder Verbraucher, der von der alten Semantik abhängt, liegt stillschweigend falsch.
Milans Beispiel:
// Before
{ "total": 100 }
// After
{ "total": { "amount": 100, "currency": "USD" } }
Gleicher Feldname. Gleicher Endpunkt. Jeder Client, der total als Zahl geparst hat, ist nun funktionsunfähig.
Das äquivalente Bibliotheksbeispiel ändert, was eine Methode zurückgibt oder wie sie ihre Eingaben interpretiert. Eine Save() Methode, die vorher überschrieben hat und jetzt anhängt. Ein Trim Parameter, dessen Standard von true zu false wechselt. Eine Methode, die bei ungültiger Eingabe einen Fehler auslöste und nun stillschweigend einen Standardwert zurückgibt.
Die vier Kompatibilitätsregeln
Milan fasst die Regeln zusammen: nichts entfernen, keine Verarbeitungsregeln ändern, optionale Dinge nicht verpflichtend machen und alles, was Sie hinzufügen, muss optional sein. Die vier Prinzipien sollten vor dem Team stehen, das für eine öffentliche API verantwortlich ist:
- Halten Sie bestehende Felder und Verhalten beibehalten.
- Machen Sie optionale Anfragedaten nicht zu erforderlichen Daten.
- Ändern Sie nicht, was eine bestehende Operation tut.
- Machen Sie alles Neue additiv und standardmäßig optional.
Diese stehen in direktem Zusammenhang mit dem Bibliotheksdesign. "Nichts wegnehmen" bedeutet, keine öffentlichen Mitglieder zu löschen. "Ändern Sie keine Verarbeitungsregeln" bedeutet, dass bestehende Methoden so funktionieren sollten, wie sie es beim Starten taten. "Optionale nicht verpflichtend machen" bedeutet, fügen Sie keine erforderlichen Parameter zu einer bestehenden Methode hinzu; stattdessen bieten Sie eine Überladung an. "Additiv und optional" bedeutet, dass neue Funktionalität in neuen Methoden oder optionalen Parametern mit sinnvollen Standardwerten gehört.
Wie das in der Praxis funktioniert
Der klarste Weg, diese Regeln zu veranschaulichen, ist mit einer echten API-Entscheidung, daher hier eine unserer Entscheidungen.
Vor einigen Veröffentlichungen musste IronPDF einen reichhaltigeren Satz von Renderoptionen für die HTML-zu-PDF-Konvertierung unterstützen: benutzerdefinierte Papiergrößen, benutzerdefinierte Ränder, CSS-Medienemulation, Kopf- und Fußzeilentemplates und mehr. Der einfache Ansatz wäre gewesen, die bestehende Render-Methode zu ändern, um die neuen Optionen zu akzeptieren. Diese Entscheidung hätte jeden Kunden, der die einfache Form der API verwendet, behindert.
Zum Kontext: Die Bibliothek wird über die Standard-.NET-Paketkanäle installiert:
# .NET CLI
dotnet add package IronPdf
# Package Manager Console
Install-Package IronPdf
# .NET CLI
dotnet add package IronPdf
# Package Manager Console
Install-Package IronPdf
Das IronPdf NuGet-Paket hat mehr als 18 Millionen Downloads angesammelt, was teilweise erklärt, warum API-Stabilität wichtig ist: Jede nicht kompatible Änderung breitet sich über so viele Integrationen aus.
Der Ansatz, den wir stattdessen veröffentlicht haben:
// The original, three-year-old API. Still works. Still unchanged.
var renderer = new ChromePdfRenderer();
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
// New rendering options live on an options object, not in the method signature.
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print;
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter { HtmlFragment = "..." };
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
// The original, three-year-old API. Still works. Still unchanged.
var renderer = new ChromePdfRenderer();
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
// New rendering options live on an options object, not in the method signature.
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print;
renderer.RenderingOptions.HtmlHeader = new HtmlHeaderFooter { HtmlFragment = "..." };
PdfDocument pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>");
' The original, three-year-old API. Still works. Still unchanged.
Dim renderer As New ChromePdfRenderer()
Dim pdf As PdfDocument = renderer.RenderHtmlAsPdf("<h1>Hello</h1>")
' New rendering options live on an options object, not in the method signature.
renderer = New ChromePdfRenderer()
renderer.RenderingOptions.PaperSize = PdfPaperSize.A4
renderer.RenderingOptions.MarginTop = 20
renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print
renderer.RenderingOptions.HtmlHeader = New HtmlHeaderFooter With {.HtmlFragment = "..."}
pdf = renderer.RenderHtmlAsPdf("<h1>Hello</h1>")
Drei Beobachtungen zur Entscheidung:
- Die ursprüngliche
RenderHtmlAsPdf(string html)Signatur bleibt unverändert. Kunden, die ein Upgrade durchführten, mussten keinen Code ändern. - Neue Fähigkeiten leben auf einem Optionsobjekt, das Verbraucher selbst auswählen. Die Methode hat keine neuen erforderlichen Parameter.
- Die Standardwerte von
RenderingOptionserzeugen eine Ausgabe, die der vorherigen API entspricht. Das Verhalten ist unverändert für jeden, der nichts konfiguriert.
Das ist Regeln 1, 2 und 4 aus Milans Liste auf einmal angewendet. Das Produkt hat sich entwickelt. Der Vertrag nicht.
Die Versuchung, RenderHtmlAsPdfV2(string html, RenderingOptions options) auszuliefern, war real. Es hätte auf der API-Referenzseite aufgeräumter ausgesehen. Es hätte jeden Kunden eine Migration gekostet. Wir entschieden uns anders.
Tolerante Leser
Der andere Aspekt von Milans Argument zur Hinzufügung statt Ersetzung ist, dass auch die Verbraucher Verantwortung tragen. Ein gut funktionierender Client sollte Felder ignorieren, die er nicht versteht.
In .NET ignoriert System.Text.Json standardmäßig unbekannte Eigenschaften, was der korrekte Standard ist. Das Risiko tritt in der Regel an zwei Stellen auf:
- Generierte SDKs mit strikten Schemata, die unerwartete Felder ablehnen
- Vertragstests, die auf exakte JSON-Gleichheit abzielen
Beide verwandeln eine erklärte Garantie "wir ignorieren unbekannte Felder" in eine Stolperfalle. Wenn Ihr CI bricht, sobald der Server eine neue optionale Eigenschaft hinzufügt, haben Sie keine Rückwärtskompatibilität. Sie haben einen Regressionsdetektor, der als Kompatibilitätspolitik getarnt ist.
Das Verhalten ist Teil des Vertrags
Milans Abschnitt über das stille Verschieben von DELETE /orders/{id} von Soft Delete zu Hard Delete ist die klarste Abhandlung zu diesem Thema, die wir gesehen haben.
Die URL bleibt gleich. Der Anfrageinhalt bleibt gleich. Die Antwortform bleibt gleich. Was die Operation auf dem Server tut, ist anders.
Dies ist die gefährlichste Kategorie von nicht kompatiblen Änderungen, da nichts in einem Schema-Diff sie erfasst. Die OpenAPI-Spezifikation ist identisch. Der generierte Client kompiliert. Die Integrationstests bestehen. Und jeder Verbraucher, der Tools entwickelt hat, die darauf basieren, dass "gelöschte Bestellungen wiederherstellbar sind", zerstört stillschweigend Daten in der Produktion.
Das äquivalente Bibliotheksbeispiel ändert, was eine Methode tut, ohne ihre Signatur zu ändern. Beispiele, die wir ausdrücklich vermieden haben:
- Eine
Save()Methode, die vorher synchron gespült hat und leise zu einem asynchronen Feuer-und-Vergessen wird - Eine OCR-Methode, die rohe Ergebnisse zurückgab und beginnt, sie nachzubearbeiten
- Ein Barcode-Reader, der bei unleserlichen Eingaben einen Fehler auslöste und beginnt, einen leeren String zurückzugeben
Jedes davon ist ein Vertragsbruch, der als Verbesserung getarnt ist. Die richtige Antwort ist dieselbe wie bei Milan: Fügen Sie eine neue Methode oder Option hinzu, lassen Sie das alte Verhalten unverändert und kennzeichnen Sie den alten Pfad nur als veraltet, wenn Telemetrie anzeigt, dass es sicher ist, dies zu tun.
Strikteres Validieren
Diese Kategorie betrifft schließlich jedes Team. Es gibt zwei Varianten desselben Fehlers:
- Ein bestehendes optionales Feld verpflichtend machen
- Ein neues Feld hinzufügen und es von Anfang an als erforderlich markieren
Beide brechen ältere Clients. Der Endpunktpfad bewegt sich nicht, aber Anfragen, die zuvor erfolgreich waren, scheitern nun zur Laufzeit.
Die Bibliotheksversion dieses Fehlers besteht darin, einen erforderlichen Konstruktorparameter hinzuzufügen oder einen bestehenden optionalen Parameter zur Pflicht zu machen. Jeder bestehende Aufrufer bricht zur Kompilierzeit, was gegenüber einem Laufzeitfehler vorzuziehen ist, aber es verursacht dennoch einen Migrationskosten für jeden Verbraucher.
Sicherere Wege:
- Fehlende Werte während eines Übergangsfensters akzeptieren und Standards dort ableiten, wo es möglich ist.
- Eine neue Überladung oder einen Builder hinzufügen, der die reichere Eingabeform erfordert.
- Eine neue Operation oder einen Konstruktor für den strikteren Arbeitsablauf einführen.
Die grundlegende Regel ist konsistent: Alles, was dem Vertrag hinzugefügt wird, muss optional sein, und alles, was zuvor optional war, muss optional bleiben. Wenn wirklich strengere Anforderungen notwendig sind, gehören sie in eine neue Operation, nicht in eine Verschärfung der bestehenden.
Eine neue Operation ist fast immer günstiger als eine neue Version
Dies ist das Prinzip, das es wert ist, sich einzuprägen.
Wenn ein Anwendungsfall tatsächlich über das hinausgewachsen ist, was ein bestehender Endpunkt sauber unterstützt, ist der übliche Reflex, den Endpunkt mit Flags zu überladen:
POST /orders?validateOnly=true&includeTaxEstimate=true&reserveInventory=true
Oder, noch disruptiver, um die Änderung als ein Versionsproblem zu deklarieren und mit der Arbeit an /v2/orders zu beginnen. Beide sind normalerweise falsch. Der sauberere Ansatz ist eine neue Operation parallel zur bestehenden:
POST /orders
POST /orders/quote
POST /checkout-sessions
Jede Operation hat einen klaren Vertrag, separate Berechtigungen, unabhängige Validierung und ihren eigenen Entwicklungsweg. Der ursprüngliche Endpunkt bleibt einfach. Der Rest der API wird nicht in ein Major-Version-Bump hineingezogen.
Im Bibliothekskontext ist das Äquivalent, eine neue Methode hinzuzufügen, anstatt eine bestehende mit optionalen Parametern zu überladen, bis sie unlesbar wird. ExtractText() bleibt der einfache Textextraktor. ExtractTextWithLayout() wird zur reichhaltigeren Variante. ExtractStructuredDocument() wird zur reichhaltigsten. Drei Methoden mit klaren Verträgen sind einer Methode mit acht optionalen Parametern vorzuziehen.
Bewusst veralten
Dies ist die Hälfte des Managements von API-Änderungen, die die meisten Teams überspringen, aber es ist die Hälfte, die darüber entscheidet, ob die Strategie funktioniert.
Echte Veralterung ist keine Notiz in einem Änderungsprotokoll. Sie umfasst vier Schritte:
- Markieren Sie das Feld oder den Endpunkt in der OpenAPI-Beschreibung als veraltet (oder mit dem
[Obsolete]Attribut in der .NET-Welt). - Signalisieren Sie das Veralten zur Laufzeit, damit der Live-Verkehr darauf zugreifen kann.
- Verlinken Sie zu einem tatsächlichen Migrationsleitfaden.
- Messen Sie die Nutzung mit Telemetrie, um festzustellen, wann das Entfernen sicher ist.
Für HTTP-APIs ist das Signalisieren zur Laufzeit einfach:
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: <https://docs.example.com/migrations/orders-total>; rel="deprecation"
Für .NET-Bibliotheken ist das Äquivalent ein [Obsolete("Verwenden Sie stattdessen NewMethod. Dies wird in v2026.x entfernt", DiagnosticId = "IRON001")] attribute paired with a UrlFormat zeigt auf eine Migrationsseite. Die Compiler-Warnung erscheint in der Build-Ausgabe jedes Verbrauchers, die Diagnose-ID erlaubt bewusstes Unterdrücken, und der Link gibt Verbrauchern einen dokumentierten Migrationspfad.
Der Telemetrieschritt ist unverhandelbar. Ohne zu wissen, welche Kunden immer noch von der veralteten Methode abhängen, wird das Entfernen zur Vermutung. Das Ergebnis ist entweder eine vorzeitige Entfernung, die aktive Integrationen bricht, oder ein unbefristeter Tragekosten, der den Zweck der Veralterung zunichtemacht.
Wann Versionierung die richtige Entscheidung ist
Milan ist nicht gegen Versionierung, und wir auch nicht. Versionierung ist angemessen, wenn:
- Die alten und neuen Semantiken wirklich nicht koexistieren können
- Das Ressourcenmodell sich grundlegend geändert hat
- Die Kompatibilitätsregeln einen Vertrag erzwingen würden, über den niemand nachdenken kann
Der Punkt ist nicht, Versionierung vollständig zu vermeiden. Der Punkt ist, darauf zurückzugreifen, weil Koexistenz gescheitert ist, nicht weil es die erste Idee auf dem Tisch war.
Wenn Versionierung notwendig ist, sollte sie mit einem echten Veralterungsprozess gepaart werden. Die schwierige Arbeit besteht nicht darin, v2 auszuliefern. Die schwierige Arbeit besteht darin, Konsumenten von v1 zu lösen.
Die Entscheidungsregel
Milans Rahmen ist der richtige, um ihn anzuwenden:
- Kann ich hinzufügen, anstatt zu ersetzen?
- Können alte und neue Verträge während eines Migrationsfensters koexistieren?
- Kann ich eine neue Operation einführen, anstatt eine alte zu ändern?
- Kann ich die alte Form mit Dokumentation, Überschriften und Telemetrie veralten?
Wenn die Antwort auf alle vier ja lautet, ist eine neue Version wahrscheinlich unnötig. Wenn die Antwort nein lautet, und die beiden Welten wirklich nicht koexistieren können, dann versionieren Sie bewusst.
Entwerfen Sie Verträge, die sich weiterentwickeln können. Betrachten Sie Verbraucher als langlebige Integrationen statt als heutigen Code. Behalten Sie die Versionierung für die Fälle vor, in denen die Kompatibilität wirklich erschöpft ist.
Für das vollständige Stück, einschließlich längerer arbeitsbasierter Beispiele, lesen Sie Milans Originalbeitrag.
Bei der Auswahl einer .NET-Bibliothek, auf die Sie sich verlassen möchten, ist die Frage, die man sich stellen sollte, die, um die Milans Beitrag aufgebaut ist: wird diese Bibliothek noch wie die API aussehen, die ich vor drei Jahren integriert habe?
Das ist die Frage, die wir mit jeder Veröffentlichung zu beantworten versuchen. Die einfachen Aufrufe von 2020 funktionieren immer noch. Neue Fähigkeiten stehen neben ihnen, optional und additiv. Keine erzwungenen Major-Version-Migrationen.
Wenn dieser Ansatz zur Bibliotheksgestaltung dem entspricht, was Sie benötigen, starten Sie eine kostenlose 30-tägige Testversion und überprüfen Sie die API-Referenz selbst. Der Fünf-Minuten-Schnellstart führt durch Installation, Lizenzaktivierung und ein erstes gerendertes PDF. Das Paket selbst ist nur einen Befehl von jedem .NET-Projekt entfernt:
Für Umgebungen, in denen NuGet nicht der bevorzugte Pfad ist, bietet der Direktdownload die DLL und den Windows-Installer an.