A result is an abstraction that allows developers to express the value as well as the outcome of an operation. A basic result can represent whether an operation was successful with a value or an error message. Developers can use results to represent common failure scenarios that occur in their codebase by expanding result implementations to include statuses for each desired state. Often developers working in APIs will use exceptions to determine what HTTP status code to return from an endpoint. Results can be substituted in place of exceptions to move away from using exceptions as control of flow to a more functional approach. The Ardalis.Result library provides a result implementation that can be easily mapped to common ASP.NET MVC responses. Let’s look at an example.
First, let’s introduce a ProjectService
with an UpdateProject
method. This method may throw an ArgumentException
, NullReferenceException
, or many other exceptions depending on its implementation. If the operation is successful, the updated project is returned.
public Project UpdateProject(int projectId, string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Name is Null or Empty");
}
var project = // Get Project by Id
project.SetName(name); // If project is null this will throw NullReferenceException
// Save changes to persistence
return project;
}
The following APIEndpoint calls the ProjectService
in a try-catch block and uses exception filtering to determine what HTTP Status Code to respond with.
[HttpPut("/Project/{projectId}")]
public override Task<ActionResult<Project>> HandleAsync([FromRoute] UpdateProjectRequest request, CancellationToken cancellationToken)
{
try
{
var project = _projectService.UpdateProject(request.ProjectId, request.Name);
return Ok(project);
}
catch (NullReferenceException ex)
{
return NotFound();
}
catch (ArgumentException ex)
{
return BadRequest();
}
catch (Exception ex)
{
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
}
}
This endpoint is using exceptions as control of flow. This can be an expensive way of handling errors and generally should be avoided if possible. Using this approach will also lead to repeated code and boilerplate across endpoints that throw similar exceptions. You can read more on this in this detailed post on the topic.
Now that we’ve seen an exception-based approach, let’s review an alternative approach using the Ardalis.Result
package.
Enter Ardalis.Result
By referencing the Ardalis.Result
package, the expected exceptions can be removed from the UpdateProject
method. The UpdateProject
method’s signature can be changed to have a return type Result<Project>
.
public Result<Project> UpdateProject(int projectId, string name)
{
if (string.IsNullOrEmpty(name))
{
return Result<Project>.Invalid;
}
var project = // Get Project by Id
if (project is null)
{
return Result<Project>.NotFound;
}
project.SetName(name); // No more NullReference exception here
// Save changes to persistence
return project;
}
Now, when exceptions are produced by this method, they can be considered truly exceptional. The UpdateProject
method implements checks for missing data and parameter validation which are commonly represented by NotFound and BadRequest statuses respectively. If the update is successful, the Project is wrapped in successful result. The Ardalis.Result
package defines additional flags for Unauthorized, Forbidden, and Error states.
Result mapping with Ardalis.Result.AspNetCore
Since ProjectService
now produces a Result<Project>
that maps to HTTP statuses like OK (200), NotFound (404), and BadRequest (400), the endpoint can now be implemented in a more concise manner. Notice the addition of the [TranslateResultToActionResult]
attribute, this enables direct mapping of the Result<T>
to an ActionResult<T>
with the appropriate HTTP Status Code. The source for the mapping of results to ASP.NET MVC ActionResults can be found here.
[TranslateResultToActionResult]
[HttpPut("/Project/{projectId}")]
public override Result<Project> HandleAsync([FromRoute] UpdateProjectRequest request, CancellationToken cancellationToken)
{
return _projectService.UpdateProject(request.ProjectId, request.Name);
}
If attributes aren’t your thing, it’s also possible to use this helper method to map between Result<T>
and ActionResult<T>
. It’s important to note the difference in return types as the attribute approach handles translation from Result<T>
to ActionResult<T>
whereas the helper method returns the ActionResult<T>
directly.
[HttpPut("/Project/{projectId}")]
public override Task<ActionResultt<Project>> HandleAsync([FromRoute] UpdateProjectRequest request, CancellationToken cancellationToken)
{
return this.ToActionResult(_projectService.UpdateProject(request.ProjectId, request.Name));
}
Results help developers better represent outcomes of their code regardless of the reason for successes or failures. The Ardalis.Result
package helps .NET developers utilize the benefits of the result pattern while covering many common scenarios of .NET applications. When combined with ASP.NET MVC and APIEndpoints, this approach enables developers to write clean result based endpoints in a standard and reuseable way.