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 here.

Other 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...
Writing an ActiveX control in C#
An ActiveX control is an object that supports a customizable programmatic interface. Using the metho...
Polymorphism and Encapsulation
Polymorphism and encapsulation are two big words in OO development, and are also a fundamental conce...
Introducing LINQ – Part 2
In the first part of this series I introduced you to the new language enhancements in C# 3.0, in thi...
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 entit...

You might also be interested in the following related blog posts


Implementing SqlBulkCopy in Linq to Sql read more
Querying with LINQ to Entities vs ObjectQuery in EF read more
Identity Maps read more
date validations read more
Dynamic Queries and LINQ Expressions read more
A few things you can't do with EF queries which you won't find out until runtime read more
Is V1 of LINQ to SQL only suitable for RAD? read more
what I miss in LINQdatasource read more
Important Entity Framework Query Improvements for .NET 4.0 read more
LINQ to SQL in multi layered + service apps read more
Top
 
 
 

Please login to rate or to leave a comment.

Product Spotlight