Code Quality Design Help

Result Pattern Introduction

The Result pattern provides a functional approach to error handling, enabling railway-oriented programming where operations can succeed or fail without throwing exceptions. This pattern makes error states explicit and composable.

Result Type Hierarchy

public abstract record Result<T> { public required string SourceId { get; init; } = string.Empty; public required HttpStatusCode StatusCode { get; init; } = HttpStatusCode.OK; public string CallerMemberName { get; init; } = string.Empty; } public record SuccessResult<T> : Result<T> { public required T Value { get; init; } public SuccessResult( [CallerMemberName] string callerMemberName = "") { CallerMemberName = callerMemberName; } } public record SuccessNullResult<T> : Result<T> { public SuccessNullResult( [CallerMemberName] string callerMemberName = "") { CallerMemberName = callerMemberName; } } public record ProblemDetailsResult<T> : Result<T> { private readonly Lazy<ProblemDetails> _problemDetailsLazy; private readonly ProblemDetails _initialProblemDetails; public required ProblemDetails ProblemDetails { get => _problemDetailsLazy.Value; [MemberNotNull(nameof(_initialProblemDetails))] init => _initialProblemDetails = value; } public ProblemDetailsResult( [CallerMemberName] string callerMemberName = "") { CallerMemberName = callerMemberName; _problemDetailsLazy = new Lazy<ProblemDetails>( CreateExtendedProblemDetails); } private ProblemDetails CreateExtendedProblemDetails() { var detailsCopy = new ProblemDetails { Title = _initialProblemDetails.Title, Status = _initialProblemDetails.Status, Detail = _initialProblemDetails.Detail, Instance = _initialProblemDetails.Instance, Type = _initialProblemDetails.Type, Extensions = new Dictionary<string, object?>( _initialProblemDetails.Extensions) }; detailsCopy.Extensions[nameof(SourceId)] = SourceId; detailsCopy.Extensions[nameof(CallerMemberName)] = CallerMemberName; return detailsCopy; } } public record EmptyContentResult : Result<EmptyContent> { public EmptyContentResult( [CallerMemberName] string callerMemberName = "") { CallerMemberName = callerMemberName; } } public record EmptyContent;

ResultFactory

The ResultFactory provides convenient factory methods for creating Result instances. It supports multiple creation patterns depending on your needs.

Creating Success Results

public static SuccessResult<T> Success<T>( T value, string sourceId, HttpStatusCode statusCode = HttpStatusCode.OK) public static SuccessNullResult<T> SuccessNull<T>( string sourceId, HttpStatusCode statusCode = HttpStatusCode.OK)

Use Success<T> when you have a value to return. Use SuccessNull<T> when the operation succeeded but has no return value (e.g., a DELETE operation).

Creating Error Results

public static ProblemDetailsResult<T> Problem<T>( string sourceId, (string title, string detail, HttpStatusCode statusCode) error)

Creates error results using a named tuple for concise error specification. For compile-time safety with a fluent builder API, see the Type-State Builder Pattern article.

Common Error Helpers

public static class Common { public static ProblemDetailsResult<T> NotFound<T>( string sourceId, string detail) public static ProblemDetailsResult<T> BadRequest<T>( string sourceId, string detail) public static ProblemDetailsResult<T> InternalServerError<T>( string sourceId, string detail) }

Pre-configured factory methods for common HTTP error responses. Additional helpers include Unauthorized, Forbidden, BadGateway, GatewayTimeout, and NotImplemented.

Usage Examples

// Creating success with value var user = new User { Id = 1, Name = "Alice" }; var result = ResultFactory.Success( user, "UserService"); // Creating success without value var deleteResult = ResultFactory.SuccessNull<User>( "UserService"); // Creating error with named tuple var notFoundError = ResultFactory.Problem<User>( "UserService", ("Not Found", "User with ID 123 not found", HttpStatusCode.NotFound)); // Using common error helper var badRequest = ResultFactory.Common.BadRequest<User>( "UserService", "Invalid user data: Email is required");

Conversion Extensions

public static class ResultExtensions { public static IActionResult ToActionResult<T>( this Result<T> result) { return result switch { SuccessResult<T> successResult => new OkObjectResult(successResult.Value), EmptyContentResult => new NoContentResult(), SuccessNullResult<T> => new NoContentResult(), ProblemDetailsResult<T> problemDetailsResult => new ObjectResult( problemDetailsResult.ProblemDetails) { StatusCode = (int)problemDetailsResult.StatusCode }, _ => throw new InvalidOperationException( $"Unknown {nameof(Result<T>)} type.") }; } public static IActionResult ToActionResultCustomSuccess<T>( this Result<T> result) where T : IActionResult { return result switch { SuccessResult<T> successResult => successResult.Value, ProblemDetailsResult<T> problemDetailsResult => new ObjectResult( problemDetailsResult.ProblemDetails) { StatusCode = (int)problemDetailsResult.StatusCode }, _ => throw new InvalidOperationException( $"Unknown {nameof(Result<T>)} type for " + $"{nameof(ToActionResultCustomSuccess)}: " + $"{result.GetType().Name}") }; } public static Result<T> ToResult<T>( this T value, string sourceId, [CallerMemberName] string callerMemberName = "") { return new SuccessResult<T> { Value = value, SourceId = sourceId, StatusCode = HttpStatusCode.OK, CallerMemberName = callerMemberName }; } public static async Task<Result<T>> ToResultAsync<T>( this Task<T> task, string successSourceId, [CallerMemberName] string callerMemberName = "") { try { var value = await task; return new SuccessResult<T> { Value = value, SourceId = successSourceId, StatusCode = HttpStatusCode.OK, CallerMemberName = callerMemberName }; } catch (Exception ex) { return ResultFactory.Common .InternalServerErrorWithTitle<T>( callerMemberName, $"Exception during {callerMemberName}", ex.Message, callerMemberName); } } public static Result<string> ValidateRequired( this string? value, string sourceId, [CallerArgumentExpression("value")] string paramName = "") { if (string.IsNullOrWhiteSpace(value)) { return ResultFactory.Common.BadRequest<string>( sourceId, $"{paramName} is a whitespace string."); } return value!.ToResult(sourceId); } }

These conversion extensions enable lifting ordinary values and operations into the Result<T> context, allowing them to participate in functional pipelines. By converting regular values into Results, you can compose operations using railway-oriented programming patterns where success and failure paths flow naturally through the pipeline.

See Also

10 November 2025