Showing posts with label Concurrency. Show all posts
Showing posts with label Concurrency. Show all posts

Saturday, July 16, 2011

Entity Framework concurrency checking on ASP.NET MVC (UpdateModel is the only sane approach when you want to use concurrency checking on EF)

UPDATE August 2, 2011

Perhaps I'm too harsh on Entity Framework, we can simplify concurrency checking if we used Timestamp attribute. ConcurrencyCheck doesn't work well on detached(e.g. web) scenario, work-arounds is needed. On the updated code, we change ConcurrencyCheck attribute to Timestamp attribute

Though for complex object persisting (e.g. many-to-many. http://www.ienablemuch.com/2011/07/using-checkbox-list-on-aspnet-mvc-with_16.html), even we used Timestamp attribute, we still need OriginalValue work-around. Directly persisting a detached entity which has children entities would result to duplicate children entities, as EF has no knowledge that the old children entities should be discarded; so for this type of data persistence we need to to re-simulate attached scenario, that means we directly work on entities that come from EF, and that means we need to inform EF the entity's original rowversion value if we want EF to handle concurrent updates well. To prevent duplicate children entities on an entity, we need to clear the attached entity's children; on our checkbox list demo, that is movie.Genres.Clear(). Don't worry, clearing the children entities is efficient, it doesn't actually delete the children rows on database, try to check the Sql Server profiler :-)

public class Song
{
    [Key]
    public int SongId { get; set; }
    public string SongName { get; set; }
    public string AlbumName { get; set; }

    [Timestamp]
    public virtual byte[] Version { get; set; }
}


Here's the updated Save method:

[HttpPost]
public ActionResult Save(Song song)
{
    SaveCommon(song);

    return View("Input", song); // song is the model
}



private void SaveCommon(Song song)
{
    using (var db = new TheMusicContext())
    {
        try
        {
            if (song.SongId == 0)
                db.Songs.Add(song);                                                
            else
                db.Entry(song).State = System.Data.EntityState.Modified;

            db.SaveChanges();

            ModelState.Remove("SongId"); // to force ASP.NET MVC to get the SongId value from the model, not from ModelState
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry = ex.Entries.Single();


            var dbValues = entry.GetDatabaseValues();

            // already deleted
            if ( dbValues == null)
            {
                ModelState.AddModelError("", "This record you are attempting to save is already deleted by other user");
                return;
            }

            
            Song songDbValues = (Song) dbValues.ToObject();
            Song userValues = (Song) entry.Entity;



            if (songDbValues.SongName != userValues.SongName)
                ModelState.AddModelError("SongName", "Current value made by other user: " + songDbValues.SongName);

            if (songDbValues.AlbumName != userValues.AlbumName)
                ModelState.AddModelError("AlbumName", "Current value made by other user: " + songDbValues.AlbumName);

            ModelState.AddModelError("", "The record you attempted to edit was already modified by another user since you last loaded it. Open the latest changes on this record");
        }
        catch (Exception ex)
        {
            ModelState.AddModelError("", ex.Message + "\n" + ex.StackTrace + ex.InnerException.Message);
        }
        
      
    }

               
}//SaveCommon

Here's the updated Delete method:

public ActionResult Delete(int id, byte[] Version)
{
    using (var db = new TheMusicContext())
    {

        var stub = new Song { SongId = id, Version = Version };
        if (Request.HttpMethod == "POST")
        {
            try
            {                        
                db.Entry(stub).State = System.Data.EntityState.Deleted;
                db.SaveChanges();
                return RedirectToAction("Index");
            }
            catch (DbUpdateConcurrencyException ex)
            {
                var entry = ex.Entries.Single();
                var dbValues = entry.GetDatabaseValues();


                // already deleted
                if (dbValues == null)
                    return RedirectToAction("Index");
                                 
                ModelState.AddModelError("", "The record you are attempting to delete was modified by other user first. Do you still want to delete this record?");
                Song songDbValues = (Song)dbValues.ToObject();       

                return View(songDbValues);
            }                    
        }
        else
        {
            var songToDelete = db.Songs.Find(id);
            return View(songToDelete);
        }
    }
}


Get the updated code here: http://code.google.com/p/ef-concurrency-on-aspnet-mvc-demo/downloads/list




**STALE post. read the recommended procedure above**

Entity Framework concurrency handling doesn't work out-of-the-box if you are using it in disconnected scenario(as typified by web apps). Contrast to this(connected): http://www.ienablemuch.com/2011/05/entity-frameworks-concurrency-handling.html

When you submitted(POST) a form, you must simulate your entity like it was just first loaded from the GET method(e.g. http://Song/Edit/1), essentially you need to simulate connected mode so EF can compare if the record's rowversion have changed since it was first loaded. On the code below, that simulation is in line #22 and 23


ASP.NET's MVC model binding doesn't seem to be of much use on Entity Framework. Entity Framework cannot work with the posted model directly. Contrast that with NHibernate http://www.ienablemuch.com/2011/07/nhibernate-concurrency-checking-on.html

[HttpPost]
public ActionResult Save(Song song)
{
    using (var db = new TheMusicContext())
    {
        try
        {
            bool isNew = song.SongId == 0;

            var songToDb = !isNew ? db.Songs.Find(song.SongId) : song;

            if (songToDb == null) throw new Exception("This record you are attempting to save is already deleted by other user");


            if (isNew)
                db.Songs.Add(songToDb); // Primary key is automatically populated
            else
            {
                // To facilitate concurrency checks, do these two lines.
                // When EF perform an update, it compares the DB value from OriginalValue.
                // Leaky abstractions rear its ugly head when you want to do concurrency checks on EF ;-)
                db.Entry(songToDb).Property("Version").OriginalValue = song.Version;
                db.Entry(songToDb).State = System.Data.EntityState.Unchanged;

                // There's too much implicitness here, UpdateModel get values from Request.Form/Request.QueryString, Model Binding (variable song) isn't of much use here.
                // But UpdateModel is the only sane approach when you want to use concurrency checking on EF,
                // as it has array parameter, to which you can selectively pass properties via Linq-to-objects
                // Automapper is another approach? http://automapper.codeplex.com/
                // Is Model Binding useless on EF?
                // One thing for sure, Model Binding is not useless on NHibernate :-)
                // http://www.ienablemuch.com/2011/07/nhibernate-concurrency-checking-on.html
                UpdateModel(songToDb, 
                      typeof(Song).GetProperties()
                      .Where(x => x.Name != "Version")
                      .Select(x => x.Name).ToArray());
            }


            db.SaveChanges();

            db.Entry(songToDb).Reload(); // Fetch the Version column from the database
            song.Version = songToDb.Version;


            ModelState.Remove("SongId"); // to force ASP.NET to get the SongId value from the model, not from ModelState

            // no need to issue ModelState.Remove on Version property; with byte array, ASP.NET MVC always get its value from the model, not from ModelState
            // ModelState.Remove("Version") 
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry = ex.Entries.Single();
            Song dbValues = (Song)entry.GetDatabaseValues().ToObject();
            Song userValues = (Song)entry.Entity;



            if (dbValues.SongName != userValues.SongName)
                ModelState.AddModelError("SongName", "Current value made by other user: " + dbValues.SongName);

            if (dbValues.AlbumName != userValues.AlbumName)
                ModelState.AddModelError("AlbumName", "Current value made by other user: " + dbValues.AlbumName);

            ModelState.AddModelError("", "The record you attempted to edit was already modified by another user since you last loaded it. Open the latest changes on this record");
        }
        catch (Exception ex)
        {
            ModelState.AddModelError("", ex.Message);
        }
        

        return View("Input", song); // song is the model

    }       
}


Model:
public class Song
{
    [Key]
    public int SongId { get; set; }
    public string SongName { get; set; }
    public string AlbumName { get; set; }

    [ConcurrencyCheck]
    public virtual byte[] Version { get; set; }
}

The View:
@model EfConcurrencyOnAspNetMvc.Models.Song

@{
    ViewBag.Title = "Input";
}

<h2>Input</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@{ string controllerName = (string) ViewContext.RouteData.Values["Controller"]; }

@using (Html.BeginForm("Save", controllerName)) {
    @Html.ValidationSummary(true)

    @Html.HiddenFor(x => x.SongId)
    @Html.HiddenFor(x => x.Version)
    <fieldset>
        <legend>Song</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.SongName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.SongName)
            @Html.ValidationMessageFor(model => model.SongName)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.AlbumName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.AlbumName)
            @Html.ValidationMessageFor(model => model.AlbumName)
        </div>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

The DDL:
create table Song
(
SongId int identity(1,1) not null primary key,
SongName varchar(50) not null,
AlbumName varchar(50) not null,
Version rowversion not null
);

Complete demo code: http://code.google.com/p/ef-concurrency-on-aspnet-mvc-demo/downloads/list


Sample Output:

Saturday, May 28, 2011

Sql Server concurrency handling using its rowversion field type and Fluent NHibernate Auto Mapping

The model:
public class Product
{        
    public virtual int ProductId { get; set; }

    public virtual string ProductName { get; set; }
    public virtual string WarehouseLocation { get; set; }
    public virtual string LastAuditedBy { get; set; }
    public virtual int Quantity { get; set; }        
    public virtual byte[] TheRowversion { get; set; }
}


The concurrent update scenario:
class Program
{
    static void Main(string[] args)
    {
        var userA = Mapper.GetSessionFactory().OpenSession();
        var userB = Mapper.GetSessionFactory().OpenSession();
        
        // User A and User B happen to open the same record at the same time
        var a = userA.Get<Product>(1);
        var b = userB.Get<Product>(1);

        a.ProductName = "New & Improved" + a.ProductName + "!";

        b.WarehouseLocation = "Behind appliances section";
        b.LastAuditedBy = "Linus";
        b.Quantity = 7;

        userA.Save(a);
        userA.Flush();


        // This will fail. As the rowversion of the same record since it was first loaded, had 
        // already changed (the record was changed by User A first) when it's User B's time to save the same record.
        userB.Save(b);
        userB.Flush();

       
    }
}


DDL:
create table Product
(
ProductId int identity(1,1) not null primary key,
ProductName nvarchar(100) not null unique,
WarehouseLocation nvarchar(100) not null,
LastAuditedBy nvarchar(100) not null,
Quantity int not null,
TheRowversion rowversion not null
) 


Boilerplate code for mapping. If you decide to make all your tables' version column use Sql Server's built-in rowversion field type and use your own name for version column, you must do both line 59 and 65 respectively. Also, add the version convention to your ISessionFactory builder, line 36. The default(convention-over-configuration) rowversion column name when you don't implement line 59,65,36 is Version.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

using NHibernate;
using NHibernate.Dialect;

using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using FluentNHibernate.Automapping;
using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.Instances;

using FluentNHibernate.Conventions.Helpers;

using TestFluentAutomappingWithRowversion.Model;

namespace TestFluentAutomappingWithRowversion
{
    public static class Mapper
    {
        static ISessionFactory _sf = null;
        public static ISessionFactory GetSessionFactory()
        {
            if (_sf != null) return _sf;

            var fc = Fluently.Configure()
                    .Database(MsSqlConfiguration.MsSql2008.ConnectionString(@"Data Source=localhost; Initial Catalog=TestDb; User Id=sa; Password=P@$$w0rd; MultipleActiveResultSets=True"))
                    .Mappings
                    (m =>
                            m.AutoMappings.Add
                            (
                                AutoMap.AssemblyOf<Program>(new CustomConfiguration())
                                    .Conventions.Add<RowversionConvention>()                                
                            )
                    // .ExportTo(@"C:\_Misc\NH")
                    );


            // Console.WriteLine( "{0}", string.Join( ";\n", fc.BuildConfiguration().GenerateSchemaCreationScript(new MsSql2008Dialect() ) ) );
            // Console.ReadLine();

            _sf = fc.BuildSessionFactory();
            return _sf;
        }


        class CustomConfiguration : DefaultAutomappingConfiguration
        {
            IList<Type> _objectsToMap = new List<Type>()
            {
                typeof(Product)
            };
            public override bool ShouldMap(Type type) { return _objectsToMap.Any(x => x == type); }
            public override bool IsId(FluentNHibernate.Member member) { return member.Name == member.DeclaringType.Name + "Id"; }

            public override bool IsVersion(FluentNHibernate.Member member) { return member.Name == "TheRowversion"; }
        }


        class RowversionConvention : IVersionConvention
        {
            public void Apply(IVersionInstance instance) { instance.Generated.Always(); }
        }

    }
}

Entity Framework's Concurrency Handling using Sql Server's rowversion field. And why modal dialog is bad for editing records

The Model:

public class Product
{
 [Key]
 public int ProductId { get; set; }
 public string ProductName { get; set; }
 public string WarehouseLocation { get; set; }
 public string LastAuditedBy { get; set; }
 public int Quantity { get; set; }

 [ConcurrencyCheck]
 public byte[] TheRowversion { get; set; }
}

The concurrent update scenario:

class Program
{
 static void Main(string[] args)
 {
  var userA = new CatalogContext();
  var userB = new CatalogContext();

  // User A and User B happen to open the same record at the same time
  var a = userA.Products.Find(1);
  var b = userB.Products.Find(1);

  
  
  a.ProductName = "New & Improved" + a.ProductName + "!";
     
  b.WarehouseLocation = "Behind appliances section";
  b.LastAuditedBy = "Linus";
  b.Quantity = 7;

  

  userA.SaveChanges();

  // This will fail. As the rowversion of the same record since it was first loaded, had 
  // already changed (the record was changed by User A first) when it's User B's time to save the same record.
  userB.SaveChanges();

}



Why a modal dialog is bad for editing a record? It's bad when you employ concurrency handling (a must) on records. Imagine your program is in kiosk mode, and let's say due to policy or technology constraints(say IE6), launching multiple instances of your app or using multiple tabs(e.g. IE6) isn't allowed/possible. How can we prevent User B from wasting his edits/time(imagine if there are 20 fields on a given record)? from losing his work? Let him open the latest changes of the record he happened to open at the same time with other users in a new tab(e.g. jQuery tab). With non-modal editing, User B can compare his changes against the latest changes of User A, and then he can copy-paste his changes to the latest changes, not much work will be wasted. You cannot facilitate this sort of thing with modal dialog editor, User B will be compelled to lose all his edits, and will just re-open the latest version from User A and re-input again all his concerning fields.


DDL:

create table Product
(
ProductId int identity(1,1) not null primary key,
ProductName nvarchar(100) not null unique,
WarehouseLocation nvarchar(100) not null,
LastAuditedBy nvarchar(100) not null,
Quantity int not null,
TheRowversion rowversion not null
) 

Sunday, May 22, 2011

NHibernate Concurrency Handling using Sql Server Rowversion

The following code implements concurrency handling on NHibernate; if you are using Sql Server 2005 onwards, it's better to use its built-in rowversion field type. Rowversion maps to byte array on .NET. NHibernate can use this field on WHERE clause whenever it do an update. If there's no record updated, it means that other user updated/deleted the current record first before the current user save the same record. To handle this scenario of non-successfully-saved record, the current user shall receive a message that the current record he/she is attempting to save is already stale. Then inform the user to re-open the updated version of the record he/she is editing


The Object(aka Model):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace NhConcurrentUpdateHandling.Models
{
    public class Product
    {
        public virtual int ProductID { get; set; }
        public virtual string ProductName { get; set; }
        public virtual int Quantity { get; set; }
        public virtual byte[] TheRowversion { get; set; }
    }
}

Relational:
create table Product
(
ProductID int identity(1,1) not null,
ProductName nvarchar(100) not null,
Quantity int not null,
TheRowversion rowversion not null
);

insert into Product(ProductName,Quantity) values
('Keyboard',9)

Mapping:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using FluentNHibernate.Mapping;

using NhConcurrentUpdateHandling.Models;


namespace NhConcurrentUpdateHandling.ModelsMappings
{
    public class ProductMapping : ClassMap<Product>
    {
        public ProductMapping()
        {
            Id(x => x.ProductID);
            Map(x => x.ProductName);
            Map(x => x.Quantity);
            Version(x => x.TheRowversion).Generated.Always();
        }
    }
}


Controller:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

using NHibernate;

using NhConcurrentUpdateHandling.Models;
using NhConcurrentUpdateHandling.ModelsMapper;



namespace NhConcurrentUpdateHandling.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            using (var db = SessionFactoryBuilder.GetSessionFactory().OpenSession())
            {
                var p = db.Get<Product>(1);
                return View(p);
            }
        }

        [HttpPost]
        public ActionResult Index(Product p)
        {

            try
            {
                
                using (var db = SessionFactoryBuilder.GetSessionFactory().OpenSession())
                {
                    var px = db.Merge(p);
                    db.Flush();

                    UpdateModel(px);

                    ViewBag.SuccessfulMessage = "Saved.";

                    return View(px);


                }
                
            }
            catch (StaleObjectStateException ex)
            {
                ViewBag.ErrorMessage = "Your changes is not saved. Please re-open this record to get its latest values. Other user already do something to this record";
                return View(p);
            }
            
        }

    }
}

