PhotoFlow implements a structured error handling system with error codes for consistent, translatable error responses.
Error Handling Architecture
Error Code System
Domain-Specific Error Codes
Error codes are organized in domain-specific enumerations:
// AlbumErrorCode.cs
public enum AlbumErrorCode
{
/// <summary>
/// The album ID is required when deleting the Album
/// </summary>
ALBUM_100A,
/// <summary>
/// The album with id {id} was not found
/// </summary>
ALBUM_101A,
/// <summary>
/// The album is not in a valid state for this operation
/// </summary>
ALBUM_102A,
/// <summary>
/// Cannot transition from {fromState} to {toState}
/// </summary>
ALBUM_103A
}
// PhotoErrorCode.cs
public enum PhotoErrorCode
{
/// <summary>
/// The photo ID is required
/// </summary>
PHOTO_100A,
/// <summary>
/// The photo with id {id} was not found
/// </summary>
PHOTO_101A
}
// StorageErrorCode.cs
public enum StorageErrorCode
{
/// <summary>
/// Provider ID is required
/// </summary>
STORAGE_99,
/// <summary>
/// Search term exceeds maximum length
/// </summary>
STORAGE_008
}
Error Code Naming Convention
Pattern
Description
Example
{DOMAIN}_{NUMBER}{LETTER}
Standard format
ALBUM_101A
{DOMAIN}
Domain prefix
ALBUM, PHOTO, STORAGE
{NUMBER}
Error category
100 = validation, 200 = not found
{LETTER}
Severity
A = error, W = warning
Exception Types
InvalidValidatorException
For validation and business rule errors:
public class InvalidValidatorException : Exception
{
public string ErrorCode { get; }
public object[] Parameters { get; }
public InvalidValidatorException(string errorCode, params object[] parameters)
: base($"Validation error: {errorCode}")
{
ErrorCode = errorCode;
Parameters = parameters;
}
}
// Usage
throw new InvalidValidatorException(
nameof(AlbumErrorCode.ALBUM_101A),
albumId);
DomainException
For domain logic errors:
public class DomainException : Exception
{
public string ErrorCode { get; }
public DomainException(string errorCode, string message)
: base(message)
{
ErrorCode = errorCode;
}
}
// Usage
throw new DomainException(
nameof(AlbumErrorCode.ALBUM_103A),
$"Cannot transition from {fromState} to {toState}");
FluentValidation Integration
Validation rules use error codes:
public class CreateAlbumCommandValidator : AbstractValidator<CreateAlbumCommand>
{
public CreateAlbumCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithErrorCode(nameof(AlbumErrorCode.ALBUM_100A));
RuleFor(x => x.ProviderId)
.NotNull()
.WithErrorCode(nameof(StorageErrorCode.STORAGE_99));
RuleFor(x => x.Name)
.MaximumLength(AlbumConfiguration.NameMaxLength)
.WithErrorCode(nameof(AlbumErrorCode.ALBUM_104A))
.WithState(_ => new FluentValidationState
{
ValidationType = ValidatorType.MaxLength,
ValidationValue = AlbumConfiguration.NameMaxLength.ToString()
});
}
}
FluentValidationState
public class FluentValidationState
{
public ValidatorType ValidationType { get; set; }
public string ValidationValue { get; set; }
}
public enum ValidatorType
{
MaxLength,
MinLength,
Required,
Email,
Regex,
Range
}
{
"code": "ALBUM_101A",
"message": "The album with ID '550e8400-e29b-41d4-a716-446655440000' was not found",
"timestamp": "2024-01-15T10:30:00Z",
"traceId": "abc123",
"details": null
}
Validation Error Response
{
"code": "VALIDATION_ERROR",
"message": "One or more validation errors occurred",
"timestamp": "2024-01-15T10:30:00Z",
"traceId": "abc123",
"details": [
{
"field": "name",
"code": "ALBUM_100A",
"message": "The album ID is required when deleting the Album"
},
{
"field": "providerId",
"code": "STORAGE_99",
"message": "Provider ID is required"
}
]
}
{
"ALBUM_100A": "The album ID is required when deleting the Album",
"ALBUM_101A": "The album with ID '{0}' was not found",
"ALBUM_102A": "The album is not in a valid state for this operation",
"ALBUM_103A": "Cannot transition from {0} to {1}",
"PHOTO_100A": "The photo ID is required",
"PHOTO_101A": "The photo with ID '{0}' was not found",
"STORAGE_99": "Provider ID is required",
"STORAGE_008": "Search term exceeds maximum length of {0} characters"
}
Vietnamese (vi.json)
{
"ALBUM_100A": "ID album là bắt buộc khi xóa Album",
"ALBUM_101A": "Không tìm thấy album có ID '{0}'",
"ALBUM_102A": "Album không ở trạng thái hợp lệ cho thao tác này",
"ALBUM_103A": "Không thể chuyển từ trạng thái {0} sang {1}",
"PHOTO_100A": "ID ảnh là bắt buộc",
"PHOTO_101A": "Không tìm thấy ảnh có ID '{0}'",
"STORAGE_99": "ID nhà cung cấp là bắt buộc",
"STORAGE_008": "Từ khóa tìm kiếm vượt quá độ dài tối đa {0} ký tự"
}
HTTP Status Codes
Exception Type
Status Code
Description
ValidationException
400
Bad Request
InvalidValidatorException
400
Bad Request
UnauthorizedException
401
Unauthorized
ForbiddenException
403
Forbidden
NotFoundException
404
Not Found
DomainException
422
Unprocessable Entity
Exception
500
Internal Server Error
MediatR Pipeline Behavior
Validation is handled in the MediatR pipeline:
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (!_validators.Any())
return await next();
var context = new ValidationContext<TRequest>(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count > 0)
throw new ValidationException(failures);
return await next();
}
}