ЧАСТЬ 1: Создание вашего первого минимального API с помощью .NET 7

Здравствуйте, сегодня я хотел бы обсудить, как обрабатывать исключения в минимальном API .NET 7 с помощью специального промежуточного программного обеспечения исключений. Кроме того, мы углубимся в настройку Serilog для ведения журналов в рамках этой минимальной настройки API. Пример, который я буду использовать, основан на прошлом проекте, который также был создан с использованием минимального API .NET.

Начнем с изучения специального промежуточного программного обеспечения исключений. Промежуточное ПО служит мощным инструментом для глобального перехвата исключений в вашем приложении. Представьте, что у вас есть функция, которая использует стороннюю службу. Вы отправляете запрос на получение данных, но по какой-то причине не получаете ожидаемого ответа, что приводит к неожиданной ошибке. Хотя локальные методы обработки ошибок, такие как блоки try-catch или операторы if-else, могут быть эффективными, они не всегда доступны или правильно реализованы. Именно здесь в игру вступает глобальное промежуточное программное обеспечение исключений.

Промежуточное программное обеспечение глобальных исключений действует как простое расширение, предназначенное для обработки ошибок на протяжении всего конвейера вашего приложения. У вас есть возможность вернуть пользовательскую модель или напрямую написать сообщение об ошибке в ответе. В этом примере я создал класс ResponseModel.cs специально для использования при генерации ответов об ошибках.

Модель ответа

 public class ResponseModel
 {
     public bool Success { get; set; }
     public string Message { get; set; }
     public int StatusCode { get; set; }
 }

Создание ExceptionHandleMiddleware

 public class ExceptionHandleMiddleware
 {
     private readonly RequestDelegate _next;

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

     public async Task Invoke(HttpContext httpContext)
     {
         try
         {
             await _next(httpContext);
         }
         catch (Exception ex)
         {
             await HandleException(ex, httpContext);
         }
     }

     private async Task HandleException(Exception ex, HttpContext httpContext)
     {


         if (ex is InvalidOperationException)
         {
             httpContext.Response.StatusCode = 400; //HTTP status code
             //httpContext.Response.WriteAsync("Invalid operation");
             //httpContext.Response.WriteAsync("Invalid operation");             
             await httpContext.Response.WriteAsJsonAsync(new ResponseModel
             {
                 Message = "Invalid operation",
                 StatusCode = 400,
                 Success = false
             });
         }
         else if (ex is ArgumentException)
         {
             await httpContext.Response.WriteAsync("Invalid argument");
         }
         else
         {
             await httpContext.Response.WriteAsync("Unknown error");
         }


     }
 }

 // Extension method used to add the middleware to the HTTP request pipeline.
 public static class ExceptionHandleMiddlewareExtensions
 {
     public static IApplicationBuilder UseExceptionHandleMiddleware(this IApplicationBuilder builder)
     {
         return builder.UseMiddleware<ExceptionHandleMiddleware>();
     }
 }

Теперь нам нужно добавить наше промежуточное ПО в класс program.cs. Нам нужно добавить перед методом app.Run(). Поэтому я добавил после app.MapControllers(); метод.

  var builder = WebApplication.CreateBuilder(args);
  


  // Add services to the container.

  builder.Services.AddControllers();
  // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
  builder.Services.AddEndpointsApiExplorer();
  builder.Services.AddSwaggerGen();
  builder.Services.AddDbContext<LiftDb>(opt => opt.UseInMemoryDatabase("LiftList"));
  //builder.Services.AddDatabaseDeveloperPageExceptionFilter();

  var app = builder.Build();

  // Configure the HTTP request pipeline.
  if (app.Environment.IsDevelopment())
  {

      app.UseSwagger();
      app.UseSwaggerUI();
  }

  app.UseHttpsRedirection();

  app.UseAuthorization();

  app.MapControllers();

  app.UseExceptionHandleMiddleware(); // Our middelware to handle exceptions

  var liftsEndPoint = app.MapGroup("/lifts");


  liftsEndPoint.MapGet("/", GetAllLifts);
  liftsEndPoint.MapGet("/getSquats", GetLiftByName);
  liftsEndPoint.MapGet("/{id}", GetLift);
  liftsEndPoint.MapPost("/", CreateLift);
  liftsEndPoint.MapPut("/{id}", UpdateLift);
  liftsEndPoint.MapDelete("/{id}", DeleteLift);
  app.MapGet("/fakeError", FakeError);

  app.Run();

Далее нам нужно интегрировать наше специальное промежуточное программное обеспечение в файл Program.cs. В частности, мы должны включить его перед вызовом метода app.Run(), чтобы гарантировать, что он вступит в силу во время конвейера запросов приложения. В моем примере я добавил его сразу после метода app.MapControllers();.

    static Task<IResult> FakeError()
    {

        throw new InvalidOperationException("Fake error");

    }

Следующим пунктом нашей повестки дня является внедрение Serilog для регистрации исключений. Serilog — очень популярная библиотека .NET, предназначенная для надежного и гибкого ведения журналов. Он предлагает различные уровни ведения журнала, такие как информация, ошибки и предупреждения, что делает его бесценным инструментом для мониторинга состояния вашего приложения.

Прежде чем углубиться, нам нужно установить несколько пакетов Serilog. Основные из них — Serilog, Serilog.AspNetCore и Serilog.Sinks.Console. Если вы хотите войти в файл, вам также нужно добавить Serilog.Sinks.File в свой список пакетов.

