## 29.6 Die Klasse »DataContext« Die schon öfter erwähnte Klasse DataContext bildet die Schnittstelle zwischen der relationalen Datenbank und der Anwendung. Eine Instanz von DataContext hat zuerst immer die Aufgabe, die Verbindung zur Datenbank aufzubauen. Darüber hinaus werden die benötigten Daten aus der Datenbank gelesen und in den dafür vorgesehenen Entitätsobjekten abgelegt. Damit sind aber noch nicht alle Aufgaben beschrieben. Das DataContext-Objekt verfolgt die Änderung an den Daten im Speicher und übermittelt diese bei Bedarf an die Datenbank. Zusammenfassend kann man sagen, dass die DataContext-Instanz den Einstiegspunkt in LINQ to SQL bildet und die Kommunikation zwischen der Datenbank und der Anwendung abwickelt. ### 29.6.1 Verbindungsaufbau Bei der Instanziierung der Klasse DataContext müssen Sie dem Konstruktor die Datenbankverbindung als Argument übergeben. Dafür eignet sich sowohl ein SqlConnection-Objekt als auch eine Zeichenfolge, die der der Klasse SqlConnection gleicht, zum Beispiel:
    string con = "Data Source=.\sqlexpress;Initial Catalog=Northwind;
                  Integrated Security=True";
    DataContext context = new DataContext(con);
DataContext besitzt zwar auch eine Eigenschaft Connection, diese ist aber schreibgeschützt. ### 29.6.2 Daten abfragen Das DataContext-Objekt hat die Fähigkeit, mit der Methode GetTable<TEntity> Daten von der Datenbank abzufragen. Bei dem Abfrageergebnis handelt es sich um eine Collection vom Typ Table<TEntity>. Möchten Sie zum Beispiel die Tabelle der Produkte abfragen, müsste die Anweisung
    Table<Product> products = context.GetTable<Product>();
lauten. Das setzt natürlich voraus, dass in der Anwendung die Entitätsklasse Product definiert ist. Der Rückgabetyp Table<Product> ist vergleichbar mit einer Collection des Typs List<Products>, ist aber auf die Belange von LINQ to SQL hin optimiert. Er enthält nach Absetzen der LINQ-Abfrage alle gemappten Datensätze, die vom Typ Product sind. Auf die Ergebnisliste products können wir alle Standard-Abfrageoperationen anwenden. Die folgende Anweisung fragt alle Artikelbezeichner und deren Preis in der Datenbank ab:
    // alle Datensätze abrufen
    var query = from prod in products
                select new { prod.ProductName, prod.UnitPrice };
    foreach (var item in query)
      Console.WriteLine("{0,-35}{1}",item.ProductName, item.UnitPrice);
Tatsächlich ausgeführt wird die Abfrage erst in dem Moment, wenn die Daten in der Schleife angefordert werden. ### 29.6.3 Von einer LINQ-Abfrage erzeugtes SQL-Statement ausgeben Um die Datenbank abzufragen, verwendet LINQ to SQL die Definition der LINQ-Abfrage, um daraus ein für die Datenbank verständliches SQL-Statement zu erzeugen. Sie können sich dieses anzeigen lassen, wenn Sie mit der Methode GetCommand unter Angabe der Abfrage die CommandText-Eigenschaft aufrufen, beispielsweise so:
    Console.WriteLine(context.GetCommand(query).CommandText);
Das Ergebnis für unsere Abfrage wird wie folgt lauten:
    SELECT [t0].[ProductName], [t0].[UnitPrice]
    FROM [dbo].[Products] AS [t0]
