Sunday, 16 March 2008

[update: I've updated my project quite a bit, and moved the state to another object. So, the code you are about to see is old!]

In the grand scheme of things, I'm building toward a set of tools that will enable me to use EntityFramework on the server and use the same businessobjects on the client-side, without having to reference EntityFramework at all.
We already have a great domain layer supporting EF, without baseclasses. To use these objects on the client, we will need to take care of changetracking ourselves. When we are done with that, we'll just have to write a custom serializer, that will know how to serialize xml representations of the objects on the client-side, and deserialize on the server and vice versa.

I'll follow up in some post that describes how you can create different versions of your domain layer for use on either client-side or server-side.

The end result

Let's skip to the end result straight away. This is a little test that runs fine:

  0    Person p = new Person();
  1    p.Name = "Ruurd";
  2    p.MyProperty = 10;
  3    p.orders.Add("234");
  4    p.orders.Add("23asdf4");
  5    p.orders.Add("234345zs");
  6
  7
  8    IEditableBusinessObject editablePerson = p as IEditableBusinessObject;
  9
  10    editablePerson.CopyCurrentToLoaded();
  11
  12    editablePerson.BeginEdit();
  13
  14    p.Name = "Boeke";
  15    p.orders.Add("zxcvzxcvzxcvzxcv");
  16
  17    Assert.IsTrue(p.Name == "Boeke");
  18    Assert.IsTrue(p.orders.Count == 4);
  19
  20    editablePerson.CancelEdit();
  21
  22    Assert.IsTrue(p.orders.Count == 3);
  23    Assert.IsTrue(p.Name == "Ruurd");

Just so we are on the same page, we did not have to implement anything on the business object itself. Only use an attribute: the EditableBusinessObjectAttribute. It will take care of everything for ya.

  0 [EditableBusinessObject]
  1 [Serializable]
  2  public class Person
  3 {
  4   public int MyProperty { get; set; }
  5
  6   public string Name { get; set; }
  7
  8   public List<string> orders { get; set; }
  9
  10   public Person()
  11   {
  12    orders = new List<string>();
  13   }
  14
  15 }

Editable business objects

There is already a great usersample available on how to use PostSharp to implement this interface. I took a different route, but you should still check out the sample, because it is actually quite well put together.

My scenario has the following requirements:

  • implement IEditableObject (obviously)
  • Have a way to copy values to a 'Loaded' state: necessary to be able to send the 'original values' to Entity Framework
  • Retrieve that loaded state: will be used by the serializer, to actually create the original value serialization

So, I created the following interface that we will implement using PostSharp:

  0  public interface IEditableBusinessObject : IEditableObject
  1 {
  2   void RegisterFieldAccessAspect(FieldInterceptionAspect aspect);
  3
  4   void CopyCurrentToLoaded();
  5
  6   object RetrieveLoadedState(string FieldName);
  7
  8   bool HasPendingTransaction();
  9 }
  10

To implement, we use the familiar CompositionAspect.

For the real magic, we need to intercept all gets and sets of all fields in the business object.

Intercept field access

A very strong feature of PostSharp is the OnFieldAccessAspect. It allows you to actually intercept field access and route it to something different.
The implementation of IEditableBusinessObject knows when the businessobject is set to beginedit and should notify all fieldaspects. We do this by 'registering' the fieldaspect at the editableBusinessObject. This way, when our object is going to beginEdit mode, all we have to do is iterate our fieldaspects and let them know.

At that point the fieldaspect makes a copy of it's value to the corresponding statebag. If our value was not a value type, we make a deepcopy. I'm contemplating adding support for ICloneable, so we can possibly skip that step.

So, the begin, cancel and endedit methods on the fieldaspect look something like this:

  0   internal void BeginEdit()
  1   {
  2    if (needsBinaryCopy)
  3    {
  4     MemoryStream m = new MemoryStream();
  5     BinaryFormatter b = new BinaryFormatter();
  6     b.Serialize(m, CurrentState);
  7     m.Position = 0;
  8     PendingState = b.Deserialize(m);
  9    }
  10    else
  11    {
  12     PendingState = CurrentState;
  13    }
  14   } 
  15  
  16   internal void CancelEdit()
  17   {
  18    PendingState = null;
  19   }
  20
  21   internal void EndEdit()
  22   {
  23    CurrentState = PendingState;
  24    PendingState = null;
  25   }

And the get and set methods of the field look like this:

  0   public override void OnGetValue(FieldAccessEventArgs eventArgs)
  1   {
  2    IEditableBusinessObject editableObjectImpl = (IEditableBusinessObject)eventArgs.Instance;
  3    editableObjectImpl.RegisterFieldAccessAspect(this);
  4
  5    eventArgs.ExposedFieldValue = editableObjectImpl.HasPendingTransaction() ? PendingState : CurrentState;
  6   }
  7
  8   public override void OnSetValue(FieldAccessEventArgs eventArgs)
  9   {
  10    IEditableBusinessObject editableObjectImpl = (IEditableBusinessObject)eventArgs.Instance;
  11    editableObjectImpl.RegisterFieldAccessAspect(this);
  12
  13
  14
  15    if (editableObjectImpl.HasPendingTransaction())
  16    {
  17     PendingState = eventArgs.ExposedFieldValue;
  18    }
  19    else
  20    {
  21     CurrentState = eventArgs.ExposedFieldValue;
  22    }
  23   }

As you can see, it checks with the instance, whether we have a pending transaction (we have started beginedit). If we do, we write to the pendingstate object, instead of the current state.

I don't feel too comfortable with the code at this point, because I took quite a bit of shortcuts. So I'm not putting that online at this point. However, if you are really interested, let me know in the comments, and I'll clean it up.

Stay tuned, because the next step will be to get this to serialize nicely, with originalvalues and such.

Sunday, 16 March 2008 19:31:22 (Romance Standard Time, UTC+01:00)
What's not clear from the code is what is actually PendingState and CurrentState. Are they instance variables of the aspect? Then remember that they are equivalent to *static* fields of the type to which the aspect is applied...

There is a kind of dirty trick to introduce instance fields into a type, but it is much an undocumented feature I would like to deprecate... The concept is called "instance tag".
Another trick to implement per-instance state is to introduce an interface (CompositionAspect); the implementation object has instance scope.

I think that the serializer-based approach will be quite slower than the dictionary-based approach. The advantage of your approach, of course, is that it takes a deep copy of the object (maybe even deeper than what is wished -- what to do with references to other business objects).

Maybe this concern (editable business objects) is very connected so in-memory transactions. See maybe possible connections with Ralf Westphal's STM...

Gael
Monday, 17 March 2008 18:57:42 (Romance Standard Time, UTC+01:00)
Going to look into it!
thnx for the headsup
Ruurd
Comments are closed.