Obsługa walidacji ModelState w ASP.NET Web API

106

Zastanawiałem się, jak mogę uzyskać walidację modelu za pomocą ASP.NET Web API. Mam taki model:

public class Enquiry
{
    [Key]
    public int EnquiryId { get; set; }
    [Required]
    public DateTime EnquiryDate { get; set; }
    [Required]
    public string CustomerAccountNumber { get; set; }
    [Required]
    public string ContactName { get; set; }
}

Następnie mam akcję Opublikuj w moim kontrolerze API:

public void Post(Enquiry enquiry)
{
    enquiry.EnquiryDate = DateTime.Now;
    context.DaybookEnquiries.Add(enquiry);
    context.SaveChanges();
}

Jak dodać, if(ModelState.IsValid)a następnie obsłużyć komunikat o błędzie, który ma zostać przekazany użytkownikowi?

CallumVass
źródło

Odpowiedzi:

186

Aby oddzielić obawy, sugerowałbym użycie filtru akcji do walidacji modelu, więc nie musisz się zbytnio przejmować, jak wykonać walidację w kontrolerze API:

using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace System.Web.Http.Filters
{
    public class ValidationActionFilter : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            var modelState = actionContext.ModelState;

            if (!modelState.IsValid)
                actionContext.Response = actionContext.Request
                     .CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
        }
    }
}
cuongle
źródło
27
Te przestrzenie nazw są potrzebne do tego System.Net.Http, System.Net System.Web.Http.Controllersi System.Web.Http.Filters.
Christopher Stevenson,
11
Podobna implementacja znajduje się również na oficjalnej stronie ASP.NET Web Api: asp.net/web-api/overview/formats-and-model-binding/ ...
Erik Schierboom
1
Nawet jeśli nie umieścisz [ValidationActionFilter] powyżej interfejsu API sieci Web, nadal wywołuje kod i wysyła mi złe żądanie.
micronyks
1
Warto zaznaczyć, że zwracana odpowiedź błędu jest kontrolowana przez IncludeErrorDetailPolicy . Domyślnie odpowiedź na żądanie zdalne zawiera tylko ogólny komunikat „Wystąpił błąd”, ale ustawienie tego na IncludeErrorDetailPolicy.Alwaysspowoduje uwzględnienie szczegółów (z ryzykiem ujawnienia szczegółów użytkownikom)
Rob
Czy istnieje konkretny powód, dla którego nie zasugerowałeś zamiast tego użycia IAsyncActionFilter?
Ravior
30

Może nie tego, czego szukałeś, ale może miło, żeby ktoś wiedział:

Jeśli używasz .net Web Api 2, możesz po prostu wykonać następujące czynności:

if (!ModelState.IsValid)
     return BadRequest(ModelState);

W zależności od błędów modelu otrzymasz taki wynik:

{
   Message: "The request is invalid."
   ModelState: {
       model.PropertyA: [
            "The PropertyA field is required."
       ],
       model.PropertyB: [
             "The PropertyB field is required."
       ]
   }
}
Czy Almaas
źródło
1
Miej na uwadze to, kiedy zadałem to pytanie Web API 1 został właśnie wydany, prawdopodobnie od tamtej pory dużo się
zmienił
Pamiętaj, aby oznaczyć właściwości jako opcjonalne, w przeciwnym razie zostanie wyświetlony nieprzydatny ogólny komunikat „Wystąpił błąd”. Komunikat o błędzie.
Bouke
1
Czy istnieje sposób na zmianę przesłania?
saquib adil
29

W ten sposób na przykład:

public HttpResponseMessage Post(Person person)
{
    if (ModelState.IsValid)
    {
        PersonDB.Add(person);
        return Request.CreateResponse(HttpStatusCode.Created, person);
    }
    else
    {
        // the code below should probably be refactored into a GetModelErrors
        // method on your BaseApiController or something like that

        var errors = new List<string>();
        foreach (var state in ModelState)
        {
            foreach (var error in state.Value.Errors)
            {
                errors.Add(error.ErrorMessage);
            }
        }
        return Request.CreateResponse(HttpStatusCode.Forbidden, errors);
    }
}

