次の方法で共有


Using DbContext in EF 4.1 Part 9: Optimistic Concurrency Patterns

 


The information in this post is out of date.

Visit msdn.com/data/ef for the latest information on current and past releases of EF.

For Optimistic Concurrency Patterns see https://msdn.com/data/jj592904


 

Introduction

Version 4.1 of the Entity Framework contains both the Code First approach and the new DbContext API. This API provides a more productive surface for working with the Entity Framework and can be used with the Code First, Database First, and Model First approaches. This is the ninth post of a twelve part series containing collections of patterns and code fragments showing how features of the new API can be used.

The posts in this series do not contain complete walkthroughs. If you haven’t used EF 4.1 before then you should read Part 1 of this series and also Code First Walkthrough or Model and Database First with DbContext before tackling this post.

Optimistic concurrency

This post is not the appropriate place for a full discussion of optimistic concurrency. The sections below assume some knowledge of concurrency resolution and show patterns for common tasks. The basic idea behind optimistic concurrency is that you optimistically attempt to save your entity to the database in the hope that the data there has not changed since the entity was loaded. If it turns out that the data has changed then an exception is thrown and you must resolve the conflict before attempting to save again.

Many of these patterns make use of the topics discussed in the Part 5 of this series—Working with property values.

Resolving concurrency issues when you are using independent associations (where the foreign key is not mapped to a property in your entity) is much more difficult than when you are using foreign key associations. Therefore if you are going to do concurrency resolution in your application it is advised that you always map foreign keys into your entities. All the examples below assume that you are using foreign key associations.

A DbUpdateConcurrencyException is thrown by SaveChanges when an optimistic concurrency exception is detected while attempting to save an entity that uses foreign key associations.

Resolving optimistic concurrency exceptions with Reload

The Reload method can be used to overwrite the current values of the entity with the values now in the database. The entity is then typically given back to the user in some form and they must try to make their changes again and re-save. For example:

 using (var context = new UnicornsContext())
{
    bool saveFailed;
    do
    {
        saveFailed = false;

        var unicorn = context.Unicorns.Find(1);
        unicorn.Name = "Franky";

        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Update the values of the entity that failed to save 
            // from the store
            ex.Entries.Single().Reload();
        }

    } while (saveFailed);
}

A good way to see this code working is to set a breakpoint on the SaveChanges call and then modify the unicorn with Id 1 in the database using another tool such as SQL Management Studio. You could also insert a line before SaveChanges to update the database directly using SqlCommand. For example:

     context.Database.SqlCommand(
        "update Unicorns set Name = 'Linqy' where Id = 1");

The Entries method on DbUpdateConcurrencyException returns the DbEntityEntry instances for the entities that failed to update. (This property currently always returns a single value for concurrency issues. It may return multiple values for general update exceptions.) An alternative for some situations might be to get entries for all entities that may need to be reloaded from the database and call reload for each of these.

Resolving optimistic concurrency exceptions as client wins

The example above that uses Reload is sometimes called database wins or store wins because the values in the entity are overwritten by values from the database. Sometimes you may wish to do the opposite and overwrite the values in the database with the values currently in the entity. This is sometimes called client wins and can be done by getting the current database values and setting them as the original values for the entity. (See Part 5 for information on current and original values.)  For example:

 using (var context = new UnicornsContext())
{
    bool saveFailed;

    var unicorn = context.Unicorns.Find(1);
    unicorn.Name = "Franky";

    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Update original values from the database
            var entry = ex.Entries.Single();
            entry.OriginalValues.SetValues(entry.GetDatabaseValues());
        }

    } while (saveFailed);
}

Custom resolution of optimistic concurrency exceptions

Sometimes you may want to combine the values currently in the database with the values currently in the entity. This usually requires some custom logic or user interaction. For example, you might present a form to the user containing the current values, the values in the database, and a default set of resolved values. The user would then edit the resolved values as necessary and it would be these resolved values that get saved to the database. This can be done using the DbPropertyValues objects returned from CurrentValues and GetDatabaseValues on the entity’s entry. For example:

 using (var context = new UnicornsContext())
{
    bool saveFailed;

    var unicorn = context.Unicorns.Find(1);
    unicorn.Name = "Franky";

    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Get the current entity values and the values in the database
            var entry = ex.Entries.Single();
            var currentValues = entry.CurrentValues;
            var databaseValues = entry.GetDatabaseValues();

            // Choose an initial set of resolved values. In this case we
            // make the default be the values currently in the database.
            var resolvedValues = databaseValues.Clone();

            // Have the user choose what the resolved values should be
            HaveUserResolveConcurrency(currentValues, databaseValues,
                                       resolvedValues);

            // Update the original values with the database values and
            // the current values with whatever the user choose.
            entry.OriginalValues.SetValues(databaseValues);
            entry.CurrentValues.SetValues(resolvedValues);
        }

    } while (saveFailed);
}

The stub for HaveUserResolveConcurrency looks like this:

 public void HaveUserResolveConcurrency(DbPropertyValues currentValues,
                                       DbPropertyValues databaseValues,
                                       DbPropertyValues resolvedValues)
{
    // Show the current, database, and resolved values to the user and have
    // them edit the resolved values to get the correct resolution.
}

Custom resolution of optimistic concurrency exceptions using objects

