ASP.NET Core Web Api – Global Exception Handling

Oluşturacağımız bir middleware ile merkezi bir hata yönetimi yapıyor olacağız. Bunun için ilk olarak “ASP.NET Core – Identity / Custom Identity Projesi – Projenin Oluşturulması #1” yazısında ki adımları takip ederek “GlobalExceptionHandling” adında bir Asp.Net Core Identity projesi oluşturuyoruz. Bir Identity projesi oluşturmadaki amacımız Identity Exceptionlarını ve ModelState hatalarını tek bir Global Exception middleware ile handle edip kullanıcıya dönen mesajların standardize etmek olacak.

Proje oluşturma aşamasından sonra projenin ana dizinine DTOs adında bir klasör ve bu klasör altına da RegisterDto.cs sınıfını oluşturuyoruz ve register işlemi için gerekli olan propertyleri ve validation işlemi için gerekli olan data annotationlarını ekliyoruz.

using System.ComponentModel.DataAnnotations;

namespace GlobalExceptionHandling.DTOs
{
    public class RegisterDto
    {
        [Required]
        public string Name { get; set; }

        [Required]
        public string Surname { get; set; }

        [Required]
        [MinLength(3)]
        public string Username { get; set; }

        [EmailAddress]
        [Required]
        public string Email { get; set; }

        [Required]
        [MinLength(8)]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}

Ardında ana dizine Exceptions adında bir klasör daha açıp isimleri CustomValidationException.cs, CustomIdentityException.cs, CustomInternalServerErrorException.cs olan ve Exception sınıfında türeyen 3 adet class oluşturup, classların üçüne de aşağıdaki şekilde düzenliyoruz.

namespace GlobalExceptionHandling.Exceptions
{
    public class CustomValidationException : Exception
    {
        public List<string> Errors { get; }

        public CustomValidationException(IEnumerable<string> errors)
        {
            Errors = errors.ToList();
        }
    }
}
namespace GlobalExceptionHandling.Exceptions
{
    public class CustomIdentityException : Exception
    {
        public List<string> Errors { get; }

        public CustomValidationException(IEnumerable<string> errors)
        {
            Errors = errors.ToList();
        }
    }
}
namespace GlobalExceptionHandling.Exceptions
{
    public class CustomInternalServerErrorException : Exception
    {
        public List<string> Errors { get; }

        public CustomInternalServerErrorException(string error)
        {
            Errors = new List<string>()
            {
                error
            };
        }
    }
}

Ve yine ana dizine Middlewares adında bir klasör daha açıp bu klasör altından da middleware ımızı oluşturacağımız CustomExceptionMiddleware.cs sınıfı oluşturup aşağıdaki şekilde düzenliyoruz.

using System.Text.Json;
using ChatBuddyServerApp.Exceptions;

namespace GlobalExceptionHandling.Middlewares
{

    public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate _next;

        public CustomExceptionMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (CustomValidationException ex)
            {
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = StatusCodes.Status400BadRequest;

                var response = new
                {
                    type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
                    title = "Validation errors occurred.",
                    status = 400,
                    traceId = context.TraceIdentifier,
                    errors = ex.Errors
                };

                var jsonResponse = JsonSerializer.Serialize(response);
                await context.Response.WriteAsync(jsonResponse);
            }
            catch (CustomIdentityException ex)
            {
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = StatusCodes.Status400BadRequest;

                var response = new
                {
                    type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
                    title = "Identity errors occurred.",
                    status = 400,
                    traceId = context.TraceIdentifier,
                    errors = ex.Errors
                };

                var jsonResponse = JsonSerializer.Serialize(response);
                await context.Response.WriteAsync(jsonResponse);
            }
            catch (CustomInternalServerErrorException ex)
            {
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = StatusCodes.Status400BadRequest;

                var response = new
                {
                    type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
                    title = "An unexpected error occurred.",
                    status = 500,
                    traceId = context.TraceIdentifier,
                    errors = ex.Errors
                };

                var jsonResponse = JsonSerializer.Serialize(response);
                await context.Response.WriteAsync(jsonResponse);
            }
            catch (Exception ex)
            {
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = StatusCodes.Status500InternalServerError;

                var response = new
                {
                    type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
                    title = "An unexpected error occurred.",
                    status = 500,
                    traceId = context.TraceIdentifier,
                    detail = ex.Message
                };

                var jsonResponse = JsonSerializer.Serialize(response);
                await context.Response.WriteAsync(jsonResponse);
            }
        }
    }
}

Şimdi AccountController adında bir controller oluşturup aşağıdaki şekilde düzenliyoruz.

using GlobalExceptionHandling.Exceptions;
using GlobalExceptionHandling.Models;
using GlobalExceptionHandling.ViewModels;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace GlobalExceptionHandling.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
   
    public class AccountController : ControllerBase
    {
        private readonly UserManager<AppUser> _userManager;

        public AccountController(UserManager<AppUser> userManager)
        {
            _userManager = userManager;
        }

        [HttpPost]
        public async Task<IActionResult> Register(RegisterViewModel registerViewModel)
        {
            if (!ModelState.IsValid)
            {
                var errors = ModelState.Values
                     .SelectMany(v => v.Errors.Select(e => e.ErrorMessage))
                     .ToArray();
                throw new CustomValidationException(errors); // Validation hatalarını handle ediyoruz.
            }

            var user = await _userManager.FindByEmailAsync(registerViewModel.Email);

            if (user == null)
            {
                user = await _userManager.FindByNameAsync(registerViewModel.Username);
            }

            user = new AppUser()
            {
                Name = registerViewModel.Name,
                Surname = registerViewModel.Surname,
                UserName = registerViewModel.Username,
                Email = registerViewModel.Email,
                CreatedDate = DateTime.Now
            };

            var result = await _userManager.CreateAsync(user, registerViewModel.Password);

            if (result.Succeeded)
            {
                return StatusCode(201);
            } else
            {              
                throw new CustomIdentityException(result.Errors.Select(x => x.Description)); // Identity hatalarını handle ediyoruz.
            }
        }

        [HttpGet]
        public async Task<IActionResult> Bolme()
        {
            int a = 10;
            int b = 0;
            int c;

            try
            {
                c = a / b;

            }
            catch (DivideByZeroException)
            {
                throw new CustomInternalServerErrorException("Sıfıra Bölme Hatası!"); // Internal Server hatalarını handle ediyoruz.
            }

            return Ok("Bölme İşlemi Başarılı");
        }
    }
}

Yukarıda yorum satırlarında da belirtildiği gibi Validation, Identity ve Internal Server hatalarını tek bir middleware üzerinden handle edip tek formatta hata mesajı döndürmüş oluyoruz. Biz yapmadık ancak burada loglama işlemleri de gerçekleştirilebilir. Son olarak tüm bunların çalışması için Program.cs de yapmamız gereken değişiklikleri yapacağız.

Öncelikle Program.cs ye aşağıdaki servisi ekliyoruz.

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});

Bu sayede program akışının AccountController.cs de bulunan (!ModelState.IsValid) kontrolüne girmesini sağlamış olacağız bunu yapmadığımız takdirde C# bu kontrolü otomatik yapacak ve AccountController’da bulunan ModelState kontrolüne girmeden Validation hata mesajlarını döndüreceği için Validation hatalarını handle edip manipüle etmemiz mümkün olmayacaktır.

Ve son olarak oluşturduğumuz middleware’ı pipeline’a eklememiz gerekmekte. Bunun için aşağıdaki düzenlemeyi yapıyoruz.

var app = builder.Build();

app.UseMiddleware<CustomExceptionMiddleware>(); // En üste eklemiş olduk.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir