Generic ObjectSpaces concurrency error handling

Update: Corrected error on Resync method (only changes original values of tracked object) and improved property retrieval logic (now through MappingSchema)

Today I spent some time working on concurrency error handling in ObjectSpaces.

I assume you are familiar with concurrency errors: the situation where you try to persist changes to your objects and someone has already modified the same data in the database. Typically this will happen in a optimistic locking scenario.

How does ObjectSpaces (OS) detect concurrency errors? Objects can be tracked by an ObjectContext. This context will keep track of the current and (more importantly) original values of the objects it tracks. Objects that are materialized from OS are automatically tracked. You can track newly made objects by calling the ObjectSpace.StartTracking method or by adding them to an ObjectSet. ObjectSets also track their objects.

The most important part in handling concurrency error that occur while persisting changes is to catch exception of type PersistenceException while calling ObjectSpace.PersistChanges. You probably want to use a PersistenceOptions object in that method call. By specifying that you want the errors to be reported after a batch update has been completed, you can handle all concurrency errors in one go. Use the PersistenceErrorBehavior.ThrowAfterCompletion for this.

publicvoid ConcurrencyErrorHandling()
{
  SqlConnection con =
new
SqlConnection
      (ConfigurationSettings.ConnectionStrings[“ObjectSpaces”]);
  ObjectSpace os =
new
ObjectSpace(ConfigurationSettings.AppSettings
      [“MappingSchema”], con);

  // Materialize object
  Title t = (Title)os.GetObject(typeof
(Title), “TitleID=’BU1111′”);
  t.BookTitle += ” modified”;
  try
  {
  PersistenceOptions options =
new
PersistenceOptions
      (Depth.SingleObject, 
      PersistenceErrorBehavior.ThrowAfterCompletion);

  // Force error
  GenerateConcurrencyError();

  os.PersistChanges(t, options);
  Console.WriteLine(“Object(s) saved successfully”);
  }

In the catch clause you can iterate through all persistence errors that occured through the Errors collection of the PersistenceException. Each item in this collection is a PersistenceError object, that gives details on the error. It also gives you the object that could not be persisted as the ErrorObject.

At this point there are two obvious approaches:

  1. Create a specific handler function for the type of object that you are persisting. In this example that would be a Title object. You can work directly with the object’s properties after casting it. The downside is that you will need to create a separate handler for every object type for which you want to handle concurrency errors.
  2. Create a generic error handler, which I will demonstrate below.

  catch (PersistenceException ex)
  {
    Console.WriteLine(“Concurrency errors detected”);
    foreach (PersistenceError err in ex.Errors)
    {
      Object objectInError = err.ErrorObject;

In an concurrency error three versions of a property exist:

  • the current values you have given the object
  • the original values of the object as it was materialized from the database
  • the persistent values inside the database, also known as the underlying values

To properly handle the error, you choose whether to keep the current or persistent values, either programmatically or by letting the user decide. OS gives you access to each of the three versions. The current and original versions are available through the ObjectContext. For the persistent values we need to make a roundtrip to the database.

You can access each of the values of a particular version by retrieving an ValueRecord. The ValueRecord object is essentially an collection of all property values of an object. They can be modified, but this will not reflect in the object, as it seems to be a copy of the values, not a reference to the actual values stored inside the ObjectContext.

      ValueRecord currentRecord, originalRecord, persistentRecord;
      CommonObjectContext ctx = (CommonObjectContext)
          ObjectContext.GetInternalContext(os);

      currentRecord = ctx.GetCurrentValueRecord(objectInError);
      originalRecord = ctx.GetOriginalValueRecord(objectInError);

After retrieving the current and original ValueRecord objects, the object needs to be resynced with the persistent values from the database. Otherwise concurrency errors would keep on occuring. By resyncing we loose the original values, but we have a copy of those in one of the two ValueRecord objects. It may seem a little strange that the persistent record is retrieved as the original ValueRecord, but we just set these values to the persistent ones.

      // Resync object with persistent values
      os.Resync(objectInError, Depth.SingleObject);
      persistentRecord = ctx.GetOriginalValueRecord(objectInError);

BTW, an alternative to getting the persistent values from the database is by using the ObjectEngine.GetPersistentValueRecord method, like so:

      MappingSchema map = new MappingSchema
          (ConfigurationSettings.AppSettings[“MappingSchema”]);
      ObjectSources sources =
new
ObjectSources();
      sources.Add(“pubs”, con);
     
persistentRecord = ObjectEngine.GetPersistentValueRecord
          (map, sources, objectInError);

Since we need to resync the object with the persistent values anyway, it would not make sense to retrieve the persistent values in a second database roundtrip.

Next, go through all properties of the object and compare the original version of the object with the persistent values. You can use the MappingSchema to retrieve the correct mapping and check the FieldMaps collection for properties that have UseForConcurrency set to true (default). For each of these properties the corresponding SchemaMember of the SchemaClass object is retrieved. For these members, you can compare the values of the properties by passing the property name (from the Name property of the SchemaMember class) as the indexer value of the ValueRecords for the current and persistent values.

      ObjectSchema objSchema = new ObjectSchema
          (ConfigurationSettings.AppSettings[“ObjectSchema”]);
      SchemaClassCollection classes = objSchema.Classes;
      SchemaClass schClass = classes[
objectInError.GetType()];    

      Map m = null;
      int index;
      for (index = 0; index < map.Maps.Count; index++)
      {
        m = map.Maps[index];
        if (m.TargetSelect == objectInError.GetType().FullName)
          break;
      }

      foreach (FieldMap fieldMap in m.FieldMaps)
      {
        if (!fieldMap.UseForConcurrency)
          continue;
        string name = fieldMap.TargetDomainField.Name;
        SchemaMember member = schClass.Members[name];

        object o = originalRecord[name];
       
if (o == null)
          continue;
        if (member.ObjectRelationship != null)
          continue;

        PropertyInfo prop = (PropertyInfo)member.MemberInfo;

If the values do not match, we ask the user to decide which value to keep: current or persistent. Since the SchemaMember object also provides a PropertyInfo member, that gives access to a PropertyInfo object. With this object you can set the value of the resynced object to the desired value. Again, it has no effect on the object if you try to change the ValueRecord directly.

After all properties have been done, try to persist again. I assumed everything will work fine now, but you can build a more robust mechanism, of course.

        // Use Equals method here, because == operator is non-virtual
        if (!(originalRecord[name].Equals(persistentRecord[name])))
        {
          Console.WriteLine(“Concurrency error found in property: ”
              + name);
          Console.Write(“nYour value: ” +
              currentRecord[name].ToString());
          Console.Write(“nOriginal value: ” +
              originalRecord[name].ToString());
          Console.Write(“nCurrent value in database: ” +
              persistentRecord[name].ToString());
          Console.Write(“nKeep your (1) or database values (2): “);

          ConsoleKeyInfo info;
          while (true)
          {
            info = Console.ReadKey(
true);
            if (info.KeyChar == ‘1’ || info.KeyChar == ‘2’)
              break;
          }

          if (info.KeyChar == ‘2’)
              prop.SetValue(t,  persistentRecord[name], null);
        }
        Console.WriteLine(name + ” : ” + prop.GetValue(t,
            null
).ToString());
      }
      os.PersistChanges(objectInError,
          new PersistenceOptions(Depth.SingleObject));

    }
  }
}

Below is the code fragment that causes an intentional concurrency error. It’s pretty straightforward.

publicvoid GenerateConcurrencyError()
{
  SqlConnection con =
new SqlConnection
      (ConfigurationSettings.ConnectionStrings[“ObjectSpaces”]);
  SqlCommand cmd =
new SqlCommand(
      “UPDATE titles SET pubdate=@theDate”, con);
  SqlParameter param = cmd.Parameters.Add(“theDate”,
      SqlDbType.DateTime);
  param.Value = DateTime.Now;
  try
  {
    con.Open();
    int affected = cmd.ExecuteNonQuery();
    if (affected == 0)
    {
      Console.WriteLine(“Could not generate concurrency error”);
    }
  }
 
finally
  {
    con.Dispose();
  }
}

For those of you interested, I’ve included the sample code as a zipped solution. You should be able to run it after modifying the App.config file with the correct file paths for the schema files and the connection string to the database (although it should work if you run a local and default instance of SQL Server). The sample uses the pubs database.

Any feedback on this is greatly appreciated.

Pubs ObjectSpaces Concurrency Error Handling.zip (82.11 KB)

Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s