2012-03-16 41 views
1

我在我的asp.net mvc web應用程序中有以下操作方法,這會根據預期產生DbUpdateConcurrencyException以處理可能發生的任何併發衝突: -我如何強制DbUpdateConcurrencyException被引發,即使我將FormCollection傳遞給我的Post操作方法而不是對象

[HttpPost] 
     public ActionResult Edit(Assessment a) 
     {   try 
      { 
       if (ModelState.IsValid) 
       { 
        elearningrepository.UpdateAssessment(a); 
        elearningrepository.Save(); 
        return RedirectToAction("Details", new { id = a.AssessmentID }); 
       } 
      } 
      catch (DbUpdateConcurrencyException ex) 
      { 
       var entry = ex.Entries.Single(); 
       var clientValues = (Assessment)entry.Entity; 

      ModelState.AddModelError(string.Empty, "The record you attempted to edit was" 
       + "modified by another user after you got the original value."); 
           } 
      catch (DataException) 
      { 
       ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator."); 
      }   
      return View(a);} 

但要避免任何在結合攻擊我已經定義的對象類[Bind(Include = "Date, Title")],但是這提出了一個問題,我爲上述操作方法會返回一個異常即使沒有發生併發衝突,因爲模型綁定器將無法綁定對象ID和其他值,所以我已將我的操作方法更改爲以下內容: -

[HttpPost] 
     public ActionResult Edit(int id, FormCollection collection) 
     { 
      Assessment a = elearningrepository.GetAssessment(id); 

      try 
      { 
       if (TryUpdateModel(a)) 
       { 
        elearningrepository.UpdateAssessment(a); 
        elearningrepository.Save(); 
        return RedirectToAction("Details", new { id = a.AssessmentID }); 
       } 
      } 
      catch (DbUpdateConcurrencyException ex) 
      { 
       var entry = ex.Entries.Single(); 
       var clientValues = (Assessment)entry.Entity; 

      ModelState.AddModelError(string.Empty, "The record you attempted to edit was" 
       + "modified by another user after you got the original value."); 
      } 
      catch (DataException) 
      {    ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator."); 
      }return View(a); 

但書面方式操作方法爲在第二種方法將不會提高DbUpdateConcurrencyException任何情況下(即使出現併發衝突!)。

所以我問題是如何確保DbUpdateConcurrencyException會在發生衝突時發生,同時確保[Bind(Include = "Date, Title")]不會發生過度綁定攻擊? 在此先感謝您的任何幫助和建議。 BR

回答

2

停止使用表單集合並使用視圖模型,這是一個更好的方法。我也有一個我寫的處理併發異常的操作過濾器(現在MVC4處理實體異常,最終有一些驗證,而不是併發異常)。它的工作正在進行中,但應該工作正常的是,這麼多已經過測試:)

 

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Web.Mvc; 
using System.Data.Entity.Infrastructure; 
using System.Reflection; 

namespace Gecko.Framework.Mvc.ActionFilters 
{ 
    /// <summary> 
    /// Author: Adam Tuliper 
    /// [email protected] 
    /// completedevelopment.blogspot.com 
    /// www.secure-coding.com 
    /// Use freely, just please retain original credit. 
    /// 
    /// This attribute attempts to intercept DbUpdateConcurrencyException to write out original/new values 
    /// to the screen for the user to review. 
    /// It assumes the following: 
    /// 1. There is a [Timestamp] attribute on an entity framework model property 
    /// 2. The only differences that we care about from the posted data to the record currently in the database are 
    /// only yhe model state field. We do not have access to a model at this point, as an exception was raised so there was no 
    /// return View(model) that we have a model to process from. 
    /// As such, we have to look at the fields in the modelstate and try to find matching fields on the entity and then display the differences. 
    /// This may not work in all cases. 
    /// This class will look at your model to get the property names. It will then check your 
    /// Entities current values vs. db values for these property names. 
    /// The behavior can be changed. 
    /// </summary> 
    public class HandleConcurrencyException : FilterAttribute, IExceptionFilter //ActionFilterAttribute 
    { 
     private PropertyMatchingMode _propertyMatchingMode; 
     /// <summary> 
     /// This defines when the concurrencyexception happens, 
     /// </summary> 
     public enum PropertyMatchingMode 
     { 
      /// <summary> 
      /// Uses only the field names in the model to check against the entity. This option is best when you are using 
      /// View Models with limited fields as opposed to an entity that has many fields. The ViewModel (or model) field names will 
      /// be used to check current posted values vs. db values on the entity itself. 
      /// </summary> 
      UseViewModelNamesToCheckEntity = 0, 
      /// <summary> 
      /// Use any non-matching value fields on the entity (except timestamp fields) to add errors to the ModelState. 
      /// </summary> 
      UseEntityFieldsOnly = 1, 
      /// <summary> 
      /// Tells the filter to not attempt to add field differences to the model state. 
      /// This means the end user will not see the specifics of which fields caused issues 
      /// </summary> 
      DontDisplayFieldClashes = 2 
     } 

