MrDave1999 / SimpleResults

A simple library to implement the Result pattern for returning from services
https://mrdave1999.github.io/SimpleResults
MIT License
105 stars 2 forks source link
aspnetcore csharp dotnet error-handling fluent-validation pattern result

SimpleResults

SimpleResults downloads

SimpleResults-AspNetCore downloads

SimpleResults-FluentValidation downloads

SimpleResults-logo

A simple library to implement the Result pattern for returning from services. It also provides a mechanism for translating the Result object to an ActionResult or IResult.

This library was inspired by Ardalis.Result.

See the API documentation for more information on this project.

Index

Operation Result Pattern

The purpose of the Result design pattern is to give an operation (a method) the possibility to return a complex result (an object), allowing the consumer to:

Why did I make this library?

Why don't I use exceptions?

I usually throw exceptions when developing open source libraries to alert the developer immediately that an unexpected error has occurred and must be corrected. In this case, it makes sense to me to throw an exception because the developer can know exactly where the error originated (by looking at the stack trace). However, when I develop applications, I very rarely find a case for using exceptions.

For example, I could throw an exception when a normal user enters empty fields but this does not make sense to me, because it is an error caused by the end user (who manages the system from the user interface). So in this case throwing an exception is useless because:

And there are many more examples of errors caused by the end user: the email is duplicated or a password that does not comply with security policies, among others.

I only throw exceptions for unexpected errors; otherwise I create result objects and use return statements in my methods to terminate execution immediately when an expected error occurs.

Differences between an expected and unexpected error

It is necessary to understand the differences between an expected and unexpected error in order to know when to throw exceptions. In fact, in practice, third-party dependencies are responsible for reporting unexpected errors, so the developer only has to worry about identifying the expected errors of his business application.

Anecdote

At work I had to implement a module to generate a report that performs a monthly comparison of income and expenses for a company, so it was necessary to create a function that is responsible for calculating the percentage of a balance per month:

Percentage.Calculate(double amount, double total);

The total parameter if it is zero, will cause a division by zero (undefined operation), however, this value was not provided by an end user, but by the income and expense reporting module, but since I did not implement this module correctly, I created a bug, so the algorithm was passing a zero value for a strange reason (I call this a logic error, caused by the developer).

Since I didn't throw an exception in the Percentage.Calculate function, it took me a couple of minutes to find out where the error originated (I didn't know that the problem was a division by zero).

Dividing a floating-point value by zero doesn't throw an exception; it result is not a number (NaN). This was a surprise to me! I didn't know! I was expecting an exception but it was not the case.

If I had thrown an exception, I would have found the error very quickly, just by looking at the stack trace. In this case, it is very useful the exception object, for me and other developers and yes, divide by zero is an unexpected error, an exception should be thrown.

What happens if exceptions are used for all situations?

There are some details to consider:

Interesting resource about exceptions

Installation

You can run any of these commands from the terminal:

dotnet add package SimpleResults
dotnet add package SimpleResults.AspNetCore
dotnet add package SimpleResults.FluentValidation

SimpleResults package is the main library (the core). The other two packages complement the main library (they are like add-ons).

Overview

You must import the namespace types at the beginning of your class file:

using SimpleResults;

This library provides four main types:

With any of these types you can handle errors and at the same time generate errors with the return statement.

This approach provides a new way to generate an error using return statements without the need to throw exceptions.

See the API documentation for more information on these types.

Using the Result type

You can use the Result class when you do not want to return any value.

Example:

public class UserService
{
    private readonly List<User> _users;
    public UserService(List<User> users) => _users = users;

    public Result Update(string id, string name)
    {
        if (string.IsNullOrWhiteSpace(id))
            return Result.Invalid("ID is required");

        if (string.IsNullOrWhiteSpace(name))
            return Result.Invalid("Name is required");

        var user = _users.Find(u => u.Id == id);
        if (user is null)
            return Result.NotFound();

        user.Name = name;
        return Result.UpdatedResource();
    }
}

You can use the Result<TValue> class when you want to return a value (such as a User object).

Example:

public class UserService
{
    private readonly List<User> _users;
    public UserService(List<User> users) => _users = users;