Dass nicht alle Felder abgefragt werden, liegt daran, dass wir nur die Felder ProductName und UnitPrice in das Ergebnis projiziert haben (siehe das Codefragment im vorigen Abschnitt). LINQ to SQL bewertet also unsere Anforderung an die Datenbank und liefert nur die wirklich benötigten Ergebnisse. Für Sie bedeutet das aber auch, dass Sie sich keine Gedanken darüber machen müssen, welche Datenbankfelder Sie in der Entitätsklasse abbilden müssen. Sprechen nicht wichtige Gründe dagegen, können Sie bedenkenlos alle abbilden. Auf die Performance und den Netzwerkverkehr hat das keinen Einfluss. Zwei weitere Möglichkeiten, um das von LINQ to SQL erzeugte SQL-Statement abzurufen, möchte ich Ihnen nicht vorenthalten. Die erste benutzt die Eigenschaft Log des DataContext-Objekts. Diese Eigenschaft leitet die Ausgabe an einen TextWriter um, bei dem es sich beispielsweise um Console.Out handeln könnte:
    context.Log = Console.Out;
Sie können auch mittels Reflection das SQL-Statement als Zeichenfolge abrufen, wie die folgende Anweisung zeigt. Den Namenspace System.Reflection sollten Sie vorher importieren.
    string strSQL = context.GetType().
        GetMethod("GetChangeText", BindingFlags.Instance |
        BindingFlags.NonPublic).Invoke(context, null) as string;
### 29.6.4 Aktualisieren der Daten Unter Verwendung des DataContext-Objekts können wir Datensätze ändern, löschen und hinzufügen. Das DataContext-Objekt verfolgt alle Änderungen und kann durch einen einzigen Methodenaufruf die Datenbank aktualisieren. #### Daten ändern Sehen wir uns am Anfang ein Beispiel an, in dem wir alle Artikel der Products-Tabelle, deren Preis höher als 50 EUR ist, um 10% billiger anbieten. Dazu besorgen wir uns zuerst eine Liste aller Artikel, die der Bedingung UnitPrice > 50 genügen. Das soll uns jedoch noch nicht genügen. Wir wollen uns die geänderten Datensätze auch an der Konsole ausgeben lassen, ehe wir sie in die Datenbank schreiben.
    // ----------------------------------------------------------------
    // ...\Beispiele\Kapitel 29\Aktualisieren-Mit_LINQ_to_SQL\Editieren
    // ----------------------------------------------------------------
    string con = "...";
    DataContext context = new DataContext(con);
    Table<Product> products = context.GetTable<Product>();
    // alle Datensätze abrufen
    var query = from prod in products
                where prod.UnitPrice > 50
                select prod;
    foreach (Product prod in query)
      Console.WriteLine("{0,-35}{1}", prod.ProductName, prod.UnitPrice);
    Console.WriteLine();
    // Preis reduzieren
    foreach (var item in query)
      item.UnitPrice = item.UnitPrice * 0.9m;
    // Abrufen der geänderten Datensätze aus der 'products'-Liste
    ChangeSet set = context.GetChangeSet();
    var liste = set.Updates;
    Console.WriteLine("Geänderte Datensätze");
    foreach(Product prod in liste)
      Console.WriteLine("{0,-35}{1}", prod.ProductName, prod.UnitPrice);
    // Datenbank aktualisieren
    context.SubmitChanges();
    Console.WriteLine("Datenbank ist aktualisiert ...");
Die Datensätze, die wir ändern wollen, müssen in der Ergebnisliste der LINQ-Abfrage stehen. In einer Schleife wird die Liste Element für Element durchlaufen und der Preis jedes Produkts verringert. Der Code sollte auch ohne weitere Erklärungen verständlich sein. Das DataContext-Objekt verfolgt die Änderungen an den Datensätzen. Die protokollierten Änderungen können wir mit der Methode GetChangeSet abrufen, die ein Objekt vom Typ ChangeSet zurückliefert. Das ChangeSet-Objekt ist ein Container, der drei Listen beherbergt: die der geänderten Datensätze, die der gelöschten und die der neu hinzugefügten. Auf diese Listen, die vom Typ List<Object> sind, haben wir Zugriff über die Eigenschaften Updates, Deletes und Inserts. Mit
    ChangeSet set = context.GetChangeSet();
    var liste = set.Updates;
