You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Error类的扩展方法ToProblemDetails中添加日志记录?

问题描述

我在业务服务中使用OneOf<>区分联合类型实现多结果返回,定义了如下错误类:

// Errors
public record Error(string Code, string Description);
public sealed record NotDeleted(int Id, string EntityName) : Error("NotDeleted",
        $"{EntityName} with Id = {Id} cannot be deleted.");
public sealed record NotFound(int Id, string EntityName) : Error("NotFound",
        $"{EntityName} with Id = {Id} cannot be found.");

业务服务类代码:

// Business Services
public sealed class MovieService(AppDbContext db)
{
    private readonly AppDbContext _db = db;
    public async Task<OneOf<NotFound, NotDeleted, Movie>> DeleteMovieAsync(int id, CancellationToken ct)
    {
        var movie = await _db.Movies
                .Include(m => m.Genres)
                .FirstOrDefaultAsync(m => m.Id == id, ct);

        if (movie is null)
            return new NotFound(id, nameof(Movie));

        _db.Movies.Remove(movie);

        return await _db.SaveChangesAsync(ct) > 0
            ? movie
            : new NotDeleted(id, nameof(Movie));
    }
}

业务服务注入到端点的代码:

// Endpoints
public static class MovieEndpoints
{
    public static void MapMovieEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("api/movies");
        group.MapDelete("{id}", DeleteMovieAsync);
    }
    private static async Task<Results<ProblemHttpResult, Ok<MovieResponse>>>
        DeleteMovieAsync(int id, MovieService ms, CancellationToken ct)
    {
        var result = await ms.DeleteMovieAsync(id, ct);

        return result.Match<Results<ProblemHttpResult, Ok<MovieResponse>>>(
           notFoundError => notFoundError.ToProblemDetails(),
           notDeletedError => notDeletedError.ToProblemDetails(),
           movie => TypedResults.Ok(movie.MapToMovieResponse()));
    }
}

我为Error类编写了生成ProblemDetails的扩展方法,现在需要在ToProblemDetails()内部标注位置添加日志记录,该如何实现?

// Mappers
public static class ErrorMapper
{
    private static ProblemHttpResult ToProblemDetails(Error error, int statusCode)
    {
        // Logging goes here!
        return TypedResults.Problem(new ProblemDetails
        {
            Title = error.Code,
            Detail = error.Description,
            Status = statusCode
        });
    }
    public static ProblemHttpResult ToProblemDetails(this NotFound error)
        => ToProblemDetails(error, StatusCodes.Status404NotFound);

    public static ProblemHttpResult ToProblemDetails(this NotDeleted error)
        => ToProblemDetails(error, StatusCodes.Status500InternalServerError);
}
解决方案

由于ErrorMapper是静态类,无法直接依赖注入ILogger,以下是三种可行实现方案:

方案一:将ILogger作为参数传入扩展方法

这是最符合依赖注入原则的方案,修改扩展方法让调用者传入ILogger实例:

  1. 更新ErrorMapper类,添加ILogger参数:
// Mappers
public static class ErrorMapper
{
    private static ProblemHttpResult ToProblemDetails(Error error, int statusCode, ILogger logger)
    {
        // 根据错误状态码选择日志级别
        if (statusCode == StatusCodes.Status404NotFound)
        {
            logger.LogWarning("Error: {Code} - {Description}", error.Code, error.Description);
        }
        else
        {
            logger.LogError("Error: {Code} - {Description}", error.Code, error.Description);
        }

        return TypedResults.Problem(new ProblemDetails
        {
            Title = error.Code,
            Detail = error.Description,
            Status = statusCode
        });
    }

    public static ProblemHttpResult ToProblemDetails(this NotFound error, ILogger logger)
        => ToProblemDetails(error, StatusCodes.Status404NotFound, logger);

    public static ProblemHttpResult ToProblemDetails(this NotDeleted error, ILogger logger)
        => ToProblemDetails(error, StatusCodes.Status500InternalServerError, logger);
}
  1. 在端点方法中注入ILogger并传入:
