N-Schichtenanwendungen und Entity Framework

Erstellen von N-Schichtenanwendungen mit EF4

Daniel Simmons

Beispielcode herunterladen.

Dieser Artikel ist der dritte einer Artikelreihe über die N-Schichtenprogrammierung mit Entity Framework (siehe msdn.microsoft.com/magazine/dd882522.aspx und msdn.microsoft.com/magazine/ee321569.aspx). Er konzentriert sich auf die Erstellung benutzerdefinierter Webdienste mit Entity Framework (EF) und Windows Communication Foundation (WCF). (In manchen Fällen ist ein REST-basierter Dienst oder eine andere Vorgehensweise besser geeignet, aber in diesem Artikel konzentriere ich mich auf benutzerdefinierte Webdienste.) Im ersten Artikel wurde eine Reihe wichtiger Entwurfsüberlegungen und Antimuster beschrieben. Im zweiten Artikel ging es um die vier Muster, die erfolgreich in einer N-Schichtenprogrammierung verwendet werden können. Dieser Artikel enthielt auch Codebeispiele, anhand derer veranschaulicht wurde, wie die erste Version von Entity Framework (EF 3.5 SP1) zur Implementierung von so genannten einfachen Entitätsmustern (Simple Entities) verwendet werden kann. In diesem Artikel werden Features, die in der zweiten Version von Entity Framework (EF4) enthalten sind, und deren Verwendung zur Implementierung der N-Schichtenmuster Self-Tracking Entities und Data Transfer Objects (DTOs) behandelt.

Während einfache Entitäten normalerweise nicht das bevorzugte Muster für N-Schichtenanwendungen sind, sind sie die praktikabelste Option in der ersten Version von EF. EF4 ändert jedoch die Optionen für die N-Schichtenprogrammierung im Framework deutlich. Zu den wichtigsten neuen Funktionen zählen Folgende:

  1. Neue Framework-Methoden, die getrennte Vorgänge wie ChangeObjectState und ChangeRelationshipState unterstützen, mit denen eine Entität oder eine Beziehung einen neuen Status erhält (z. B. hinzugefügt oder geändert); ApplyOriginalValues, mit dem Sie die Originalwerte für eine Entität festlegen können; und das neue ObjectMaterialized-Ereignis, das ausgelöst wird, wenn eine Entität vom Framework erstellt wurde.
  2. Unterstützung von Plain Old CLR-Objekten (POCO) und Fremdschlüsselwerten für Entitäten. Mit diesen Funktionen können Sie Entitätsklassen erstellen, die zwischen der Mittelschicht-Dienstimplementierung und anderen Schichten gemeinsam genutzt werden können, selbst wenn diese nicht dieselbe Version von Entity Framework (beispielsweise .NET 2.0 oder Silverlight) haben. POCO-Objekte mit Fremdschlüsseln haben außerdem ein einfaches Serialisierungsformat, das die Interoperabilität mit Plattformen wie z. B. Java erleichtert. Die Verwendung von Fremdschlüsseln ermöglicht darüber hinaus ein viel einfacheres Parallelitätsmodell für Beziehungen.
  3. T4-Vorlagen zum Anpassen der Codeerstellung. Diese Vorlagen bieten eine Möglichkeit zum Erstellen von Klassen, die Self-Tracking Entities- oder DTO-Muster implementieren.

Das Entity Framework-Team hat diese Funktionen zur Implementierung der Self-Tracking Entities-Muster in einer Vorlage verwendet, damit diese Muster besser zugänglich sind, und während DTOs noch immer die meiste Arbeit während der ersten Implementierung erfordern, ist dieser Prozess mit EF4 etwas einfacher. (Die Self-Tracking Entities-Vorlage und einige andere EF-Funktionen sind als Teil eines Web-Download Feature Community Technology Preview (CTP) verfügbar, nicht jedoch in der Visual Studio 2010/.NET 4-Box. Bei den Beispielen in diesem Artikel wird davon ausgegangen, dass Visual Studio 2010/.NET 4 und Feature-CTP installiert sind.) Mit diesen neuen Funktionen besteht eine Möglichkeit zur Bewertung der vier beschriebenen Muster (Simple Entities, Change Set, Self-Tracking Entities und DTOs) in Form eines Kompromisses zwischen Archtitekturqualität (Trennung von Bereichen/lose Kopplung, Vertragsstärke, effizientes Übertragungsformat und Interoperabilität) sowie einfache Implementierung und Produkteinführungszeit. Wenn Sie die vier Muster in einem Diagramm für diesen Kompromiss darstellen, sieht das Ergebnis in etwa wie in Abbildung 1 aus.


