Afonsomtsm.Result
1.0.0
dotnet add package Afonsomtsm.Result --version 1.0.0
NuGet\Install-Package Afonsomtsm.Result -Version 1.0.0
<PackageReference Include="Afonsomtsm.Result" Version="1.0.0" />
paket add Afonsomtsm.Result --version 1.0.0
#r "nuget: Afonsomtsm.Result, 1.0.0"
// Install Afonsomtsm.Result as a Cake Addin #addin nuget:?package=Afonsomtsm.Result&version=1.0.0 // Install Afonsomtsm.Result as a Cake Tool #tool nuget:?package=Afonsomtsm.Result&version=1.0.0
A Result type for CSharp which allows for error passing without exceptions
The goal of this package is to make errors a part of the domain being modeled in the code and leaving exceptions to do the job they were meant to do which is to warn you of exceptional circumstances. The way i like to think about it is, errors are for the user, exceptions are for the developer. It is also a way to make writing code which can fail more streamlined and simple.
Starting with Options. They allow you to do more and more simply than a simple null reference.
public class User
{
private string _firstName;
private Option<string> _lastName;
// Will concat the first and last name if a last name exists
// otherwise will return the first name.
public string FullName() => _lastName
.Map(last => _firstName + last) // will return an Option<string>
.Or(_firstName);
public bool HasLastName() => _lastName.IsSome();
public int NameLength() => _lastName
.Map(last => last.Length + _firstName.Length) // will return Option<int>
.Or(_firstName.Length);
}
Then we have Results. Results come in two kinds, The Simple result (Result) and a result encapsulating data. A simple result is meant to be used on actions which can mutate data but are expected to fail in the normal course of the application, such as paying an order with insufficient funds.
where before you would do something like:
public void PayOrder(decimal amount)
{
if (amount < _orderTotal) throw new NotEnoughFundsException(amount);
// Order payment logic
}
The consumer has no way of knowing that this operation can fail, unless the code is well documented, and the documentation is up to date.
With a simple result it can be made more clear that this operation can fail.
public Result PayOrder(decimal amount)
{
if (amount < _orderTotal) return Result.Fail(new NotEnoughFunds(amount));
// Order payment logic
}
It is advised that all mutations are done after the checks so that a failed result describes that no operations were performed.
These results can also be mapped so that all subsequent operations are dependant on the success of the original one.
public class OrderPaymentProcess
{
private Order _order;
private User _user;
public OrderPaymentProcess(Order order, User user)
{
_order = order;
_user = user;
}
public Result Process(decimal amount) => _order
.PayOrder(amount)
.Do(UpdateUserFunds)
.Do(SendCustomerInvoice); // If any of the operations fail it will return a failed result
public Result SendCustomerInvoice() => // send invoice;
public Result UpdateUserFunds() => // updates user funds
}
You can also use the IfFailed method on the result to run any undo actions that might be needed.
public class OrderPaymentProcess
{
private Order _order;
private User _user;
public OrderPaymentProcess(Order order, User user)
{
_order = order;
_user = user;
}
public Result Process(decimal amount) => _order
.PayOrder(amount)
.Do(UpdateUserFunds)
.IfFailed(() => UndoProcess(amount))
.Do(SendCustomerInvoice); // If any of the operations fail it will return a failed result
public Result SendCustomerInvoice() => // send invoice.
public Result UpdateUserFunds() => //update funds.
public void UndoProcess(decimal amount) => // undo logic
}
Notice that the return type of the UndoProcess method is void as it cannot fail in the normal function of the application, but you still get a result back from the method call as it returns the Result it was called with, as to not confuse downstream consumers of the state of the operation.
Complex results provide the same functionality but allowing you to pass data in between calls.
public class OrderService
{
private IDatabase _database;
public Result<OrderItem> CreateOrderItem(Guid productId, int amountPurchased) => _database.Products
.Unique(id) // returns an Option<Product>
.ToResult(new NotFoundError("Product", productId)) // Maps the option to a Result<Product> using the passed IError if its empty.
.Map(product => OrderItem.Create(product, amountPurchased))
}
Simple results and complex results can map in between each other
public class ProductService
{
private IDatabase _database;
public Result Delete(Guid id)) => _database.Products
.Unique(id)
.ToResult(new NotFoundError("Product", productId))
.Map(product => MangleName(product))
.Do(product => Update(product, id))
.IfFailed(() => UndoDeletion(id));
public Result<Product> MangleName(Product product) => // mangles product name;
public Result Update(Product updated, Guid id) => // updates db state;
public void UndoDeletion(Guid id) => // undoes deletion;
}
Both result types use an IError when they fail, which being only an interface can be easily extended to provide any functionality needed.
public interface IHttpMappableError : IError
{
ObjectResult GetHttpResponse();
}
So when you get an error you can use C# type matching to perform any specific actions required
public class Controller
{
private ProductService _service;
public ActionResult<Product> Delete(Guid id)
{
Result deletionResult = _service.Delete(id);
if (deletionReuslt.Succeeded) {
return Ok();
}
if (deletionResult.Error is IHttpMappableError httpError)
{
return httpError.GetHttpResponse();
}
return new InternalServerError();
}
}
There is support for mapping simple results and complex results with asynchronous functions
public class ProductService
{
private IDatabase _database;
public Task<Result> Delete(Guid id)) => _database.Products
.Unique(id) // returns a Task of Option
.ToResultAsync(new NotFoundError("Product", productId))
.MapAsync(product => MangleName(product))
.DoAsync(product => Update(product, id))
.IfFailedAsync(() => UndoDeletion(id));
public Result<Product> MangleName(Product product) => // mangles product name;
public Task<Result> Update(Product updated, Guid id) => // updates db state;
public Task UndoDeletion(Guid id) => // undoes deletion;
}
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. |
-
net6.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Afonsomtsm.Result:
Package | Downloads |
---|---|
Afonsomtsm.Money
A package to more easily deal with money |
|
Afonsomtsm.Filtering
A package to provide the structure for reusable filtering |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
1.0.0 | 107 | 1/16/2025 |
0.9.6.2 | 202 | 9/15/2024 |
0.9.6.1-alpha | 109 | 6/17/2024 |
0.9.6 | 132 | 5/31/2024 |
0.9.5.6 | 141 | 4/3/2024 |
0.9.5.5 | 103 | 4/1/2024 |
0.9.5.4 | 116 | 4/1/2024 |
0.9.5.3 | 150 | 3/15/2024 |
0.9.5.2 | 127 | 3/15/2024 |
0.9.5 | 120 | 3/15/2024 |
0.9.4 | 153 | 2/29/2024 |
0.9.3.8 | 116 | 2/29/2024 |
0.9.3 | 211 | 7/28/2023 |
0.9.2 | 197 | 7/20/2023 |
0.9.1 | 183 | 7/18/2023 |