Tuesday, July 19, 2011

Using RIA Services Contrib's EntityGraph to easily do partial saves to entities

UPDATE (29 Nov 2011): The RIA Services contrib project now has built in support for this functionality, see here.

I love RIA Services. Using it for our latest project saved us a stack of time, however... An area I found particularly painful was doing partial saves, particularly when you may have a group of related entites that you wish to save, without having to do a full save via RIA's SubmitChanges() method. The problem with SubmitChanges() is that it saves everything, which often is not what you want.

The system we have just finished and released to production makes extensive use of RIA Services and Prism. It allows the user to have multiple edits all happening at once on the one tree of hierarchical information, with a rich UI that updates all open windows instantly when edits occur. It also has full support for concurrent users all working on the one tree of records at the same time.

Thanks to the post here, the SubmitPartialChanges method allowed us to initially do saves of single modified Entites without inadvertenly saving all other modified entities the user had open. However, as the project continued, the need arose for not only a single Entity to be saved, but related modified Entities too, all in the one save operation.

To cut a long story short, at the time, it took a lot of code to make doing this perform well, and be transactional. Making multiple calls to SubmitPartialChanges() (one for each modified Entity) is slow, puts a lot of load on the server, and is not transactional. Writing the code to do it yourself is time consuming and difficult to read. Towards the end of the project, I found the recent work done in RIA Services Contrib on Codeplex that introduces the concept of the Entity Graph.

