Проверка MVC и EF с добавленным элементом ValidationContext

У меня есть сценарий, в котором я хотел бы добавить элемент в 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");
}

person Alex    schedule 13.03.2017    source источник
comment
Вы пытаетесь выполнить проверку бизнес-правил или проверку целостности данных, таких как ограничения уникальности и ограничения внешнего ключа? Если вы пытаетесь сделать первое, я уверен, что некоторые внешние библиотеки, такие как FluentValidation, больше подходят для твои нужды.   -  person cleftheris    schedule 16.03.2017
comment
@cleftheris На самом деле это и то, и другое. У меня есть обратный код генератора poco, который уже имеет атрибуты целостности данных для объектов. Затем у меня есть более сложные бизнес-правила, которые я добавляю вручную в метод Validate для сущностей, и для которых мне нужно обратиться к базе данных, чтобы проверить текст ошибки (настроенный пользователем). Эта дополнительная проверка db — вот почему мне нужен контекст в validationcontext.   -  person Alex    schedule 16.03.2017
comment
Насколько я понимаю ситуацию, я думаю, вам следует изменить свой подход: использовать популярную структуру проверки для сложной бизнес-логики, где некоторые правила могут выполнять внутренние запросы к базе данных для завершения.   -  person cleftheris    schedule 16.03.2017


Ответы (4)


Первая проверка выполняется для модели, созданной MVC, которая передается контроллеру. MVC использует класс ModelBinder для создания, заполнения и проверки данных клиентской http-формы в модели. Любая неудачная проверка будет возвращена клиенту. Затем действующая модель может быть изменена контроллером, поэтому при сохранении EF выполняет вторую проверку. Я считаю, что при сохранении проверка EF запускается только в том случае, если свойство является новым или имеет данные, отличные от исходного значения.

Теоретически должно быть возможно иметь собственный MVC ModelValidator и перехватывать метод Validate для установки элементов ValidationContext. Однако я НЕ мог понять, как это сделать. Однако я нашел немного другое решение, которое работает для меня. Возможно, его можно будет адаптировать под ваши нужды.

В моем случае я хотел, чтобы EF DbContext (в моем коде он назывался CmsEntities) был доступен для методов проверки, чтобы я мог запрашивать базу данных (и выполнять сложную проверку бизнес-логики). Контроллер имеет DbContext, но проверка модели вызывается ModelBinder перед передачей ее в действие контроллера.

Мое решение:

1) Добавьте свойство DbContext в мой объект (используя частичный класс или базовый объект, от которого наследуются все объекты)

2) Создайте Custom ModelBinder, который получит DbContext от контроллера и заполнит его моделью.

3) Зарегистрируйте Custom ModelBinder в Application_Start().

Теперь внутри любого метода проверки модель будет иметь заполненный 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();
    }
person RitchieD    schedule 22.03.2017
comment
Я не в восторге от того, что контекст является свойством сущностей, но это единственный ответ, который до сих пор касался вопроса, спасибо! - person Alex; 23.03.2017

Прежде всего, вы должны рассмотреть, относится ли WizardStep к контексту или к объекту, который изменяется во время отдельных шагов? Другое дело, почему бы не использовать ie. Стратегия обработки логики проверки на отдельных этапах?

Что касается проверки, я вижу, вы смешиваете две вещи.

Одним из них является проверка в контексте, где вы должны обрабатывать логику проверки для каждого типа объекта, который у вас есть в контексте.

Другой — это реализация IValidatableObject.Validate, которая должна автоматически вызываться для сущности в SaveChanges.

Я бы решил и выбрал один путь, и из информации, которую вы нам предоставили, я думаю, что наличие только IValidatableObject.Validate имеет больше смысла, но тогда вам придется либо поместить шаг в объект, который проверяется, либо каким-то образом ввести этот шаг другим способом только для проверки .

person mariozski    schedule 22.03.2017

Вы можете сделать это следующим образом:

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);
            }
person anis programmer    schedule 23.03.2017

Просто поделитесь моим решением для проверки 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" });
                }
            }
        }
    }
}
person LS.Lernord    schedule 23.03.2017