Exception Mappers
Exception Mappers
Exception mappers transform exceptions into structured error responses. Create custom mappers for your domain exceptions to return meaningful HTTP responses with appropriate status codes and error data.
Creating a Custom Mapper
Inherit from ExceptionMapper<TException> and implement the MapException method:
using System.Net;
using AspNetConventions.ExceptionHandling.Mappers;
using AspNetConventions.ExceptionHandling.Models;
using AspNetConventions.Http.Services;
using Microsoft.Extensions.Logging;
public class OrderNotFoundExceptionMapper : ExceptionMapper<OrderNotFoundException>
{
public override ExceptionDescriptor MapException(
OrderNotFoundException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = "ORDER_NOT_FOUND",
StatusCode = HttpStatusCode.NotFound,
Message = exception.Message,
Value = new { exception.OrderId },
LogLevel = LogLevel.Warning,
ShouldLog = true
};
}
}
Registering Your Mapper
builder.Services.AddControllers()
.AddAspNetConventions(options =>
{
options.Exceptions.Mappers.Add(new OrderNotFoundExceptionMapper());
});
ExceptionMapper<T> Base Class
The ExceptionMapper<TException> base class provides:
public abstract class ExceptionMapper<TException> : IExceptionMapper
where TException : Exception
{
// Override this to customize type matching
public virtual bool CanMapException(Exception exception, RequestDescriptor request)
{
return exception is TException;
}
// Implement this to transform the exception
public abstract ExceptionDescriptor MapException(
TException exception,
RequestDescriptor request);
}
CanMapException
Override CanMapException for custom matching logic:
public class HttpExceptionMapper : ExceptionMapper<HttpRequestException>
{
public override bool CanMapException(Exception exception, RequestDescriptor request)
{
// Match any HttpRequestException, including derived types
return exception is HttpRequestException;
}
public override ExceptionDescriptor MapException(
HttpRequestException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = "HTTP_REQUEST_FAILED",
StatusCode = HttpStatusCode.BadGateway,
Message = "External service request failed.",
LogLevel = LogLevel.Error
};
}
}
Building ExceptionDescriptor
The ExceptionDescriptor controls every aspect of the error response:
Simple Descriptor
return new ExceptionDescriptor
{
Type = "INVALID_REQUEST",
StatusCode = HttpStatusCode.BadRequest,
Message = "The request was invalid."
};
Full Descriptor
return new ExceptionDescriptor
{
Type = "PAYMENT_FAILED",
StatusCode = HttpStatusCode.PaymentRequired,
Message = "Payment processing failed.",
Value = new
{
TransactionId = exception.TransactionId,
ErrorCode = exception.ErrorCode,
Reason = exception.Reason
},
ShouldLog = true,
LogLevel = LogLevel.Critical
};
See ExceptionDescriptor for more information.
Common Mapper Patterns
Not Found Exception
public class ResourceNotFoundException : Exception
{
public string ResourceType { get; }
public string ResourceId { get; }
public ResourceNotFoundException(string resourceType, string resourceId)
: base($"{resourceType} '{resourceId}' was not found.")
{
ResourceType = resourceType;
ResourceId = resourceId;
}
}
public class ResourceNotFoundExceptionMapper : ExceptionMapper<ResourceNotFoundException>
{
public override ExceptionDescriptor MapException(
ResourceNotFoundException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = "RESOURCE_NOT_FOUND",
StatusCode = HttpStatusCode.NotFound,
Message = exception.Message,
Value = new
{
exception.ResourceType,
exception.ResourceId
},
LogLevel = LogLevel.Warning
};
}
}
Validation Exception
public class BusinessValidationException : Exception
{
public Dictionary<string, string[]> Errors { get; }
public BusinessValidationException(Dictionary<string, string[]> errors)
: base("Business validation failed.")
{
Errors = errors;
}
}
public class BusinessValidationExceptionMapper : ExceptionMapper<BusinessValidationException>
{
public override ExceptionDescriptor MapException(
BusinessValidationException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = "BUSINESS_VALIDATION_ERROR",
StatusCode = HttpStatusCode.UnprocessableEntity,
Message = exception.Message,
Value = exception.Errors,
ShouldLog = false // Expected validation errors don't need logging
};
}
}
Conflict Exception
public class DuplicateEntityException : Exception
{
public string EntityType { get; }
public string ConflictingField { get; }
public object ConflictingValue { get; }
public DuplicateEntityException(string entityType, string field, object value)
: base($"A {entityType} with {field} '{value}' already exists.")
{
EntityType = entityType;
ConflictingField = field;
ConflictingValue = value;
}
}
public class DuplicateEntityExceptionMapper : ExceptionMapper<DuplicateEntityException>
{
public override ExceptionDescriptor MapException(
DuplicateEntityException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = "DUPLICATE_ENTITY",
StatusCode = HttpStatusCode.Conflict,
Message = exception.Message,
Value = new
{
EntityType = exception.EntityType,
Field = exception.ConflictingField,
Value = exception.ConflictingValue
},
LogLevel = LogLevel.Warning
};
}
}
Rate Limit Exception
public class RateLimitExceededException : Exception
{
public int RetryAfterSeconds { get; }
public string Limit { get; }
public RateLimitExceededException(int retryAfter, string limit)
: base("Rate limit exceeded.")
{
RetryAfterSeconds = retryAfter;
Limit = limit;
}
}
public class RateLimitExceptionMapper : ExceptionMapper<RateLimitExceededException>
{
public override ExceptionDescriptor MapException(
RateLimitExceededException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = "RATE_LIMIT_EXCEEDED",
StatusCode = HttpStatusCode.TooManyRequests,
Message = $"Rate limit exceeded. Retry after {exception.RetryAfterSeconds} seconds.",
Value = new
{
RetryAfterSeconds = exception.RetryAfterSeconds,
Limit = exception.Limit
},
ShouldLog = false // Expected condition, no need to log
};
}
}
External Service Exception
public class ExternalServiceException : Exception
{
public string ServiceName { get; }
public int? ServiceErrorCode { get; }
public ExternalServiceException(string serviceName, string message, int? errorCode = null)
: base(message)
{
ServiceName = serviceName;
ServiceErrorCode = errorCode;
}
}
public class ExternalServiceExceptionMapper : ExceptionMapper<ExternalServiceException>
{
public override ExceptionDescriptor MapException(
ExternalServiceException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = "EXTERNAL_SERVICE_ERROR",
StatusCode = HttpStatusCode.BadGateway,
Message = $"External service '{exception.ServiceName}' is unavailable.",
Value = new
{
Service = exception.ServiceName,
ErrorCode = exception.ServiceErrorCode
},
LogLevel = LogLevel.Error
};
}
}
Mappers with Dependencies
For mappers that need injected services, create them with dependencies and register appropriately:
public class LoggingExceptionMapper : ExceptionMapper<MyException>
{
private readonly ILogger<LoggingExceptionMapper> _logger;
private readonly IMetricsService _metrics;
public LoggingExceptionMapper(
ILogger<LoggingExceptionMapper> logger,
IMetricsService metrics)
{
_logger = logger;
_metrics = metrics;
}
public override ExceptionDescriptor MapException(
MyException exception,
RequestDescriptor request)
{
// Use injected services
_metrics.IncrementCounter("my_exception_count");
return new ExceptionDescriptor
{
Type = "MY_ERROR",
StatusCode = HttpStatusCode.InternalServerError,
Message = exception.Message
};
}
}
// Registration with DI
var serviceProvider = builder.Services.BuildServiceProvider();
var mapper = new LoggingExceptionMapper(
serviceProvider.GetRequiredService<ILogger<LoggingExceptionMapper>>(),
serviceProvider.GetRequiredService<IMetricsService>());
builder.Services.AddControllers()
.AddAspNetConventions(options =>
{
options.Exceptions.Mappers.Add(mapper);
});
Logging Best Practices
When to Log:
| Scenario | ShouldLog | LogLevel |
|---|---|---|
| Unexpected errors | true |
Error or Critical |
| Client errors (bad input) | true |
Warning |
| Validation failures | false |
— |
| Rate limiting | false |
— |
| Not found (expected) | true |
Warning or Information |
| External service failures | true |
Error |
Log Level Guidelines:
// Critical — System is unusable
LogLevel = LogLevel.Critical // Database down, critical service failure
// Error — Operation failed
LogLevel = LogLevel.Error // Unexpected exceptions, failed operations
// Warning — Something unexpected but handled
LogLevel = LogLevel.Warning // Not found, invalid arguments
// Information — Normal operation details
LogLevel = LogLevel.Information // Successful but noteworthy events