-
Notifications
You must be signed in to change notification settings - Fork 19
FluentResults usage
In modern development, the approach to coding logic is shifting from the ‘happy path + exception’ model to a ‘Result and exceptions’ model. Exceptions are used only when something unpredictable occurs, such as a network issue. Essentially, the ‘Result’ is an object that can have two states: Ok or Not Ok. Arc4u uses FluentResults to manage this.
So, what additional value does Arc4u bring to this existing concept?
In my opinion, the issue with ‘Result’ is the additional number of if/else statements in our code to handle each instance of the ‘IsSuccess’ or ‘IsFailed’ methods. Moreover, when the result is ‘Ok’, we may need to test whether the value is null or not. I began to notice that a common scenario was becoming lengthy to implement and less readable.
To address this, I started adding extension methods to ‘Result’ or ‘Result’. This allows us to focus more on the business implementation in a fluent manner.
With the Result type I have no content only the information that the action was running well or not and based on this I will perform or not some other actions.
// if else
var result = await db.SaveAsync(...);
if (result.IsSuccess)
{
// do next logic with maybe other results...
// Synchronous or asynchronous
}
else
{
// log information or report a ProblemDetails to the user of the Api...
}
// With the usage of the fluent we can build a code more elegant.
await db.SaveAsync(...)
.OnSuccess(() =>
{
// do next logic synchronously with maybe other results...
})
.OnSuccessAsync(async () =>
{
// perform asynchronous call here!
})
.LogIfFailed()
.OnFailed( _ =>
{
// Report a ProblemDetails to the user of the Api...
})
.OnFailedAsync(async () =>
{
// perform asynchronous call here!
}).ConfigureAwait(false);
This pattern provides better code readability in terms of what is expected in the case where the code executes correctly or not.
The LogIfFailed method clearly indicates that errors will be logged in the logging system.
The OnFailed and OnFailedAsync APIs receive a collection of errors (of type IError) as parameter, so the code can act according to these errors.
When working with an entity, different scenarios can arise depending on whether the data is null or not. To ease that, 2 other extra extension method exists: OnSuccessNull(Async) and OnSuccessNotNull(Async).
await db.CreateAsync(...)
.OnSuccess((entity) =>
{
// entity can be null here. Only IsSuccess is checked to go here.
// do next logic synchronously with maybe other results...
})
.OnSuccessAsync(async (entity) =>
{
// perform asynchronous call here!
})
.OnSuccessNull(() =>
{
// Do what you need synchronously when the result is a success but the data is null.
})
.OnSuccessNullAsync(async () =>
{
// Do what you need asynchronously when the result is a success but the data is null.
})
.OnSuccessNotNull((entity) =>
{
// Do what you need synchronously when the result is a success but the data is not null.
})
.OnSuccessNotNullAsync(async (entity) =>
{
// Do what you need asynchronously when the result is a success but the data is not null.
})
.LogIfFailed()
.OnFailed( _ =>
{
// Report a ProblemDetails to the user of the Api...
})
.OnFailedAsync(async () =>
{
// perform asynchronous call here!
}).ConfigureAwait(false);
FluentResult natively add 2 implementation of IError: Error and ExceptionalError.
Reporting exceptions is done by adding an ExceptionalError => result.WithError(exceptionError).
When we are resulting a problem to a consumer of API, the best is to report a ProblemDetails (see RFC 7807). So it becomes possible to give a Detail, Title but also a Type, etc...
This is the reason Arc4u is bringing the concept of ProblemDetailError.
This is a specific error with all the attributes needed to build a ProblemDetails message to give back to the consumer of the Api.
Another concept added by Arc4u is the ValidationError to report an error from a ValidationResult (concept coming from the FluentValidation library).
This will be used to report another implementation of the RFC 7807 but containing a specific collection of Errors.
The fluent api is built by returning the original result! Meaning that when you use OnSucess(() => ) for example you don't have the capability to change the result on the fly.
This is done by purpose to avoid unexpected behavior between what you expect when reading the code and what you run.
This means that if you create a method returning a result and the code is itself chaining OnSuccess and OnFailed methods, the question can be how I can handle the final result of the method?
Task<Result> DoSomethingAsync()
{
// If no problem arose, the result is fine.
Result result = Result.Ok();
await db.CreateAsync(...)
.OnSuccess((entity) =>
{
// entity can be null here. Only IsSuccess is checked to go here.
// do next logic synchronously with maybe other results...
})
.LogIfFailed()
.OnFailed( _ =>
{
// Report a ProblemDetails to the user of the Api...
==> }, result).ConfigureAwait(false);
return result;
}
The OnFailed[Async] methods can be populated by a globalResult, where errors are automatically added to the result’s error collection.
This means that result.IsSuccess will be false.
I have not yet found a better way to manage this. Please feel free to start a discussion if you have a better approach.
I hope this concept will help you better organize your implementation logic!