The code above uses DbPropertyValues instances for passing around current, database, and resolved values. Sometimes it may be easier to use instances of your entity type for this. This can be done using the ToObject and SetValues methods of DbPropertyValues. For example:

 using (var context = new UnicornsContext())
{
    bool saveFailed;

    var unicorn = context.Unicorns.Find(1);
    unicorn.Name = "Franky";

    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Get the current entity values and the values in the database
            // as instances of the entity type
            var entry = ex.Entries.Single();
            var databaseValues = entry.GetDatabaseValues();
            var databaseValuesAsUnicorn = (Unicorn)databaseValues.ToObject();

            // Choose an initial set of resolved values. In this case we
            // make the default be the values currently in the database.
            var resolvedValuesAsUnicorn = (Unicorn)databaseValues.ToObject();

            // Have the user choose what the resolved values should be
            HaveUserResolveConcurrency((Unicorn)entry.Entity,
                                       databaseValuesAsUnicorn,
                                       resolvedValuesAsUnicorn);

            // Update the original values with the database values and
            // the current values with whatever the user choose.
            entry.OriginalValues.SetValues(databaseValues);
            entry.CurrentValues.SetValues(resolvedValuesAsUnicorn);
        }

    } while (saveFailed);
}

The stub for HaveUserResolveConcurrency now looks like this:

 public void HaveUserResolveConcurrency(Unicorn entity,
                                       Unicorn databaseValues,
                                       Unicorn resolvedValues)
{
    // Show the current, database, and resolved values to the user and have
    // them update the resolved values to get the correct resolution.
}        

Summary

In this part of the series we looked at various patterns for resolving optimistic concurrency exceptions, including database wins, client wins, and a hybrid approach in which the user chooses the resolved values.

As always we would love to hear any feedback you have by commenting on this blog post.

For support please use the Entity Framework Forum.

Arthur Vickers

Developer

ADO.NET Entity Framework

Comments

  • Anonymous
    February 03, 2011
    Just wanted to say how useful I'm finding this series of posts - great work!

  • Anonymous
    February 06, 2011
    In the optimistic concurrency frameworks I've built over the years I've always provided an auto-merge option to automatically deal with the (fairly common) situation where the database changes and the local user changes do not conflict on a column level. In such cases the framework can often safely merge the changes and retry the save. In other words, merge mode rather than pure database wins or client wins. Does DbContext provide an easy way to accomplish this in the event of a concurrency exception?

  • Anonymous
    February 07, 2011
    @James: Glad it's useful. :-) @Daniel: This is not built in to EF but I think you could implement it quite easily using CurrentValue, OriginalValues, and GetDatabaseValues. I will add the idea to our backlog as something to consider adding more automated support for.

  • Anonymous
    February 25, 2011
    Like Daniel said it would be helpful. Something like below would be nice. So you don't have to go through all that trouble of reloading just catch their concurrency properties in the savechanges event. var client = context.Clients.Find(1); client.Concurrency(ConcurrencyType.ClientWins,"Name","Description"); client.Concurrency(ConcurrencyType.DBWins,"Price","Ect"); client.ClientWins( c => c.Age > 15);   Things like that or even if need be add it as modelmetada on properties themselves.

  • Anonymous
    March 16, 2011
    Hi, I cant get my concurrency check to work at all, here what I have setup in my mvc 3 project

Class Movie with a property [Timestamp] [ConcurrencyCheck] [DatabaseGenerated(DatabaseGeneratedOption.Computed)] public byte[] RowVersion { get; set; } 2. [HttpPost]        public ActionResult Edit(Movie model)        {            try            {                var movie = db.Movies.Find(model.ID)                  db.Movies.Attach(movie);                TryUpdateModel(movie);                db.SaveChanges();                return RedirectToAction("Index");            }            catch (DbUpdateConcurrencyException e)            {                ModelState.AddModelError("", "optimistic concurrency exception");            }            catch (Exception e)            {                ModelState.AddModelError("", "Simple Catch");            }            return View(model);        } The controller never throw an exception. any help please? Thanks, Sam

  • Anonymous
    April 07, 2011
    It's funny, but both the samples look to me like "client wins" strategy. After all, both are iteratively trying to push client-provided values into the DB until EF accepts them. The only difference is that they're doing it in a slightly different ways. Any comments? :)

  • Anonymous
    April 27, 2011
    Yeah the two first samples look a little weird to me as well. But after some testing it occured to me that some other property change might have caused the exception (for example "PrincessId" value changed in the database) - then we need to choose between the database value or the client value of that other property. Hmm... That makes me just more confused though. I mean, why should we care to synchronize the "PrincessId" property value? All the sample code does is changing the "Name" property. That is pretty independent from the "PrincessId" property.

  • Anonymous
    January 24, 2012
    @Sam: I don’t see anything obviously wrong with your code, other than the Attach call is unnecessary. Without seeing more details it’s hard to know why you’re not getting an exception—maybe the data hasn’t changed in the database between Find and SaveChanges? @Andrey @Cactus: Ultimately the application is trying to save client data to the database. The point is that in a database-wins case the data from the client doesn’t automatically overwrite the data in the database if it has changed since it was loaded. The examples for concurrency have to be somewhat contrived to fit in a blog post and be reproducible. Typically there would be more than one property that might change—for example, a user’s outstanding balance might be changed by one application at the same time that another application is changing a user’s work address. Yes, this is still contrived, but it is perhaps easier to see how in this case both the new address and the new outstanding balance should make it to the database. Thanks, Arthur

  • Anonymous
    March 28, 2012
    how is "client wins" different from "dont do concurrency"?

  • Anonymous
    April 02, 2012
    @brian - It's really not any different if you just do it without prompting the user. But it can be useful in situations where you tell the user there was a problem and give them the option to overwrite the other users changes with their own.