     /// <summary> 
     /// The main method, called by the mvc runtime when an exception has occured. 
     /// This must be added as a global filter, or as an attribute on a class or action method. 
     /// </summary> 
     /// <param name="filterContext"></param> 
     public void OnException(ExceptionContext filterContext) 
     { 
      if (!filterContext.ExceptionHandled && filterContext.Exception is DbUpdateConcurrencyException) 
      { 
       //Get original and current entity values 
       DbUpdateConcurrencyException ex = (DbUpdateConcurrencyException)filterContext.Exception; 
       var entry = ex.Entries.Single(); 
       //problems with ef4.1/4.2 here because of context/model in different projects. 
       //var databaseValues = entry.CurrentValues.Clone().ToObject(); 
       //var clientValues = entry.Entity; 
       //So - if using EF 4.1/4.2 you may use this workaround 
       var clientValues = entry.CurrentValues.Clone().ToObject(); 
       entry.Reload(); 
       var databaseValues = entry.CurrentValues.ToObject(); 

       List<string> propertyNames; 

       filterContext.Controller.ViewData.ModelState.AddModelError(string.Empty, "The record you attempted to edit " 
         + "was modified by another user after you got the original value. The " 
         + "edit operation was canceled and the current values in the database " 
         + "have been displayed. If you still want to edit this record, click " 
         + "the Save button again to cause your changes to be the current saved values."); 
       PropertyInfo[] entityFromDbProperties = databaseValues.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance); 

       if (_propertyMatchingMode == PropertyMatchingMode.UseViewModelNamesToCheckEntity) 
       { 
        //We dont have access to the model here on an exception. Get the field names from modelstate: 
        propertyNames = filterContext.Controller.ViewData.ModelState.Keys.ToList(); 
       } 
       else if (_propertyMatchingMode == PropertyMatchingMode.UseEntityFieldsOnly) 
       { 
        propertyNames = databaseValues.GetType().GetProperties(BindingFlags.Public).Select(o => o.Name).ToList(); 
       } 
       else 
       { 
        filterContext.ExceptionHandled = true; 
        UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues); 
        filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData }; 
        return; 
       } 



       UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues); 

       //Get all public properties of the entity that have names matching those in our modelstate. 
       foreach (var propertyInfo in entityFromDbProperties) 
       { 

        //If this value is not in the ModelState values, don't compare it as we don't want 
        //to attempt to emit model errors for fields that don't exist. 

        //Compare db value to the current value from the entity we posted. 

        if (propertyNames.Contains(propertyInfo.Name)) 
        { 
         if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null)) 
         { 
          var currentValue = propertyInfo.GetValue(databaseValues, null); 
          if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString())) 
          { 
           currentValue = "Empty"; 
          } 

          filterContext.Controller.ViewData.ModelState.AddModelError(propertyInfo.Name, "Current value: " 
           + currentValue); 
         } 
        } 

        //TODO: hmm.... how can we only check values applicable to the model/modelstate rather than the entity we saved? 
        //The problem here is we may only have a few fields used in the viewmodel, but many in the entity 
        //so we could have a problem here with that. 
       } 

       filterContext.ExceptionHandled = true; 

       filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData }; 
      } 
     } 

     public HandleConcurrencyException() 
     { 
      _propertyMatchingMode = PropertyMatchingMode.UseViewModelNamesToCheckEntity; 
     } 

     public HandleConcurrencyException(PropertyMatchingMode propertyMatchingMode) 
     { 
      _propertyMatchingMode = propertyMatchingMode; 
     } 


     /// <summary> 
     /// Searches the database loaded entity values for a field that has a [Timestamp] attribute. 
     /// It then writes a string version of ther byte[] timestamp out to modelstate, assuming 
     /// we have a timestamp field on the page that caused the concurrency exception. 
     /// </summary> 
     /// <param name="filterContext"></param> 
     /// <param name="entityFromDbProperties"></param> 
     /// <param name="databaseValues"></param> 
     private void UpdateTimestampField(ExceptionContext filterContext, PropertyInfo[] entityFromDbProperties, object databaseValues) 
     { 
      foreach (var propertyInfo in entityFromDbProperties) 
      { 
       var attributes = propertyInfo.GetCustomAttributesData(); 

       //If this is a timestamp field, we need to set the current value. 
       foreach (CustomAttributeData attr in attributes) 
       { 
        if (typeof(System.ComponentModel.DataAnnotations.TimestampAttribute).IsAssignableFrom(attr.Constructor.DeclaringType)) 
        { 
         //This currently works only with byte[] timestamps. You can use dates as timestampts, but support is not provided here. 
         byte[] timestampValue = (byte[])propertyInfo.GetValue(databaseValues, null); 
         //we've found the timestamp. Add it to the model. 
         filterContext.Controller.ViewData.ModelState.Add(propertyInfo.Name, new ModelState()); 
         filterContext.Controller.ViewData.ModelState.SetModelValue(propertyInfo.Name, 
          new ValueProviderResult(Convert.ToBase64String(timestampValue), Convert.ToBase64String(timestampValue), null)); 
         break; 
        } 
       } 

      } 
     } 

    } 
} 

 
+0