wird im Code die Liste aller von der Änderung betroffenen Datensätze abgerufen und in der Variablen liste gespeichert. Da wir wissen, dass die Liste nur Product-Entitäten enthält, können wir in der foreach-Schleife die Artikel mit ihrem neuen Preis abfragen. Zu diesem Zeitpunkt sind nur die Daten im lokalen Speicher von der Änderung betroffen. Um auch die Datenbank zu aktualisieren, müssen Sie nur die SubmitChanges-Methode des DataContext-Objekts aufrufen:
    context.SubmitChanges();
Das Aktualisierungsverhalten des DataContext-Objekts erinnert sehr an das Verhalten eines DataSet, das zunächst ebenfalls erst alle Änderungen intern speichert. Im Beispiel sind von der Änderung des Preises mehrere Entitäten gleichzeitig betroffen. Soll nur ein Datensatz editiert werden, hilft die Erweiterungsmethode Single weiter. Im folgenden Beispiel wird ein Datensatz unter Angabe des Primärschlüssels ausgewählt:
    var query = products.Single(prod => prod.ProductID == 17);
Single hat aber einen Nachteil. Wird der gesuchte Datensatz in der Datenbank nicht gefunden, wird eine Exception ausgelöst, die Sie mit einer try/catch-Fehlerbehandlung behandeln müssen. Verwenden Sie anstelle von Single die nahezu gleichwertige Methode SingleOrDefault, können Sie auf die Fehlerbehandlung verzichten. SingleOrDefault löst keine Ausnahme aus, wenn der Datensatz nicht gefunden wird, sondern liefert null zurück.
    var query = products.SingleOrDefault(prod => prod.ProductID == 171789);
    if (query != null) {
      Console.Write("'{0}' ändern in ", query.ProductName);
      query.ProductName = Console.ReadLine();
      context.SubmitChanges();
      Console.WriteLine("Datenbank ist aktualisiert ...");
    }
    else {
      Console.WriteLine("Datensatz nicht gefunden.");
    }
Beachten Sie beim Einsatz der Methoden Single bzw. SingleOrDefault, dass der Rückgabewert vom Typ Product ist. Sie können damit direkt auf die Spalten zugreifen und deren Inhalt, wie im Codefragment gezeigt, ändern. #### Datensatz hinzufügen Um einen neuen Datensatz in die Datenbank zu schreiben, benötigen wir zuerst ein Entitätsobjekt zur Übergabe der neuen Daten an das DataContext-Objekt. Anschließend weisen wir dem Objekt die neuen Werte zu und fügen es der Liste aller Entitäten hinzu. Dazu rufen Sie die Methode InsertOnSubmit auf der Entitätenliste products auf. Die Methode SubmitChanges fügt den neuen Datensatz der Datenbank hinzu. Den Programmcode, der erforderlich ist, um ein neues Entitätsobjekt zu erzeugen und als Datensatz in der Datenbank zu speichern, können Sie dem folgenden Beispielprogramm entnehmen:
    // ------------------------------------------------------------------
    // ...\Beispiele\Kapitel 29\Aktualisieren-Mit_LINQ_to_SQL\Hinzufügen
    // ------------------------------------------------------------------
    string con = "...";
    DataContext context = new DataContext(con);
    Table<Product> products = context.GetTable<Product>();
    // neue 'Product'-Entität
    Product newProduct = new Product();
    newProduct.ProductName = "Kartoffelsalat";
    newProduct.Discontinued = true;
    // zur Liste hinzufügen
    products.InsertOnSubmit(newProduct);
    // Datenbank aktualisieren
    context.SubmitChanges();
    Console.WriteLine("Datensatz hinzugefügt.");
    Console.ReadLine();