Вы можете легко установить эти пакеты через NuGet или, если вы предпочитаете использовать CLI dotnet, вам пригодятся следующие команды:

dotnet add package Serilog
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

После установки Serilog следующим шагом будет инкапсуляция кода в файл Program.cs с помощью блоков try-catch-finally. Это позволит нам эффективно обрабатывать любые исключения, которые могут возникнуть. Кроме того, мы добавим в этот файл конфигурации ведения журнала Serilog. Ниже представлена ​​окончательная версия файла Program.cs:

using LearningCenter.WhatIsMinimalApi.Entity;
using LearningCenter.WhatIsMinimalApi.Middleware;
using LearningCenter.WhatIsMinimalApi.Repository;
using Microsoft.EntityFrameworkCore;
using Serilog;



Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateLogger();

try
{
    Log.Information("Starting web application");

    var builder = WebApplication.CreateBuilder(args);
    builder.Host.UseSerilog(); // <-- Add this line


    // Add services to the container.

    builder.Services.AddControllers();
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    builder.Services.AddDbContext<LiftDb>(opt => opt.UseInMemoryDatabase("LiftList"));
    //builder.Services.AddDatabaseDeveloperPageExceptionFilter();

    var app = builder.Build();

    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {

        app.UseSwagger();
        app.UseSwaggerUI();
    }

    app.UseHttpsRedirection();

    app.UseAuthorization();

    app.MapControllers();

    app.UseExceptionHandleMiddleware(); // Our middelware to handle exceptions

    var liftsEndPoint = app.MapGroup("/lifts");


    liftsEndPoint.MapGet("/", GetAllLifts);
    liftsEndPoint.MapGet("/getSquats", GetLiftByName);
    liftsEndPoint.MapGet("/{id}", GetLift);
    liftsEndPoint.MapPost("/", CreateLift);
    liftsEndPoint.MapPut("/{id}", UpdateLift);
    liftsEndPoint.MapDelete("/{id}", DeleteLift);
    app.MapGet("/fakeError", FakeError);

    app.Run();

    static async Task<IResult> GetAllLifts(LiftDb db)
    {
        return TypedResults.Ok(await db.Lifts.ToArrayAsync());
    }

    static async Task<IResult> GetLiftByName(LiftDb db)
    {
        return TypedResults.Ok(await db.Lifts.Where(t => t.Name == Lift.LiftName.Squat).ToListAsync());
    }

    static async Task<IResult> GetLift(int id, LiftDb db)
    {
        return await db.Lifts.FindAsync(id)
            is Lift todo
                ? Results.Ok(todo)
                : Results.NotFound();
    }

    static async Task<IResult> CreateLift(Lift lift, LiftDb db)
    {
        db.Lifts.Add(lift);
        await db.SaveChangesAsync();

        return TypedResults.Created($"/todoitems/{lift.Id}", lift);
    }

    static async Task<IResult> UpdateLift(int id, Lift inputLift, LiftDb db)
    {
        var lift = await db.Lifts.FindAsync(id);

        if (lift is null) return Results.NotFound();

        lift.Name = inputLift.Name;
        lift.Weight = inputLift.Weight;
        lift.Reps = inputLift.Reps;

        await db.SaveChangesAsync();

        return TypedResults.NoContent();
    }

    static async Task<IResult> DeleteLift(int id, LiftDb db)
    {
        if (await db.Lifts.FindAsync(id) is Lift todo)
        {
            db.Lifts.Remove(todo);
            await db.SaveChangesAsync();
            return Results.NoContent();
        }


        return TypedResults.NotFound();
    }

    static Task<IResult> FakeError()
    {

        throw new InvalidOperationException("Fake error");

    }
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}

Далее мы настроим Serilog для регистрации действий нашего приложения, направляя вывод на консоль. Чтобы регистрировать исключения специально в нашем специальном промежуточном программном обеспечении, мы также интегрируем туда Serilog. В моем примере я добавил Serilog в функцию HandleException для захвата и регистрации любых возникающих исключений.

    private async Task HandleException(Exception ex, HttpContext httpContext)
    {

        Log.Error(ex, "Error happend!"); // serilog

        if (ex is InvalidOperationException)
        {
            //httpContext.Response.WriteAsync("Invalid operation");
            //httpContext.Response.WriteAsync("Invalid operation");
            httpContext.Response.StatusCode = 400;
            await httpContext.Response.WriteAsJsonAsync(new ResponseModel
            {
                Message = "Invalid operation",
                StatusCode = 400,
                Success = false
            });
        }
        else if (ex is ArgumentException)
        {
            await httpContext.Response.WriteAsync("Invalid argument");
        }
        else
        {
            await httpContext.Response.WriteAsync("Unknown error");
        }


    }

Вот и все! Наш API теперь полностью настроен и готов к использованию. Благодаря интеграции Serilog вы теперь можете отслеживать вывод консоли, чтобы увидеть, что происходит, когда в приложении возникают исключения.

Если вы также хотите включить ведение журнала файлов, вы можете легко сделать это, внеся небольшую корректировку в исходный код конфигурации Serilog в файле Program.cs.

Log.Logger = new LoggerConfiguration()
    .WriteTo
    .Console()
    .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day) // add this
    .CreateLogger();

Спасибо за чтение. Надеюсь, вы нашли это руководство полезным для настройки обработки исключений и входа в минимальный API .NET 7 с использованием специального промежуточного программного обеспечения и Serilog.

https://github.com/fahricankacan/LearningCenter/tree/master/LearningCenter.WhatIsMinimalApi