12 Ekim 2020

Asp Net Core ile ModelState Validation için Custom Yanıt Dönen Middleware

ASP.Net Core Web Api de DataAnnotations attribute ile uymayan durumlarda dönen yanıt örnek User.cs için aşağıdaki gibidir. 

 { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", 

 "title": "One or more validation errors occurred.",

 "status": 400, 

 "traceId": "|c591e8b8-46918ec46443b7d8.", 

"errors": { 

"Username": [ "The Username field is required.", "En az sekiz karakterli bir kullanıcı adı giriniz." ] 

    } 

 }

 Bizim tüm uygulama boyunca generic api responseda formatı aşağıda ki gibi istediğimizi varsayalım 

{ "Status": 400,

 "IsSuccess": false,

 "Result": null,

 "Errors":

 "The Username field is required. \r\n En az sekiz karakterli bir kullanıcı adı giriniz."

  }

 Bunun için yapılan adımlar aşağıdaki gibidir.

#
// ApiResponse.cs //

using System;
using Microsoft.Extensions.Logging;

namespace CustomMiddlewareForModelState.Models
{
public class ApiResponse<TResult>
{
public int Status { get; set; }
public bool IsSuccess { get; set; }
public TResult Result { get; set; }
public string Errors { get; set; }

public ApiResponse<TResult> Invoke(Func<TResult> func, ILogger _logger)
{
try
{
Result = func.Invoke();
Status = 200;
IsSuccess = true;
Errors = string.Empty;
}
catch (Exception ex)
{
Guid? exceptionId = Guid.NewGuid();
Result = default(TResult);
IsSuccess = false;
Errors = $"Genel bir hata meydana geldi. Lütfen {exceptionId} takip numarası ile geri dönüşte bulunun. ";

_logger?.LogError("ExceptionId : " + exceptionId.ToString()
+ " , Exception: " + ex.ToString());
}
return this;
}
}
}

// ModelStateFeature.cs //

using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace CustomMiddlewareForModelState.Models
{
public class ModelStateFeature
{
public ModelStateDictionary ModelState { get; set; }

public ModelStateFeature(ModelStateDictionary state)
{
ModelState = state;
}
}

}

// ModelStateFeatureFilter.cs //


using Microsoft.AspNetCore.Mvc.Filters;
using System.Threading.Tasks;
using CustomMiddlewareForModelState.Models;

namespace CustomMiddlewareForModelState.Filters
{
public class ModelStateFeatureFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var state = context.ModelState;
context.HttpContext.Features.Set(new ModelStateFeature(state));
await next();
}
}
}

// ModelStateResponseMiddleware.cs //

using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using CustomMiddlewareForModelState.Models;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace CustomMiddlewareForModelState.Middlewares{
public class ModelStateResponseMiddleware{
private readonly RequestDelegate next;
public ModelStateResponseMiddleware(RequestDelegate next)
{
this.next = next;
}

public async Task Invoke(HttpContext context)
{
Stream originalBody = context.Response.Body;
try
{
using (var ms = new MemoryStream())
{
context.Response.Body = ms;
await next(context);
ms.Position = 0;
string responseBody = new StreamReader(ms).ReadToEnd();

if (context.Response.StatusCode == 400)
{
var modelState = context.Features.Get<ModelStateFeature>()?.ModelState;
if (modelState != null
&& !modelState.IsValid
&& modelState.Count > 0)
{
var errors = modelState.Values.Where(v => v.Errors.Count > 0)
.SelectMany(v => v.Errors)
.Select(v => v.ErrorMessage)
.ToList();

var apiResponse = new ApiResponse<object>(){
IsSuccess = false,
Result = null,
Status = 400,
Errors = string.Join(" \r\n ", errors)
};

context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.Response.ContentType = "application/json";
string strApiResponse = JsonConvert.SerializeObject(apiResponse);
var data = System.Text.Encoding.UTF8.GetBytes(strApiResponse);
ms.Position = 0;
await ms.WriteAsync(data);
ms.SetLength(data.Length);
}
}
ms.Position = 0;
await ms.CopyToAsync(originalBody);
}
}
finally
{
context.Response.Body = originalBody;
}
}
}
}

// Startup.cs //
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CustomMiddlewareForModelState.Filters;
using CustomMiddlewareForModelState.Middlewares;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace CustomMiddlewareForModelState
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(opts => {
opts.Filters.Add(typeof(ModelStateFeatureFilter),int.MinValue);
});
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseMiddleware<ModelStateResponseMiddleware>();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

// User.cs //

using System.ComponentModel.DataAnnotations;

namespace CustomMiddlewareForModelState.Models
{
public class User
{
[Required]
[StringLength(int.MaxValue, MinimumLength = 1, ErrorMessage = "En az sekiz karakterli bir kullanıcı adı giriniz.")]
public string Username { get; set; }

[Required]
[StringLength(int.MaxValue, MinimumLength = 1, ErrorMessage = "En az sekiz karakterli bir şifre giriniz.")]
public string Password { get; set; }
}
}

// TestController.cs //


using System;
using Microsoft.AspNetCore.Mvc;
using CustomMiddlewareForModelState.Models;

namespace CustomMiddlewareForModelState.Controllers{
[Route("api/[controller]")]
[ApiController]
public class TestController : Controller
{
[HttpPost]
public IActionResult Post(User user)
{
var resp = new ApiResponse<Guid>().Invoke(() =>
{
if (ModelState.IsValid)
{
return Guid.NewGuid();
}
throw new Exception("Validasyon hatası!");
}, null);

if (resp.Status == 200)
return Ok(resp);
else return BadRequest(resp);
}
}
}

ModelState Başarısızsa dönen yanıt

{
"Status": 400,
"IsSuccess": false,
"Result": null,
"Errors": "The Username field is required. \r\n En az sekiz karakterli bir kullanıcı adı giriniz."
}

Başarılıysa Dönen Yanıt

{
"status": 200,
"isSuccess": true,
"result": "fdc56d1b-1ee8-43ce-b7d8-46ba36bee668",
"errors": ""
}
 #