#### Datensatz löschen Jetzt fehlt uns nur noch das Löschen eines Datensatzes. Es können nur Datensätze gelöscht werden, die in der vom DataContext-Objekt gefüllten Entitätsliste enthalten sind. Das Prinzip des Löschens ähnelt dem des Änderns. Zuerst muss der zu löschende Datensatz ermittelt werden. Hierzu bieten sich wieder die beiden Methoden Single und SingleOrDefault an. Die Methode DeleteOnSubmit, die auf der Liste der Product-Entitäten aufgerufen wird, markiert den Datensatz als zur Löschung anstehend. SubmitChanges löscht den Datensatz endgültig in der Datenbank.
    // ----------------------------------------------------------------
    // ...\Beispiele\Kapitel 29\Aktualisieren-Mit_LINQ_to_SQL\Löschen
    // ----------------------------------------------------------------
    string con = "...";
    DataContext context = new DataContext(con);
    Table<Product> products = context.GetTable<Product>();
    // zu löschenden Datensatz suchen
    var prod = products
           .SingleOrDefault(delProd => delProd.ProductID == 13);
    if (prod != null) {
      // Datensatz markieren
      products.DeleteOnSubmit(prod);
      // Datenbank aktualisieren
      context.SubmitChanges();
      Console.WriteLine("Datensatz gelöscht.");
    }
    else {
      Console.WriteLine("Datensatz nicht gefunden.");
    }
    Console.ReadLine();
### 29.6.5 Konflikte behandeln Schreiben wir eine Anwendung für einen Benutzer, brauchen wir uns nicht allzu viele Gedanken um eventuell auftretende Konflikte zu machen. Aber der Alltag sieht meist anders aus, denn die Systeme werden von mehreren Usern gleichzeitig benutzt. In diesen Situationen muss damit gerechnet werden, dass zwei oder noch mehr Benutzer gleichzeitig denselben Datensatz bearbeiten. Da LINQ to SQL genauso wie auch ADO.NET mit verbindungslosen Daten arbeitet, müssen Sie als Entwickler die potenziell möglichen Konfliktszenarien berücksichtigen. Beim Zurückschreiben gilt per Vorgabe von LINQ to SQL das First-In-Wins-Szenario. Mit anderen Worten: Der erste Benutzer, der einen geänderten Datensatz in der Datenbank aktualisiert, hat Erfolg. Der andere Benutzer wird bei dem Versuch scheitern, seine eigene Änderung an demselben Datensatz in die Datenbank zu schreiben. Er wird zwar darüber informiert, dass die Daten zwischenzeitlich in der Datenbank geändert wurden, aber der beabsichtigte Speichervorgang kann nicht ausgeführt werden. Dieses Konfliktverhalten wird auch als Optimistic Concurrency bezeichnet. #### Konfliktverhalten steuern Bereits beim Bereitstellen der Entitätsklassen haben wir bisher, ohne es zu wissen, Optimistic Concurrency eingestellt. Rufen wir die Methode SubmitChanges auf dem DataContext-Objekt auf, wird automatisch ein passendes SQL-Statement erzeugt, in dem alle Spalten der SELECT-Abfrage in der WHERE-Klausel zur Identifizierung des Datensatzes in der Datenbanktabelle angegeben werden. Nehmen wir an, Sie würden die Tabelle Products durch die Entitätsklasse Product abbilden. In der Klasse seien nur die drei Spalten ProductID, ProductName und UnitPrice beschrieben. Ändern Sie an einem der gemappten Datensätze im Feld ProductName den Wert, würde LINQ to SQL das folgende parametrisierte UPDATE-Statement erzeugen:
    UPDATE [dbo].[Products]
    SET [ProductName] = @p3
    WHERE ([ProductID] = @p0) AND
          ([ProductName] = @p1) AND
          ([UnitPrice] = @p2)
