PhotoFlow Documentation Help

Error Handling

PhotoFlow implements a structured error handling system with error codes for consistent, translatable error responses.

Error Handling Architecture

Response

Exception Middleware

Error Types

Request Flow

Controller

MediatR Handler

FluentValidation

Domain Service

ValidationException

DomainException

InvalidValidatorException

ExceptionHandler

IStringLocalizer

Error Response JSON

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 }

Exception Middleware

Global Exception Handler

public class ExceptionHandlingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<ExceptionHandlingMiddleware> _logger; private readonly IStringLocalizer<ErrorMessages> _localizer; public async Task InvokeAsync(HttpContext context) { try { await _next(context); } catch (InvalidValidatorException ex) { await HandleValidationExceptionAsync(context, ex); } catch (DomainException ex) { await HandleDomainExceptionAsync(context, ex); } catch (Exception ex) { await HandleUnexpectedExceptionAsync(context, ex); } } private async Task HandleValidationExceptionAsync( HttpContext context, InvalidValidatorException ex) { var message = _localizer[ex.ErrorCode, ex.Parameters]; var response = new ErrorResponse { Code = ex.ErrorCode, Message = message, Timestamp = DateTime.UtcNow }; context.Response.StatusCode = 400; await context.Response.WriteAsJsonAsync(response); } }

Error Response Format

{ "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" } ] }

Localization

Resource Files

Error messages support multiple languages:

Resources/i18n/ErrorMessage/ ├── en.json └── vi.json

English (en.json)

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

Frontend Error Handling

Angular Error Interceptor

@Injectable() export class ErrorInterceptor implements HttpInterceptor { constructor( private translateService: TranslocoService, private snackBar: MatSnackBar ) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { if (error.error?.code) { const message = this.translateService.translate( `errors.${error.error.code}` ); this.snackBar.open(message, 'Close', { duration: 5000 }); } return throwError(() => error); }) ); } }
Last modified: 20 December 2025