    public Result<User> GetById(string id)
    {
        if(string.IsNullOrWhiteSpace(id))
            return Result.Invalid("ID is required");

        var user = _users.Find(u => u.Id == id);
        if(user is null)
            return Result.NotFound();

        return Result.Success(user, "User found");
    }
}

Using the ListedResult type

You can use the ListedResult<TValue> class when you want to return a set of values (such as a collection of objects of type User).

Example:

public class UserService
{
    private readonly List<User> _users;
    public UserService(List<User> users) => _users = users;

    public ListedResult<User> GetAll()
    {
        if(_users.Count == 0)
            return Result.Failure("No user found");

        return Result.ObtainedResources(_users);
    }
}

Using the PagedResult type

You can use the PagedResult<TValue> class when you want to include paged information and a data collection in the result.

Example:

public class UserService
{
    private readonly List<User> _users;
    public UserService(List<User> users) => _users = users;

    public PagedResult<User> GetPagedList(int pageNumber, int pageSize)
    {
        if(pageNumber <= 0)
            return Result.Invalid("PageNumber must be greater than zero");

        int itemsToSkip = (pageNumber - 1) * pageSize;
        var data = _users
            .Skip(itemsToSkip)
            .Take(pageSize);

        if (data.Any())
        {
            var pagedInfo = new PagedInfo(pageNumber, pageSize, _users.Count);
            return Result.Success(data, pagedInfo);
        }

        return Result.Failure("No results found");
    }
}

Creating a resource with Result<T> type

You can tell the method to return a successfully created resource as a result by using the Result.CreatedResource method. In addition, you can use the CreatedGuid class to specify the ID assigned to the created resource.

Example:

public class UserService
{
    private readonly List<User> _users;
    public UserService(List<User> users) => _users = users;

    public Result<CreatedGuid> Create(string name)
    {
        if(string.IsNullOrWhiteSpace(name))
            return Result.Invalid("Name is required");

        var guid = Guid.NewGuid();
        _users.Add(new User { Id = guid.ToString(), Name = name });
        return Result.CreatedResource(guid);
    }
}

You can also use the CreatedId class when using an integer as identifier.

An example using Entity Framework Core:

public class UserModel 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class UserService
{
    private readonly DbContext _db;
    public UserService(DbContext db) => _db = db;

    public Result<CreatedId> Create(string name)
    {
        if(string.IsNullOrWhiteSpace(name))
            return Result.Invalid("Name is required");

        var user = new UserModel { Name = name };
        _db.Add(user);
        _db.SaveChanges();
        return Result.CreatedResource(user.Id);
    }
}

Designing errors and success messages

You can create an object that represents an error or success message. The advantage is all the relevant information of an error or success is encapsulated within one object.

Example:

public readonly ref struct StartDateIsAfterEndDateError
{
    public string Message { get; }
    public StartDateIsAfterEndDateError(DateTime startDate, DateTime endDate)
    { 
        Message = string.Format(
            "The start date {0} is after the end date {1}", 
            startDate.ToString("yyyy-MM-dd"), 
            endDate.ToString("yyyy-MM-dd"));
    }
}

This approach allows you to change the format of the message without having to make changes elsewhere.

And then you can use it in your service:

public class UserService
{
    public Result<List<User>> GetUsersByDateRange(DateTime startDate, DateTime endDate)
    {
        if(startDate > endDate)
            return Result.Invalid(new StartDateIsAfterEndDateError(startDate, endDate).Message);

        // Do something..
    }
}

Integration with ASP.NET Core

You can convert the Result object to a Microsoft.AspNetCore.Mvc.ActionResult using the ToActionResult extension method.

You need to install the SimpleResults.AspNetCore package to have access to the extension method. See the ResultExtensions class to find all extension methods.

Example:

public class UserRequest 
{ 
    public string Name { get; init; }
}

[ApiController]
[Route("[controller]")]
public class UserController
{
    private readonly UserService _userService;
    public UserController(UserService userService) => _userService = userService;

    [HttpPost]
    public ActionResult<Result<CreatedGuid>> Create([FromBody]UserRequest request)
        => _userService.Create(request.Name).ToActionResult();

