Examples
Examples
Complete working examples demonstrating Exception Handling across MVC Controllers and Minimal APIs.
Domain Exceptions Library
A common set of domain exceptions for your application:
// Base domain exception
public abstract class DomainException : Exception
{
public string ErrorCode { get; }
protected DomainException(string errorCode, string message)
: base(message)
{
ErrorCode = errorCode;
}
}
// Not found exceptions
public class EntityNotFoundException : DomainException
{
public string EntityType { get; }
public object EntityId { get; }
public EntityNotFoundException(string entityType, object entityId)
: base("ENTITY_NOT_FOUND", $"{entityType} with ID '{entityId}' was not found.")
{
EntityType = entityType;
EntityId = entityId;
}
}
// Validation exception
public class DomainValidationException : DomainException
{
public Dictionary<string, string[]> Errors { get; }
public DomainValidationException(Dictionary<string, string[]> errors)
: base("VALIDATION_FAILED", "One or more validation errors occurred.")
{
Errors = errors;
}
}
// Conflict exception
public class ConflictException : DomainException
{
public string Resource { get; }
public string ConflictReason { get; }
public ConflictException(string resource, string reason)
: base("CONFLICT", $"Conflict: {reason}")
{
Resource = resource;
ConflictReason = reason;
}
}
// Unauthorized exception
public class UnauthorizedAccessException : DomainException
{
public string Resource { get; }
public string RequiredPermission { get; }
public UnauthorizedAccessException(string resource, string permission)
: base("UNAUTHORIZED", $"Access denied to {resource}. Required permission: {permission}")
{
Resource = resource;
RequiredPermission = permission;
}
}
// Rate limit exception
public class RateLimitException : DomainException
{
public int RetryAfterSeconds { get; }
public RateLimitException(int retryAfter)
: base("RATE_LIMITED", "Too many requests. Please slow down.")
{
RetryAfterSeconds = retryAfter;
}
}
Exception Mappers
Mappers for the domain exceptions:
public class EntityNotFoundExceptionMapper : ExceptionMapper<EntityNotFoundException>
{
public override ExceptionDescriptor MapException(
EntityNotFoundException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = exception.ErrorCode,
StatusCode = HttpStatusCode.NotFound,
Message = exception.Message,
Value = new
{
EntityType = exception.EntityType,
EntityId = exception.EntityId
},
LogLevel = LogLevel.Warning,
ShouldLog = true
};
}
}
public class DomainValidationExceptionMapper : ExceptionMapper<DomainValidationException>
{
public override ExceptionDescriptor MapException(
DomainValidationException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = exception.ErrorCode,
StatusCode = HttpStatusCode.BadRequest,
Message = exception.Message,
Value = exception.Errors,
ShouldLog = false // Validation errors are expected
};
}
}
public class ConflictExceptionMapper : ExceptionMapper<ConflictException>
{
public override ExceptionDescriptor MapException(
ConflictException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = exception.ErrorCode,
StatusCode = HttpStatusCode.Conflict,
Message = exception.Message,
Value = new
{
Resource = exception.Resource,
Reason = exception.ConflictReason
},
LogLevel = LogLevel.Warning
};
}
}
public class RateLimitExceptionMapper : ExceptionMapper<RateLimitException>
{
public override ExceptionDescriptor MapException(
RateLimitException exception,
RequestDescriptor request)
{
return new ExceptionDescriptor
{
Type = exception.ErrorCode,
StatusCode = HttpStatusCode.TooManyRequests,
Message = exception.Message,
Value = new { RetryAfterSeconds = exception.RetryAfterSeconds },
ShouldLog = false // Expected behavior, no logging needed
};
}
}
MVC Controller
A complete controller using domain exceptions:
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
// GET /api/orders/{id}
[HttpGet("{id}")]
public ActionResult<Order> GetOrder(int id)
{
var order = _orderService.GetById(id)
?? throw new EntityNotFoundException("Order", id);
return Ok(order);
}
// POST /api/orders
[HttpPost]
public ActionResult<Order> CreateOrder([FromBody] CreateOrderRequest request)
{
// Domain validation
var errors = _orderService.ValidateOrder(request);
if (errors.Any())
throw new DomainValidationException(errors);
// Check for conflicts
if (_orderService.HasDuplicateReference(request.Reference))
throw new ConflictException("Order", $"Order with reference '{request.Reference}' already exists");
var order = _orderService.Create(request);
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
// PUT /api/orders/{id}
[HttpPut("{id}")]
public ActionResult<Order> UpdateOrder(int id, [FromBody] UpdateOrderRequest request)
{
var order = _orderService.GetById(id)
?? throw new EntityNotFoundException("Order", id);
var errors = _orderService.ValidateUpdate(request);
if (errors.Any())
throw new DomainValidationException(errors);
var updated = _orderService.Update(id, request);
return Ok(updated);
}
// DELETE /api/orders/{id}
[HttpDelete("{id}")]
public ActionResult DeleteOrder(int id)
{
var order = _orderService.GetById(id)
?? throw new EntityNotFoundException("Order", id);
_orderService.Delete(id);
return NoContent();
}
}
Response Examples:
GET /api/orders/999 (Not Found):
POST /api/orders (Validation Error):
POST /api/orders (Conflict):
{
"status": "failure",
"statusCode": 404,
"type": "ENTITY_NOT_FOUND",
"message": "Order with ID '999' was not found.",
"errors": {
"entityType": "Order",
"entityId": 999
},
"metadata": { ... }
}
{
"status": "failure",
"statusCode": 400,
"type": "VALIDATION_FAILED",
"message": "One or more validation errors occurred.",
"errors": {
"CustomerEmail": ["Email is required", "Invalid email format"],
"Items": ["At least one item is required"]
},
"metadata": { ... }
}
{
"status": "failure",
"statusCode": 409,
"type": "CONFLICT",
"message": "Conflict: Order with reference 'ORD-123' already exists",
"errors": {
"resource": "Order",
"reason": "Order with reference 'ORD-123' already exists"
},
"metadata": { ... }
}
Minimal API
The same functionality using Minimal APIs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IOrderService, OrderService>();
var app = builder.Build();
var api = app.UseAspNetConventions(options =>
{
// Exception handling configuration
options.Exceptions.Mappers.Add(new EntityNotFoundExceptionMapper());
// Other mappers..
});
// GET /api/orders/{id}
api.MapGet("/api/orders/{id}", (int id, IOrderService orderService) =>
{
var order = orderService.GetById(id)
?? throw new EntityNotFoundException("Order", id);
return Results.Ok(order);
});
// POST /api/orders
api.MapPost("/api/orders", (CreateOrderRequest request, IOrderService orderService) =>
{
var errors = orderService.ValidateOrder(request);
if (errors.Any())
throw new DomainValidationException(errors);
if (orderService.HasDuplicateReference(request.Reference))
throw new ConflictException("Order", $"Order with reference '{request.Reference}' already exists");
var order = orderService.Create(request);
return Results.Created($"/api/orders/{order.Id}", order);
});
// DELETE /api/orders/{id}
api.MapDelete("/api/orders/{id}", (int id, IOrderService orderService) =>
{
var order = orderService.GetById(id)
?? throw new EntityNotFoundException("Order", id);
orderService.Delete(id);
return Results.NoContent();
});
app.Run();
Alerting Integration
Send alerts for critical errors:
options.Exceptions.Hooks.AfterMappingAsync = async (descriptor, mapper, request) =>
{
// Send alerts for critical errors
if (descriptor.LogLevel >= LogLevel.Error)
{
var alertService = request.HttpContext.RequestServices
.GetRequiredService<IAlertService>();
await alertService.SendAlertAsync(new Alert
{
Severity = descriptor.LogLevel.ToString(),
Type = descriptor.Type,
Message = descriptor.Message,
Path = request.Path,
TraceId = request.TraceId,
Timestamp = DateTime.UtcNow
});
}
return descriptor;
};