Özellikle server-side geliştirme yapan kişiler için validation olmazsa olmazlardan biridir. Yazmış olduğunuz api'a client'lar belli başlı parametreler göndererek request'te bulunurlar ve sürekli bir veri alış verişi söz konusudur ve server-side geliştirici olarak asla gönderilen input'a güvenmememiz gerekir. Metotlara gönderilen parametreleri belli başlı bazı güvenlik adımlarından&validasyonlardan geçirdikten sonra işlemlere devam ediyor veya etmiyor olmamız gerekir. FluentValidation kütüphanesi ile bu gibi durumlar için belli validation-rule'lar oluşturarak unexpected-input dediğimiz istenmeyen parametrelere karşı önlemler alabiliriz.
Örnek üzerinden ilerleyecek olursa; Bir Web Api projemiz olsun ve bu proje için Para Gönderme (Eft & Havale) işlemi yapan bir modül yazalım. MoneyTransferRequest adında bir model'imiz olsun ve bu model üzerinden para göndermek için gerekli olan bilgiler kullanıcıdan alalım. MoneyTransferRequest model için FluentValidation kütüphanesini kullanarak gerekli validation işlemlerini tanımlayalım validation-failed olduğunda bu durumdan client'ı hata mesajları göndererek bilgilendirelim, yani validationMessage'ı response olarak client'a return edelim.
İlk adım olarak vs'de açtığımız Web Api projemize nuget'ten FluentValidation.WebApi kütüphanesini indirip kuralım
MoneyTransferRequest.cs class'ımız aşağıdaki gibi olacaktır.
public class MoneyTransferRequest
{
public decimal Amount { get; set; }
public string SenderIBAN { get; set; }
public string ReceiverIBAN { get; set; }
public DateTime TransactionDate { get; set; }
}
Şimdi ise sırada MoneyTransferRequest için yazacağımız Validator class'ı var. Bu class AbstractValidator<T> class'ından inherit olmak zorundadır ve request modelimizin property'leri için geçerli olan rule'ları burada tanımlayacağız.
public class MoneyTransferRequestValidator : AbstractValidator<MoneyTransferRequest>
{
public MoneyTransferRequestValidator()
{
RuleFor(x => x.Amount).GreaterThan(0).WithMessage("Amount cannot be less than or equal to 0.");
RuleFor(x => x.SenderIBAN).NotEmpty().WithMessage("The Sender IBAN cannot be blank.").Length(16, 26).WithMessage("The Sender IBAN must be at least 16 characters long and at most 26 characters long.");
RuleFor(x => x.ReceiverIBAN).NotEmpty().WithMessage("The Receiver IBAN cannot be blank.").Length(16, 26).WithMessage("The Receiver IBAN must be at least 16 characters long and at most 26 characters long.");
RuleFor(x => x.TransactionDate).GreaterThanOrEqualTo(DateTime.Today).WithMessage("Transaction Date cannot be any past date.");
}
}
Tanımlamış olduğumuz MoneyTransferRequestValidator class'ını MoneyTransferRequest class'ı için olduğunu belirten tanımlamayı aşağıda olduğu gibi attribute gibi class ismi üzerinde belirtiyoruz.
[Validator(typeof(MoneyTransferRequestValidator))]
public class MoneyTransferRequest
{
public decimal Amount { get; set; }
public string SenderIBAN { get; set; }
public string ReceiverIBAN { get; set; }
public DateTime TransactionDate { get; set; }
}
FluentValidationModelValidatorProvider'ı WebApiConfig class'ı içerisinde aşağıdaki enable ederek validator için config işlemlerimizi tamamlamış oluyoruz.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
FluentValidationModelValidatorProvider.Configure(config);
}
}
Yapılan request'leri tek bir yerden handle edip valid bir işlem mi değil mi kontrolü için custom ActionFilter'ımızı tanımlayalım. Bu actionFilter validation'dan success alınamaz ise yani MoneyTransferRequest modeli için tanımladığımız validasyon kuralları sağlanmaz ise client'a yeni bir response dönüp içerisine validationMessage'ları set ediyor olacağız.
public class ValidateModelStateFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
Bu action filter için register işlemini aşağıdaki gibi yapıyoruz.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ValidateModelStateFilter());//register ettik
FluentValidationModelValidatorProvider.Configure(config);
}
}
Client'a döndüğümüz response'lar için bir base response tanımlayalım.
public class BaseResponse
{
public BaseResponse(object content, List<string> modelStateErrors)
{
this.Result = content;
this.Errors = modelStateErrors;
}
public List<string> Errors { get; set; }
public object Result { get; set; }
}
Şimdi ise custom ResponseHandler'ımızı tanımlicaz. Bu handler her bir response'u kontrol ederek yukarıda tanımlamış olduğumuz BaseResponse'a convert edicek ve client'a bu response modeli dönecek.
public class ResponseWrappingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
return BuildApiResponse(request, response);
}
private HttpResponseMessage BuildApiResponse(HttpRequestMessage request, HttpResponseMessage response)
{
object content;
var modelStateErrors = new List<string>();
if (response.TryGetContentValue(out content) && !response.IsSuccessStatusCode)
{
var error = content as HttpError;
if (error != null)
{
content = null;
if (error.ModelState != null)
{
var httpErrorObject = response.Content.ReadAsStringAsync().Result;
var anonymousErrorObject = new { message = "", ModelState = new Dictionary<string, string[]>() };
var deserializedErrorObject = JsonConvert.DeserializeAnonymousType(httpErrorObject, anonymousErrorObject);
var modelStateValues = deserializedErrorObject.ModelState.Select(kvp => string.Join(". ", kvp.Value));
for (var i = 0; i < modelStateValues.Count(); i++)
{
modelStateErrors.Add(modelStateValues.ElementAt(i));
}
}
}
}
var newResponse = request.CreateResponse(response.StatusCode, new BaseResponse(content, modelStateErrors));
foreach (var header in response.Headers)
{
newResponse.Headers.Add(header.Key, header.Value);
}
return newResponse;
}
}
ResponseHandler'ı da api için register ediyoruz ve WebApiConfig class'ının son hali aşağıdaki gibi olacaktır.
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Filters.Add(new ValidateModelStateFilter());
config.MessageHandlers.Add(new ResponseWrappingHandler());
FluentValidationModelValidatorProvider.Configure(config);
}
Artık Api'ı test edebiliriz. Ben genelde bu tarz işlemler için postman rest client'ı kullanıyorum. Aşağıdaki gibi hatalı parametreler ile bir request'te bulunduğumuzda nasıl response alıyoruz inceleyelim.
Görüldüğü üzre Amount, SenderIBAN ve TransactionDate alanlarını hatalı girdiğimizde yukarıdaki gibi validation message'larının döndüğü bir response alıyoruz. ReceiverIBAN alanı validasyona takılmadığından bu alan ile ilgili herhangi bir mesaj almadık.
Özetle yazımızın başında da belirttiğim gibi Validasyon oldukça önemli bir konudur ve client hatalı bir input gönderirse anlaşılması kolay bir response oluşturarak ilgili validasyon mesajını client'a return etmek işimizi oldukça kolaylaştıracaktır. Yapmış olduğumuz bu geliştirme ile birlikte otomatik bir biçimde FluentValidation tarafından fırlatılan validasyon mesajları tam response dönme anında handle edilip client'a döndürmektedir.