感謝您的回覆,你的方法seesm全新的我,但我會嘗試。但是你是否意味着我正在使用的方法不會總是捕獲併發異常? BR – 2012-03-16 22:43:22

+0

,我不想使用MVC 4現在廣告它仍然是一個測試版本,並在其deactmentation中提到它不應該用於現場系統... – 2012-03-16 22:50:38

+0

我試過你的方法,但我得到以下錯誤「使用泛型類型'System.Collections.Generic.List '需要1個類型參數」on「列表propertyNames;」在上面的類裏面。有關此錯誤的任何建議? BR – 2012-03-17 02:26:59

0

你知道爲什麼我不得不改變

if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null)) 

if (!Equals(propertyInfo.GetValue(databaseValues,null), propertyInfo.GetValue(clientValues,null))) 

它我對任何不是字符串的東西都給予了誤報。

我做了改變後,它爲我工作。

下面是完整的修改功能

 public void OnException(ExceptionContext filterContext) 
    { 
     if (!filterContext.ExceptionHandled && filterContext.Exception is DbUpdateConcurrencyException) 
     { 
      //Get original and current entity values 
      DbUpdateConcurrencyException ex = (DbUpdateConcurrencyException)filterContext.Exception; 
      var entry = ex.Entries.Single(); 

      //problems with ef4.1/4.2 here because of context/model in different projects. 
      //var databaseValues = entry.CurrentValues.Clone().ToObject(); 
      //var clientValues = entry.Entity; 
      //So - if using EF 4.1/4.2 you may use this workaround     
      var clientValues = entry.CurrentValues.Clone().ToObject(); 
      entry.Reload(); 
      var databaseValues = entry.CurrentValues.ToObject(); 



      List<string> propertyNames; 

      filterContext.Controller.ViewData.ModelState.AddModelError(string.Empty, "The record you attempted to edit " 
        + "was modified by another user after you got the original value. The " 
        + "edit operation was canceled and the current values in the database " 
        + "have been displayed. If you still want to edit this record, click " 
        + "the Save button again to cause your changes to be the current saved values."); 
      PropertyInfo[] entityFromDbProperties = databaseValues.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance); 

      if (_propertyMatchingMode == PropertyMatchingMode.UseViewModelNamesToCheckEntity) 
      { 
       //We dont have access to the model here on an exception. Get the field names from modelstate: 
       propertyNames = filterContext.Controller.ViewData.ModelState.Keys.ToList(); 
      } 
      else if (_propertyMatchingMode == PropertyMatchingMode.UseEntityFieldsOnly) 
      { 
       propertyNames = databaseValues.GetType().GetProperties(BindingFlags.Public).Select(o => o.Name).ToList(); 
      } 
      else 
      { 
       filterContext.ExceptionHandled = true; 
       UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues); 
       filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData }; 
       return; 
      } 



      UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues); 

      //Get all public properties of the entity that have names matching those in our modelstate. 
      foreach (var propertyInfo in entityFromDbProperties) 
      { 

       //If this value is not in the ModelState values, don't compare it as we don't want 
       //to attempt to emit model errors for fields that don't exist. 

       //Compare db value to the current value from the entity we posted. 

       if (propertyNames.Contains(propertyInfo.Name)) 
       { 
        //if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null)) 
        if (!Equals(propertyInfo.GetValue(databaseValues,null), propertyInfo.GetValue(clientValues,null))) 
        { 
         var currentValue = propertyInfo.GetValue(databaseValues, null); 
         if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString())) 
         { 
          currentValue = "Empty"; 
         } 

         filterContext.Controller.ViewData.ModelState.AddModelError(propertyInfo.Name, "Database value: " 
          + currentValue); 
        } 
       } 

       //TODO: hmm.... how can we only check values applicable to the model/modelstate rather than the entity we saved? 
       //The problem here is we may only have a few fields used in the viewmodel, but many in the entity 
       //so we could have a problem here with that. 
      } 

      filterContext.ExceptionHandled = true;     
      filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData }; 
     } 
    } 
相關問題