Take a moment to check out what it does. It was too late for us (it is still in Beta at the time of this writing, and didn't exist at the beginning of our project). Recently I've taken a look at using EntityGraph to allow for easily saving the Entities in the graph.

Below is a code snippet that uses some new extension methods to easily perform partial saves. The code is quite concise, and much better than what it would have looked like otherwise. The two extension methods of note are CloneEntityGraphAndAttach() and ApplyEntityGraphState().


...

EntityGraph graph = MechanicModel.EntityGraph(tbl_Mechanic.RepairJobPartShape);

if (MechanicModel.EntityState == EntityState.New ||
MechanicModel.EntityState == EntityState.Detached)
{
addingNewMechanic = true;

// Add the new Mechanic to the temporary domain context
tempDomainContext.tbl_Mechanics.Add(MechanicModel);

// Create the repairJob-mechanic link
repairJobMechanicLink = new tbl_RepairJob_Mechanic();
repairJobMechanicLink.RepairJobId = repairJob.Id;
repairJobMechanicLink.tbl_Mechanic = MechanicModel;
tempDomainContext.tbl_RepairJob_Mechanics.Add(repairJobMechanicLink);

// Create the links to each part of the repairJob
foreach (tbl_Part part in repairJob.tbl_Part)
{
tbl_Part_Mechanic_RepairJob partLink =
new tbl_Part_Mechanic_RepairJob();
partLink.PartId = part.Id;
partLink.tbl_RepairJob_Mechanic = repairJobMechanicLink;
tempDomainContext.tbl_Part_Mechanic_RepairJobs.Add(partLink);
}
}
else
{
// Clone the object graph into the temporary context
tempDomainContext.CloneEntityGraphAndAttach(graph);
}

// Update the temporary context with the server
tempDomainContext.SubmitChanges(
submitOperation =>
{
if (submitOperation.HasError)
{
...
}
else
{
if (addingNewMechanic)
{
// Clone and then attach the new mechanic's object graph to the
// real domain context
DomainContext.tbl_Mechanics.Attach(graph.Clone());
}
else
{
// Apply the saved state back to the real entities...
DomainContext.ApplyEntityGraphState
(tempDomainContext.tbl_Mechanics.First().EntityGraph
(tbl_Mechanic.RepairJobPartShape));
}

...
}
}

...



And below are the extension methods used.


using System.ServiceModel.DomainServices.Client;
using System.Collections;
using System.Reflection;
using System;
using System.ComponentModel.DataAnnotations;
using RiaServicesContrib.DomainServices.Client;
using RiaServicesContrib.Extensions;

namespace Infrastructure
{
public static partial class DomainContextExtensions
{
/// <summary>
/// Applies the state of each Entity in the graph to the
/// clones in this DomainContext.
/// </summary>
/// <param name="graph"></param>
/// <param name="contextToApplyState"></param>
/// <remarks>This only currently works for Entities with an
/// integer Key value</remarks>
public static void ApplyEntityGraphState
(
this DomainContext contextToApplyState,
EntityGraph graph
)
{
foreach (Entity entity in graph)
{
// Apply the state of the cloned entity to the real one....
ApplyStateToMatchingEntity(contextToApplyState, entity);
}

}

/// <summary>
/// Clones the given Entity and attaches it to this DomainContext.
/// </summary>
/// <param name="contextToAttach"></param>
/// <param name="graph"></param>
/// <remarks>This only currently works for Entities with an integer
/// Key value</remarks>
public static void CloneEntityGraphAndAttach
(
this DomainContext contextToAttach,
EntityGraph graph
)
{
Entity clone = graph.Clone();

// Attach a clone of the entity graph to the temporary domain context
contextToAttach.EntityContainer.GetEntitySet(clone.GetType())
.Attach(clone);

// Apply the state of each entity to the cloned entity
foreach (Entity realEntity in graph)
{
ApplyStateToMatchingEntity(contextToAttach, realEntity);
}
}

/// <summary>
/// Applies the state of the given entity to the matching clone
/// in this DomainContext.
/// </summary>
/// <param name="domainContextToSearch"></param>
/// <param name="realEntity"></param>
/// <remarks>This only currently works for Entities with an
/// integer Key value</remarks>
public static void ApplyStateToMatchingEntity
(
this DomainContext domainContextToSearch,
Entity realEntity
)
{
FindMatchingEntity(domainContextToSearch, realEntity)
.ApplyState(realEntity.ExtractState
(RiaServicesContrib.ExtractType.OriginalState),
realEntity.ExtractState
(RiaServicesContrib.ExtractType.ModifiedState));
}

/// <summary>
/// Finds an entity with the same Key as the given entity
/// </summary>
/// <param name="realEntity"></param>
/// <param name="domainContextToSearch"></param>
/// <returns>The matching Entity, or null if not found</returns>
/// <remarks>This only currently works for Entities with an
/// integer Key value</remarks>
public static Entity FindMatchingEntity
(
this DomainContext domainContextToSearch,
Entity realEntity
)
{
// Find the entity set holding the clone
EntitySet entitySet = domainContextToSearch.EntityContainer
.GetEntitySet(realEntity.GetType());

// Find the clone within this entity set...
IEnumerator enumerator = entitySet.GetEnumerator();

// Find the Key property for the Entity
PropertyInfo keyProperty = null;
PropertyInfo[] properties = entitySet.EntityType.GetProperties();
foreach (PropertyInfo propertyInfo in properties)
{
foreach (Attribute attribute in entitySet.EntityType
.GetProperty(propertyInfo.Name).GetCustomAttributes(false))
{
if (attribute.GetType() == typeof(KeyAttribute))
{
keyProperty = propertyInfo;
break;
}
}
}

// Use the key property to find the clone with the same Id
while (enumerator.MoveNext())
{
Entity candidate = enumerator.Current as Entity;

if ((int)keyProperty.GetValue(candidate, null) ==
(int)keyProperty.GetValue(realEntity, null))
{
return candidate;
}
}

return null;
}
}
}

2 comments:

  1. Hi jdb,

    Thanks for your post. I am facing a similar scenario like yours using RIA service and PRISM. Can you please explain how the entity relationship is in your case. I am unable to relate my Entity model with your sample. Any help is appreciated.

    Thanks Syam

    ReplyDelete
  2. I recently added the missing pieces to EntityGraph to make partial save really easy. You can read about it here: http://riaservicescontrib.codeplex.com/wikipage?title=PartialSaveWithEntityGraph

    ReplyDelete