refactor: add Result<T> pattern and global exception handler for consistent error handling

This commit is contained in:
V1ck3s
2026-01-08 19:31:05 +01:00
parent cb37c7f69a
commit 43c9a2e808
4 changed files with 334 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
namespace octo_fiesta.Services.Common;
/// <summary>
/// Represents a typed error with code, message, and metadata
/// </summary>
public class Error
{
/// <summary>
/// Unique error code identifier
/// </summary>
public string Code { get; }
/// <summary>
/// Human-readable error message
/// </summary>
public string Message { get; }
/// <summary>
/// Error type/category
/// </summary>
public ErrorType Type { get; }
/// <summary>
/// Additional metadata about the error
/// </summary>
public Dictionary<string, object>? Metadata { get; }
private Error(string code, string message, ErrorType type, Dictionary<string, object>? metadata = null)
{
Code = code;
Message = message;
Type = type;
Metadata = metadata;
}
/// <summary>
/// Creates a Not Found error (404)
/// </summary>
public static Error NotFound(string message, string? code = null, Dictionary<string, object>? metadata = null)
{
return new Error(code ?? "NOT_FOUND", message, ErrorType.NotFound, metadata);
}
/// <summary>
/// Creates a Validation error (400)
/// </summary>
public static Error Validation(string message, string? code = null, Dictionary<string, object>? metadata = null)
{
return new Error(code ?? "VALIDATION_ERROR", message, ErrorType.Validation, metadata);
}
/// <summary>
/// Creates an Unauthorized error (401)
/// </summary>
public static Error Unauthorized(string message, string? code = null, Dictionary<string, object>? metadata = null)
{
return new Error(code ?? "UNAUTHORIZED", message, ErrorType.Unauthorized, metadata);
}
/// <summary>
/// Creates a Forbidden error (403)
/// </summary>
public static Error Forbidden(string message, string? code = null, Dictionary<string, object>? metadata = null)
{
return new Error(code ?? "FORBIDDEN", message, ErrorType.Forbidden, metadata);
}
/// <summary>
/// Creates a Conflict error (409)
/// </summary>
public static Error Conflict(string message, string? code = null, Dictionary<string, object>? metadata = null)
{
return new Error(code ?? "CONFLICT", message, ErrorType.Conflict, metadata);
}
/// <summary>
/// Creates an Internal Server Error (500)
/// </summary>
public static Error Internal(string message, string? code = null, Dictionary<string, object>? metadata = null)
{
return new Error(code ?? "INTERNAL_ERROR", message, ErrorType.Internal, metadata);
}
/// <summary>
/// Creates an External Service Error (502/503)
/// </summary>
public static Error ExternalService(string message, string? code = null, Dictionary<string, object>? metadata = null)
{
return new Error(code ?? "EXTERNAL_SERVICE_ERROR", message, ErrorType.ExternalService, metadata);
}
/// <summary>
/// Creates a custom error with specified type
/// </summary>
public static Error Custom(string code, string message, ErrorType type, Dictionary<string, object>? metadata = null)
{
return new Error(code, message, type, metadata);
}
}
/// <summary>
/// Categorizes error types for appropriate HTTP status code mapping
/// </summary>
public enum ErrorType
{
/// <summary>
/// Validation error (400 Bad Request)
/// </summary>
Validation,
/// <summary>
/// Resource not found (404 Not Found)
/// </summary>
NotFound,
/// <summary>
/// Authentication required (401 Unauthorized)
/// </summary>
Unauthorized,
/// <summary>
/// Insufficient permissions (403 Forbidden)
/// </summary>
Forbidden,
/// <summary>
/// Resource conflict (409 Conflict)
/// </summary>
Conflict,
/// <summary>
/// Internal server error (500 Internal Server Error)
/// </summary>
Internal,
/// <summary>
/// External service error (502 Bad Gateway / 503 Service Unavailable)
/// </summary>
ExternalService
}

View File

@@ -0,0 +1,99 @@
namespace octo_fiesta.Services.Common;
/// <summary>
/// Represents the result of an operation that can either succeed with a value or fail with an error.
/// This pattern allows explicit error handling without using exceptions for control flow.
/// </summary>
/// <typeparam name="T">The type of the value returned on success</typeparam>
public class Result<T>
{
/// <summary>
/// Indicates whether the operation succeeded
/// </summary>
public bool IsSuccess { get; }
/// <summary>
/// Indicates whether the operation failed
/// </summary>
public bool IsFailure => !IsSuccess;
/// <summary>
/// The value returned on success (null if failed)
/// </summary>
public T? Value { get; }
/// <summary>
/// The error that occurred on failure (null if succeeded)
/// </summary>
public Error? Error { get; }
private Result(bool isSuccess, T? value, Error? error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}
/// <summary>
/// Creates a successful result with a value
/// </summary>
public static Result<T> Success(T value)
{
return new Result<T>(true, value, null);
}
/// <summary>
/// Creates a failed result with an error
/// </summary>
public static Result<T> Failure(Error error)
{
return new Result<T>(false, default, error);
}
/// <summary>
/// Implicit conversion from T to Result&lt;T&gt; for convenience
/// </summary>
public static implicit operator Result<T>(T value)
{
return Success(value);
}
/// <summary>
/// Implicit conversion from Error to Result&lt;T&gt; for convenience
/// </summary>
public static implicit operator Result<T>(Error error)
{
return Failure(error);
}
}
/// <summary>
/// Non-generic Result for operations that don't return a value
/// </summary>
public class Result
{
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error? Error { get; }
private Result(bool isSuccess, Error? error)
{
IsSuccess = isSuccess;
Error = error;
}
public static Result Success()
{
return new Result(true, null);
}
public static Result Failure(Error error)
{
return new Result(false, error);
}
public static implicit operator Result(Error error)
{
return Failure(error);
}
}