2010-09-08 14 views
5

Pomyślałem o kilku sposobach na zrobienie tego, ale chcę uzyskać opinię społeczności. Mam wrażenie, że odpowiedź jest prosta - nie boję się wyglądać głupio (moje dzieci odbierały mi ten strach dawno temu!)Zapobieganie wykonywaniu metody Action MVC, jeśli parametr ma wartość null

Piszę usługę sieci Web XML REST, używając MVC2. Wszystkie typy XML, które odbiorcy usług internetowych będą odbierać i wysyłać, są zarządzane przez prosty, ale obszerny XSD, a parametry te będą powiązane z xml w treści żądania za pośrednictwem niestandardowego domyślnego dostawcy wiążącego i wartościowego modelu.

Mam sporą liczbę kontrolerów, z których każda zawiera spore ilości metod działania (niezbyt wygórowanych - po prostu "dobre";)) - w niemal każdym przypadku te metody działania będą akceptować typy modeli, które są wszystkie typy referencyjne.

Praktycznie w każdym przypadku wystąpi błąd, jeśli osoba dzwoniąca nie poda tych wartości parametrów i jako taki standardowy komunikat o błędzie, taki jak "The parameter {name} type:{ns:type} is required", może zostać odesłany.

To, co chcę zrobić, to sprawdzenie poprawności parametrów nie jest puste przed wykonaniem metody akcji; a następnie, aby zwrócić ActionResult, który reprezentuje błąd dla klienta (dla tego mam już typ XMLResult) bez konieczności samodzielnego sprawdzania samych parametrów przez samą metodę działania.

Więc zamiast:

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

Coś jak:

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

wiem, że to dokładnie ten rodzaj cięcia poprzecznego, które mogą być osiągnięte w MVC.

Wydaje mi się, że albo zastąpienie kontrolera podstawowego OnActionExecuting lub niestandardowy ActionFilter jest najbardziej prawdopodobnym sposobem wykonania tego.

Chciałbym również móc rozszerzyć system tak, aby automatycznie pobierał błędy sprawdzania poprawności schematu XML (dodane do ModelState podczas wiązania przez dostawcę wartości niestandardowej), zapobiegając w ten sposób kontynuowaniu metody działania, jeśli którykolwiek z parametrów wartości nie mogą być poprawnie załadowane, ponieważ żądanie XML jest źle sformułowane.

Odpowiedz

2

Oto wdrożenie że mam wymyślić (czekając na nic lepszego pomysłu :))

Jest to ogólne podejście i myślę, że jest dość skalowalny - umożliwiając nadzieją podobny rodzaj sprawdzania głębi do parametrów, jak w przypadku sprawdzania poprawności modelu w tym samym czasie, co funkcja automatycznego reagowania na błędy (gdy stan modelu zawiera jeden lub więcej błędów), którego szukałem.

Mam nadzieję, że to nie jest zbyt dużo kodu dla odpowiedzi SO (!); Miałem mnóstwo komentarzy do dokumentacji, które usunąłem, aby było krótsze.

Tak, w moim scenariuszu Mam dwa typy modelu błędu, że jeżeli one wystąpią, należy zablokować wykonanie metody działania:

  • Niepowodzenie sprawdzania poprawności schematu XML, z którego wartość parametru zostanie zbudowana
  • Missing (null) parametr wartość

walidacja Schema jest aktualnie wykonywane podczas modelu wiązania i automatycznie dodaje do modelu błędy ModelState - tak to świetnie. Potrzebuję więc sposobu na sprawdzenie automatycznego zerowania.

W końcu stworzył dwie klasy, aby zakończyć sprawdzanie poprawności:

[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)); 
    } 
} 

Dzięki temu mogę wtedy to zrobić:

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

Ale to na własną rękę nie robić nic oczywiście, więc teraz potrzebujemy czegoś, aby faktycznie uruchomić sprawdzanie poprawności, aby uzyskać błędy modelu i dodać je do stanu modelu.

Wprowadź 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); 
    } 
} 

Uff! Prawie tam teraz ...

Tak, teraz możemy to zrobić:

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

pełnego puzzli potrzebujemy czegoś, co można zbadać modelowych błędy i automatycznie reagować, jeśli takie istnieją. Jest to najmniej schludny klas (I hate nazwę i typ parametru używany) i prawdopodobnie będę go zmienić w moim projekcie, ale działa tak wyślę to tak:

[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); 
} 

W moim Aplikacja Mam XmlResult, który pobiera instancję modelu i serializuje do odpowiedzi przy użyciu albo DataContractSerializer lub XmlSerializer - tak więc stworzyłem RespondWithXmlModelErrorsAttribute, który dziedziczy z tego ostatniego typu, aby sformułować jeden z tych z modelem jako klasę Errors, która po prostu zawiera każdą z nich błędów modelu jako łańcuchów. Kod odpowiedzi jest również automatycznie ustawiony na 400 złych żądań.

Tak więc, teraz mogę to zrobić:

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

W przypadku stron internetowych nie musi być wymagane ten ostatni etap, ponieważ błędy modelowe są zwykle zawarte w re-rendering strony, który wysłał dane w pierwszej kolejności, a istniejące podejście MVC odpowiada tej grzywnie.

Ale dla usług internetowych (zarówno XML, jak i JSON) możliwość odciążenia raportowania błędów na coś innego sprawia, że ​​pisanie właściwej metody działania jest o wiele łatwiejsze - i czuję się o wiele bardziej ekspresyjnie.

+0

, ponieważ działa to bardzo dobrze dla mnie i myślę, że jest odpowiednio rozszerzalny, aby być naprawdę ważnym dodatkiem do naszego wewnętrzna biblioteka rozszerzeń Mvc. –

+0

To jest świetne rozwiązanie i to, po czym byłem .. Pozdrawiam Andras. Moja pierwsza próba była prawie na miejscu, ale widzę, czego mi brakowało. – horHAY

1

Cóż można dodać ograniczenia za pomocą wyrażeń regularnych do poszczególnych wartości trasy. Następnie, jeżeli ograniczenia te nie są przestrzegane, sposób działania nie będzie hit:

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

Alternatywnie można utworzyć niestandardowe ograniczenia do sprawdzania poprawności wartości trasy razem jako opakowanie. To prawdopodobnie byłaby lepsza strategia dla ciebie. Rzucić okiem tutaj: Creating a Custom Route Constraint

+0

Dzięki - niestety to nie zadziała, ponieważ parametry te są powiązane z xml w treści żądania, więc nie są łatwo dostępne dla silnika routingu; Nie chcę sprawdzać żądania xml na poziomie routingu, aby wykonać tę walidację. Ułatwiłoby to również powrót do ładnego komunikatu o błędzie. –

+0

Zmieniono, aby podkreślić, że te parametry będą pochodzić z ciała żądania i dlatego należy to zrobić po zaakceptowaniu tej odpowiedzi przez model wiążący –

Powiązane problemy