Abbildung 1 Vergleich von N-Schichtmustern mit EF4

Das richtige Muster für eine bestimmte Situation hängt von einer Menge Faktoren ab. Im Allgemeinen bieten DTOs viele architektonische Vorteile bei anfänglich hohen Implementierungskosten. Das Change Set weist wenige gute Architektureigenschaften auf, ist aber einfach zu implementieren (wenn es für eine bestimmte Technologie verfügbar ist, z. B. das DataSet im herkömmlichen ADO.NET).

Ich empfehle eine pragmatische/agile Balance zwischen diesen Bereichen durch den Beginn mit Self-Tracking Entities, die dann bis hin zu DTOs fortgeführt werden können, wenn die Situation es erfordert. Häufig haben Sie mit Self-Tracking Entities einen schnellen Einstieg und können trotzdem wichtige Architekturziele erreichen. Diese Vorgehensweise stellt einen besseren Kompromiss dar als Change Set oder Simple Entities, die nur zu empfehlen sind, wenn es keine andere geeignete Option gibt. DTOs dagegen sind die beste Wahl, wenn die Anwendung größer und komplexer wird oder wenn Ihre Anforderungen nicht von Self-Tracking Entities erfüllt werden können, z. B. bei unterschiedlichen Änderungsraten zwischen Client und Server. Diese beiden Muster sind die wichtigsten Tools in Ihrer Toolbox, werfen wir also einen genaueren Blick darauf.

Self-Tracking Entities

Um dieses Muster mit Entity Framework zu verwenden, erstellen Sie zuerst ein Entitätsdatenmodell, das Ihre konzeptuellen Entitäten darstellt, und ordnen Sie es einer Datenbank zu. Sie können auch ein Modell aus einer vorhandenen Datenbank rekonstruieren und anpassen oder ein Modell von Grund auf erstellen und dann eine passende Datenbank anlegen (eine weitere neue Funktion in EF4). Sobald das Modell und die Zuordnung erstellt sind, ersetzen Sie die standardmäßige Codeerstellungsvorlage durch die Self-Tracking Entities-Vorlage, indem Sie mit der rechten Maustaste auf die Entitäts-Designeroberfläche klicken und "Add Code Generation Item" (Codeerstellungselement hinzufügen) auswählen.

Wählen Sie anschließend eine Self-Tracking Entities-Vorlage aus der Liste der installierten Vorlagen aus. Dieser Schritt deaktiviert die standardmäßige Codeerstellung und fügt zwei Vorlagen zum Projekt hinzu: eine Vorlage erstellt den Objektkontext und die andere Vorlage die Entitätsklassen. Durch das Aufteilen der Codeerstellung in zwei Vorlagen ist es möglich, den Code in separate Assemblys zu teilen: einen für die Entitätsklassen und einen für den Kontext.

Der größte Vorteil dieser Vorgehensweise ist, dass Sie die Entitätsklassen in einer Assembly haben können, die keine Abhängigkeiten von Entity Framework hat. Auf diese Weise kann die Entitäts-Assembly (oder zumindest der von ihr erzeugte Code) und die von Ihnen implementierte Geschäftslogik auf Wunsch von der mittleren Schicht und dem Client gemeinsam genutzt werden. Der Kontext bleibt in einer Assembly, die Abhängigkeiten von den Entitäten und dem EF enthält. Wenn der Client Ihres Dienstes mit .NET 4 ausgeführt wird, können Sie ganz einfach vom Clientprojekt aus auf die Entitäts-Assembly verweisen. Falls der Client mit einer früheren .NET-Version oder Silverlight ausgeführt wird, sollten Sie Verknüpfungen vom Clientprojekt auf die erstellten Dateien hinzufügen und die Entitätsquelle in diesem Projekt erneut kompilieren (mit der entsprechenden CLR als Ziel).