View:

@model NhConcurrentUpdateHandling.Models.Product

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@ViewBag.ErrorMessage

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Product</legend>

        @Html.HiddenFor(model => model.ProductID)

        <div class="editor-label">
            @Html.LabelFor(model => model.ProductName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.ProductName)
            @Html.ValidationMessageFor(model => model.ProductName)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Quantity)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Quantity)
            @Html.ValidationMessageFor(model => model.Quantity)
        </div>

        @Html.HiddenFor(x => x.TheRowversion)

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

@if(!string.IsNullOrEmpty(ViewBag.SuccessfulMessage)) {
    <b>@ViewBag.SuccessfulMessage</b>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>


Sample generated UPDATE statement when using SQL Server's rowversion as the means of concurrent update handling:

exec sp_executesql 
N'UPDATE [Product] SET ProductName = @p0, Quantity = @p1 
WHERE ProductID = @p2 AND TheRowversion = @p3',
N'@p0 nvarchar(4000),@p1 int,@p2 int,@p3 varbinary(8000)',
@p0=N'Keyboard',@p1=21,@p2=1,@p3=0x00000000000007E3

Notice that the ORM-generated UPDATE statement not only uses primary key on the WHERE clause, it also include the rowversion as well; so the ORM can determine if the record that was being saved is not modified by other users first. If there's no updated record, it means the rowversion had already changed, this happens when other users saved/deleted the record first. Exception will be thrown, and the simplest course of action is for the late-saver user to honor the record that was saved by the other user, the late-saver user shall re-open the saved record by the other user.



Boilerplate code for database connection: http://www.ienablemuch.com/2011/04/boilerplate-code-for-fluent-nh.html