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