April 2018

Band 33, Nummer 4

Data Points: Entitäten im Besitz von EF Core 2 und vorübergehende Problemumgehungen

Von Julie Lerman

Julie LermanDas neue Feature „Entität im Besitz“ (Owned Entity) in EF Core 2.0 ersetzt das Feature „komplexer Typ“ (Complex Type) des „klassischen“ Entity Framework (EF bis EF6). Entitäten im Besitz erlauben die Zuordnung von Wertobjekten zum Datenspeicher. Es ist durchaus üblich, eine Geschäftsregel zu verwenden, die erlaubt, dass Eigenschaften, die auf Wertobjekten basieren, NULL sind. Da Wertobjekte unveränderlich sind, ist es außerdem wichtig, Eigenschaften, die ein Wertobjekt enthalten, ersetzen zu können. Die aktuelle Version von EF Core lässt keines dieser Szenarien standardmäßig zu, obwohl beide in kommenden Iterationen unterstützt werden. Anstatt diese Einschränkungen als Hinderungsgründe für diejenigen von uns zu betrachten, die die Vorteile von DDD-Mustern (Domain-Driven Design) lieben, wird dieser Artikel Ihnen zeigen, wie Sie diese Einschränkungen in der Zwischenzeit umgehen können. Ein DDD-Praktiker mag diese temporären Muster immer noch ablehnen, weil sie die Prinzipien von DDD nicht genau genug befolgen, aber der Pragmatiker in mir ist zufrieden, bestärkt durch das Wissen, dass sie einfach nur temporäre Lösungen sind.

Beachten Sie, dass ich „standardmäßig“ geschrieben habe. Offensichtlich gibt es eine Möglichkeit, dass EF Core die Verantwortung für die Durchsetzung der eigenen Regel zu NULL-Entitäten im Besitz übernimmt und die Ersetzung von Wertobjekten ermöglicht, ohne Ihre Domänenklassen oder Ihre Geschäftsregeln dramatisch zu beeinträchtigen. In dieser Kolumne zeige ich Ihnen, wie dies zu bewerkstelligen ist.

Es gibt einen anderen Weg, um das NULL-Zulässigkeitsproblem zu umgehen, nämlich den Wertobjekttyp einfach seiner eigenen Tabelle zuzuordnen und damit die Daten des Wertobjekts physisch vom Rest des Objekts zu trennen, zu dem es gehört. Auch wenn dies eine gute Lösung für einige Szenarien sein mag, ist es im Allgemeinen eine Strategie, die ich nicht verwenden möchte. Deshalb ziehe ich es vor, meine Problemumgehung zu verwenden, die im Mittelpunkt dieser Kolumne steht. Aber zuerst möchte ich sicherstellen, dass Sie verstehen, warum dieses Problem und die Lösung so wichtig sind, dass ich diese Kolumne dem Thema widme.

Eine kurze Einführung in Wertobjekte

Wertobjekte sind ein Typ, mit dem Sie mehrere Werte in einer einzigen Eigenschaft kapseln können. Eine Zeichenfolge ist ein gutes Beispiel für ein Wertobjekt. Zeichenfolgen bestehen aus einer Sammlung von Zeichen. Und sie sind unveränderlich – Unveränderlichkeit ist eine wichtige Facette eines Wertobjekts. Die Kombination und Reihenfolge der Buchstaben T, a und g ergeben eine bestimmte Bedeutung. Wenn Sie sie verändern würden, z.B. indem Sie den letzten Buchstaben in ein „t“ ändern, würden Sie die Bedeutung vollständig ändern.  Das Objekt wird durch die Kombination aller seiner Werte definiert. Somit ist die Tatsache, dass das Objekt nicht verändert werden kann, Bestandteil seines Vertrags. Und es gibt noch einen weiteren wichtigen Aspekt eines Wertobjekts: Es besitzt keine eigene Identität. Es kann nur als Eigenschaft einer anderen Klasse verwendet werden, genau wie eine Zeichenfolge. Wertobjekte haben weitere Vertragsregeln, aber die oben genannten sind die wichtigsten, wenn das Konzept neu für Sie ist.