    [HttpPut("{id}")]
    public ActionResult<Result> Update(string id, [FromBody]UserRequest request)
        => _userService.Update(id, request.Name).ToActionResult();

    [HttpGet("{id}")]
    public ActionResult<Result<User>> Get(string id)
        => _userService.GetById(id).ToActionResult();

    [HttpGet("paged")]
    public ActionResult<PagedResult<User>> GetPagedList([FromQuery]PagedRequest request)
        => _userService
        .GetPagedList(request.PageNumber, request.PageSize)
        .ToActionResult();

    [HttpGet]
    public ActionResult<ListedResult<User>> Get()
        => _userService.GetAll().ToActionResult();
}

See the API documentation for a list of available extension methods.

Using TranslateResultToActionResult as an action filter

You can also use the TranslateResultToActionResult filter to translate the Result object to ActionResult.

TranslateResultToActionResultAttribute class will internally call the ToActionResult method and perform the translation.

Example:

[TranslateResultToActionResult]
[ApiController]
[Route("[controller]")]
public class UserController
{
    private readonly UserService _userService;
    public UserController(UserService userService) => _userService = userService;

    [HttpGet("{id}")]
    public Result<User> Get(string id) => _userService.GetById(id);
}

The return value of Get action is a Result<User>. After the action is executed, the filter (i.e. TranslateResultToActionResult) will run and translate the Result<User> to ActionResult.

See the source code, it is very simple.

Add action filter as global

If you do not want to use the filter on each controller, you can add it globally for all controllers (see sample).

builder.Services.AddControllers(options =>
{
    // Add filter for all controllers.
    options.Filters.Add<TranslateResultToActionResultAttribute>();
});

This way you no longer need to add the TranslateResultToActionResult attribute on each controller or individual action.

Support for Minimal APIs

As of version 2.3.0, a feature has been added to convert the Result object to an implementation of Microsoft.AspNetCore.Http.IResult.

You only need to use the extension method called ToHttpResult. See the ResultExtensions class to find all extension methods.

Example:

public static class UserEndpoint
{
    public static void AddRoutes(this WebApplication app)
    {
        var userGroup = app
            .MapGroup("/User")
            .WithTags("User");

        userGroup
            .MapGet("/", (UserService service) => service.GetAll().ToHttpResult())
            .Produces<ListedResult<User>>();

        userGroup
            .MapGet("/{id}", (string id, UserService service) => service.GetById(id).ToHttpResult())
            .Produces<Result<User>>();

        userGroup.MapPost("/", ([FromBody]UserRequest request, UserService service) =>
        {
            return service.Create(request.Name).ToHttpResult();
        })
        .Produces<Result<CreatedGuid>>();
    }
}

You can also use the TranslateResultToHttpResult filter to translate the Result object to an implementation of IResult.

TranslateResultToHttpResultFilter class will internally call the ToHttpResult method and perform the translation.

Example:

public static class UserEndpoint
{
    public static void AddRoutes(this WebApplication app)
    {
        var userGroup = app
            .MapGroup("/User")
            .WithTags("User")
            .AddEndpointFilter<TranslateResultToHttpResultFilter>();

        userGroup
            .MapGet("/{id}", (string id, UserService service) => service.GetById(id))
            .Produces<Result<User>>();
    }
}

The endpoint handler returns a Result<User>. After the handler is executed, the filter (i.e. TranslateResultToHttpResult) will run and translate the Result<User> to an implementation of IResult.

See the source code, it is very simple.

Validating with the ModelState property

SimpleResults.AspNetCore package also adds extension methods for the ModelStateDictionary type.

See the ModelStateDictionaryExtensions class to find all extension methods.

The ModelStateDictionary type contains the validation errors that are displayed to the client. Somehow these errors must be included in an instance of type Result.

Manual validation

Manual validation is performed directly in the controller action.

Example:

[TranslateResultToActionResult]
[Route("[controller]")]
public class OrderController : ControllerBase
{
    private readonly OrderService _orderService;
    public OrderController(OrderService orderService) => _orderService = orderService;

    [HttpPost]
    public Result<CreatedGuid> Create([FromBody]CreateOrderRequest request)
    {
        if (ModelState.IsFailed())
            return ModelState.Invalid();

        return _orderService.Create(request);
    }
}

