Lost updates

Oct 25, 2009 at 1:45 PM
Edited Oct 25, 2009 at 8:24 PM

I am a student studying 3rd year Software Development at a university in Cape Town South Africa. I am using the MultiTierLinqToSql code to help me with a project in C# ASP.Net with the MVC pattern. I have come across an issue while running tests on my application. When I debug the website and do the following manually, the behavior of the website is not as expected. Here is what I do:

In two different browsers:


Scenario  1:

User1 logs in.

User2 logs in.

User1 reads record1

User2 reads record1

User1 updates record1

User2 updates record1


All of the above succeeds, and User1's update is lost. The behavior above is what I am confused about.

My integration test is shown below. The service layer does not catch the error "row not found or changed".


// In my Web Application Test project - I am simulating a different request for each database action, as would happen in a real scenario:

[TestMethod]
public void LostUpdatesTransactionTest()
{
var writer = new System.IO.StringWriter();
var wr = new System.Web.Hosting.SimpleWorkerRequest("", "", "", "", writer);

HttpContext context1 = new HttpContext(wr);
HttpContext context2 = new HttpContext(wr);
HttpContext context3 = new HttpContext(wr);
HttpContext context4 = new HttpContext(wr);
HttpContext context5 = new HttpContext(wr);
HttpContext context6 = new HttpContext(wr);
var controller1 = new CompanyController();
var controller2 = new CompanyController();

HttpContext.Current = context1;
Company company1 = new Company();
company1.name = "NameOriginal";

var result1 = (RedirectToRouteResult)controller1.Create(company1);
Assert.AreEqual("List", result1.RouteValues["action"]);

HttpContext.Current = context2;
var result2 = (ViewResult)controller1.Details(company1.id, 1);
Company company2 = (Company)result2.ViewData.Model;

HttpContext.Current = context3;
var result3 = (ViewResult)controller2.Details(company1.id, 1);
Company company3 = (Company)result3.ViewData.Model;

HttpContext.Current = context4;
company2.name = "NameChange1";
HttpContext.Current = context2;
company3.name = "NameChange2";

HttpContext.Current = context5;
controller1.Edit(company2);
HttpContext.Current = context6;

var result = (ViewResult)controller2.Edit(company3);

Assert.AreEqual("Edit", result.ViewName);
Assert.AreEqual("row not found or changed", result.ViewData["database_error"]);
}


// In my Web Application project:

