none
Resolving concurrency conflicts in RIA Services

    Question

  • We are developing a Silverlight 4 application using EF and RIA Services.  I am attempting to resolve concurrency conflicts in RIA Services.  My requirements are to display a Yes/No message box to the user which describes the type of concurrency error and then let him/her choose how to resolve.  For example, in the case where a concurrency error occurs such that the local copy of the entity was modified and the remote copy was modified (Modify / Modify scenario) I display a message box basically stating "Click Yes to have your local changes committed to the database.  Click No to discard your changes and retrieve the current data from the database."

    The scenarios I need to support are as follows:

    1. Local Modify / Remote Modify

    2. Local Modify / Remote Delete

    3. Local Delete / Remote Modify

    4. Local Delete / Remote Delete

     

    if (concurrencyConflict.MessageBoxResult == MessageBoxResult.Yes)

    {

    // here we want to commit the local changes to the database

    EntityQuery query = domainContext.GetType().GetMethod("GetAssaysQuery").Invoke(domainContext, null) as EntityQuery;

    domainContext.Load(query, LoadBehavior.MergeIntoCurrent, ModifyDeleteLoadCompleted, null);

    }

    else

    {

    // here we want to get the value from the database

    EntityQuery query = domainContext.GetType().GetMethod("GetAssaysQuery").Invoke(domainContext, null) as EntityQuery;

    domainContext.Load(query, LoadBehavior.RefreshCurrent, ModifyDeleteLoadCompleted, null);

    }

    In the LoadCompleted callback I call SubmitChanges() if the state of any entities is Modified.

    First off is this the correct way to handle concurrency?  Second how do I go about handling cases where the Remote entity is deleted?  In that case I want to display a message box that basically states "The entity modified locally has been deleted from the database.  Click Yes to have your updates written to the database anyways.  Click No to discard your changes."

    Any help or guidance here would be appreciated or if anyone can point me to a complete sample that demonstrates how to resolve different types of concurrency errors that would be great.  Thank you!

     

     

    Monday, November 08, 2010 8:59 AM

Answers

  • The bug fix will set this value for you, but for now to work around this you can use the following helper which will allow you to "apply state" to the entity instance, bypassing validation. To use this, you call entity.ApplyingState(true) before setting the values, then be sure to call entity.ApplyingState(false) afterwards. During deserialization, we of course bypass validation - the below code is using that mechanism to apply state.

        public static class RIAExtensions
        {
            public static void ApplyingState(this Entity entity, bool applyingState)
            {
                if (applyingState)
                {
                    entity.OnDeserializing(new  System.Runtime.Serialization.StreamingContext());           
                }
                else
                {
                    entity.OnDeserialized(new System.Runtime.Serialization.StreamingContext());
                }
            }
        }
    Tuesday, November 09, 2010 2:51 PM

