c# - 更新选择性字段时 EF5 无法处理并发

标签 c# asp.net-mvc database entity-framework concurrency

我正在使用 EF5 和数据优先方法来更新实体。 我正在使用其他问题建议的方法有条件地仅更新实体中修改的属性。

Oki 所以这是我的 Controller 使用 POCO 对象调用服务并从服务获取 POCO 对象的场景,服务层与数据层对话,数据层在内部使用 EF5 从数据库中检索实体并在数据库中更新它们。

View 数据由 Controller 从服务层检索到的 DTO 对象加载。 用户对 View 进行更改并将 JSON 数据回发到 Controller ,该 Controller 映射到 Controller 中的 DTO 对象(由 MVC 提供)。 Controller 使用 DTO 对象 (POCO) 对象调用服务层。 该服务将 POCO 对象映射到 EF 实体对象,并调用传入 EF 实体的数据层(即存储库)的更新方法。 在存储库中,我从数据库中获取现有实体并调用 ApplyCurrentvaluesValues 方法,然后检查是否修改了任何属性。 如果修改了属性,那么我将我的自定义逻辑应用于与当前实体无关的其他实体,并更新当前实体的“UpdatedAdminId”和“UpdationDate”。 发布这个我在 Centext 上调用“SaveChanges”方法。

上面我提到的每件事都工作正常,除非我在“SaveChanges”调用中插入一个断点并将用户修改的某些字段更新为不同的值,然后 EF5 不会抛出“DbUpdateConcurrencyException”。 即,当我感兴趣的属性被修改为完美运行时,我可以获得条件更新并触发我的自定义逻辑。 但是在并发的情况下我没有收到错误,即 EF 没有引发“DbUpdateConcurrencyException”,以防在我从 DB 获取记录、更新记录和保存记录之间更新记录。

在真实场景中,有一个离线 cron 正在运行,它检查新创建的事件并为它们创建投资组合,并将下面的 IsPortfolioCreated 属性标记为 true,同时用户可以编辑事件,甚至可以将标志设置为 false尽管 cron 已经创建了投资组合。

为了复制并发场景,我在 SaveChanges 上设置了一个断点,然后从同一实体的 MS-Sql 企业管理器更新 IsPortfolioCreated 字段,但即使存储中的数据已更新,也不会抛出“DbUpdateConcurrencyException”。

这是我的引用代码,

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;
}

最佳答案

Entity Framework 在您(作为开发人员)将其配置为检查并发问题之前,不会对并发做任何特殊的事情。

您正在 try catch DbUpdateConcurrencyException,此异常的文档说:“当预期实体的 SaveChanges 会导致数据库更新时 DbContext 抛出的异常但实际上没有行在数据库受到影响。“,你可以阅读它here

在数据库优先方法中,您必须为“固定”列设置属性“并发模式”(默认为无)。看看这个截图:enter image description here

列 Version 是 SQL SERVER TIMESTAMP 类型,一种每次行更改时自动更新的特殊类型,请阅读 here .

使用此配置,如果一切都按预期工作,您可以尝试这个简单的测试:

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

在上面的示例中,来自内部上下文的更新将被提交,来自外部上下文的更新将抛出 DbUpdateConcurrencyException,因为 EF 将尝试使用 2 列作为过滤器更新实体:Id 和 Version 列。

希望这对您有所帮助!

关于c# - 更新选择性字段时 EF5 无法处理并发,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36171301/

相关文章:

c# - Azure DocumentDB 性能缓慢

asp.net-mvc - PRG 模式有没有在验证失败时不重定向的名称?

php - Yajra 数据表全局搜索不起作用

database - 如何从转储文件中获取oracle数据库版本

c# - xs :key, 为什么当键值不是键引用的成员时验证通过?

c# - 使用 System.Print 在 "Microsoft Print to PDF"打印机中设置文件名

c# - 无法添加对 'System.Net.Http' 的引用。请确保它在全局程序集缓存中

c# - ASP.net MVC - 自定义 HandleError Filter - 根据异常类型指定 View

c# - ASP.NET MVC5 在 ListView 中获取所有注册的 AspNetUsers

C# 和 Access 数据库通过名称搜索