Unabhängig davon, wie Sie Ihr Projekt strukturieren, arbeiten die beiden Vorlagen zusammen, um das Self-Tracking Entities-Muster zu implementieren. Die erzeugten Entitätsklassen sind einfache POCO-Klassen, deren einzige Funktion außer dem Speichern der Entitätseigenschaften das Erfassen von Änderungen an den Entitäten ist: der Gesamtstatus einer Entität, Änderungen an wichtigen Eigenschaften wie beispielsweise Parallelitätstoken und Änderungen in Beziehungen zwischen Entitäten. Diese zusätzlichen Nachverfolgungsinformationen sind Teil der DataContract-Definition für die Entitäten (wenn Sie also eine Entität von oder an einen WCF-Dienst senden, werden die Nachverfolgungsinformationen mit übertragen).

Auf dem Client des Dienstes werden Änderungen an Entitäten automatisch verfolgt, auch wenn die Entitäten nicht mit einem Kontext verknüpft sind. Jede erstellte Entität hat einen Code wie den Folgenden für jede Eigenschaft. Wenn Sie beispielsweise einen Eigenschaftswert für eine Entität mit dem Status "Unchanged" (Unverändert) ändern, wechselt der Status zu "Modified" (Geändert).

[DataMember]
public string ContactName
{
    get { return _contactName; }
    set
    {
            if (!Equals(_contactName, value))
            {
                _contactName = value;
                OnPropertyChanged("ContactName");
            }
    }
}
private string _contactName;

Genauso gilt, wenn neue Entitäten zu einem Diagramm hinzugefügt werden oder Entitäten aus einem Diagramm gelöscht werden, werden auch diese Informationen verfolgt. Da der Status jeder Entität in der Entität selbst erfasst wird, funktioniert der Verfolgungsmechanismus auch dann wie erwartet, wenn Entitäten miteinander verknüpft werden, die bei mehr als einem Dienstaufruf abgerufen wurden. Wenn Sie eine neue Beziehung erstellen, wird nur diese Änderung erfasst; alle einbezogenen Entitäten behalten ihren Status bei, als ob sie alle mit einem einzigen Dienstaufruf abgerufen wurden.

Die Kontextvorlage fügt eine neue Methode zum erstellten Kontext hinzu, ApplyChanges. ApplyChanges fügt ein Diagramm mit Entitäten zum Kontext hinzu und ändert die Informationen in ObjectStateManager so, dass sie mit den in den Entitäten verfolgten Informationen übereinstimmen. Mit den Informationen, die die Entitäten über sich selbst erfasst haben, und ApplyChanges bearbeitet der erstellte Code sowohl den Änderungsverfolgungs- als auch den Parallelitätsbereich; die beiden schwierigsten Teile beim der korrekten Implementierung einer N-Schichtenlösung.

Als konkretes Beispiel sehen Sie in Abbildung 2 einen einfachen Dienstvertrag (ServiceContract), den Sie mit Self-Tracking Entities verwenden können, um ein N-Schichtenbestellsystem zu erstellen, basierend auf der Northwind-Beispieldatenbank.

Abbildung 2 Einfacher Dienstvertrag für Self-Tracking Entities-Muster

[ServiceContract]
public interface INorthwindSTEService
{
    [OperationContract]
    IEnumerable<Product> GetProducts();

    [OperationContract]
    Customer GetCustomer(string id);

    [OperationContract]
    bool SubmitOrder(Order order);

    [OperationContract]
    bool UpdateProduct(Product product);
}

Die Dienstmethode GetProducts wird verwendet, um auf dem Client Referenzdaten über den Produktkatalog abzurufen. Diese Informationen werden normalerweise lokal zwischengespeichert und nicht oft auf dem Client aktualisiert. GetCustomer ruft einen Kunden und eine Liste der Bestellungen dieses Kunden ab. Die Implementierung dieser Methode ist ganz einfach, wie hier gezeigt:

public Customer GetCustomer(string id)
{
    using (var ctx = new NorthwindEntities())
    {
        return ctx.Customers.Include("Orders")
        .Where(c => c.CustomerID == id)
        .SingleOrDefault();
    }
}

Dies ist im Wesentlichen derselbe Code, den Sie für eine Implementierung dieser Art Methode mit dem Simple Entities-Muster schreiben würden. Der Unterschied ist, dass die zurückgegebenen Entitäten selbstverfolgend sind, was bedeutet, dass der Clientcode für die Verwendung dieser Methoden ebenfalls ganz einfach ist, aber viel mehr erreichen kann.