Da sich ein Wertobjekt aus seinen Eigenschaften zusammensetzt und dann als Ganzes als Eigenschaft in einer anderen Klasse verwendet wird, ist das persistente Speichern seiner Daten mit besonderem Aufwand verbunden. Mit einer nicht relationalen Datenbank wie einer Dokumentdatenbank ist es einfach, den Graphen eines Objekts und seine eingebetteten Wertobjekte zu speichern. Aber das ist nicht der Fall, wenn die Speicherung in einer relationalen Datenbank erfolgt. Schon in der allerersten Version enthielt Entity Framework den ComplexType, der die Eigenschaften der Eigenschaft der Datenbank zuordnen konnte, in der EF Ihre Daten persistent speicherte. Ein häufiges Beispiel für ein Wertobjekt ist PersonName, das aus einer FirstName-Eigenschaft und einer LastName-Eigenschaft bestehen kann. Wenn Sie einen Kontakttyp mit einer PersonName-Eigenschaft verwenden, speichert EF Core standardmäßig die Werte FirstName und LastName als zusätzliche Spalten in der Tabelle, der der Kontakt zugeordnet wird.

Ein Beispiel für ein Wertobjekt im Gebrauch

Ich habe festgestellt, dass das Durchsehen einer Vielzahl von Beispielen für Wertobjekte mir geholfen hat, das Konzept besser zu verstehen, also werde ich noch ein weiteres Beispiel verwenden: eine SalesOrder-Entität und ein PostalAddress-Wertobjekt. Eine Bestellung enthält in der Regel sowohl eine Lieferadresse als auch eine Rechnungsadresse. Während diese Adressen für andere Zwecke vorhanden sein können, sind sie im Kontext der Bestellung ein integraler Bestandteil ihrer Definition. Wenn eine Person an einen neuen Ort umzieht, möchten Sie trotzdem wissen, wohin die Bestellung geliefert wurde. Daher ist es sinnvoll, die Adressen in die Bestellung einzubetten. Aber um Adressen in meinem System konsistent zu behandeln, ziehe ich es vor, die Werte, aus denen eine Adresse besteht, in ihrer eigenen Klasse (PostalAddress) zu kapseln, wie in Abbildung 1 gezeigt.

Abbildung 1: PostalAddress-Wertobjekt

public class PostalAddress : ValueObject<PostalAddress>
{
  public static PostalAddress Create (string street, string city,
                                      string region, string postalCode)   {
    return new PostalAddress (street, city, region, postalCode);
  }
  private PostalAddress () { }
  private PostalAddress (string street, string city, string region,
                         string postalCode)   {
    Street = street;
    City = city;
    Region = region;
    PostalCode = postalCode;
  }
  public string Street { get; private set; }
  public string City { get; private set; }
  public string Region { get; private set; }
  public string PostalCode { get; private set; }
  public PostalAddress CopyOf ()   {
    return new PostalAddress (Street, City, Region, PostalCode);
  }
}

PostalAddress erbt von einer von Jimmy Bogard erstellten ValueObject-Basisklasse (bit.ly/2EpKydG). ValueObject stellt einen Teil der obligatorischen Logik bereit, die für ein Wertobjekt erforderlich ist. Zum Beispiel weist es eine Außerkraftsetzung von Object.Equals auf, die sicherstellt, dass jede Eigenschaft verglichen wird. Denken Sie daran, dass eine ausgiebige Verwendung von Reflektion stattfindet. Dies kann sich auf die Leistung einer Produktions-App auswirken.

