Result Pattern that provides a structured way to represent success or failure with optional details, enhancing readability and maintainability in codebases and designed to be used either in-process or over the network. It is robust, leveraging nullability annotations, immutability (init properties), and factory methods for clarity.
ModResult package contains Result and Result<TValue> implementations with a default Failure implementation which are ready to be used out of the box. It also contains a Result<TValue, TFailure> implementation, but this requires further development for a custom failure class at least.
Additional packages provide out of the box extensions for Result objects to be used in various scenarios:
-
To create invalid Result and Result<TValue> instances from failed FluentValidations.Results.ValidationResult objects, ModResults.FluentValidations package contains default implementations.
-
To convert Result and Result<TValue> instances in either Ok or any FailureType state to Microsoft.AspNetCore.Http.IResult, ModResults.MinimalApis package provide default implementations.
-
To be able to use Result and Result<TValue> objects in Microsoft.Orleans projects, surrogate and converter implementations required by Orleans serializer are implemented in ModResults.Orleans package.
An Ok Result instance can be created by calling Result.Ok() or Result<TValue>.Ok(TValue value) static methods. Result<TValue> also has an implicit operator to convert an instance of type TValue to a Result<TValue> in Ok state.
A Failed Result instance can be created by calling corresponding static method for each FailureType (i.e. Result.NotFound() or Result<TValue>.NotFound() for creating a Result object in Failed state with FailureType NotFound).
public async Task<Result<GetBookByIdResponse>> GetBookById(GetBookByIdRequest req, CancellationToken ct)
{
var entity = await db.Books.FirstOrDefaultAsync(b => b.Id == req.Id, ct);
return entity is null ?
Result<GetBookByIdResponse>.NotFound() :
Result.Ok(new GetBookByIdResponse(
Id: entity.Id,
Title: entity.Title,
Author: entity.Author,
Price: entity.Price));
}
State of a result is either Ok or Failed. State of a result instance can be checked from IsOk and IsFailed boolean properties.
If state is Ok, Result<TValue> instance contains a not null Value property of type TValue.
If state is Failed, Result and Result<TValue> instances contain a not null Failure property of type Failure.
public async Task<Result> PerformGetBookById(GetBookByIdRequest req, CancellationToken ct)
{
var result = await GetBookById(req, ct);
if (result.IsOk)
{
Console.WriteLine($"GetBookById is successful. Book title is {result.Value.Title}");
}
else
{
Console.WriteLine($"GetBookById has failed.");
}
return result
}
All types of Result implementations contain a Statement property which encapsulates collections of Fact and Warning classes.
See various WithFact and WithWarning extension methods to add fact and warning information to result instances.
Default Failure implementation used in Result and Result<TValue> objects, has a Type property holding FailureType and also contains a collection of Errors.
See various static methods of Result objects to create a Failed Result containing Error information. Errors can only be attached to a Failed Result instance during Result instance creation and cannot be added or removed afterwards.
public async Task<Result<GetBookByIdResponse>> GetBookById(GetBookByIdRequest req, CancellationToken ct)
{
var entity = await db.Books.FirstOrDefaultAsync(b => b.Id == req.Id, ct);
return entity is null ?
Result<GetBookByIdResponse>.NotFound($"Book with id: {0} not found.", req.Id)
.WithFact($"dbContextId: {db.ContextId}") :
Result.Ok(new GetBookByIdResponse(
Id: entity.Id,
Title: entity.Title,
Author: entity.Author,
Price: entity.Price))
.WithFact($"dbContextId: {db.ContextId}");
}
A Failed Result instance can be created from an exception by calling corresponding static method with an exception input parameter for each FailureType, or can be left to implicit operator which creates a Failed Result with FailureType CriticalError by default.
Exception object is converted to an Error object and added to Error collection of Failure.
public async Task<Result<GetBookByIdResponse>> GetBookById(GetBookByIdRequest req, CancellationToken ct)
{
try
{
var entity = await db.Books.FirstOrDefaultAsync(b => b.Id == req.Id, ct);
return entity is null ?
Result<GetBookByIdResponse>.NotFound() :
Result.Ok(new GetBookByIdResponse(
Id: entity.Id,
Title: entity.Title,
Author: entity.Author,
Price: entity.Price));
}
catch (Exception ex)
{
return ex;
}
}
Converting a Result<TValue> object to Result is straightforward, and can be achieved by calling parameterless ToResult() method of Result<TValue> instance or can be left to implicit operator.
Any Failure information, Errors, Facts and Warnings are automatically copied to output Result.
These types of conversions require a TValue object creation for Ok state of output Result<TValue> object.
There are various overloads of ToResult() and ToResultAsync() extension methods that accepts TValue object factory functions and additional parameters for such conversions. Any Failure information, Errors, Facts and Warnings are automatically copied to output Result.
public record Request(string Name);
public record Response(string Reply);
public Result<Response> GetResponse(Request req, CancellationToken ct)
{
Result<string> result = await GetMeAResultOfString(req.Name, ct);
return result.ToResult(x => new Response(x));
}
private async Task<Result<string>> GetMeAResultOfString(string name, CancellationToken ct)
{
//some async stuff
await Task.CompletedTask;
return $"Hello {name}";
}
These types of conversions require two seperate output object factory functions for Ok state and Failed state of input Result object.
There are various overloads of Map() and MapAsync() extension methods that accepts object factory functions and additional parameters for such conversions.
ToResult extension methods described previously, also use these Map methods underneath.
public static Result<TTargetValue> ToResult<TValue, TState, TTargetValue>(
this Result<TValue> result,
Func<TValue, TState, TTargetValue> valueFuncOnOk,
TState state)
{
return result.Map<TValue, TState, Result<TTargetValue>>(
(okResult, state) => Result<TTargetValue>.Ok(
valueFuncOnOk(
okResult.Value!,
state))
.WithStatementsFrom(okResult),
(failResult, _) => Result<TTargetValue>.Fail(failResult),
state);
}
}
Convert Result or Result<TValue> object to Minimal Apis Microsoft.AspNetCore.Http.IResult
ModResults.MinimalApis project contains ToResponse() method implementations to convert Result and Result<TValue> instances in either Ok or Failed state to Microsoft.AspNetCore.Http.IResult.
public record GetBookByIdRequest(Guid Id);
public record GetBookByIdResponse(Guid Id, string Title, string Author, decimal Price);
app.MapPost("GetBookById/{Id}",
async Task<IResult> (
[AsParameters] GetBookByIdRequest req,
[FromServices] IBookService svc,
CancellationToken cancellationToken) =>
{
Result<GetBookByIdResponse> result = await svc.GetBookById(req.Id, cancellationToken);
return result.ToResponse();
}).Produces<GetBookByIdResponse>();
If you are using Minimal Apis and want to map a Result or Result<TValue> to api response, do have a look at WebServiceEndpoint implementation in ModEndpoints project which can organize ASP.NET Core Minimal Apis in REPR format endpoints and is integrated with result pattern out of box, which will also handle response mapping.