In the latest release of Ardalis.Result, a community requested feature was added to convert results from one type to another. This allows for flexible transformation of a Result’s value while preserving the outcome of the Result’s status. This is a common scenario that is useful when you want to transform the value of a Result that contains a domain entity to a Result containing a DTO type before converting the Result to an ActionResult
using the ToActionResult
extension.
How it works
This new mapping feature is implemented with a Map
extension that extends a Result<TSource>
and produces a Result<TDestination>
. It takes a Func<TSource, TDestination>>
as a parameter and uses it to transform the value of the Result<TSource>
if the result is in a success state. If the source Result was in a failure state, the destination Result will be in a failure state with the same error message or validation errors preserved. This extension method uses a switch statement to determine the outcome of the transformation. The implementation is as follows:
public static Result<TDestination> Map<TSource, TDestination>(this Result<TSource> result, Func<TSource, TDestination> func)
{
switch (result.Status)
{
case ResultStatus.Ok: return func(result);
case ResultStatus.NotFound: return Result<TDestination>.NotFound();
case ResultStatus.Unauthorized: return Result<TDestination>.Unauthorized();
case ResultStatus.Forbidden: return Result<TDestination>.Forbidden();
case ResultStatus.Invalid: return Result<TDestination>.Invalid(result.ValidationErrors);
case ResultStatus.Error: return Result<TDestination>.Error(result.Errors.ToArray());
default:
throw new NotSupportedException($"Result {result.Status} conversion is not supported.");
}
}
Naturally, this extension method lends itself to being a great candidate for unit testing.
A simple use case
A common approach when using the Ardalis.Result package in the context of API endpoints is to call a domain service that returns a Result and map the result to an ActionResult using ToActionResult()
. An example of this style of endpoint is briefly demonstrated in the following snippet.
[HttpPut("/Project/{projectId}")]
public override Task<ActionResult<Project>> HandleAsync([FromRoute] UpdateProjectRequest request, CancellationToken cancellationToken) =>
_projectService.UpdateProject(request.ProjectId, request.Name)
.ToActionResult(this);
Very concise! But this approach breaks down quickly if the endpoint returns a different type than what the Result contains. Often, domain services will return entities from the application’s core domain layer but API endpoints may want to return DTO models specific to what the consumer of the API expects especially in projects using the Backends For Frontends (BFF) pattern. The following snippet demonstrates how an endpoint might be implemented for this case.
[HttpPut("/Project/{projectId}")]
public override Task<ActionResultt<ProjectDto>> HandleAsync([FromRoute] UpdateProjectRequest request, CancellationToken cancellationToken)
{
var result = _projectService.UpdateProject(request.ProjectId, request.Name);
if(!result.IsSuccess)
{
return result.ToActionResult(this);
}
var project = result.Value;
var projectDto = new ProjectDTO(project.Id, project.Name);
return Ok(projectDto);
}
Certainly not ideal since this increases the cyclomatic complexity of the endpoint. This approach can be improved using the new Map
extension.
[HttpPut("/Project/{projectId}")]
public override Task<ActionResultt<ProjectDto>> HandleAsync([FromRoute] UpdateProjectRequest request, CancellationToken cancellationToken) =>
_projectService.UpdateProject(request.ProjectId, request.Name)
.Map(project => new ProjectDTO(project.Id, project.Name))
.ToActionResult(this);
Much better! The Map
extension allows the endpoint to keep it’s pipeline style, railway-oriented approach without checking the state of the Result or unpacking the Result’s value explicitly. This should feel familiar to fans of LINQ or those with functional programming experience.
Wrapping up
The new Map
extension aids those aiming to transform the values of their Results in a concise and easy to use manner. This feature is now available in version 4.1.0 of the Ardalis.Result package. Get started using this method with Ardalis.Result today!