Angenommen, Sie möchten in dem Bestellannahmeprozess nicht nur eine Bestellung mit den entsprechenden Bestelldetailzeilen erstellen, sondern auch Teile der Kundenentität mit den aktuellen Kontaktinformationen aktualisieren. Außerdem möchten Sie alle Bestellungen löschen, die das Bestelldatum Null haben (weil das System beispielsweise damit abgelehnte Bestellungen markiert). Mit dem Simple Entities-Muster erfordert die Kombination aus Hinzufügen, Ändern und Löschen von Entitäten in einem einzigen Diagramm mehrere Dienstaufrufe für jeden Vorgang oder eine sehr komplizierte benutzerdefinierte Vertrags- und Dienstimplementierung, wenn Sie versuchen würden, etwas ähnliches wie Self-Tracking Entities in der ersten Version von EF zu implementieren. Mit EF4 könnte der Clientcode etwa so aussehen wie in Abbildung 3.

Abbildung 3 Clientcode für Self-Tracking Entities-Muster

var svc = new ChannelFactory<INorthwindSTEService>(
    "INorthwindSTEService")
    .CreateChannel();

var products = new List<Product>(svc.GetProducts());
var customer = svc.GetCustomer("ALFKI");

customer.ContactName = "Bill Gates";

foreach (var order in customer.Orders
    .Where(o => o.OrderDate == null).ToList())
{
    customer.Orders.Remove(order);
}

var newOrder = new Order();
newOrder.Order_Details.Add(new Order_Detail()
    {
        ProductID = products.Where(p => p.ProductName == "Chai")
                    .Single().ProductID,
        Quantity = 1
    });
customer.Orders.Add(newOrder);

var success = svc.SubmitOrder(newOrder);

Dieser Code erstellt den Dienst, ruft die ersten beiden Methoden dafür auf, um die Produktliste und die Kundenentität abzurufen, und nimmt dann die Änderungen am Kundenentitätsdiagramm vor, mit demselben Code, den Sie schreiben würden, wenn Sie eine Entity Framework-Zweischichtenanwendung erstellen würden, die direkt mit der Datenbank kommuniziert, oder einen Dienst in der mittleren Schicht implementieren würden. (Wenn Sie mit dieser Art des Erstellens von WCF-Dienstclients nicht vertraut sind: er erstellt automatisch einen Clientproxy für Sie, ohne Proxys für die Entitäten zu erstellen, da wir die Entitätsklassen aus der Self-Tracking Entities-Vorlage verwenden. Sie können auch den Client verwenden, der mit dem Befehl "Add Service Reference" (Dienstverweis hinzufügen) in Visual Studio erzeugt wird.) Doch hier wird kein Objektkontext einbezogen. Sie bearbeiten nur die Entitäten an sich. Abschließend ruft der Client die Dienstmethode "SubmitOrder" (Bestellung absenden) auf, um die Änderungen in die mittlere Schicht zu übernehmen. 

Natürlich würden in einer echten Anwendung die Änderungen des Clients im Diagramm wahrscheinlich über eine grafische Benutzeroberfläche kommen und Sie würden Ausnahmebehandlung für die Dienstaufrufe hinzufügen (besonders wichtig, wenn Sie über ein Netzwerk kommunizieren), aber der Code in Abbildung 3 verdeutlicht das Prinzip. Weiterhin sollten Sie beachten, dass Sie beim Erstellen der Bestelldetailentität für die neue Bestellung lediglich die ProductID-Eigenschaft festlegen, nicht die Produktentität selbst. Dies ist eine neue Fremdschlüssel-Beziehungsfunktion. Sie reduziert die Informationsmenge, die über das Netzwerk übertragen wird, da Sie nur die ProductID zurück in die mittlere Schicht serialisieren, nicht jedoch eine Kopie der Produktentität.

Bei der Implementierung der SubmitOrder-Dienstmethode ist Self-Tracking Entities am leistungsstärksten:

public bool SubmitOrder(Order newOrder)
{
    using (var ctx = new NorthwindEntities())
    {
        ctx.Orders.ApplyChanges(newOrder);
        ValidateNewOrderSubmission(ctx, newOrder);
        return ctx.SaveChanges() > 0;
    }
}