Die Parameter werden mit den entsprechenden Daten des betroffenen Datensatzes gefüllt, wobei die Parameter in der WHERE-Klausel die ursprünglichen Originalwerte aus der Datenbank beschreiben. Wird in der Datenbank kein Datensatz gefunden, der den durch WHERE beschriebenen Kriterien entspricht, kann auch keine Aktualisierung erfolgen, was als Konflikt bewertet wird. Anschließend wird eine ChangeConflictException ausgelöst. Sie können das Aktualisierungsverhalten beliebig an Ihre eigenen Anforderungen anpassen. Dazu müssen Sie das Attribut Column, das mit jeder Spalte der von der Entitätsklasse beschriebenen Tabelle verknüpft ist, um den Parameter UpdateCheck ergänzen, beispielsweise:
    [Column(Storage="_ProductName",
            DbType="NVarChar(40) NOT NULL",
            CanBeNull=false,
            UpdateCheck=UpdateCheck.WhenChanged)]
    public string ProductName {
      ...
    }
Dieser Parameter kann drei Werte annehmen, die durch die Enumeration UpdateCheck beschrieben werden. Die möglichen Werte können Sie Tabelle 29.3 entnehmen.

Tabelle 29.3 Die Mitglieder der Enumeration »UpdateCheck«

Konstante | Beschreibung
Always | Die Spalte wird immer in die WHERE-Klausel einbezogen.
Never | Die Spalte wird nie in die WHERE-Klausel einbezogen.
WhenChanged | Die Spalte wird nur bei einer Änderung in die WHERE-Klausel einbezogen.
Wird der Parameter UpdateCheck nicht angegeben, gilt die Vorgabe UpdateCheck.Always. Alle zur Entität gehörenden Spalten werden dann zur Bestimmung der zu aktualisierenden Datenzeile herangezogen. Daraus resultiert das zuvor beschriebene First-In-Wins-Szenario. In einem Last-In-Wins-Szenario wird zur Identifizierung des betroffenen Datensatzes ausschließlich die Primärschlüsselspalte angegeben. Das erreichen Sie, indem Sie für alle anderen Spalten den Parameter auf den Wert UpdateCheck.Never einstellen. Sie können übrigens auch die mit einem Primärschlüssel gekennzeichnete Spalte mit UpdateCheck.Never kennzeichnen. Allerdings wird diese Einstellung keine Auswirkungen haben, da diese Spalte grundsätzlich immer im Filter der WHERE-Klausel verwendet wird. #### Auf Konflikte reagieren LINQ to SQL weist eine hohe Flexibilität auf, wenn es darum geht, Einfluss darauf auszuüben, wann ein Konflikt auftreten soll. Trotz der vielen Beeinflussungsmöglichkeiten kann in einer Mehrbenutzerumgebung ein Konflikt nicht vermieden werden, wenn zwei User Änderungen am gleichen Datensatz vornehmen. Der Benutzer, der als zweiter versucht, den Datensatz in der Datenbank zu aktualisieren, wird mit einer ChangeConflictException konfrontiert, die behandelt werden muss. Abhängig von der vom DataContext-Objekt protokollierten Anzahl an Aktualisierungen können beim Aufruf der Methode SubmitChanges durchaus mehrere Datensätze einen Konflikt auslösen. Rufen Sie SubmitChanges parameterlos auf, wird die Aktualisierung beim ersten Parallelitätskonflikt abgebrochen. Eine Überladung der Methode gestattet es jedoch, alle aufgetretenen Parallelitätskonflikte zu sammeln. Übergeben Sie SubmitChanges dazu das Argument ConflictMode.ContinueOnConflict, z. B. so:
    context.SubmitChanges(ConflictMode.ContinueOnConflict)
ConflictMode ist eine Enumeration, die nur zwei Konstanten beschreibt. Die zweite, FailOnFirstConflict, ist die Vorgabe, wenn Sie keine spezifische Angabe machen.

Tabelle 29.4 Mitglieder der Enumeration »ConflictMode«