Zwei weitere wichtige Eigenschaften meines PostalAddress-Wertobjekts sind, dass es keine Identitätsschlüsseleigenschaft besitzt und dass sein Konstruktor die invariante Regel erzwingt, dass jede Eigenschaft mit Daten aufgefüllt werden muss. Damit eine Entität im Besitz jedoch einen als Wertobjekt definierten Typ zuordnen kann, besteht die einzige Regel darin, dass es keinen eigenen Identitätsschlüssel besitzt. Eine Entität im Besitz hat nichts mit den anderen Attributen eines Wertobjekts zu tun.

Wenn PostalAddress definiert ist, kann ich das Objekt nun als die ShippingAddress- und BillingAddress-Eigenschaften meiner SalesOrder-Klasse verwenden (siehe Abbildung 2). Es handelt sich dabei nicht um Navigationseigenschaften zu verwandten Daten, sondern nur um weitere Eigenschaften, die den skalaren Eigenschaften Notes und OrderDate ähneln.

Abbildung 2: Die SalesOrder-Klasse enthält Eigenschaften, die PostalAddress-Typen sind

public class SalesOrder {
  public SalesOrder (DateTime orderDate, decimal orderTotal)   {
    OrderDate = orderDate;
    OrderTotal = orderTotal;
    Id = Guid.NewGuid ();
  }
  private SalesOrder () { }
  public Guid Id { get; private set; }
  public DateTime OrderDate { get; private set; }
  public decimal OrderTotal { get; private set; }
  private PostalAddress _shippingAddress;
  public PostalAddress ShippingAddress => _shippingAddress;
  public void SetShippingAddress (PostalAddress shipping)
  {
    _shippingAddress = shipping;
  }
  private PostalAddress _billingAddress;
  public PostalAddress BillingAddress => _billingAddress;
  public void CopyShippingAddressToBillingAddress ()
  {
    _billingAddress = _shippingAddress?.CopyOf ();
  }
  public void SetBillingAddress (PostalAddress billing)
  {
    _billingAddress = billing;
  }
}

Diese Adressen sind nun in SalesOrder enthalten und können unabhängig von der aktuellen Adresse der Person, die die Bestellung aufgegeben hat, genaue Informationen bereitstellen. Ich werde immer wissen, wohin die Bestellung geliefert wurde.

Zuordnen eines Wertobjekts als Entität im Besitz von EF Core

In früheren Versionen konnte EF automatisch Klassen erkennen, die mit einem ComplexType zugeordnet werden sollten, indem ermittelt wurde, dass die Klasse als Eigenschaft einer anderen Entität verwendet wurde und keine Schlüsseleigenschaft aufwies. EF Core kann jedoch nicht automatisch Entitäten im Besitz ableiten. Sie müssen dies in den DbContext-Zuordnungen der Fluent-API in der OnModelCreating-Methode mit der neuen OwnsOne-Methode angeben, um anzugeben, welche Eigenschaft dieser Entität die Entität in Besitz ist:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<SalesOrder>().OwnsOne(s=>s.BillingAddress);
  modelBuilder.Entity<SalesOrder>().OwnsOne(s=>s.ShippingAddress);
}

Ich habe EF Core Migrationen verwendet, um eine Migrationsdatei zu erstellen, die die Datenbank beschreibt, zu der mein Modell Zuordnungen vornimmt. Abbildung 3 zeigt den Abschnitt der Migration, der die SalesOrder-Tabelle darstellt. Sie können erkennen, dass EF Core verstanden hat, dass die Eigenschaften von PostalAddress für jede der beiden Adressen Teil von SalesOrder sind. Die Spaltennamen entsprechen der EF Core-Konvention, obwohl Sie diese mit der Fluent-API beeinflussen können.

Abbildung 3: Die Migration für die SalesOrder-Tabelle, einschließlich aller Spalten für die PostalAddress-Eigenschaften