public class CompanyController : Controller
{
private ICompanyService _service;

public CompanyController()
{
_service = new CompanyService(new ModelStateWrapper(this.ModelState));
}

[Authorize(Roles = "Admin")]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit([Bind]Company entity)
{
try
{
bool valid = _service.Update(entity);
if (valid)
{
return RedirectToAction("List", new { status = "Company Successfully edited" });
}
else
{
return View("Edit", entity);
}
}
catch (System.NotSupportedException e)
{
SetError(e.Message);
return View("Error");
}
}

public ActionResult Details(int id)
{
try
{
Company entity = _service.GetEntity(id);
return View("Details", entity);
}
catch (Exception e)
{
SetError(e.Message);
return View("Error");
}
}


// In my Service Layer project:

public class CompanyService : ICompanyService
{
private IValidationDictionary _validatonDictionary;
private ICompanyRepository _repository;
private IRepsoitoryFactory repository_factory;

public CompanyService(IValidationDictionary validationDictionary)
{
repository_factory = new RepsoitoryFactory();
_repository = (ICompanyRepository)repository_factory.GetRepository <ICompanyRepository>("DefaultCompanyRepository");
_validatonDictionary = validationDictionary;
}

public Company GetEntity(int id)
{
return _repository.GetEntity(id);
}

public bool Update(Company entity)
{
if (!ValidateEntity(entity))
{
return false;
}
try
{
_repository.Update(entity);
}
catch (Exception e)
{
_validatonDictionary.AddError("database_error", e.Message);
return false;
}
return true;
}

// In my Data Layer project

public partial class CompanyRepository : GenericRepository<Company, LaraLINQDataContext>, ICompanyRepository
{
}

I know this has to do with object tracking, but don't know what to do or where to look to solve this problem. Any help would be much appreciated. Thanks in advance. What would I have to do to make my application track changes to the entities? I want my application to be able to prevent lost updates as in Scenario 1 shown above.

Developer
Oct 26, 2009 at 10:59 AM
Edited Oct 26, 2009 at 11:30 AM

Howzit! I am originally from Durban :>

I think you need to read up on optimistic and pessimistic concurrency. Linq to sql by default uses optimistic concurrency.

To get around the disconnected nature of the web we are doing something sneaky in the update method. We are loading the entity from the database and then updating its properties from the entity we are passing it to update (trying to avoid attach method here), I suspect this may be the root of your problems, as it loads the object right before it updates it (so you always have the latest version), hence the 0 concurrency errors. So the strategy we are using here is that last write wins.

Datacontext is lost as soon as the "edit" page is rendered (unless you do something crazy and serialize the object graph and *shudder* create something like asp.net form's viewstate). When submit button is hit it, it loads the entity and then makes changes to it.

To keep it simple the way I would do it is add a timestamp (sql rowversion) column and use linq to sql attach. What ever you do work on a single entity and not on an object graph (this will lead to social suicide). 

We haven't figured out a way of using attach nicely in the repository (must be asp.net web form friendly). Would love someone to contribute a fix for this :>

This should make for some good reading on the topic:
http://www.west-wind.com/weblog/posts/246222.aspx#246646

 

 

        public void Update(TEntity entity, bool submitChanges)
{
TEntity original = GetEntity(entity);
UpdateOriginalFromChanged(ref original, entity);

if (submitChanges)
DataContext.SubmitChanges();
}

protected void UpdateOriginalFromChanged(ref TEntity destination, TEntity source)
{
foreach (PropertyInfo pi in DatabaseProperties)
{
pi.SetValue(destination, pi.GetValue(source, null), null);
}
}

private List<PropertyInfo> _databaseProperties;
protected List<PropertyInfo> DatabaseProperties
{
get
{
if (_databaseProperties == null)
{
Type entityType = typeof (TEntity);
MetaTable mapping = DataContext.Mapping.GetTable(typeof(TEntity));
_databaseProperties = mapping.RowType.DataMembers
.Where(x => x.DbType != null)
.Select(x => entityType.GetProperty(x.Name))
.ToList();
}
return _databaseProperties;
}
}

 

 

 

Developer
Oct 26, 2009 at 11:12 AM

To help you on your way:

        public object Insert(object item)
        {
            using (TransactionScope ts = new TransactionScope())
            {
                ITable itbl = DataContext.GetTable(item.GetType());
                itbl.InsertOnSubmit(item);
                itbl.Context.SubmitChanges();
                ts.Complete();
            }
            return item;
        }
        public object Delete(object item)
        {
            using (TransactionScope ts = new TransactionScope())
            {
                ITable itbl = DataContext.GetTable(item.GetType());
                itbl.DeleteOnSubmit(item);
                itbl.Context.SubmitChanges();
                ts.Complete();
            }
            return item;
        }

        public object Update(object item)
        {
            using (TransactionScope ts = new TransactionScope())
            {
                ITable itbl = DataContext.GetTable(item.GetType());
                itbl.Attach(item);
                itbl.Context.Refresh(RefreshMode.KeepCurrentValues, item);
                itbl.Context.SubmitChanges();
                ts.Complete();
            }
            return item;
        }

 

Oct 26, 2009 at 8:32 PM

Thanks taliesins for your quick response. I'm taking on your recommended simple solution, using the timestamp column.

Oct 27, 2009 at 1:52 PM
Edited Oct 27, 2009 at 2:01 PM

I added a timestamp column to each table in my database (called each column "row_version"). I then modified the following code in the Generic Repository to stop the method from changing the timestamp column. I also added a method to the GenericRepository to get the Version column


protected void UpdateOriginalFromChanged(ref TEntity destination, TEntity source)
        {
            foreach (PropertyInfo pi in DatabaseProperties)
            {
                if (pi.Name != MetaVersion.Name)
                {
                    pi.SetValue(destination, pi.GetValue(source, null), null);
                }
            }
        }

private MetaDataMember _metaVersion; private MetaDataMember MetaVersion { get { if (_metaVersion == null) { Type entityType = typeof(TEntity); MetaTable mapping = DataContext.Mapping.GetTable(entityType); _metaVersion = mapping.RowType.DataMembers.Single(d => d.IsVersion); } return _metaVersion; } }


The code below seems to work (obviously all tables must have a timestamp attribute for it to catch lost updates):


// Version1
public void Update(TEntity entity, bool submitChanges)
{
using (TransactionScope ts = new TransactionScope())
{
TEntity original = GetEntity(entity);
UpdateOriginalFromChanged(ref original, entity);

Type entityType = typeof(TEntity);

PropertyInfo propertyInfo = entityType.GetProperty(_metaVersion.Name);
Binary modifiedVersion = (Binary)propertyInfo.GetValue(entity, null);
Binary originalVersion = (Binary)propertyInfo.GetValue(original, null);

if (modifiedVersion != originalVersion)
{
throw new NotSupportedException("The row has been changed or deleted since first read.");
}

if (submitChanges)
DataContext.SubmitChanges();
ts.Complete();
}
}

The code below works in debug, but throws the following error when I run the test (from my initial post). This error is caught when the Attach method is called:

"An attempt has been made to Attach or Add an entity that is not new, perhaps having been loaded from another DataContext. This is not supported."


// I modified and tried this method in the GenericRepositorty class and it fails:
public
void Update(TEntity entity, bool submitChanges)
{
using (TransactionScope ts = new TransactionScope())
{
ITable itbl = DataContext.GetTable(entity.GetType());
itbl.Attach(entity, true);

My code in Version1 seems to be working, but there must be a better way to handle the concurrency check. Is there a way for an entity to be read by one DataContext and be updated by another DataContext?