Konstante | Beschreibung
FailOnFirstConflict | Gibt an, dass weitere Versuche zur Aktualisierung der Datenbank sofort abgebrochen werden sollen, wenn der erste Konflikt gefunden wird.
ContinueOnConflict | Gibt an, dass alle Aktualisierungen für die Datenbank geprüft und dass Parallelitätskonflikte zusammengefasst und am Ende der Aktualisierungsoperation zurückgegeben werden sollen.
Tritt ein Konflikt auf, haben Sie prinzipiell zwei Lösungsmöglichkeiten: Sie lösen das aufgetretene Problem pauschal, ohne dass Sie den Benutzer mit Detailinformationen zum Konflikt versorgen. Sie zeigen dem Benutzer detailliert den Grund des Konflikts an. Der Anwender kann dann seinerseits entscheiden, wie im Einzelfall auf den Konflikt reagiert werden muss. Beide Varianten wollen wir nun untersuchen. #### Allgemeine Konfliktlösung Das DataContext-Objekt verfolgt nicht nur die Änderungen an den Entitätsobjekten, es hilft uns auch bei einem oder mehreren aufgetretenen Konflikten weiter. Diese werden vom DataContext in einer Collection gesammelt, auf die wir Zugriff über die Eigenschaft ChangeConflicts haben. Um festzulegen, wie Parallelitätskonflikte gelöst werden sollen, bietet uns die Collection die Methode ResolveAll an, der wir ein Argument vom Typ der Enumeration RefreshMode übergeben müssen, also zum Beispiel:
    context.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);
In der Enumeration RefreshMode sind drei Konstanten definiert, die Sie Tabelle 29.5 entnehmen können.

Tabelle 29.5 Mitglieder der Enumeration »RefreshMode«

Konstante | Beschreibung
KeepCurrentValues | Kombiniert die neuen Werte in der Datenbank mit den aktuellen Werten.
KeepChanges | Die aktuellen Werte werden beibehalten, die neuen Werte in der Datenbank ignoriert.
OverwriteCurrentValues | Die neuen Werte in der Datenbank überschreiben die aktuellen Werte.
Möchten Sie die Änderungen, die ein zweiter Benutzer zuvor vorgenommen hat, mit den Änderungen des aktuellen Benutzers kombinieren, stellen Sie die Option RefreshMode.KeepCurrentValues ein. Das hört sich zwar verlockend an, hat aber einen Nachteil: Haben beide Anwender dieselbe Spalte geändert, wird der Wert des Anwenders, der zuerst aktualisiert hat, einfach überschrieben. Mit RefreshMode.OverwriteCurrentValues werden die Werte des konfliktverursachenden Datensatzes aus der Datenbank gelesen. Diese überschreiben die aktuellen Werte. Damit hat der Anwender, der mit dem Konflikt konfrontiert wird, einerseits die neuesten Daten vorliegen, muss aber andererseits seine eigenen Änderungen noch einmal überarbeiten. RefreshMode.KeepChanges ist die brutale Art, auf einen Konflikt zu reagieren. Änderungen, die ein zweiter Anwender zuvor vorgenommen hat, werden einfach ignoriert und durch die eigenen beim folgenden SubmitChanges-Aufruf überschrieben.
    try {
     context.SubmitChanges(ConflictMode.ContinueOnConflict);
    }
    catch (ChangeConflictException ex) {
      context.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);
      context.SubmitChanges();
    }