migrationBuilder.CreateTable(
  name: "SalesOrders",
  columns: table => new
  {
    Id = table.Column(nullable: false)
              .Annotation("Sqlite:Autoincrement", true),
    OrderDate = table.Column(nullable: false),
    OrderTotal = table.Column(nullable: false),
    BillingAddress_City = table.Column(nullable: true),
    BillingAddress_PostalCode = table.Column(nullable: true),
    BillingAddress_Region = table.Column(nullable: true),
    BillingAddress_Street = table.Column(nullable: true),
    ShippingAddress_City = table.Column(nullable: true),
    ShippingAddress_PostalCode = table.Column(nullable: true),
    ShippingAddress_Region = table.Column(nullable: true),
    ShippingAddress_Street = table.Column(nullable: true)
  }

Wie bereits erwähnt, ist das Speichern der Adressen in der SalesOrder-Tabelle eine Konvention und meine bevorzugte Vorgehensweise. Dieser alternative Code teilt sie in separate Tabellen auf und vermeidet das Problem der NULL-Zulässigkeit vollständig:

modelBuilder.Entity<SalesOrder> ().OwnsOne (
  s => s.BillingAddress).ToTable("BillingAddresses");
modelBuilder.Entity<SalesOrder> ().OwnsOne (
  s => s.ShippingAddress).ToTable("ShippingAddresses");

Erstellen einer SalesOrder in Code

Das Einfügen einer Bestellung mit Rechnungsadresse und Lieferadresse ist einfach:

private static void InsertNewOrder()
{
  var order=new SalesOrder{OrderDate=DateTime.Today, OrderTotal=100.00M};
  order.SetShippingAddress (PostalAddress.Create (
    "One Main", "Burlington", "VT", "05000"));
  order.SetBillingAddress (PostalAddress.Create (
    "Two Main", "Burlington", "VT", "05000"));
  using(var context=new OrderContext()){
    context.SalesOrders.Add(order);
    context.SaveChanges();
  }
}

Aber nehmen wir an, dass meine Geschäftsregeln es erlauben, eine Bestellung zu speichern, auch wenn die Liefer- und Rechnungsadresse noch nicht eingegeben wurden, und ein Benutzer die Bestellung zu einem anderen Zeitpunkt abschließen kann. Ich werde den Code auskommentieren, der die BillingAddress-Eigenschaft einfügt:

// order.BillingAddress=new Address("Two Main","Burlington", "VT", "05000");

Wenn SaveChanges aufgerufen wird, versucht EF Core herauszufinden, welche Eigenschaften die BillingAddress aufweist, damit sie in die SalesOrder-Tabelle gepusht werden können. In diesem Fall tritt jedoch ein Fehler auf, da BillingAddress NULL ist. Intern verfügt EF Core über eine Regel, die besagt, dass eine konventionell zugeordnete Typeigenschaft im Besitz nicht NULL sein darf.

EF Core geht davon aus, dass der Typ in Besitz verfügbar ist, damit seine Eigenschaften gelesen werden können. Entwickler könnten dies als Hindernis für die Verwendung von Wertobjekten oder (schlimmer noch) für die Verwendung von EF Core betrachten, da Wertobjekte für ihren Softwareentwurf sehr wichtig sind. Zuerst dachte ich auch so, aber ich war in der Lage, eine Problemumgehung zu erstellen.

Temporäre Problemumgehung zum ermöglichen von NULL-Wertobjekten

Das Ziel der Problemumgehung besteht im Sicherstellen, dass EF Core eine ShippingAddress, BillingAddress oder einen anderen Typ im Besitz erhält, und zwar unabhängig davon, ob der Benutzer eine solche Angabe bereitgestellt hat. Das bedeutet, dass der Benutzer nicht gezwungen ist, eine Liefer- oder Rechnungsadresse anzugeben, nur um der Persistenzschicht gerecht zu werden. Wenn der Benutzer keine Adressen angibt, wird ein PostalAddress-Objekt mit NULL-Werten in seinen Eigenschaften vom DbContext hinzugefügt, wenn es an der Zeit ist, eine SalesOrder zu speichern.

Ich habe eine kleine Anpassung an der PostalAddress-Klasse vorgenommen, indem ich eine zweite Factorymethode (Empty) hinzugefügt habe, damit der DbContext einfach eine leere PostalAddress erstellen kann:

public static PostalAddress Empty()
{
  return new PostalAddress(null,null,null,null);
}

Zusätzlich habe ich die ValueObject-Basisklasse um eine neue Methode (IsEmpty) erweitert, die in Abbildung 4 gezeigt wird, damit Code leicht feststellen kann, ob ein Objekt nur NULL-Werte in seinen Eigenschaften aufweist. IsEmpty nutzt Code, der bereits in der ValueObject-Klasse vorhanden ist. Die Methode iteriert durch die Eigenschaften. Wenn eine der Eigenschaften einen Wert aufweist, wird FALSE zurückgegeben, was anzeigt, dass das Objekt nicht leer ist. Andernfalls wird TRUE zurückgegeben.

Abbildung 4: Die IsEmpty-Methode, die zur ValueObject-Basisklasse hinzugefügt wurde

public bool IsEmpty ()
{
  Type t = GetType ();
  FieldInfo[] fields = t.GetFields
    (BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
  foreach (FieldInfo field in fields)
  {
    object value = field.GetValue (this);
    if (value != null)
    {
      return false;
    }
  }
  return true;
}

Aber meine Lösung zum Zulassen von NULL-Entitäten im Besitz war noch nicht vollständig. Ich musste noch immer all diese neue Programmlogik verwenden, um sicherzustellen, dass neue SalesOrders immer eine Lieferadresse und eine Rechnungsadresse aufweisen, damit EF Core sie in der Datenbank speichern kann. Als ich diese letzte Komponente meiner Lösung hinzugefügt habe, war ich anfangs damit nicht zufrieden, weil das letzte Stück Code (das ich nicht weitergeben werde) die SalesOrder-Klasse dazu brachte, die Regel von EF Core durchzusetzen – den Fluch des domänengesteuerten Designs.

Voilà! Eine elegante Lösung

Zum Glück hielt ich (wie jeden Herbst) einen Vortrag auf der DevIntersection, bei der auch Diego Vega und Andrew Peters vom EF-Team eine Präsentation vorstellten. Ich zeigte ihnen meine Problemumgehung und erklärte ihnen, was mich störte: die Notwendigkeit, ShippingAddress- und BillingAddress-Eigenschaften ungleich NULL in der SalesOrder durchzusetzen. Und sie waren meiner Meinung. Andrew kam schnell auf die Idee, die Arbeit, die ich in die ValueObject-Basisklasse gesteckt hatte, und die Änderungen, die ich an der PostalAddress vorgenommen hatte, um EF Core zu zwingen, sich um das Problem zu kümmern, ohne SalesOrder diese Verpflichtung aufzuerlegen, zu nutzen. Die Magie geschieht in der Außerkraftsetzung der SaveChanges-Methode meiner DbContext-Klasse, wie in Abbildung 5 gezeigt.

Abbildung 5: Überschreiben von SaveChanges zum Bereitstellen von Werten für NULL-Typen im Besitz

public override int SaveChanges()
{
  foreach (var entry in ChangeTracker.Entries()
             .Where(e => e.Entity is SalesOrder && e.State == EntityState.Added))
  {
    if (entry.Entity is SalesOrder)
    {
      if (entry.Reference("ShippingAddress").CurrentValue == null)
      {
        entry.Reference("ShippingAddress").CurrentValue = PostalAddress.Empty();
      }
      if (entry.Reference("BillingAddress").CurrentValue == null)
      {
        entry.Reference("BillingAddress").CurrentValue = PostalAddress.Empty();
      }
  }
  return base.SaveChanges();
}

Aus der Sammlung der Einträge, die der DbContext nachverfolgt, iteriert SaveChanges durch diejenigen, die als SalesOrders gekennzeichnet sind, um der Datenbank hinzugefügt zu werden, und stellt sicher, dass sie wie ihre leeren Gegenstücke mit Daten aufgefüllt werden.

Wie erfolgt die Abfrage dieser leeren Typen im Besitz?

Nachdem EF Core die Notwendigkeit erfüllt hat, NULL-Wertobjekte zu speichern, ist es nun an der Zeit, diese aus der Datenbank erneut abzufragen. Aber EF Core löst diese Eigenschaften in ihrem leeren Zustand auf. Jede ShippingAddress oder BillingAddress, die ursprünglich NULL war, wird als Instanz mit NULL-Werten in ihren Eigenschaften zurückgegeben. Nach jeder Abfrage muss meine Logik alle leeren PostalAddress-Eigenschaften durch NULL ersetzen.

Ich habe recht viel Zeit damit verbracht, nach einem eleganten Weg zu suchen, dies zu erreichen. Leider gibt es noch keinen Lebenszyklushook, um Objekte zu ändern, während sie aus Abfrageergebnissen materialisiert werden. In der Abfragepipeline gibt es einen ersetzbaren Dienst namens CreateReadValueExpression in der internen EntityMaterializerSource-Klasse, der jedoch nur für skalare Werte und nicht für Objekte verwendet werden kann. Ich habe zahlreiche andere Ansätze ausprobiert, die immer komplizierter wurden, und habe schließlich einen langen inneren Monolog über die Tatsache geführt, dass dies eine vorübergehende Problemumgehung ist, sodass ich eine einfachere Lösung akzeptieren kann, auch wenn sie ein wenig nach schlechtem Code riecht. Und diese Aufgabe ist nicht allzu schwer zu steuern, wenn Ihre Abfragen in einer Klasse gekapselt sind, die für die Durchführung von EF Core-Aufrufen an die Datenbank vorgesehen ist.

Ich habe die Methode FixOptionalValueObjects genannt:

private static void FixOptionalValueObjects (SalesOrder order) {
  if (order.ShippingAddress.IsEmpty ()) { order.SetShippingAddress (null); }
  if (order.BillingAddress.IsEmpty ()) { order.SetBillingAddress (null); }
}

Jetzt habe ich eine Lösung, bei der der Benutzer Wertobjekte NULL lassen kann und EF Core sie als Nicht-NULL-Werte speichern und abrufen kann, sie aber trotzdem als NULL-Werte an meine Codebasis zurückgibt.

Ersetzen von Wertobjekten

Ich habe eine weitere Einschränkung in der aktuellen Version von EF Core 2 erwähnt, nämlich die Unmöglichkeit, Entitäten im Besitz zu ersetzen. Wertobjekte sind per Definition unveränderlich. Wenn Sie also ein Wertobjekt ändern müssen, besteht die einzige Möglichkeit darin, es zu ersetzen. Unter logischen Aspekten bedeutet dies, dass Sie die SalesOrder so ändern, als hätten Sie ihre OrderDate-Eigenschaft geändert. Aber aufgrund der Art und Weise, wie EF Core die eigenen Entitäten nachverfolgt, wird das Framework immer denken, dass die Ersetzung hinzugefügt wird, selbst wenn sein Host (z.B. SalesOrder) nicht neu ist.

Ich habe eine Änderung an der SaveChanges-Außerkraftsetzung vorgenommen, um dieses Problem zu beheben (siehe Abbildung 6). Die Außerkraftsetzung filtert jetzt nach SalesOrders, die hinzugefügt oder geändert werden, und stellt mit den zwei neuen Codezeilen, die den Zustand der Verweiseigenschaften ändern, sicher, dass ShippingAddress und BillingAddress den gleichen Zustand wie die Bestellung aufweisen: entweder „Hinzugefügt“ (Added) oder „Geändert“ (Modified). Geänderte SalesOrder-Objekte können nun auch die Werte der ShippingAddress- und BillingAddress-Eigenschaften in ihre UPDATE-Befehle einbeziehen.

Abbildung 6: Bewirken, dass SaveChanges ersetzte Typen im Besitz versteht, indem diese als „Geändert“ (Modified) markiert werden

public override int SaveChanges () {
  foreach (var entry in ChangeTracker.Entries ().Where (
    e => e.Entity is SalesOrder &&
    (e.State == EntityState.Added || e.State == EntityState.Modified))) {
    if (entry.Entity is SalesOrder order) {
      if (entry.Reference ("ShippingAddress").CurrentValue == null) {
        entry.Reference ("ShippingAddress").CurrentValue = PostalAddress.Empty ();
      }
      if (entry.Reference ("BillingAddress").CurrentValue == null) {
        entry.Reference ("BillingAddress").CurrentValue = PostalAddress.Empty ();
      }
      entry.Reference ("ShippingAddress").TargetEntry.State = entry.State;
      entry.Reference ("BillingAddress").TargetEntry.State = entry.State;
    }
  }
  return base.SaveChanges ();
}

Dieses Muster funktioniert, weil ich mit einer anderen Instanz von OrderContext als der abgefragten Instanz speichere. Sie besitzt daher keine vorgefasste Vorstellung vom Zustand der PostalAddress-Objekte. Ein alternatives Muster für nachverfolgte Objekte finden Sie in den Kommentaren zum GitHub-Problem unter bit.ly/2sxMECT.

Pragmatische Lösung für den kurzfristigen Einsatz

Wenn Änderungen, die optionale Entitäten im Besitz und das Ersetzen von Entitäten erlauben, nicht in Sicht wären, würde ich höchstwahrscheinlich Schritte unternehmen, um ein separates Datenmodell für den Umgang mit der Datenpersistenz in meiner Software zu erstellen. Aber diese temporäre Lösung erspart mir den zusätzlichen Arbeitsaufwand und die Zeitinvestition, und ich weiß, dass ich bald meine Problemumgehungen entfernen und meine Domänenmodelle direkt meiner Datenbank zuordnen kann, sodass EF Core das Datenmodell definieren kann. Ich habe die Zeit, den Aufwand und die Gedanken in die Entwicklung der Problemumgehungen gern investiert, damit ich beim Entwerfen meiner Lösungen Wertobjekte und EF Core 2 verwenden kann – und andere Entwickler dabei unterstützen kann, ebenso vorzugehen.

Beachten Sie, dass der Download, der diesen Artikel begleitet, in einer Konsolenanwendung enthalten ist, um die Lösung zu testen und die Daten in einer SQLite-Datenbank persistent zu speichern. Ich verwende die Datenbank, anstatt nur Tests mit dem InMemory-Anbieter zu schreiben, weil ich die Datenbank untersuchen wollte, um zu 100 Prozent sicher zu sein, dass die Daten so gespeichert werden, wie ich es erwartet hatte.


Julie Lerman ist Microsoft Regional Director, Microsoft MVP, Coach für das Softwareteam und Unternehmensberaterin. Sie lebt in den Bergen von Vermont. Sie hält bei User Groups und Konferenzen in der ganzen Welt Vorträge zum Thema Datenzugriff und zu anderen Themen. Julie Lerman führt unter thedatafarm.com/blog einen Blog. Sie ist die Autorin von „Programming Entity Framework“ sowie der Ausgaben „Code First“ und „DbContext“ (alle bei O’Reilly Media erschienen). Folgen Sie ihr auf Twitter: @julielerman, und sehen Sie sich ihre Pluralsight-Kurse unter juliel.me/PS-Videos an.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Andriy Svyryd
Andriy Svyryd ist ein ukrainischer .NET Entwickler, der seit 2010 im Entity Framework-Team arbeitet. Alle Projekte, an denen er beteiligt ist, finden Sie unter github.com/AndriySvyryd.


Diesen Artikel im MSDN Magazine-Forum diskutieren