Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF5 can not handle Concurrency when Updating selective fields

I am using EF5 and Data First approach to Update entities. I am using approach suggested by other questions to conditionally update only modified properties in the Entities.

Oki so here's the scenario My controller call Service with POCO objects and gets POCO objects from Service, The Service layer talks with Data layer which internally uses EF5 to retrieve entity from DB and Update them in DB.

The View data is loaded by controller from DTO object retrieved from Service layer. User makes changes to View and Posts back JSON data to controller which gets mapped to DTO object in controller (courtesy MVC). The controller makes call to Service layer with the DTO object (POCO) object. The Service maps the POCO object to EF entity object and calls the Data layer's(i.e Repository) Update method passing in the EF entity. In the Repository I fetch the existing entity from DB and call ApplyCurrentvaluesValues method, then I check if any properties are modified . If properties are modified then I apply my custom logic to other entities which are not related to current entity and also Update the "UpdatedAdminId" & "UpdationDate" of current entity. Post this I call "SaveChanges" method on Centext.

Every thing above I mentioned is working fine , except if I insert a break point in "SaveChanges" call and update some field modified by User to different value then "DbUpdateConcurrencyException" is not thrown by EF5. i.e. I can get conditional Update & fire my custom logic when properties of my interest are modified to work perfectly. But I am not getting error in case of the concurrency i.e the EF is not raising "DbUpdateConcurrencyException" in case a record is updated in between me fetching the record from DB , updating the record and saving it.

In real scenario there is a offline cron running which checks for newly created campaign and creates portfolio for them and marks the IsPortfolioCreated property below as true, in the mean time user can edit the campaign and the flag can be set to false even though the cron has created the portfolios.

To replicate the concurrency scenario I put a break point on SaveChanges and then Update the IsPortfolioCreated feild from MS-Sql enterprise manager for the same entity, but the "DbUpdateConcurrencyException" is not thrown even though the Data in Store has been updated.

Here's my code for reference,

Public bool EditGeneralSettings(CampaignDefinition campaignDefinition)
{
    var success = false;
    //campaignDefinition.UpdatedAdminId is updated in controller by retreiving it from RquestContext, so no its not comgin from client
    var updatedAdminId = campaignDefinition.UpdatedAdminId;
    var updationDate = DateTime.UtcNow;
    CmsContext context = null;
    GlobalMasterContext globalMasterContext = null;

    try
    {
        context = new CmsContext(SaveTimeout);

        var contextCampaign = context.CampaignDefinitions.Where(x => x.CampaignId == campaignDefinition.CampaignId).First();

        //Always use this fields from Server, no matter what comes from client
        campaignDefinition.CreationDate = contextCampaign.CreationDate;
        campaignDefinition.UpdatedAdminId = contextCampaign.UpdatedAdminId;
        campaignDefinition.UpdationDate = contextCampaign.UpdationDate;
        campaignDefinition.AdminId = contextCampaign.AdminId;
        campaignDefinition.AutoDecision = contextCampaign.AutoDecision;
        campaignDefinition.CampaignCode = contextCampaign.CampaignCode;
        campaignDefinition.IsPortfolioCreated = contextCampaign.IsPortfolioCreated;

        var campaignNameChanged = contextCampaign.CampaignName != campaignDefinition.CampaignName;

        // Will be used in the below if condition....
        var originalSkeForwardingDomain = contextCampaign.skeForwardingDomain.ToLower();
        var originalMgForwardingDomain = contextCampaign.mgForwardingDomain.ToLower();

        //This also not firing concurreny  exception....
        var key = ((IObjectContextAdapter) context).ObjectContext.CreateEntityKey("CampaignDefinitions", campaignDefinition);
        ((IObjectContextAdapter)context).ObjectContext.AttachTo("CampaignDefinitions", contextCampaign);
        var updated = ((IObjectContextAdapter)context).ObjectContext.ApplyCurrentValues(key.EntitySetName, campaignDefinition);
        ObjectStateEntry entry = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager.GetObjectStateEntry(updated);
        var modifiedProperties = entry.GetModifiedProperties();

        //Even tried this , works fine but no Concurrency exception
        //var entry = context.Entry(contextCampaign);
        //entry.CurrentValues.SetValues(campaignDefinition);
        //var modifiedProperties = entry.CurrentValues.PropertyNames.Where(propertyName => entry.Property(propertyName).IsModified).ToList();

        // If any fields modified then only set Updation fields
        if (modifiedProperties.Count() > 0)
        {
            campaignDefinition.UpdatedAdminId = updatedAdminId;
            campaignDefinition.UpdationDate = updationDate;
            //entry.CurrentValues.SetValues(campaignDefinition);
            updated = ((IObjectContextAdapter)context).ObjectContext.ApplyCurrentValues(key.EntitySetName, campaignDefinition);

            //Also perform some custom logic in other entities... Then call save changes

            context.SaveChanges();

            //If campaign name changed call a SP in different DB..
            if (campaignNameChanged)
            {
                globalMasterContext = new GlobalMasterContext(SaveTimeout);
                globalMasterContext.Rename_CMS_Campaign(campaignDefinition.CampaignId, updatedAdminId);
                globalMasterContext.SaveChanges();
            }
        }
        success = true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        //Code never enters here, if it does then I am planning to show the user the values from DB and ask him to retry
        //In short Store Wins Strategy

        //Code in this block is not complete so dont Stackies don't start commenting about this section and plague the question...

        // 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();


        // 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);

    }
    catch (Exception ex)
    {
        if (ex.InnerException != null)
            throw ex.InnerException;
        throw;
    }
    finally
    {
        if (context != null) context.Dispose();
        if (globalMasterContext != null) globalMasterContext.Dispose();
    }
    return success;
}
like image 223
Vipresh Avatar asked Oct 30 '22 07:10

Vipresh


1 Answers

Entity framework it's not doing anything special about concurrency until you (as developer) configure it to check for concurrency problems.

You are trying to catch DbUpdateConcurrencyException, the documentation for this exception says: "Exception thrown by DbContext when it was expected that SaveChanges for an entity would result in a database update but in fact no rows in the database were affected. ", you can read it here

In a database first approach, you have to set the property 'Concurrency Mode' for column on 'Fixed' (the default is None). Look at this screenshot: enter image description here

The column Version is a SQL SERVER TIMESTAMP type, a special type that is automatically updated every time the row changes, read about it here.

With this configuration, you can try with this simple test if all is working as expected:

try
{
    using (var outerContext = new testEntities())
    {
        var outerCust1 = outerContext.Customer.FirstOrDefault(x => x.Id == 1);
        outerCust1.Description += "modified by outer context";
        using (var innerContext = new testEntities())
        {
            var innerCust1 = innerContext.Customer.FirstOrDefault(x => x.Id == 1);
            innerCust1.Description += "modified by inner context";
            innerContext.SaveChanges();
        }
        outerContext.SaveChanges();
    }
}
catch (DbUpdateConcurrencyException ext)
{
    Console.WriteLine(ext.Message);
}

In the example above the update from the inner context will be committed, the update from the outer context will thrown a DbUpdateConcurrencyException, because EF will try to update the entity using 2 columns as a filters: the Id AND the Version column.

Hope this helps!

like image 194
omar.ballerani Avatar answered Nov 15 '22 04:11

omar.ballerani