Getting Changed Entities from a LINQ to SQL DataContext

Published: 18 Apr 2007
By: Ryan Haney

In this article you will learn how to get the changes made to your objects prior to updating using generics, reflection and extension methods.

Introduction

Getting data from the database to the UI is a snap with LINQ to SQL. Getting the changes made to your entities is a little more difficult.

Please note: This article assumes knowledge of LINQ to SQL and C#. If you are new to LINQ, I recommend you to read the following article.

For this example, I have run the SqlMetal utility against the Northwind database. I feel it is better to start with the results, and then discuss the steps to get there.

The Results

// Instantiate the DataContext
Northwind db = new Northwind("Data Source=.\SQLExpress;Initial
                   Catalog=Northwind;Integrated Security=True");

// Get a list of Customers whos City starts with "M"
var customers = from customer in db.Customers
                where customer.City.StartsWith("M")
                orderby customer.City ascending
                select customer;

// Write the City of each customer, and change 
// the city to some new value
foreach (var customer in customers)
{
  Console.WriteLine(customer.City);
  customer.City = "XXX";
}

Console.ReadLine();

// Get a list of changed items - NOTE: The "GetChangedItems" 
// method is a new extension method
// we have created...See the next section for details

List<ChangedItems<Customer>> changedItems = db.GetChangedItems<Customer>();
// Write the old city and new city values for each customer
foreach (var changedCustomer in changedItems)
{
  Console.WriteLine("ChangedCustomer[{0}].City from {1} to {2}", 
    changedCustomer.Current.CustomerID, 
    changedCustomer.Original.City, 
    changedCustomer.Current.City);
}

Console.ReadLine();

Output:

Madrid
Madrid
Madrid
Mannheim
Marseille
México D.F.
México D.F.
México D.F.
México D.F.
México D.F.
Montréal
München
Münster

Changed Customer[BOLID].City from Madrid to XXX
Changed Customer[FISSA].City from Madrid to XXX
Changed Customer[ROMEY].City from Madrid to XXX
Changed Customer[BLAUS].City from Mannheim to XXX
Changed Customer[BONAP].City from Marseille to XXX
Changed Customer[ANATR].City from México D.F. to XXX
Changed Customer[ANTON].City from México D.F. to XXX
Changed Customer[CENTC].City from México D.F. to XXX
Changed Customer[PERIC].City from México D.F. to XXX
Changed Customer[TORTU].City from México D.F. to XXX
Changed Customer[MEREP].City from Montréal to XXX
Changed Customer[FRANK].City from München to XXX
Changed Customer[TOMSP].City from Münster to XXX

The Implementation - Where the magic happens

To get the list of changed items, I needed to create an extension method that used reflection. I made the decision to use generics simply for the added benefit of strong typing.

The GetChangedItems method first interrogates the DataContext instance for its private member called services It then queries for services private member tracker, followed by a query for the tracker's private member items. Whenever an object has changed, the DataContext attaches a reference of the current and original objects to the items, which is of type IDictionary. The method loops through this dictionary, adding each item of the type specified to our list.

public static class Extensions
{
  public static List<ChangedItems<TItem>> GetChangedItems<TItem> 
                (this DataContext context)
  {
    // create a dictionary of type TItem for return to caller
    List<ChangedItems<TItem>> changedItems = new List<ChangedItems<TItem>>();

    // use reflection to get changed items from data context
    object services = context.GetType().BaseType.GetField("services", 
      BindingFlags.NonPublic | 
      BindingFlags.Instance | 
      BindingFlags.GetField).GetValue(context);
    
    object tracker = services.GetType().GetField("tracker", 
      BindingFlags.NonPublic | 
      BindingFlags.Instance | 
      BindingFlags.GetField).GetValue(services);
    System.Collections.IDictionary trackerItems = 
      (System.Collections.IDictionary)tracker.GetType().GetField("items", 
      BindingFlags.NonPublic | 
      BindingFlags.Instance | 
      BindingFlags.GetField).GetValue(tracker);

      // iterate through each item in context, adding
      // only those that are of type TItem to the changedItems dictionary
      foreach (System.Collections.DictionaryEntry entry in trackerItems)
      {
        object original = entry.Value.GetType().GetField("original", 
                          BindingFlags.NonPublic | 
                          BindingFlags.Instance | 
                          BindingFlags.GetField).GetValue(entry.Value);

        if (entry.Key is TItem && original is TItem)
        {
          changedItems.Add(
            new ChangedItems<TItem>((TItem)entry.Key, (TItem)original)
          );
        }
      }
    return changedItems;
  }
}

You may have noticed the ChangedItems class in the above code. This class is a container that identifies the Current and Original objects.

public class ChangedItems<TItem>
{
  public ChangedItems(TItem Current, TItem Original)
  {
    this.Current = Current;
    this.Original = Original;
  }
  public TItem Current { get; set; }
  public TItem Original { get; set; }
}

Future Enhancements - Implementing Business Logic and Server-Side Validation

Now that we have implemented a mechanism for detecting changes, we can easily implement the business logic and validation. Since the classes generated by SqlMetal are partial classes, we can easily extend them. We would first need to create an interface called IBusinessObject via the Validate() method that returns an List<string> object. Second, we could extend our Customer class to implement the IBusinessObject interface, and fill out the logic for the Validate() method. Third, we can call our GetChangedItems method, pass an IBusinessObject for our type to search for, and loop through the results calling the Validate() method on each to get a list of validation errors.

Now suppose we wanted to go a little bit further and enforce that each changed IBusinessObject was persisted ONLY if the validation passed. We could override the DataContext.SubmitChanges method, calling GetChangedItems<IBusinessObject>(), invoking Validate() on each IBusinessObject in the list adding the results to a list of validation errors. If there aren't any errors in the list, we can call the base.SubmitChanges.

Summary

In short, there is a lot of functionality in the .NET Framework, much of which is abstracted for us. This article serves as an example in which extension methods can be used to tap into the functionality when needed.

About Ryan Haney

Sorry, no bio is available

View complete profile

Top Articles in this category

Introducing LINQ – Part 1
Introducing LINQ is the first part of a series of articles on Language Integrated Query (LINQ). This series will cover the core essentials of LINQ and its use with other technologies like ASP.NET, Win Forms and WPF.

Polymorphism and Encapsulation
Polymorphism and encapsulation are two big words in OO development, and are also a fundamental concept of software development. This article will demystify these concepts by showing you some real world examples.

Writing an ActiveX control in C#
An ActiveX control is an object that supports a customizable programmatic interface. Using the methods, events and properties exposed by the control, web developers can automate their web pages to give the functionality which is equivalent to that of a desktop application.

Introducing LINQ – Part 2
In the first part of this series I introduced you to the new language enhancements in C# 3.0, in this part we will look at querying relational data.

Introducing LINQ – Part 3
In Part 2 we took a look at LINQ to SQL, how to generate an entity, and also how to query that entity. In this part we will look a little more at what entities are, as well as taking a closer look at the key types we can use and their application.

Top
 
 
 

Please login to rate or to leave a comment.

Product Spotlight