private static async Task<Results<ProblemHttpResult, Ok<MovieResponse>>>
    DeleteMovieAsync(int id, MovieService ms, ILogger<MovieEndpoints> logger, CancellationToken ct)
{
    var result = await ms.DeleteMovieAsync(id, ct);

    return result.Match<Results<ProblemHttpResult, Ok<MovieResponse>>>(
       notFoundError => notFoundError.ToProblemDetails(logger),
       notDeletedError => notDeletedError.ToProblemDetails(logger),
       movie => TypedResults.Ok(movie.MapToMovieResponse()));
}

方案二:将ErrorMapper改为可注入的非静态类

如果不想每次调用都传递ILogger,可以重构ErrorMapper为依赖注入类:

  1. 修改ErrorMapper为非静态类,注入ILogger:
// Mappers
public class ErrorMapper
{
    private readonly ILogger<ErrorMapper> _logger;

    public ErrorMapper(ILogger<ErrorMapper> logger)
    {
        _logger = logger;
    }

    public ProblemHttpResult ToProblemDetails(Error error, int statusCode)
    {
        if (statusCode == StatusCodes.Status404NotFound)
        {
            _logger.LogWarning("Error: {Code} - {Description}", error.Code, error.Description);
        }
        else
        {
            _logger.LogError("Error: {Code} - {Description}", error.Code, error.Description);
        }

        return TypedResults.Problem(new ProblemDetails
        {
            Title = error.Code,
            Detail = error.Description,
            Status = statusCode
        });
    }

    public ProblemHttpResult ToProblemDetails(NotFound error)
        => ToProblemDetails(error, StatusCodes.Status404NotFound);

    public ProblemHttpResult ToProblemDetails(NotDeleted error)
        => ToProblemDetails(error, StatusCodes.Status500InternalServerError);
}
  1. 在Program.cs中注册服务:
builder.Services.AddScoped<ErrorMapper>();
  1. 在端点方法中注入ErrorMapper并使用:
private static async Task<Results<ProblemHttpResult, Ok<MovieResponse>>>
    DeleteMovieAsync(int id, MovieService ms, ErrorMapper errorMapper, CancellationToken ct)
{
    var result = await ms.DeleteMovieAsync(id, ct);

    return result.Match<Results<ProblemHttpResult, Ok<MovieResponse>>>(
       notFoundError => errorMapper.ToProblemDetails(notFoundError),
       notDeletedError => errorMapper.ToProblemDetails(notDeletedError),
       movie => TypedResults.Ok(movie.MapToMovieResponse()));
}

方案三:通过ServiceProvider获取ILogger(不推荐)

此方案会破坏依赖注入原则,增加代码耦合,仅作为临时备选:

// 在Program.cs中保存ServiceProvider引用
public static class ServiceProviderHolder
{
    public static IServiceProvider? ServiceProvider { get; set; }
}

// Program.cs中初始化
var app = builder.Build();
ServiceProviderHolder.ServiceProvider = app.Services;

// 修改ErrorMapper的ToProblemDetails方法
private static ProblemHttpResult ToProblemDetails(Error error, int statusCode)
{
    if (ServiceProviderHolder.ServiceProvider is null)
        throw new InvalidOperationException("ServiceProvider未初始化");

    var logger = ServiceProviderHolder.ServiceProvider.GetRequiredService<ILogger<ErrorMapper>>();
    
    if (statusCode == StatusCodes.Status404NotFound)
    {
        logger.LogWarning("Error: {Code} - {Description}", error.Code, error.Description);
    }
    else
    {
        logger.LogError("Error: {Code} - {Description}", error.Code, error.Description);
    }

    return TypedResults.Problem(new ProblemDetails
    {
        Title = error.Code,
        Detail = error.Description,
        Status = statusCode
    });
}

内容的提问来源于stack exchange,提问作者D G

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 23:19:53