Search code examples
c#loggingasp.net-core-webapiextension-methods

How can I log from inside an extension method?


I have error classes that are used in business services. Discriminated union OneOf<> is used to allow the service method return one result from multiple possible results.

// 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));
    }
}

The business services are injected into endpoints.

// 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()));
    }
}

I extend Error class to produce ProblemDetails as follows. How can I log inside ToProblemDetails() (indicated with // Logging goes here!)?

// 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(NotDeleted error)
        => ToProblemDetails(error, StatusCodes.Status500InternalServerError);
}

Solution

  • You would need to a) pass the logger instance as a parameter, or b) add a static logger to the ErrorMapper class.

    But in reality that is not a good place to log the error. Static methods can't be mocked and logging isn't really the purpose of that function.

    You should be logging in the controller. Something like:

        private static async Task<Results<ProblemHttpResult, Ok<MovieResponse>>>
            DeleteMovieAsync(int id, MovieService ms, CancellationToken ct)
        {
            var result = await ms.DeleteMovieAsync(id, ct);
    
            var response = result.Match<Results<ProblemHttpResult, Ok<MovieResponse>>>(
               notFoundError => notFoundError.ToProblemDetails(),
               notDeletedError => notDeletedError.ToProblemDetails(),
               movie => TypedResults.Ok(movie.MapToMovieResponse()));
    
             if (response is Error) {
                 _logger.Error(response);
             }
    
             return response;
        }