Запретить выполнение метода действия MVC, если параметр имеет значение null - PullRequest
5 голосов
/ 08 сентября 2010

Я думал о нескольких способах сделать это, но я хочу узнать мнение сообщества. У меня такое ощущение, что ответ очень прост - я не боюсь выглядеть глупо (мои дети давно убрали этот страх у меня!)

Я пишу веб-сервис XML REST с использованием MVC2. Все типы XML, которые пользователи веб-службы будут получать и отправлять, регулируются простым, но обширным XSD, и эти параметры будут связаны с xml в теле запроса через настраиваемую привязку модели по умолчанию и поставщика значений.

У меня есть достаточное количество контроллеров, у каждого из которых большое количество методов действия (не чрезмерных - просто «хорошо»;)) - и почти во всех случаях эти методы действия будут принимать типы моделей, которые являются эталонными типы.

Практически в каждом случае для вызывающего абонента будет ошибкой не указывать эти значения параметров, и поэтому стандартное сообщение об ошибке, такое как "The parameter {name} type:{ns:type} is required", может быть отправлено обратно.

То, что я хочу сделать, - это уметь проверять, что параметры не равны NULL до выполнения метода действия; а затем вернуть ActionResult, который представляет ошибку клиенту (для этого у меня уже есть тип XMLResult), при этом сам метод действия не должен проверять сами параметры.

Итак, вместо:

public ActionResult ActionMethod(RefType model)
{
  if(model == null)
      return new Xml(new Error("'model' must be provided"));
}

Что-то вроде:

public ActionResult ActionMethod([NotNull]RefType model)
{
  //model now guaranteed not to be null.
}

Я знаю, что это именно тот тип сквозных операций, который может быть достигнут в MVC.

Мне кажется, что либо переопределение базового контроллера OnActionExecuting, либо пользовательский ActionFilter - наиболее вероятный способ сделать это.

Я также хотел бы иметь возможность расширять систему, чтобы она автоматически обнаруживала ошибки проверки схемы XML (добавленные в ModelState во время привязки пользовательским поставщиком значений), предотвращая тем самым продолжение действия метода, если любое из значений параметра не может быть загружен правильно, потому что XML-запрос плохо сформирован.

Ответы [ 2 ]

2 голосов
/ 08 сентября 2010

Вот реализация, которую я придумала (ожидая каких-либо лучших идей :))

Это общий подход, и я думаю, что он довольно масштабируемый - с учетом, возможно, глубины, аналогичной параметрупроверка, как вы получаете, с проверкой модели в то же время, что и предоставление функции автоответа на ошибку (когда состояние модели содержит одну или несколько ошибок), которую я искал.

Надеюсь, это не слишком много кодаза SO ответ (!);У меня было множество комментариев к документации, которые я вынул, чтобы сделать его короче.

Итак, в моем сценарии у меня есть два типа ошибок модели, которые, в случае их возникновения, должны блокировать выполнение действияметод:

  • Не удалось проверить схему XML, из которой будет построено значение параметра
  • Отсутствует (нулевое) значение параметра

В настоящее время проверка схемы выполняетсявыполняется во время привязки модели и автоматически добавляет ошибки модели в ModelState - это здорово.Поэтому мне нужен способ выполнить автоматическую проверку нуля.

В конце я создал два класса, чтобы завершить проверку:

[AttributeUsage(AttributeTargets.Parameter, 
 AllowMultiple = false, Inherited = false)]
public abstract class ValidateParameterAttribute : Attribute
{
  private bool _continueValidation = false;

  public bool ContinueValidation 
  { get { return _continueValidation; } set { _continueValidation = value; } }

  private int _order = -1;
  public int Order { get { return _order; } set { _order = value; } }

  public abstract bool Validate
    (ControllerContext context, ParameterDescriptor parameter, object value);

  public abstract ModelError CreateModelError
    (ControllerContext context, ParameterDescriptor parameter, object value);

  public virtual ModelError GetModelError
    (ControllerContext context, ParameterDescriptor parameter, object value)
  {
    if (!Validate(context, parameter, value))
      return CreateModelError(context, parameter, value);
    return null;
  }
}

[AttributeUsage(AttributeTargets.Parameter, 
 AllowMultiple = false, Inherited = false)]
public class RequiredParameterAttribute : ValidateParameterAttribute
{
  private object _missing = null;

  public object MissingValue 
    { get { return _missing; } set { _missing = value; } }

  public virtual object GetMissingValue
    (ControllerContext context, ParameterDescriptor parameter)
  {
    //using a virtual method so that a missing value could be selected based
    //on the current controller's state.
    return MissingValue;
  }

  public override bool Validate
    (ControllerContext context, ParameterDescriptor parameter, object value)
  {
    return !object.Equals(value, GetMissingValue(context, parameter));
  }

