Podbijając wersje .NET Core w projekcie, doświadczymy sytuacji zmiany logiki związanej z walidacją modelu oraz typem domyślnym dla odpowiedzi HTTP 400. Automatyczne sprawdzenie stanu modelu jest świetnym rozwiązaniem. Jednak co w przypadku scenariusza, gdy chcemy podmienić domyślny obiekt odpowiedzi HTTP 400 na własną implementację. I o tym jak to zrobić będzie ten wpis. Zapraszam do lektury.
Domyślna automatyczna odpowiedź HTTP 400
W ASP.NET Core, gdy stosujemy atrybut ApiController na kontrolerze następuje automatyczne sprawdzenie poprawności przesłanego obiektu. W przypadku nieprawidłowej walidacji obiektu zostanie automatycznie zwrócony kod błędu HTTP 400. Nie nastąpi tu wejście do ciała akcji.
Na początek przekonajmy się jak to działa w praktyce. Stworzyłem VikingsController z akcją POST. Walidacja dla modelu Viking została skonfigurowana za pomocą FluentValidation. Osoby zainteresowane podstawami biblioteki do walidacji, odsyłam do mojego wcześniejszego artykułu „Fluent Validation z ASP.NET Core Web API„.
using API.Interfaces;
using API.Models;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class VikingsController : ControllerBase
{
private readonly IVikingsService _vikingsService;
public VikingsController(IVikingsService vikingsService) => _vikingsService = vikingsService;
[HttpPost]
public IActionResult Post(Viking viking)
{
_vikingsService.Add(viking);
return Created("", null);
}
}
}
namespace API.Models
{
public class Viking
{
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
}
}
using API.Models;
using FluentValidation;
namespace API.Validators
{
public class VikingValidator : AbstractValidator
{
public VikingValidator()
{
RuleFor(v => v.Name).NotNull().NotEmpty();
RuleFor(v => v.Email).NotNull().NotEmpty().EmailAddress();
RuleFor(v => v.Age).InclusiveBetween(15, 50);
}
}
}
Wykorzystując narzędzie Postman wysyłam żądanie POST z obiektem nie spełniającym reguły walidacji.

W odpowiedzi uzyskałem kod 400 Bad Request. Dla .NET Core 2.2 i nowszego domyślnym typem odpowiedzi jest obiekt ValidationProblemDetails. Odpowiedź zawiera pola jak na powyższym zrzucie z Postman. We wcześniejszych wersjach .NET Core (2.1 i starszych) domyślnym typem odpowiedzi jest obiekt SerializableError.
Własny obiekt odpowiedzi
Załóżmy, że nie podoba nam się format obiektu odpowiedzi ValidationProblemDetails, albo dodatkowo chcemy zalogować informacje o nieprawidłowej walidacji. Zauważcie, że w ciele akcji POST nie następuje sprawdzenie ModelState. Gdy stosujemy atrybut APIController, ASP.NET Core robi to z automatu za nas. Jak zatem obsłużyć automatyczną odpowiedź na błąd wynikający z przesłania nieprawidłowego obiektu.
Pierwszą opcją jest cofnięcie się do starego rozwiązania i wyłącznie automatycznej obsługi błędu 400. Możemy to zrobić poprzez usunięcie atrybutu APIController z kontrolera, albo dokonać tego poprzez ustawienie właściwości SuppressModelStateInvalidFilter na wartość true w metodzie ConfigureServices w klasie Startup.
services
.AddControllers()
.ConfigureApiBehaviorOptions(options => { options.SuppressModelStateInvalidFilter = true; });
Sprawdźmy poprawność powyższej konfiguracji. Dorzuciłem sprawdzenie ModelState w ciele akcji POST i wysłałem takie jak wcześniej żądanie POST z narzędzia Postman. Jak widać na poniższym zrzucie, nie dostaliśmy automatycznej odpowiedzi, tylko program zatrzymał się na breakpoint postawionym w miejscu obsługi błędu.

Wadą powyższego rozwiązania jest powielenie kodu sprawdzającego stan walidacji modelu w pozostałych akcjach kontrolerów. Zastanówmy się jak to zrobić globalnie.
Drugą opcją to pójście dalej z duchem zmian wprowadzonych w kolejny wersjach .NET Core. Czyli zostawiamy mechanizm automatycznej odpowiedzi. W celu podmiany domyślnego typu odpowiedzi na obiekt ErrorDetails wykorzystujemy InvalidModelStateResponseFactory. Zdefiniujmy na początek nasz model ErrorDetails.
using System.Collections.Generic;
using System.Text.Json;
namespace API.Models
{
public class ErrorDetails
{
public int StatusCode { get; set; }
public List ErrorMessages { get; set; }
public ErrorDetails(int statusCode, List errorMessages)
{
StatusCode = statusCode;
ErrorMessages = errorMessages;
}
public override string ToString() => JsonSerializer.Serialize(this);
}
}
W celu wyciągnice kolekcji komunikatów błędów walidacji dodałem extension method (GetErrorMessages) dla klasy ModelStateDictionary.
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace API.Extensions
{
public static class ModelStateDictionaryExtension
{
public static List GetErrorMessages(this ModelStateDictionary modelState)
=> modelState.Keys.SelectMany(key => modelState[key].Errors.Select(x => $"{key} : [{x.ErrorMessage}]")).ToList();
}
}
Została jeszcze konfiguracja zwracania obiektu ErrorDetails zamiast domyślnego ValidationProblemDetails. Zmieniamy aktualną konfigurację na poniższą w metodzie ConfigureServices w klasie Startup. InvalidModelStateResponseFactory to delegat wywołany na akcje z ApiControllerAttribute, konwertuje nieprawidłowy ModelStateDictionary na IActionResult.
services.AddControllers()
.AddFluentValidation(fv =>
{
fv.RegisterValidatorsFromAssemblyContaining();
fv.LocalizationEnabled = false;
})
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errorMessages = context.ModelState.GetErrorMessages();
var errorDetails = new ErrorDetails((int) HttpStatusCode.BadRequest, errorMessages);
var result = new BadRequestObjectResult(errorDetails)
{ContentTypes = {MediaTypeNames.Application.Json, MediaTypeNames.Application.Xml}};
return result;
};
});

Wykonałem jeszcze raz to samo żądanie, ale tym razem już nie dostałem domyślnego obiektu, tylko nasz ErrorDetails. Misja powiodła się, udało zmienić się domyślny obiekt odpowiedzi na własną implementację. Powodzenia i zapraszam do lektury starszych wpisów, oraz do oczekiwania na kolejne 🙂