A couple of years ago I found myself using the results pattern quite a lot whilst working on projects with my company, Purple Piranha. I found however that there was a lot of repetition between projects and wanted to standardise the way that it worked.
There were two key things that I wanted to ensure that my solution would have:
- A fluent syntax - I had seen examples of how this could work, but I couldn't find any existing libraries that did this. A fluent syntax is beneficial as it gives better readability.
- Strongly typed failures - all of the examples I had seen used some kind of hard-coded constant and this didn't sit right. By creating a type for each failure it's easier to make a decision as to how to proceed for each failure and also to return any additional data that may be necessary.
I started work on an open-source library called Fluent Results. It's available as a Nuget Package and the source code can be found on GitHub.
This library is now one of the standard libraries that my company uses for pretty much every project.
What is the Result Pattern?
The Result Pattern encapsulates the outcome of an operating, distinctly separating a success from a failure. This is done by wrapping the outcome in a Result object which contains the operation's return data (if any) for a success or the details of the error for a failure.
For example, a method to get a user from the database:
public Result<User> GetUserById(int userId)
{
var user = dbContext.Set<User>().SingleOrDefault(u => u.Id == userId);
if (user is null)
return Result.FailureResult(new UserDoesNotExistFailure(userId));
return Result.SuccessResult(user);
}
Then handling this in our application:
var result = userService.GetUserById(userId);
if (result.IsFailure)
DisplayErrorMessage(result.Failure);
else
DisplayUserDetails(result.Value);
Why use the Result Pattern?
- It gives a clear and concise structure to the code and improves readability and consistency throughout an application.
- Errors are dealt with by default and it encourages developers to think about the types of errors that they will need to handle for a given operation. It reduces the likelihood that Exceptions will be used for control flow rather than for a genuine exception.
- Debugging is easier as the result can carry quite detailed information about the failure.
What's different about the Fluent Results package?
As I said earlier, I could see that a fluent syntax would be beneficial. The Fluent Results package combines this along with the traditional syntax shown above. This fluent syntax improves readability by allowing us to handle the result in the following way:
var result = userService.GetUserById(userId);
result
.OnSuccess(user => {
DisplayUserDetails(user);
})
.OnFailure(failure => {
DisplayErrorMessage(failure);
})
It is also possible to use the package asynchronously:
var result = await userService.GetUserByIdAsync(userId);
await result
.OnSuccess(async user => {
await DisplayUserDetailsAsync(user);
})
.OnFailure(async failure => {
await DisplayErrorMessageAsync(failure);
});
There are times when you may need to return a value from a fluent operation. For example, you may need to return something based on the operation, or cascade a failure down. You may also want to use result handling in a controller method that returns an ActionResult. These can all be achieved simply:
// Synchronous version
public Result<string> GetUserName(int userId)
{
var user = userService.GetUserById(userId);
return result
.Returning<string>()
.OnSuccess(user => {
return $"{ user.FirstName } { user.LastName }";
})
.OnFailure(failure => {
return Result.FailureResult(failure); // failure propagated down
})
.Return();
}
// Asynchronous version
public Task<Result<string>> GetUserNameAsync(int userId)
{
var user = await userService.GetUserByIdAsync(userId);
return result
.AsyncReturning<string>()
.OnSuccess(async user => {
var userName = await FunkyUserNameProcessingMethodAsync(user);
return userName;
})
.OnFailure(async failure => {
return Result.FailureResult(failure); // failure propagated down
})
.ReturnAsync();
}
// Action Results
[HttpPost]
public async Task<IActionResult> Register(RegisterViewModel model)
{
var result = ... // some operating to register a user
return await result
.AsyncReturningActionResult()
.OnSuccess(async user => {
await LoginUser(user);
return RedirectToAction(nameof(VerifyEmailAddress));
})
.OnFailure(async failure => {
// Handle Failure - omitted for brevity
})
.ReturnAsync();
}
Creating and handling failure types
All failure types inherit from a base class of 'Failure'. It's sensible to create a separate base class for each application and for large systems maybe on a per module basis:
public abstract class MyApplicationFailure : Failure
{
public MyApplicationFailure(
string failureName,
string message,
object? obj = null
) : base($"Dummy.MyApplication.{failureName}", message, obj) { }
}
public abstract class UserModuleFailure : MyApplicationFailure
{
public UserModuleFailure (
string failureName,
string message,
object? obj = null
) : base($"UserModule.{failureName}", message, obj) { }
}
public class UserDoesNotExistFailure : UserModuleFailure
{
public UserDoesNotExistFailure(int userId) :
base($"{nameof(UserDoesNotExistFailure)}", $"User '{userId}' does not exist.", userId) {}
public int UserId => (int)base.Obj;
}
Handling failures is then quite simple within our OnFailure
method.
[HttpPost]
public async Task<IActionResult> Register(RegisterViewModel model)
{
...
return await result
.AsyncReturningActionResult()
.OnSuccess(async user => {
await LoginUser(user);
return RedirectToAction(nameof(VerifyEmailAddress));
})
.OnFailure(async failure => {
if (failure is NotAuthorisedFailure)
return StatusCode(401);
if (failure is UserDoesNotExistFailure userDoesNotExistFailure)
model.SetMessage(
$"Sorry, we couldn't find a user with the identifier '{userDoesNotExistFailure.UserId}'!!!"
);
// handle any other failures
return View(model);
})
.ReturnAsync();
}
Wrapping up
The Fluent Results package allows us to control flow within an application in a clearly defined and logical way. It aids readability of code and makes for a consistent approach throughout all layers of an application. For more details on how to implement it please take a look at GitHub.