  public override ModelError CreateModelError
    (ControllerContext context, ParameterDescriptor parameter, object value)
  {
    return new ModelError(
      string.Format("Parameter {0} is required", parameter.ParameterName));
  }
}

С этим я могу сделать следующее:

public void ActionMethod([RequiredParameter]MyModel p1){ /* code here */ }

Но это само по себе ничего не делает, конечно, поэтому теперь нам нужно что-то, чтобы фактически запустить проверку, получить ошибки модели и добавить их в состояние модели.

Введите ParameterValidationAttribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
                Inherited = false)]
public class ParameterValidationAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    var paramDescriptors = filterContext.ActionDescriptor.GetParameters();
    if (paramDescriptors == null || paramDescriptors.Length == 0)
      return;

    var parameters = filterContext.ActionParameters;
    object paramvalue = null;
    ModelStateDictionary modelState 
      = filterContext.Controller.ViewData.ModelState;
    ModelState paramState = null;
    ModelError modelError = null;

    foreach (var paramDescriptor in paramDescriptors)
    {
      paramState = modelState[paramDescriptor.ParameterName];
      //fetch the parameter value, if this fails we simply end up with null
      parameters.TryGetValue(paramDescriptor.ParameterName, out paramvalue);

      foreach (var validator in paramDescriptor.GetCustomAttributes
                (typeof(ValidateParameterAttribute), false)
                .Cast<ValidateParameterAttribute>().OrderBy(a => a.Order)
              )
      {
        modelError = 
          validator.GetModelError(filterContext, paramDescriptor, paramvalue);

        if(modelError!=null)
        {
          //create model state for this parameter if not already present
          if (paramState == null)
            modelState[paramDescriptor.ParameterName] = 
              paramState = new ModelState();

          paramState.Errors.Add(modelError);
          //break if no more validation should be performed
          if (validator.ContinueValidation == false)
            break;
        }
      }
    }

    base.OnActionExecuting(filterContext);
  }
}

Вот так!Почти сейчас ...

Итак, теперь мы можем сделать это:

[ParameterValidation]
public ActionResult([RequiredParameter]MyModel p1)
{
  //ViewData.ModelState["p1"] will now contain an error if null when called
}

Для завершения головоломки нам нужно что-то, что может исследовать ошибки модели и автоматически реагировать, если они есть.Это наименее аккуратный из классов (я ненавижу название и используемый тип параметра), и я, вероятно, изменю его в своем проекте, но он работает, поэтому я все равно опубликую его:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
                Inherited = false)]
public abstract class RespondWithModelErrorsAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    ModelStateDictionary modelState = 
      filterContext.Controller.ViewData.ModelState;

    if (modelState.Any(kvp => kvp.Value.Errors.Count > 0))
      filterContext.Result = CreateResult(filterContext, 
                     modelState.Where(kvp => kvp.Value.Errors.Count > 0));

    base.OnActionExecuting(filterContext);
  }

  public abstract ActionResult CreateResult(
    ActionExecutingContext filterContext, 
    IEnumerable<KeyValuePair<string, ModelState>> modelStateWithErrors);
}

В моем приложении у меня есть XmlResult, который принимает экземпляр Model и сериализует ответ, используя DataContractSerializer или XmlSerializer - поэтому я создал RespondWithXmlModelErrorsAttribute, который наследует от этого последнего типа, чтобы сформулировать один из них с моделью в виде Errors класс, который просто содержит каждую из ошибок модели в виде строк.Код ответа также автоматически устанавливается на 400 Плохой запрос.

Таким образом, теперь я могу сделать это:

[ParameterValidation]
[RespondWithXmlModelErrors(Order = int.MaxValue)]
public ActionResult([RequiredParameter]MyModel p1)
{
  //now if p1 is null, the method won't even be called.
}

В случае веб-страниц этот последний этап не обязательно потребуетсяТак как ошибки модели обычно включаются в повторную визуализацию страницы, которая сначала отправляла данные, и существующий подход MVC подходит для этого.

Но для веб-служб (XML или JSON) можноперегрузка отчетов об ошибках на что-то еще делает написание фактического метода действия намного проще - и я чувствую себя гораздо более выразительным.

1 голос
/ 08 сентября 2010

Ну, вы можете добавить ограничения, используя регулярные выражения для отдельных значений маршрута.Затем, если эти ограничения не поддерживаются, метод действия не будет задействован:

routes.MapRoute ("SomeWebService", "service/{userId}",
                 new { controller = "Service", action = "UserService" },
                 new { userId = @"\d+" });

В качестве альтернативы вы можете создать пользовательские ограничения для проверки значений маршрута вместе как пакета.Это, вероятно, будет лучшей стратегией для вас.Посмотрите здесь: Создание пользовательского ограничения маршрута

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...