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.
10 November 2025