All replies

  • All the concurrency conflict information required for your scenario is provided on Entity.EntityConflict, which will be non-null after a failed submit for each entity with conflicts. The conflict already provides all the current store values in EntityConflict.StoreEntity to use for your local merge, so you don't need to requery. To resolve the conflict, you should use the store values and perform the merge/resolution by setting property values, then call EntityConflict.Resolve

    To handle your delete/modify scenario (in cases where EntityConflict.IsDeleted == true), if the user chooses to discard their changes, you just detach the entity (EntitySet.Detach). To preserve the changes made to the deleted entity, you'd need to convert the update into an insert (an "Upsert"). You could do this by detaching as above, then adding the entity as new (EntitySet.Add). These operations convert the update into an insert.

    Monday, November 08, 2010 12:44 PM
  • Thank you for the quick response Mathew!  I hope you don't mind but could you explain to me in a bit more detail what you mean by "To resolve the conflict, you should use the store values and perform merge/resolution by setting property values".

    If I don't requery, I understand that within the EntityConflict object I have the current, original and store versions of the entity with the conflict.  I guess I'm not sure what to do with these values.  For example, if I want to write my local changes to the database do I just loop through the properties of the current object and write those to the properties of the store object?  If I want to discard my local changes and retrieve the values from the store do I loop through the properties and write the values in the store to current?

    After doing that I then call Resolve?  I see that Resolve sets the EntityConflict to null, do I then need to SubmitChanges?

    Thank you once again!

    Monday, November 08, 2010 2:35 PM
  • Using the information provided in the EntityConflict, your resolution code should set values on the current entity in conflict such that it represents the merged/resolved values you want to push to the store. So no, you don't write any values to StoreEntity. After performing your resolution modifications to the entity, you should resolve the conflict (EntityConflict.Resolve) and resubmit. The high level is that your job is take the failed changeset, modify the entities to remove conflicts and resubmit.

    Monday, November 08, 2010 2:44 PM
  • I'm missing something here Mathew.  I have an object "Employee".  Employee has a FirstName property and a LastName property.  In the database I have also created a TimeStamp column (type = timestamp) in my Employees table.  I force a concurrency conflict that results in the following in the EntityConflict object:

    CurrentEntity => FirstName = John_Local, LastName = Smith

    OriginalEntity => FirstName = John, LastName = Smith

    StoreEntity => FirstName = John_Remote, LastName = Smith

    I display a messagebox to the user that asks what do they want to do, commit local changes to the database or discard local changes and retrieve the values in the database

    If I choose to commit local changes to the database I don't need to perform any updates to any properties on CurrentEntity since it already contains the values I want to write to the database (FirstName = John_Local, LastName = Smith) so all I do is call currentEntity.EntityConflict.Resolve()

    This sets the EntityConflict = null, I then call domainContext.SubmitChanges(SubmitCallback, null)

    This results in the same concurrency conflict I thought I just resolved and the database is not updated.

    What am I missing?  I'm sure I'm doing something wrong here.

     

    Monday, November 08, 2010 3:14 PM
  • Yes, Resolve should be doing what you expect it to - it resolves the conflict by ensuring that the concurrency values used for the subsequent submit are the current values in the store. Currently the framework does this by copying StoreEntity values into the "original values" of the entity. However, there is currently a bug - in a Timestamp case like yours, the original values aren't used for concurrency - the TS value of the entity is. In timestamp cases, Resolve should be copying StoreEntity.Timestamp into current.Timestamp. We'll fix this bug for SP1 RTM, but for now, you'll have to do that copy manually. If you do, your second submit should succeed.

    Monday, November 08, 2010 4:37 PM
  • Thank you for the explanation Mathew!  We have moved to the WCF RIA Services SP1 Beta which explains my problem.

    So per your suggestion, I am attempting to copy the value of the Store timestamp into the value of the Current timestamp and I'm getting the following error:

    The 'TimeStamp' property is read-only?

    Do I have to set up the concurrency column a specific way to make it writable?

    For my TimeStamp field I have the following:

    Concurrency Mode = Fixed

    StoreGeneratedPattern = Computed

    Type = Binary

    Tuesday, November 09, 2010 8:02 AM
  • The bug fix will set this value for you, but for now to work around this you can use the following helper which will allow you to "apply state" to the entity instance, bypassing validation. To use this, you call entity.ApplyingState(true) before setting the values, then be sure to call entity.ApplyingState(false) afterwards. During deserialization, we of course bypass validation - the below code is using that mechanism to apply state.

        public static class RIAExtensions
        {
            public static void ApplyingState(this Entity entity, bool applyingState)
            {
                if (applyingState)
                {
                    entity.OnDeserializing(new  System.Runtime.Serialization.StreamingContext());           
                }
                else
                {
                    entity.OnDeserialized(new System.Runtime.Serialization.StreamingContext());
                }
            }
        }
    Tuesday, November 09, 2010 2:51 PM
  • Thank you so much Mathew for your help with this issue!

    Wednesday, November 10, 2010 7:01 AM
  • Dear Mathew,

     I try to implement that solution and I am stuck with the deletion case, where I want to implement your suggesion by using EntitySet.Detach and then EntitySet.Add ....

    The issue is that the complier doesn't regonize the "EntitySet.Detach(entity)".Underlined in the code below

    How can I get the current EntitySet ??!

    I am implementing this in the submitchanges callback as follows:

    if (so.Error != null)
    			{
    				Application.Current.RootVisual.Dispatcher.BeginInvoke((Action)(() =>
    				{
    				foreach (var entity in so.EntitiesInError)
    				{
    					if (entity.EntityConflict.IsDeleted)
    					{
    						Controller.ShowWindow("Deleted, Do you want to readd it again", AppStrings.ConfirmTitle, MessageType.Error);
    						Controller.MainWindowClosed += (s, e) =>
    						{
    							if (Controller.CloseResult == 1)
    							{
    								EntitySet.Detach(entity);
    								//GoBack();
    							}
    							else
    							{
    								Cancel();
    								//GoBack();
    							}
    						};
    					}
    				}
    				}));
    			}

    Best regards


    Waleed


    Sunday, May 05, 2013 6:27 PM