Der Aufruf von ApplyChanges funktioniert wie von selbst. Er liest die Änderungsinformationen aus den Entitäten und wendet sie auf den Kontext an, sodass das Ergebnis dasselbe ist, als wenn diese Änderungen an den zum Kontext gehörigen Entitäten tatsächlich die ganz Zeit über ausgeführt worden wären.

Prüfen der Änderungen

Beachten Sie, dass die SubmitOrder-Implementierung der Aufruf für ValidateNewOrderSubmission ist. Diese Methode, die ich zur Dienstimplementierung hinzugefügt habe, überprüft ObjectStateManager, um sicherzustellen, dass nur die in dem SubmitOrder-Aufruf erwarteten Änderungen vorhanden sind.

Dieser Schritt ist sehr wichtig, da ApplyChanges an sich alle Änderungen, die im gesamten Diagramm der zugehörigen Objekte vorhanden sind, in den Kontext übernimmt. Die Erwartung, dass der Client nur neue Bestellungen hinzufügt, den Kunden aktualisiert usw. heißt nicht, dass ein fehlerhafter (oder sogar bösartiger) Client nicht noch andere Änderungen vornehmen würde. Was, wenn er den Produktpreis ändert, um die Bestellung billiger oder teurer zu machen? Die genauen Details der Prüfungsdurchführung sind nicht so wichtig wie die unumgängliche Regel, dass Sie Änderungen immer prüfen sollten, bevor Sie sie in der Datenbank speichern. Diese Regel gilt unabhängig vom verwendeten N-Schichtmuster.

 Ein zweites wichtiges Entwurfsprinzip ist die Entwicklung von separaten, spezifischen Dienstmethoden für jeden Vorgang. Ohne diese separaten Abläufe haben Sie keinen starken Vertrag, der angibt, was zwischen zwei Schichten erlaubt ist oder nicht, und die ordnungsgemäße Prüfung der Änderungen kann unmöglich werden. Wenn Sie eine einzelne SaveEntities-Dienstmethode statt separater SubmitOrder- und UpdateProduct-Methoden haben (die nur von Benutzern aufgerufen werden können, die den Produktkatalog ändern dürfen), können Sie das Anwenden und Speichern mit dieser Methode ganz leicht implementieren, aber nicht ordnungsgemäß überprüfen, da Sie nicht wissen, wann Produktaktualisierungen zulässig sind und wann nicht.

Datentransferobjekte

Das Self-Tracking Entities-Muster vereinfacht den N-Schichtenprozess, und wenn Sie spezifische Dienstmethoden erstellen und jede prüfen, kann es eine solide Architektur bieten. Doch auch dann sind der Verwendung des Musters Grenzen gesetzt. Wenn Sie auf diese Grenzen stoßen, sind DTOs ein Ausweg.

Statt eine einzelne Entitätsimplementierung zwischen mittlerer Schicht und Client gemeinsam zu nutzen, erstellen Sie in DTOs ein benutzerdefiniertes Objekt, das nur für den Datentransfer über den Dienst verwendet wird, und entwickeln separate Entitätsimplementierungen für die mittlere Schicht und den Client. Diese Änderung bietet zwei wichtige Vorteile: sie isoliert den Dienstvertrag von den Implementierungsproblemen zwischen mittlerer Schicht und Client und ermöglicht so, dass der Vertrag stabil bleibt, selbst wenn sich die Implementierung in den Schichten ändert; und Sie können steuern, welche Daten über das Netzwerk übertragen werden. Deshalb können Sie das Senden von unnötigen Daten (oder Daten, für die der Client keine Zugriffsberechtigung hat) vermeiden oder die Daten so formatieren, wie es für den Dienst zweckmäßig ist. Im Allgemeinen wird der Dienstvertrag im Hinblick auf die Clientszenarios entworfen, sodass das Daten zwischen den Entitäten der mittleren Schicht und den DTOs neu formatiert werden können (z. B. durch Kombination von mehreren Entitäten in ein DTO und Überspringen der Eigenschaften, die auf dem Client nicht benötigt werden), während die DTOs direkt auf dem Client verwendet werden können.