#### Detaillierte Konfliktbeschreibung Eine gute Lösung eines Parallekitätskonflikts setzt voraus, dass dem Anwender alle notwendigen Detailangaben dazu zur Verfügung gestellt werden. Dazu gehört der Name der Tabelle, in der der Konflikt verursacht worden ist, ebenso wie die betroffene Spalte. Darüber hinaus müssen auch drei Feldinformationen vorliegen: der neue Wert in der Datenbank der aktuelle Wert, der sich als Konfliktverursacher erwiesen hat der ursprüngliche Originalwert, von dem ausgehend das Feld des Datensatzes aktualisiert worden ist An alle benötigten Informationen zu kommen, stellt kein Problem dar. Dazu müssen wir die ChangeConflicts-Collection durchlaufen. Alle dort registrierten Konflikte werden durch Objekte vom Typ ObjectChangeConflict beschrieben. Da ein Konflikt seine Ursache auch in mehreren Feldern eines Datensatzes haben kann, werden diese Felder in einer Collection gesammelt, auf die wir Zugriff über die Eigenschaft MemberConflicts des ObjectChangeConflict-Objekts haben. Da wir nun in Form einer Referenz genau das Feld in Erfahrung gebracht haben, das für den Konflikt verantwortlich ist, können wir alle notwendigen Informationen zusammentragen. Das wollen wir uns nun an einem konkreten Beispiel ansehen. In der Entitätsklasse werden die drei Spalten ProductID, ProductName und UnitPrice beschrieben. Für alle drei Spalten ist der Parameter UpdateCheck des Attributs Column auf seinen Standardwert Always festgelegt.
    // -------------------------------------------------------
    // ...\Beispiele\Kapitel 29\Konfliktanalyse
    // -------------------------------------------------------
    string con = "...";
    DataContext context = new DataContext(con);
    Table<Product> products = context.GetTable<Product>();
    var prod = products.SingleOrDefault(delProd =>
                                        delProd.ProductID == 1);
    // Feldwert aktualisieren
    prod.ProductName = "Senf";
    prod.UnitPrice = 115;
    Console.WriteLine("Jetzt 2. User simulieren ...");
    Console.ReadLine();
    try {
      // Versuch der Datenbankaktualisierung
      context.SubmitChanges(ConflictMode.ContinueOnConflict);
    }
    catch (ChangeConflictException ex) {
      // Detailinformationen abrufen und anzeigen
      var detail = from conflict in context.ChangeConflicts
                   from member in conflict.MemberConflicts
                   select new {
                          Membername = member.Member.Name,
                          CurrentValue = member.CurrentValue,
                          Originalvalue = member.OriginalValue,
                          DataBaseValue = member.DatabaseValue
                   };
      foreach (var item in detail) {
        Console.WriteLine("Feldname:      {0}", item.Membername);
        Console.WriteLine("CurrentValue:  {0}", item.CurrentValue);
        Console.WriteLine("Originalvalue: {0}", item.Originalvalue);
        Console.WriteLine("DataBaseValue: {0}", item.DataBaseValue);
      }
    }
    Console.ReadLine();
Sie können das Programmbeispiel testen, indem Sie die Anwendung starten und der Aufforderung an der Konsole folgen, einen zweiten Benutzer zu simulieren. Dazu ist das SQL Server Management Studio sehr gut geeignet. Ändern Sie hier in der ersten Datenzeile der Tabelle Products den Wert der beiden Spalten ProductName und UnitPrice. Wechseln Sie danach wieder an die Konsole, und setzen Sie durch Drücken der -Taste die Ausführung fort. Danach sehen Sie, dass beide geänderten Spalten für den Konflikt verantwortlich sind. Zudem zeigt die Auswertung neben dem alten Originalwert und dem aktuellen Wert auch den neuen Wert in der Datenbank an. Hier klicken, um das Bild zu vergrößern Abbildung 29.1 Ausgabe des Beispielprogramms »Konfliktanalyse« Sehen wir uns nun den catch-Zweig etwas genauer an. Mit
    var detail = from conflict in context.ChangeConflicts
besorgen wir uns zuerst die Liste aller aufgetretenen Konflikte. Dabei handelt es sich um Objekte vom Typ ObjectChangeConflict. Jedes dieser Objekte beschreibt eine eigene Collection, die MemberChangedConflict-Objekte verwaltet. Diese Liste wird mit
    from member in conflict.MemberConflicts
abgerufen. Über die Eigenschaft Member des MemberChangedConflict-Objekts können wir die für uns interessanten Details erfahren und einem anonymen Typ übergeben. Dazu gehören der Name der konfliktauslösenden Spalte sowie der aktuelle Wert, der Originalwert und der in der Datenbank gespeicherte neue Wert.
    select new {
                  Membername = member.Member.Name,
                  CurrentValue = member.CurrentValue,
                  Originalvalue = member.OriginalValue,
                  DataBaseValue = member.DatabaseValue
               };