In this example a manual validation is performed with ModelState.IsFailed() (an extension method), so if the model state is failed, an invalid result type is returned. What ModelState.Invalid() does is to convert the instance of ModelStateDictionary to an instance of type Result, so in the result object the validation errors will be added.

After the controller action is executed, the TranslateResultToActionResult filter will translate the Result object to an instance of type ActionResult.

You can also return the ActionResult directly in the controller action instead of using the action filter.

Example:

[Route("[controller]")]
public class OrderController : ControllerBase
{
    private readonly OrderService _orderService;
    public OrderController(OrderService orderService) => _orderService = orderService;

    [HttpPost]
    public ActionResult<Result<CreatedGuid>> Create([FromBody]CreateOrderRequest request)
    {
        if (ModelState.IsFailed())
            return ModelState.BadRequest();

        return _orderService
            .Create(request)
            .ToActionResult();
    }
}

ModelState.BadRequest() has a behavior similar to ModelState.Invalid(), the difference is that the first one returns an instance of type BadRequestObjectResult in which contains the instance of type Result.

Automatic validation

You need to make a setting in the Program.cs to convert the instance of type ModelStateDictionary to an instance of type Result when the model validation fails.

Example:

builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
    options.InvalidModelStateResponseFactory = (ActionContext context) => context.ModelState.BadRequest();
});

This delegate is only invoked on actions annotated with ApiControllerAttribute and will execute the context.ModelState.BadRequest() call when a model validation failure occurs. If a validation failure occurs in the model, the controller action will never be executed.

Your controller no longer needs to perform manual validation, for example:

[ApiController]
[TranslateResultToActionResult]
[Route("[controller]")]
public class OrderController : ControllerBase
{
    private readonly OrderService _orderService;
    public OrderController(OrderService orderService) => _orderService = orderService;

    [HttpPost]
    public Result<CreatedGuid> Create([FromBody]CreateOrderRequest request)
        => _orderService.Create(request);
}

The ApiController is necessary because it allows to activate the ModelStateInvalid filter to perform the model validation before executing the controller action.

Translate Result object to HTTP status code

SimpleResults.AspNetCore package is responsible for translating the status of a Result object into an HTTP status code.

The following table is used as a reference to know which type of result corresponds to an HTTP status code:

Result type HTTP status code
Result.Success 200 - Ok
Result.CreatedResource 201 - Created
Result.UpdatedResource 200 - Ok
Result.DeletedResource 200 - Ok
Result.ObtainedResource 200 - Ok
Result.ObtainedResources 200 - Ok
Result.File 200 - Ok
Result.Invalid 400 - Bad Request
Result.NotFound 404 - Not Found
Result.Unauthorized 401 - Unauthorized
Result.Conflict 409 - Conflict
Result.Failure 422 - Unprocessable Entity
Result.CriticalError 500 - Internal Server Error
Result.Forbidden 403 - Forbidden

Integration with Fluent Validation

You need to install the SimpleResults.FluentValidation package to have access to the extension methods.

Example:

public class UserService
{
    public Result Create(CreateUserRequest request)
    {
        ValidationResult result = new CreateUserValidator().Validate(request);
        if(result.IsFailed())
            return result.Invalid();

        // Some code..
    }
}

See the API documentation for a list of available extension methods.

Samples

You can find a complete and functional example in these projects:

Language settings

SimpleResults has resources that contain response messages. See the source code.

At the moment there are only two resources:

The loading of these resources depends on your locale settings. For example, if your computer has the language as Spanish, the resource that will be loaded will be ResponseMessages.es.resx. Likewise, if it is set to English, the default resource will be loaded: ResponseMessages.resx.

And if the configuration is set to French, the resource that will be loaded will be the default one (i.e. ResponseMessages.resx), since there is no resource called ResponseMessages.fr.resx.

You can explicitly specify the culture to ensure that a resource is loaded regardless of your computer's language settings:

Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("es");

In ASP.NET Core applications, the UseRequestLocalization) extension method is used:

app.UseRequestLocalization("es");

Contribution

Any contribution is welcome! Remember that you can contribute not only in the code, but also in the documentation or even improve the tests.

Follow the steps below: