Вот реализация, которую я придумала (ожидая каких-либо лучших идей :))
Это общий подход, и я думаю, что он довольно масштабируемый - с учетом, возможно, глубины, аналогичной параметрупроверка, как вы получаете, с проверкой модели в то же время, что и предоставление функции автоответа на ошибку (когда состояние модели содержит одну или несколько ошибок), которую я искал.
Надеюсь, это не слишком много кодаза 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) можноперегрузка отчетов об ошибках на что-то еще делает написание фактического метода действия намного проще - и я чувствую себя гораздо более выразительным.