The Result Pattern in .NET
Return success or failure results in your .net applications elegantly.
Start by defining the Error and Result models
// Error.cs
public record Error(string Code, string Message);
// Result.cs
public class Result<T> : Result
{
[JsonPropertyName("data")]
[JsonPropertyOrder(2)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public T Value { get; }
[JsonConstructor]
private Result(int statusCode, bool isSuccess, T value, Error error) : base(statusCode, isSuccess, error)
{
StatusCode = statusCode;
Value = value;
}
public static Result<T> Success(T value, int statusCode = 200) =>
new(statusCode, true, value, null!);
public new static Result<T> Failure(int statusCode, Error error) =>
new(statusCode, false, default!, error);
}
public class Result
{
[JsonPropertyName("status_code")]
[JsonPropertyOrder(1)]
public int StatusCode { get; set; }
[JsonPropertyName("success")]
[JsonPropertyOrder(0)]
public bool IsSuccess { get; }
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public bool IsFailure => !IsSuccess;
[JsonPropertyOrder(3)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Error Error { get; }
[JsonConstructor]
protected Result(int statusCode, bool isSuccess, Error error)
{
StatusCode = statusCode;
IsSuccess = isSuccess;
Error = error;
}
public static Result Success(int statusCode = 200) =>
new(statusCode, true, null!);
public static Result Failure(int statusCode, Error error) => new(statusCode, false, error);
}
Note that the generic result type Result<T> is only required if you want to return data
Define static Error classes based on your application needs. For example:
public static class UserErrors
{
public static Error NotFound =>
new("NOT_FOUND", "The requested user was not found.");
public static Error ProfileUpdateFailed =>
new("PROFILE_UPDATE_FAILED", "Failed to update user profile. Please try again later.");
}
Then use your newly found superpowers like so
if (user is null)
{
logger.LogWarning("User not found for changing password: {UserId}", userId);
return Result.Failure(UserErrors.NotFound);
}
// ...
logger.LogInformation("Password changed successfully for user ID: {UserId}", userId);
return Result.Success();
If you would like to return additional data, then…
return Result<UserProfileDto>.Success(user);
I'd take this over exceptions for flow-control any day. Exceptions have their place; this is not it.