使用添加的ValidationContext项

时间:2017-03-13 21:12:20

标签: c# asp.net-mvc entity-framework validation

我有一个场景,我想在ValidationContext中添加一个项目,并在EF触发的实体验证中检查它。我在向导中执行此操作,因此我只能在特定步骤上验证某些内容。 (如果有一个好的模式,请分享它。)

问题是在控制器操作被击中之前,实际上已经触发了两次验证。我希望我理解为什么。在发生这种情况之前,我不确定如何在ValidationContext中获取该项,因此我无法告知验证我正在进行哪一步。

此外,如果我只通过在下面的代码中检查项目来触发保存更改时进行自定义验证,那么当页面刷新时我不会显示自动模型验证错误。

在我的自定义上下文中:

public WizardStep Step { get; set; }

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
    items.Add("ValidationStep", Step);
    return base.ValidateEntity(entityEntry, items);
}

设置实体的服务:

public void SaveChanges(WizardStep step)
{
    _context.Step = step;
    _context.SaveChanges();
}

在我的实体中

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
    // Step will only be present when called from save changes.  Calls from model state validation won't have it
    if (validationContext.Items.ContainsKey("ValidationStep"))
    {
        var validationStep = (WizardStep)validationContext.Items["ValidationStep"];
        if (validationStep == WizardStep.Introduction)
        {
            if (criteria)
            {
                yield return new ValidationResult($"Error message  ", new[] { "field" });
            }
        }
    }
}

控制器:

public ActionResult MyAction(HomeViewModel vm)
{
    try
    {
        _incidentService.AddOrUpdate(vm.Enttiy);
        _incidentService.SaveChanges(WizardStep.Introduction);
    }
    catch (Exception ex)
    {
        return View(vm);
    }
    return RedirectToAction("Index");
}

4 个答案:

答案 0 :(得分:2)

第一个验证是在MVC创建的模型上传递给控制器​​。 MVC使用ModelBinder类来构造,填充和验证客户端http表单数据到模型中。任何失败的验证都将返回给客户端。然后,控制器可以更改有效模型,因此在保存时由EF完成第二次验证。我相信在保存时,只有当属性是新属性或者具有与原始值不同的数据时才会触发EF验证。

理论上应该可以使用自定义MVC ModelValidator并拦截Validate方法来设置ValidationContext项。但是,我无法弄清楚如何做到这一点。然而,我找到了一个对我有用的略有不同的解决方案。也许它可以根据您的需求进行调整。

就我而言,我希望验证方法可以使用EF DbContext(在我的代码中命名为CmsEntities),这样我就可以查询数据库(并进行丰富的复杂业务逻辑验证)。控制器具有DbContext,但模型验证在将其传递给控制器​​的操作之前由ModelBinder调用。

我的解决方案是:

1)向我的实体添加DbContext属性(使用部分类,或在所有实体继承的基本实体中)

2)创建一个Custom ModelBinder,它将从控制器获取DbContext并将其填充到模型中

3)在Application_Start()

中注册Custom ModelBinder

现在,在任何验证方法中,模型都会有一个填充的DbContext。 

自定义ModelBinder

public class CmsModelBinder : DefaultModelBinder
{
    protected override bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // Copy CmsEntities from Controller to the Model (Before we update and validate the model)
        var modelPropertyInfo = bindingContext.Model.GetType().GetProperty("CmsEntities");
        if (modelPropertyInfo != null)
        {
            var controllerPropertyInfo = controllerContext.Controller.GetType().GetProperty("CmsEntities");
            if (controllerPropertyInfo != null)
            {
                CmsEntities cmsEntities = controllerPropertyInfo.GetValue(controllerContext.Controller) as CmsEntities;
                modelPropertyInfo.SetValue(bindingContext.Model, cmsEntities);
            }
        }            
        return base.OnModelUpdating(controllerContext, bindingContext);
    }

的Global.asax.cs

    protected void Application_Start()
    {
        ...
        ModelBinders.Binders.DefaultBinder = new CmsModelBinder();
    }

答案 1 :(得分:0)

首先,您应该考虑WizardStep是否属于上下文或在单独步骤中修改的对象? 其他的是为什么不使用ie。在不同步骤处理验证逻辑的策略?

关于验证,我看到你混合了两件事。

一个是对上下文的验证,您应该在上下文中处理每个实体类型的验证逻辑。

另一个是IValidatableObject.Validate的实现,应该在SaveChanges上为实体自动调用。

我会决定并选择一种方式去,从你给我们的信息我认为只有IValidatableObject.Validate更有意义,但是你必须要把步骤放入正在验证的实体或以某种方式注入该步骤的其他方式仅用于验证。

答案 2 :(得分:0)

您可以这样做:

try
{
  //write code

} 
catch (System.Data.Entity.Validation.DbEntityValidationException ex)
            {
                var outputLines = new List<string>();
                foreach (var eve in ex.EntityValidationErrors)
                {
                    outputLines.Add(string.Format(
                        "{0}: Entity of type \"{1}\" in state \"{2}\" has the following validation errors:",
                        DateTime.Now, eve.Entry.Entity.GetType().Name, eve.Entry.State));
                    foreach (var ve in eve.ValidationErrors)
                    {
                        outputLines.Add(string.Format(
                            "- Property: \"{0}\", Error: \"{1}\"",
                            ve.PropertyName, ve.ErrorMessage));
                    }
                }
                System.IO.File.AppendAllLines(@"c:\temp\errors.txt", outputLines);
            }

答案 3 :(得分:0)

只需分享我的mvc验证解决方案:

public class TestController:Controller
{
    public ActionResult Action1(MyModel data)
    {
        try
        {
            if (!ModelState.IsValid)
            {
                var errors = ModelState.Values.Where(c => c.Errors.Count > 0).SelectMany(c => c.Errors.Select(o => o.ErrorMessage));
                var errorMsg = String.Join("<br/>", errors);
                return Json(new
                {
                    IsSuccess = false,
                    Message = errorMsg
                });
            }
            //deal business
            return Json(new { IsSuccess = true, Message = "success" });
        }
        catch (Exception ex)
        {
            return Json(new { IsSuccess = false, Message = "fail" });
        }
    }
}
public class MyModel : IValidatableObject
{
    [Required(ErrorMessage = "{0} is required")]
    public decimal TotalPrice { get; set; }
    [Required(ErrorMessage = "{0} is required")]
    public decimal TotalPriceWithoutCoupon { get; set; }
    public ContactStruct Contact { get; set; }
    public bool Condition{ get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var instance = validationContext.ObjectInstance as MyModel;
        if (instance == null)
        {
            yield break;
        }
        if (instance.Condition)
        {
            if (instance.Contact == null)
            {
                yield return new ValidationResult("contact is required", new string[] { "Contact" });
            }
            else
            {
                if (string.IsNullOrEmpty(instance.Contact.phone))
                {
                    yield return new ValidationResult("the phone of contact is required", new string[] { "Contact.phone" });
                }
            }
        }
    }
}