Diese Vorteile haben jedoch den Preis, dass Sie eine oder mehrere Ebenen von Objekten und Zuordnungen erstellen und verwalten müssen. Um das Beispiel der Bestellannahme weiterzuführen, könnten Sie eine Klasse nur für den Zweck der Annahme neuer Bestellungen erstellen. Diese Klasse kombiniert die Eigenschaften der Kundenentität mit den Eigenschaften aus der Bestellung, die im neuen Bestellszenario festgelegt sind, aber die Klasse lässt Eigenschaften aus beiden Entitäten weg, die in der mittleren Schicht berechnet werden oder auf einer anderen Stufe des Prozesses festgelegt werden. Dadurch wird das DTO so klein und effizient wie möglich. Die Implementierung könnte dann folgendermaßen aussehen:

public class NewOrderDTO
{
    public string CustomerID { get; set; }
    public string ContactName { get; set; }
    public byte[] CustomerVersion { get; set; }
    public List<NewOrderLine> Lines { get; set; }
}

public class NewOrderLine
{
    public int ProductID { get; set; }
    public short Quantity { get; set; }
}

Genau genommen sind das sogar zwei Klassen – eine für die Bestellung und eine für die Bestelldetailzeilen – aber die Datengröße ist so klein wie möglich gehalten. Die einzig scheinbar überflüssige Information im Code ist das Feld "CustomerVersion", das die Zeilenversionsinformationen enthält, die für die Parallelitätsprüfung der Kundenentität verwendet werden. Sie benötigen diese Information für die Kundenentität, da die Entität bereits in der Datenbank vorhanden ist. Für die Bestell- und Detailzeilen sind dies neue Entitäten, die an die Datenbank gesendet werden, daher werden ihre Versionsinformationen und die Bestell-ID ("OrderID") nicht benötigt; sie werden von der Datenbank erstellt, wenn die Änderungen dauerhaft übernommen werden.

Die Dienstmethode, die dieses DTO akzeptiert, verwendet dieselben untergeordneten Entity Framework-APIs, die die Self-Tracking Entities-Vorlage zum Ausführen dieser Aufgabe verwendet; doch Sie müssen diese APIs jetzt direkt aufrufen, statt sie durch den erzeugten Code aufrufen zu lassen. Die Implementierung erfolgt in zwei Schritten. Zuerst erstellen Sie ein Diagramm mit Kunden-, Bestell- und Bestelldetail-Entitäten basierend auf den Informationen im DTO (siehe Abbildung 4).

Abbildung 4 Erstellen eines Entitätsdiagramms

var customer = new Customer
    {
        CustomerID = newOrderDTO.CustomerID,
        ContactName = newOrderDTO.ContactName,
        Version = newOrderDTO.CustomerVersion,
    };

var order = new Order
    {
        Customer = customer,
    };

foreach (var line in newOrderDTO.Lines)
{
    order.Order_Details.Add(new Order_Detail
        {
            ProductID = line.ProductID,
            Quantity = line.Quantity,
        });
}

Anschließend verknüpfen Sie das Diagramm mit dem Kontext und stellen die entsprechenden Statusinformationen ein:

ctx.Customers.Attach(customer);
var customerEntry = ctx.ObjectStateManager.GetObjectStateEntry(customer);
customerEntry.SetModified();
customerEntry.SetModifiedProperty("ContactName");

ctx.ObjectStateManager.ChangeObjectState(order, EntityState.Added);
foreach (var order_detail in order.Order_Details)
{
    ctx.ObjectStateManager.ChangeObjectState(order_detail, 
       EntityState.Added);
}

return ctx.SaveChanges() > 0;

Die erste Zeile verknüpft das gesamte Diagramm mit dem Kontext, doch wenn das passiert, hat jede Entität den Status "Unchanged" (Unverändert). Deshalb teilen Sie dem ObjectStateManager mit, die Kundenentität auf den Status "Modified" (Geändert) zu setzen, doch so, dass nur eine Eigenschaft, nämlich ContactName, als geändert gekennzeichnet wird. Dies ist wichtig, da Sie nicht alle Kundeninformationen haben – nur die Informationen, die im DTO enthalten waren. Wenn Sie alle Eigenschaften als geändert markieren würden, würde Entity Framework versuchen, die anderen Felder in der Kundenentität mit Nullwerten auszufüllen.