To zwróci następującą odpowiedź (zakładając JSON, ale ta sama podstawowa zasada dla XML):

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
(some headers removed here)

["A value is required.","The field First is required.","Some custom errorm essage."]

Możesz oczywiście skonstruować swój obiekt / listę błędów w dowolny sposób, na przykład dodając nazwy pól, identyfikatory pól itp.

Nawet jeśli jest to „jednokierunkowe” wywołanie Ajax, takie jak POST nowej jednostki, nadal powinieneś zwrócić coś do dzwoniącego - coś, co wskazuje, czy żądanie się powiodło. Wyobraź sobie witrynę, w której użytkownik doda informacje o sobie za pośrednictwem żądania AJAX POST. Co się stanie, jeśli informacje, które próbowali wprowadzić, są nieprawidłowe - skąd będą wiedzieć, czy ich akcja zapisywania zakończyła się powodzeniem, czy nie?

Najlepszym sposobem na to jest użycie dobrych starych kodów stanu HTTP, takich jak 200 OKi tak dalej. W ten sposób Twój JavaScript może poprawnie obsługiwać awarie przy użyciu poprawnych wywołań zwrotnych (błąd, sukces itp.).

Oto fajny samouczek dotyczący bardziej zaawansowanej wersji tej metody, wykorzystującej ActionFilter i jQuery: http://asp.net/web-api/videos/getting-started/custom-validation

Anders Arpi
źródło
To po prostu zwraca mój enquiryobiekt, ale nie mówi, które właściwości są nieprawidłowe? Więc jeśli zostawiłem CustomerAccountNumberpuste, powinien on zawierać domyślny komunikat walidacji (pole CusomterAccountNumber jest wymagane ..)
CallumVass
Rozumiem, czy jest to zatem „poprawny” sposób obsługi walidacji modelu? Wydaje mi się trochę niechlujne ...
CallumVass
Istnieją również inne sposoby, aby to zrobić, na przykład połączenie z walidacją jQuery. Oto ładny przykład firmy Microsoft: asp.net/web-api/videos/getting-started/custom-validation
Anders Arpi
Ta metoda i metoda wybrana jako odpowiedź „powinny być” funkcjonalnie identyczne, więc ta odpowiedź ma tę wartość dodaną, że pokazuje, jak możesz to zrobić samodzielnie bez filtra akcji.
Shaun Wilson,
Musiałem zmienić linię errors.Add(error.ErrorMessage);na, errors.Add(error.Exception.Message);żeby to działało dla mnie.
Caltor
9

Możesz użyć atrybutów z System.ComponentModel.DataAnnotationsprzestrzeni nazw, aby ustawić reguły walidacji. Szczegółowe informacje można znaleźć w sekcji Walidacja modelu - autor: Mike Wasson .

Zobacz także wideo ASP.NET Web API, część 5: Custom Validation - Jon Galloway

Inne referencje

  1. Wybierz się na spacer po stronie klienta dzięki interfejsowi WebAPI i formularzom internetowym
  2. Jak interfejs API sieci Web ASP.NET wiąże wiadomości HTTP z modelami domeny i jak pracować z formatami multimediów w interfejsie API sieci Web.
  3. Dominick Baier - Zabezpieczanie interfejsów API sieci Web ASP.NET
  4. Podłączanie walidacji AngularJS do walidacji ASP.NET Web API
  5. Wyświetlanie błędów ModelState za pomocą AngularJS w ASP.NET MVC
  6. Jak renderować błędy klientowi? AngularJS / WebApi ModelState
  7. Walidacja wprowadzana przez zależności w interfejsie API sieci Web
LCJ
źródło
8

Lub, jeśli szukasz prostego zbioru błędów dla swoich aplikacji ... oto moja implementacja tego:

public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var modelState = actionContext.ModelState;

        if (!modelState.IsValid) 
        {

            var errors = new List<string>();
            foreach (var state in modelState)
            {
                foreach (var error in state.Value.Errors)
                {
                    errors.Add(error.ErrorMessage);
                }
            }

            var response = new { errors = errors };

            actionContext.Response = actionContext.Request
                .CreateResponse(HttpStatusCode.BadRequest, response, JsonMediaTypeFormatter.DefaultMediaType);
        }
    }

Odpowiedź na komunikat o błędzie będzie wyglądać następująco:

{
  "errors": [
    "Please enter a valid phone number (7+ more digits)",
    "Please enter a valid e-mail address"
  ]
}
sandeep talabathula
źródło
5

Dodaj poniższy kod w pliku startup.cs

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2).ConfigureApiBehaviorOptions(options =>
            {
                options.InvalidModelStateResponseFactory = (context) =>
                {
                    var errors = context.ModelState.Values.SelectMany(x => x.Errors.Select(p => new ErrorModel()
                   {
                       ErrorCode = ((int)HttpStatusCode.BadRequest).ToString(CultureInfo.CurrentCulture),
                        ErrorMessage = p.ErrorMessage,
                        ServerErrorMessage = string.Empty
                    })).ToList();
                    var result = new BaseResponse
                    {
                        Error = errors,
                        ResponseCode = (int)HttpStatusCode.BadRequest,
                        ResponseMessage = ResponseMessageConstants.VALIDATIONFAIL,

                    };
                    return new BadRequestObjectResult(result);
                };
           });
MayankGaur
źródło
3

Tutaj możesz sprawdzić, czy jeden po drugim wyświetla się błąd stanu modelu

 public HttpResponseMessage CertificateUpload(employeeModel emp)
    {
        if (!ModelState.IsValid)
        {
            string errordetails = "";
            var errors = new List<string>();
            foreach (var state in ModelState)
            {
                foreach (var error in state.Value.Errors)
                {
                    string p = error.ErrorMessage;
                    errordetails = errordetails + error.ErrorMessage;

                }
            }
            Dictionary<string, object> dict = new Dictionary<string, object>();



            dict.Add("error", errordetails);
            return Request.CreateResponse(HttpStatusCode.BadRequest, dict);


        }
        else
        {
      //do something
        }
        }

}

Debendra Dash
źródło
3

DO#

    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.ModelState.IsValid == false)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, actionContext.ModelState);
            }
        }
    }

...

    [ValidateModel]
    public HttpResponseMessage Post([FromBody]AnyModel model)
    {

Javascript

$.ajax({
        type: "POST",
        url: "/api/xxxxx",
        async: 'false',
        contentType: "application/json; charset=utf-8",
        data: JSON.stringify(data),
        error: function (xhr, status, err) {
            if (xhr.status == 400) {
                DisplayModelStateErrors(xhr.responseJSON.ModelState);
            }
        },
....


function DisplayModelStateErrors(modelState) {
    var message = "";
    var propStrings = Object.keys(modelState);

    $.each(propStrings, function (i, propString) {
        var propErrors = modelState[propString];
        $.each(propErrors, function (j, propError) {
            message += propError;
        });
        message += "\n";
    });

    alert(message);
};
Nick Hermans
źródło
2

Miałem problem z implementacją zaakceptowanego wzorca rozwiązania, w którym ModelStateFilterzawsze zwracałem false(a następnie 400) actionContext.ModelState.IsValiddla niektórych obiektów modelu:

public class ModelStateFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = new HttpResponseMessage { StatusCode = HttpStatusCode.BadRequest};
        }
    }
}

Akceptuję tylko JSON, więc zaimplementowałem niestandardową klasę segregatora modelu:

public class AddressModelBinder : System.Web.Http.ModelBinding.IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, System.Web.Http.ModelBinding.ModelBindingContext bindingContext)
    {
        var posted = actionContext.Request.Content.ReadAsStringAsync().Result;
        AddressDTO address = JsonConvert.DeserializeObject<AddressDTO>(posted);
        if (address != null)
        {
            // moar val here
            bindingContext.Model = address;
            return true;
        }
        return false;
    }
}

Które rejestruję się bezpośrednio po moim modelu przez

config.BindParameter(typeof(AddressDTO), new AddressModelBinder());
user326608
źródło