ServiceResult.cs
1 namespace GUNRPG.Application.Results; 2 3 /// <summary> 4 /// Discriminated status for service results to enable pattern matching. 5 /// </summary> 6 public enum ResultStatus 7 { 8 Success, 9 NotFound, 10 InvalidState, 11 ValidationError 12 } 13 14 /// <summary> 15 /// Represents the outcome of a service operation with explicit success/failure states. 16 /// </summary> 17 public abstract class ServiceResultBase 18 { 19 public bool IsSuccess { get; } 20 public ResultStatus Status { get; } 21 public string? ErrorMessage { get; } 22 23 protected ServiceResultBase(bool isSuccess, ResultStatus status, string? errorMessage = null) 24 { 25 IsSuccess = isSuccess; 26 Status = status; 27 ErrorMessage = errorMessage; 28 } 29 } 30 31 /// <summary> 32 /// Service result that carries a value on success. 33 /// </summary> 34 public sealed class ServiceResult<T> : ServiceResultBase 35 { 36 public T? Value { get; } 37 38 private ServiceResult(bool isSuccess, ResultStatus status, T? value, string? errorMessage) 39 : base(isSuccess, status, errorMessage) 40 { 41 Value = value; 42 } 43 44 public static ServiceResult<T> Success(T value) => new(true, ResultStatus.Success, value, null); 45 public static ServiceResult<T> NotFound(string? message = null) => new(false, ResultStatus.NotFound, default, message ?? "Resource not found"); 46 public static ServiceResult<T> InvalidState(string message) => new(false, ResultStatus.InvalidState, default, message); 47 public static ServiceResult<T> ValidationError(string message) => new(false, ResultStatus.ValidationError, default, message); 48 49 /// <summary> 50 /// Creates a ServiceResult<T> from a non-generic ServiceResult, preserving the error state. 51 /// This should only be used to convert error results; Success results should use Success(T value) directly. 52 /// </summary> 53 public static ServiceResult<T> FromResult(ServiceResult result) 54 { 55 return result.Status switch 56 { 57 ResultStatus.Success => throw new InvalidOperationException("Cannot convert Success result without a value. Use Success(T value) instead."), 58 ResultStatus.NotFound => NotFound(result.ErrorMessage), 59 ResultStatus.InvalidState => InvalidState(result.ErrorMessage!), 60 ResultStatus.ValidationError => ValidationError(result.ErrorMessage!), 61 _ => InvalidState(result.ErrorMessage!) 62 }; 63 } 64 65 /// <summary> 66 /// Creates a ServiceResult<T> from another ServiceResult<TOther>, preserving the error state. 67 /// This should only be used to convert error results; Success results should use Success(T value) directly. 68 /// </summary> 69 public static ServiceResult<T> FromResult<TOther>(ServiceResult<TOther> result) 70 { 71 return result.Status switch 72 { 73 ResultStatus.Success => throw new InvalidOperationException("Cannot convert Success result without a value. Use Success(T value) instead."), 74 ResultStatus.NotFound => NotFound(result.ErrorMessage), 75 ResultStatus.InvalidState => InvalidState(result.ErrorMessage!), 76 ResultStatus.ValidationError => ValidationError(result.ErrorMessage!), 77 _ => InvalidState(result.ErrorMessage!) 78 }; 79 } 80 } 81 82 /// <summary> 83 /// Service result without a value. 84 /// </summary> 85 public sealed class ServiceResult : ServiceResultBase 86 { 87 private ServiceResult(bool isSuccess, ResultStatus status, string? errorMessage) 88 : base(isSuccess, status, errorMessage) 89 { 90 } 91 92 public static ServiceResult Success() => new(true, ResultStatus.Success, null); 93 public static ServiceResult NotFound(string? message = null) => new(false, ResultStatus.NotFound, message ?? "Resource not found"); 94 public static ServiceResult InvalidState(string message) => new(false, ResultStatus.InvalidState, message); 95 public static ServiceResult ValidationError(string message) => new(false, ResultStatus.ValidationError, message); 96 }