Anschließend ändern Sie den Status der Bestellung und alle ihre Bestelldetails auf "Added" (Hinzugefügt) und rufen dann "SaveChanges" (Änderungen speichern) auf.

Doch wo ist der Überprüfungscode? In diesem Fall führen Sie die Überprüfung während des Vorgangs durch, da Sie ein spezielles DTO für Ihr Szenario haben und dieses Objekt interpretieren, während Sie die Informationen daraus Ihren Entitäten zuordnen. Es gibt keine Möglichkeit, dass der Code ungewollt den Produktpreis ändert, da Sie nicht mit der Produktentität in Berührung kommen. Dies ist ein weiterer Vorteil des DTO-Musters, aber nur indirekt. Sie müssen trotzdem die Überprüfung durchführen; das Muster erzwingt nur eine Ebene der Überprüfung. In vielen Fällen muss der Code eine zusätzliche Überprüfung der Werte oder anderer Geschäftsregeln beinhalten.

Eine weitere Überlegung ist die ordnungsgemäße Behandlung von Parallelitätsausnahmen. Wie bereits erwähnt sind die Versionsinformationen für die Kundenentität im DTO enthalten, sodass Sie in der Lage sind, Parallelitätsprobleme aufzudecken, wenn jemand denselben Kunden bearbeitet. Ein umfassenderes Beispiel würde diese Ausnahme entweder an einen WCF-Fehler weiterleiten, sodass der Client den Konflikt lösen kann, oder die Ausnahme erkennen und eine Art automatische Richtlinie für die Konfliktbehandlung anwenden.

Wenn Sie das Beispiel noch erweitern möchten, indem Sie einen weiteren Vorgang wie die Möglichkeit zum Ändern einer Bestellung hinzufügen, erstellen Sie ein weiteres DTO speziell für dieses Szenario mit den entsprechenden Informationen dazu. Dieses Objekt würde in etwa wie NewOrderDTO aussehen, aber es würde Version- und OrderID-Eigenschaften für die Bestellung und Bestelldetailentitäten enthalten sowie alle Eigenschaften, die mit dem Dienstaufruf aktualisiert werden können. Diese Dienstmethodenimplementierung funktioniert ähnlich wie die oben erläuterte SubmitOrderDTO-Methode: Durchlaufen der DTO-Daten, Erstellen der entsprechenden Entitätsobjekte und Einstellen des Status im Statusmanager, bevor die Änderungen in der Datenbank gespeichert werden.

Wenn Sie die Bestellaktualisierungsmethode mit Self-Tracking Entities und Data Transfer Objects implementieren möchten, werden Sie feststellen, dass die Self-Tracking Entities-Implementierung die Entitäten wiederverwendet und fast denselben Dienstimplementierungscode wie die Bestellannahmemethode für neue Bestellungen verwendet; der einzige Unterschied ist der Überprüfungscode, und selbst der kann zum Teil gemeinsam verwendet werden. Die DTO-Implementierung erfordert jedoch einen separate DTO-Klasse für jede der beiden Dienstmethoden, und die Methodenimplementierungen weisen ähnliche Muster auf, aber haben kaum Code zur gemeinsamen Verwendung.

Tipps aus der Praxis

Hier finden Sie einige Tipps, worauf Sie achten müssen.

  • **Verwenden Sie unbedingt den von der Self-Tracking Entity-Vorlage erzeugten Entitätscode auf dem Client wieder. **Wenn Sie den von "Dienstverweis hinzufügen" in Visual Studio oder einem anderen Tool erzeugten Proxycode verwenden, sieht zwar auf den ersten Blick alles richtig aus, aber Sie werden feststellen, dass die Entitäten die Änderungen auf dem Client nicht nachverfolgen.
  • **Erstellen Sie eine neue ObjectContext-Instanz in einer Using-Anweisung für jede Dienstmethode, sodass sie verworfen wird, bevor die Methode zurückgegeben wird. ** Dieser Schritt ist wichtig für die Skalierbarkeit des Dienstes. Er stellt sicher, dass die Datenbankverbindungen nicht über Dienstaufrufe hinweg offen gehalten werden und dass der von einem bestimmten Vorgang verwendete temporäre Status vom Garbage Collector bereinigt wird, sobald der Vorgang abgeschlossen ist. Entity Framework speichert Metadaten und andere benötigte Informationen automatisch in der Anwendungsdomäne zwischen, und ADO.NET fasst Datenbankverbindungen in einem Pool zusammen, sodass die Kontextwiedererstellung immer sehr schnell geht.
  • Verwenden Sie wenn möglich die neue Fremdschlüssel-Beziehungsfunktion.  Sie erleichtert das Ändern von Beziehungen zwischen Entitäten ungemein. Mit unabhängigen Zuordnungen (die einzige Art von Beziehung, die in der ersten Version von Entity Framework vorhanden war) werden Parallelitätsprüfungen für Beziehungen unabhängig von den Parallelitätsprüfungen für Entitäten durchgeführt, und es gibt keine Möglichkeit, diese Beziehungsparallelitätsprüfungen abzuwählen. Das führt dazu, dass Ihre Dienste die ursprünglichen Beziehungswerte beinhalten müssen und diese im Kontext festlegen müssen, bevor Beziehungen geändert werden. Mit Fremdschlüsselbeziehungen dagegen ist die Beziehung einfach eine Eigenschaft der Entität, und wenn die Entität die Parallelitätsprüfung besteht, ist keine weitere Überprüfung erforderlich. Sie können eine Beziehung ändern, indem Sie einfach nur den Fremdschlüsselwert ändern.
  • **Vermeiden Sie EntityKey-Konflikte beim Verknüpfen eines Diagramms mit einem ObjectContext. ** Wenn Sie beispielsweise DTOs verwenden und Teile des Diagramms neu hinzugefügte Entitäten darstellen, für die keine Entitätsschlüsselwerte eingestellt wurden, da diese in der Datenbank erstellt werden sollen, sollten Sie die AddObject-Methode aufrufen, um zuerst das gesamte Diagramm mit den Entitäten hinzuzufügen und dann den Entitäten, die nicht den Status "Added" (Hinzugefügt) haben, den gewünschten Status zuzuweisen, statt die Attach-Methode aufzurufen und anschließend den Added-Entitäten diesen Status zuzuweisen. Andernfalls, wenn Sie zuerst "Attach" aufrufen, geht Entity Framework davon aus, dass jede Entität den Status "Unchanged" (Unverändert) erhalten soll und dass die Entitätsschlüsselwerte endgültig sind. Wenn mehr als eine Entität eines bestimmten Typs denselben Schlüsselwert hat (z. B. 0), gibt Entity Framework einen Ausnahmefehler aus. Indem Sie eine Entität mit dem Status "Added" starten, vermeiden Sie dieses Problem, da Entity Framework nicht erwartet, dass Added-Entitäten eindeutige Schlüsselwerte haben.
  • Deaktivieren Sie automatisches Lazy Loading (Träges Laden; eine weitere neue EF4-Funktion) beim Zurückgeben von Entitäten von Dienstmethoden. Wenn Sie dies nicht tun, löst die Serialisierung Lazy Loading aus und versucht, zusätzliche Entitäten aus der Datenbank abzurufen, was dazu führt, dass mehr Daten als geplant zurückgegeben werden (wenn Ihre Entitäten umfassend verbunden sind, serialisieren Sie möglicherweise die ganze Datenbank) oder es tritt ein Fehler auf, da der Kontext verworfen wird, bevor die Serialisierung versucht, die Daten abzurufen. Für Self-Tracking Entities ist standardmäßig kein Lazy Loading aktiviert, aber wenn Sie eine DTO-Lösung erstellen, sollten Sie darauf achten.

Schlussgedanke

Die .NET 4-Version von Entity Framework erleichtert das Erstellen von N-Schichtenanwendungen mit solider Architektur. Für die meisten Anwendungen sollten Sie mit der Self-Tracking Entities-Vorlage starten, die den Prozess vereinfacht und eine weitgehende Wiederverwendung ermöglicht. Wenn Sie unterschiedliche Änderungsraten zwischen Dienst und Client haben oder die absolute Kontrolle über das Übertragungsformat wünschen, sollten Sie die DTO-Implementierung verwenden. Unabhängig davon, welches Muster Sie wählen, beachten Sie immer die Schlüsselprinzipien, die die Muster und Antimuster darstellen – und vergessen Sie niemals, die Daten vor dem Speichern zu überprüfen.

Daniel Simmons ist Architekt im Entity Framework-Team bei Microsoft.