Repository files navigation

Ardalis.Result - NuGetNuGetBuild Status

Ardails.Result.AspNetCore - NuGetNuGetArdails.Result.FluentValidation - NuGetNuGet

Follow @ardalisFollow @nimblepros

Result

A result abstraction that can be mapped to HTTP response codes if needed.

Docs

Docs are located in the /docs folder and available online at result.ardalis.com Please add issues for new docs requests and pull requests for docs issues are welcome!

Learn More

What Problem Does This Address?

Many methods on service need to return some kind of value. For instance, they may be looking up some data and returning a set of results or a single object. They might be creating something, persisting it, and then returning it. Typically, such methods are implemented like this:

publicCustomerGetCustomer(intcustomerId){// more logicreturncustomer;}publicCustomerCreateCustomer(stringfirstName,stringlastName){// more logicreturncustomer;}

This works great as long as we're only concerned with the happy path. But what happens if there are multiple failure modes, not all of which make sense to be handled by exceptions?

  • What happens if customerId is not found?
  • What happens if required lastName is not provided?
  • What happens if the current user doesn't have permission to create new customers?

The standard way to address these concerns is with exceptions. Maybe you throw a different exception for each different failure mode, and the calling code is then required to have multiple catch blocks designed for each type of failure. This makes life painful for the consumer, and results in a lot of exceptions for things that aren't necessarily exceptional. Like this:

[HttpGet]publicasyncTask<ActionResult<CustomerDTO>>GetCustomer(intcustomerId){try{varcustomer=_repository.GetById(customerId);varcustomerDTO=CustomerDTO.MapFrom(customer);returnOk(customerDTO);}catch(NullReferenceExceptionex){returnNotFound();}catch(Exceptionex){returnnewStatusCodeResult(StatusCodes.Status500InternalServerError);}}

Another approach is to return a Tuple of the expected result along with other things, like a status code and additional failure mode metadata. While tuples can be great for individual, flexible responses, they're not as good for having a single, standard, reusable approach to a problem.

The Result pattern provides a standard, reusable way to return both success as well as multiple kinds of non-success responses from .NET services in a way that can easily be mapped to API response types. Although the Ardalis.Result package has no dependencies on ASP.NET Core and can be used from any .NET Core application, the Ardalis.Result.AspNetCore companion package includes resources to enhance the use of this pattern within ASP.NET Core web API applications.

Sample Usage

Creating a Result

The sample folder includes some examples of how to use the project. Here are a couple of simple uses.

Imagine the snippet below is defined in a domain service that retrieves WeatherForecasts. When compared to the approach described above, this approach uses a result to handle common failure scenarios like missing data denoted as NotFound and or input validation errors denoted as Invalid. If execution is successful, the result will contain the random data generated by the final return statement.

publicResult<IEnumerable<WeatherForecast>>GetForecast(ForecastRequestDtomodel){if(model.PostalCode=="NotFound")returnResult<IEnumerable<WeatherForecast>>.NotFound();// validate modelif(model.PostalCode.Length>10){returnResult<IEnumerable<WeatherForecast>>.Invalid(newList<ValidationError>{newValidationError{Identifier=nameof(model.PostalCode),ErrorMessage="PostalCode cannot exceed 10 characters."}});}varrng=newRandom();returnnewResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1,5).Select(index =>newWeatherForecast{Date=DateTime.Now.AddDays(index),TemperatureC=rng.Next(-20,55),Summary=Summaries[rng.Next(Summaries.Length)]}).ToArray());}

Translating Results to ActionResults

Continuing with the domain service example from the previous section, it's important to show that the domain service doesn't know about ActionResult or other MVC/etc types. But since it is using a Result<T> abstraction, it can return results that are easily mapped to HTTP status codes. Note that the method above returns a Result<IEnumerable<WeatherForecast> but in some cases, it might need to return an Invalid result, or a NotFound result. Otherwise, it returns a Success result with the actual returned value (just like an API would return an HTTP 200 and the actual result of the API call).

You can apply the [TranslateResultToActionResult] attribute to an API Endpoint (or controller action if you still use those things) and it will automatically translate the Result<T> return type of the method to an ActionResult<T> appropriately based on the Result type.

[TranslateResultToActionResult][HttpPost("Create")]publicResult<IEnumerable<WeatherForecast>>CreateForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetForecast(model);}

Alternatively, you can use the ToActionResult helper method within an endpoint to achieve the same thing:

[HttpPost("/Forecast/New")]publicoverrideActionResult<IEnumerable<WeatherForecast>>Handle(ForecastRequestDtorequest){returnthis.ToActionResult(_weatherService.GetForecast(request));// alternately// return _weatherService.GetForecast(request).ToActionResult(this);}

Translating Results to Minimal API Results

Similar to how the ToActionResult extension method translates Ardalis.Results to ActionResults, the ToMinimalApiResult translates results to the new Microsoft.AspNetCore.Http.ResultsIResult types in .NET 6+. The following code snippet demonstrates how one might use the domain service that returns a Result<IEnumerable<WeatherForecast>> and convert to a Microsoft.AspNetCore.Http.Results instance.

app.MapPost("/Forecast/New",(ForecastRequestDtorequest,WeatherServiceweatherService)=>{returnweatherService.GetForecast(request).ToMinimalApiResult();}).WithName("NewWeatherForecast");

The full Minimal API sample can be found in the sample folder.

Mapping Results From One Type to Another

A common use case is to map between domain entities to API response types usually represented as DTOs. You can map a result containing a domain entity to a Result containing a DTO by using the Map method. The following example calls the method _weatherService.GetSingleForecast which returns a Result<WeatherForecast> which is then converted to a Result<WeatherForecastSummaryDto> by the call to Map. Then, the result is converted to an ActionResult<WeatherForecastSummaryDto> using the ToActionResult helper method.

[HttpPost("Summary")]publicActionResult<WeatherForecastSummaryDto>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetSingleForecast(model).Map(wf =>newWeatherForecastSummaryDto(wf.Date,wf.Summary)).ToActionResult(this);}

Chaining Result Operations with Bind and BindAsync

You can chain together multiple operations that return Result<T> using the Bind and BindAsync methods. This allows you to perform a sequence of operations where each operation can fail, and if any operation fails, the subsequent operations will not be executed. This can be useful for eliminating checking for success after each Result operation with a more readable and maintainable chain of operations.

[HttpPost("Summary")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){varresult1=await_weatherService.GetSingleForecast(model);if(!result1.IsSuccess){returnresult1.ToActionResult(this);}varresult2=await_weatherService.GetForecastDetailsAsync(result1.Value.Id);returnresult2.Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

With the Bind and BindAsync methods, you can simplify this code by chaining the operations together. The following example demonstrates how to use BindAsync to achieve the same result in a more concise manner:

[HttpPost("BindedForecast")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecastWithBind([FromBody]ForecastRequestDtomodel){returnawait_weatherService.GetSingleForecast(model).BindAsync(wf =>_weatherService.GetForecastDetailsAsync(wf.Id)).Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

In this example, if GetSingleForecast returns a failure result, GetForecastDetailsAsync will not be called, and the failure will propagate through the chain. If all operations succeed, the final result will be mapped to a WeatherForecastSummaryDto and returned as an ActionResult.

ASP.NET API Metadata

By default, Asp Net Core and API Explorer know nothing about [TranslateResultToActionResult] and what it is doing. To reflect [TranslateResultToActionResult] behavior in metadata generated by API Explorer (which is then used by tools like Swashbuckle, NSwag etc.), you can use ResultConvention:

services.AddControllers(mvcOptions =>mvcOptions.AddDefaultResultConvention());

This will add [ProducesResponseType] for every known ResultStatus to every endpoint marked with [TranslateResultToActionResult]. To customize ResultConvention behavior, one may use the AddResultConvention method:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap()));

This code is functionally equivalent to the previous example.

From here you can modify the ResultStatus to HttpStatusCode mapping

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).For(ResultStatus.Error,HttpStatusCode.InternalServerError)));

ResultConvention will add [ProducesResponseType] for every result status configured in ResultStatusMap. AddDefaultMap() maps every known ResultType, so if you want to exclude certain ResultType from being listed (e.g. your app doesn't have authentication and authorization, and you don't want 401 and 403 to be listed as SupportedResponseType) you can do this:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).Remove(ResultStatus.Forbidden).Remove(ResultStatus.Unauthorized)));

Alternatively, you can specify which (failure) result statuses are expected from a certain endpoint:

[TranslateResultToActionResult()][ExpectedFailures(ResultStatus.NotFound,ResultStatus.Invalid)][HttpDelete("Remove/{id}")]publicResultRemovePerson(intid){// Method logic}

!!! If you list a certain Result status in ExpectedFailures, it must be configured in ResultConvention on startup.

Another configurable feature is what part of the Result object is returned in case of specific failure:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Error,HttpStatusCode.BadRequest, resultStatusOptions =>resultStatusOptions.With((ctrlr,result)=>string.Join("\r\n",result.ValidationErrors)))));

Using Results with FluentValidation

We can use Ardalis.Result.FluentValidation on a service with FluentValidation like that:

publicasyncTask<Result<BlogCategory>>UpdateAsync(BlogCategoryblogCategory){if(Guid.Empty==blogCategory.BlogCategoryId)returnResult<BlogCategory>.NotFound();varvalidator=newBlogCategoryValidator();varvalidation=awaitvalidator.ValidateAsync(blogCategory);if(!validation.IsValid){returnResult<BlogCategory>.Invalid(validation.AsErrors());}varitemToUpdate=(awaitGetByIdAsync(blogCategory.BlogCategoryId)).Value;if(itemToUpdate==null){returnResult<BlogCategory>.NotFound();}itemToUpdate.Update(blogCategory.Name,blogCategory.ParentId);returnResult<BlogCategory>.Success(await_blogCategoryRepository.UpdateAsync(itemToUpdate));}

Getting Started

If you're building an ASP.NET Core Web API you can simply install the Ardalis.Result.AspNetCore package to get started. Then, apply the [TranslateResultToActionResult] attribute to any actions or controllers that you want to automatically translate from Result types to ActionResult types.

About

A result abstraction that can be mapped to HTTP response codes if needed.

Topics

Resources

Code of conduct

Contributing

Stars

1.0k stars

Watchers

12 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Ardalis.Result - NuGetNuGetBuild Status

Ardails.Result.AspNetCore - NuGetNuGetArdails.Result.FluentValidation - NuGetNuGet

Follow @ardalisFollow @nimblepros

Result

A result abstraction that can be mapped to HTTP response codes if needed.

Docs

Docs are located in the /docs folder and available online at result.ardalis.com Please add issues for new docs requests and pull requests for docs issues are welcome!

Learn More

What Problem Does This Address?

Many methods on service need to return some kind of value. For instance, they may be looking up some data and returning a set of results or a single object. They might be creating something, persisting it, and then returning it. Typically, such methods are implemented like this:

publicCustomerGetCustomer(intcustomerId){// more logicreturncustomer;}publicCustomerCreateCustomer(stringfirstName,stringlastName){// more logicreturncustomer;}

This works great as long as we're only concerned with the happy path. But what happens if there are multiple failure modes, not all of which make sense to be handled by exceptions?

  • What happens if customerId is not found?
  • What happens if required lastName is not provided?
  • What happens if the current user doesn't have permission to create new customers?

The standard way to address these concerns is with exceptions. Maybe you throw a different exception for each different failure mode, and the calling code is then required to have multiple catch blocks designed for each type of failure. This makes life painful for the consumer, and results in a lot of exceptions for things that aren't necessarily exceptional. Like this:

[HttpGet]publicasyncTask<ActionResult<CustomerDTO>>GetCustomer(intcustomerId){try{varcustomer=_repository.GetById(customerId);varcustomerDTO=CustomerDTO.MapFrom(customer);returnOk(customerDTO);}catch(NullReferenceExceptionex){returnNotFound();}catch(Exceptionex){returnnewStatusCodeResult(StatusCodes.Status500InternalServerError);}}

Another approach is to return a Tuple of the expected result along with other things, like a status code and additional failure mode metadata. While tuples can be great for individual, flexible responses, they're not as good for having a single, standard, reusable approach to a problem.

The Result pattern provides a standard, reusable way to return both success as well as multiple kinds of non-success responses from .NET services in a way that can easily be mapped to API response types. Although the Ardalis.Result package has no dependencies on ASP.NET Core and can be used from any .NET Core application, the Ardalis.Result.AspNetCore companion package includes resources to enhance the use of this pattern within ASP.NET Core web API applications.

Sample Usage

Creating a Result

The sample folder includes some examples of how to use the project. Here are a couple of simple uses.

Imagine the snippet below is defined in a domain service that retrieves WeatherForecasts. When compared to the approach described above, this approach uses a result to handle common failure scenarios like missing data denoted as NotFound and or input validation errors denoted as Invalid. If execution is successful, the result will contain the random data generated by the final return statement.

publicResult<IEnumerable<WeatherForecast>>GetForecast(ForecastRequestDtomodel){if(model.PostalCode=="NotFound")returnResult<IEnumerable<WeatherForecast>>.NotFound();// validate modelif(model.PostalCode.Length>10){returnResult<IEnumerable<WeatherForecast>>.Invalid(newList<ValidationError>{newValidationError{Identifier=nameof(model.PostalCode),ErrorMessage="PostalCode cannot exceed 10 characters."}});}varrng=newRandom();returnnewResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1,5).Select(index =>newWeatherForecast{Date=DateTime.Now.AddDays(index),TemperatureC=rng.Next(-20,55),Summary=Summaries[rng.Next(Summaries.Length)]}).ToArray());}

Translating Results to ActionResults

Continuing with the domain service example from the previous section, it's important to show that the domain service doesn't know about ActionResult or other MVC/etc types. But since it is using a Result<T> abstraction, it can return results that are easily mapped to HTTP status codes. Note that the method above returns a Result<IEnumerable<WeatherForecast> but in some cases, it might need to return an Invalid result, or a NotFound result. Otherwise, it returns a Success result with the actual returned value (just like an API would return an HTTP 200 and the actual result of the API call).

You can apply the [TranslateResultToActionResult] attribute to an API Endpoint (or controller action if you still use those things) and it will automatically translate the Result<T> return type of the method to an ActionResult<T> appropriately based on the Result type.

[TranslateResultToActionResult][HttpPost("Create")]publicResult<IEnumerable<WeatherForecast>>CreateForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetForecast(model);}

Alternatively, you can use the ToActionResult helper method within an endpoint to achieve the same thing:

[HttpPost("/Forecast/New")]publicoverrideActionResult<IEnumerable<WeatherForecast>>Handle(ForecastRequestDtorequest){returnthis.ToActionResult(_weatherService.GetForecast(request));// alternately// return _weatherService.GetForecast(request).ToActionResult(this);}

Translating Results to Minimal API Results

Similar to how the ToActionResult extension method translates Ardalis.Results to ActionResults, the ToMinimalApiResult translates results to the new Microsoft.AspNetCore.Http.ResultsIResult types in .NET 6+. The following code snippet demonstrates how one might use the domain service that returns a Result<IEnumerable<WeatherForecast>> and convert to a Microsoft.AspNetCore.Http.Results instance.

app.MapPost("/Forecast/New",(ForecastRequestDtorequest,WeatherServiceweatherService)=>{returnweatherService.GetForecast(request).ToMinimalApiResult();}).WithName("NewWeatherForecast");

The full Minimal API sample can be found in the sample folder.

Mapping Results From One Type to Another

A common use case is to map between domain entities to API response types usually represented as DTOs. You can map a result containing a domain entity to a Result containing a DTO by using the Map method. The following example calls the method _weatherService.GetSingleForecast which returns a Result<WeatherForecast> which is then converted to a Result<WeatherForecastSummaryDto> by the call to Map. Then, the result is converted to an ActionResult<WeatherForecastSummaryDto> using the ToActionResult helper method.

[HttpPost("Summary")]publicActionResult<WeatherForecastSummaryDto>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetSingleForecast(model).Map(wf =>newWeatherForecastSummaryDto(wf.Date,wf.Summary)).ToActionResult(this);}

Chaining Result Operations with Bind and BindAsync

You can chain together multiple operations that return Result<T> using the Bind and BindAsync methods. This allows you to perform a sequence of operations where each operation can fail, and if any operation fails, the subsequent operations will not be executed. This can be useful for eliminating checking for success after each Result operation with a more readable and maintainable chain of operations.

[HttpPost("Summary")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){varresult1=await_weatherService.GetSingleForecast(model);if(!result1.IsSuccess){returnresult1.ToActionResult(this);}varresult2=await_weatherService.GetForecastDetailsAsync(result1.Value.Id);returnresult2.Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

With the Bind and BindAsync methods, you can simplify this code by chaining the operations together. The following example demonstrates how to use BindAsync to achieve the same result in a more concise manner:

[HttpPost("BindedForecast")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecastWithBind([FromBody]ForecastRequestDtomodel){returnawait_weatherService.GetSingleForecast(model).BindAsync(wf =>_weatherService.GetForecastDetailsAsync(wf.Id)).Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

In this example, if GetSingleForecast returns a failure result, GetForecastDetailsAsync will not be called, and the failure will propagate through the chain. If all operations succeed, the final result will be mapped to a WeatherForecastSummaryDto and returned as an ActionResult.

ASP.NET API Metadata

By default, Asp Net Core and API Explorer know nothing about [TranslateResultToActionResult] and what it is doing. To reflect [TranslateResultToActionResult] behavior in metadata generated by API Explorer (which is then used by tools like Swashbuckle, NSwag etc.), you can use ResultConvention:

services.AddControllers(mvcOptions =>mvcOptions.AddDefaultResultConvention());

This will add [ProducesResponseType] for every known ResultStatus to every endpoint marked with [TranslateResultToActionResult]. To customize ResultConvention behavior, one may use the AddResultConvention method:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap()));

This code is functionally equivalent to the previous example.

From here you can modify the ResultStatus to HttpStatusCode mapping

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).For(ResultStatus.Error,HttpStatusCode.InternalServerError)));

ResultConvention will add [ProducesResponseType] for every result status configured in ResultStatusMap. AddDefaultMap() maps every known ResultType, so if you want to exclude certain ResultType from being listed (e.g. your app doesn't have authentication and authorization, and you don't want 401 and 403 to be listed as SupportedResponseType) you can do this:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).Remove(ResultStatus.Forbidden).Remove(ResultStatus.Unauthorized)));

Alternatively, you can specify which (failure) result statuses are expected from a certain endpoint:

[TranslateResultToActionResult()][ExpectedFailures(ResultStatus.NotFound,ResultStatus.Invalid)][HttpDelete("Remove/{id}")]publicResultRemovePerson(intid){// Method logic}

!!! If you list a certain Result status in ExpectedFailures, it must be configured in ResultConvention on startup.

Another configurable feature is what part of the Result object is returned in case of specific failure:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Error,HttpStatusCode.BadRequest, resultStatusOptions =>resultStatusOptions.With((ctrlr,result)=>string.Join("\r\n",result.ValidationErrors)))));

Using Results with FluentValidation

We can use Ardalis.Result.FluentValidation on a service with FluentValidation like that:

publicasyncTask<Result<BlogCategory>>UpdateAsync(BlogCategoryblogCategory){if(Guid.Empty==blogCategory.BlogCategoryId)returnResult<BlogCategory>.NotFound();varvalidator=newBlogCategoryValidator();varvalidation=awaitvalidator.ValidateAsync(blogCategory);if(!validation.IsValid){returnResult<BlogCategory>.Invalid(validation.AsErrors());}varitemToUpdate=(awaitGetByIdAsync(blogCategory.BlogCategoryId)).Value;if(itemToUpdate==null){returnResult<BlogCategory>.NotFound();}itemToUpdate.Update(blogCategory.Name,blogCategory.ParentId);returnResult<BlogCategory>.Success(await_blogCategoryRepository.UpdateAsync(itemToUpdate));}

Getting Started

If you're building an ASP.NET Core Web API you can simply install the Ardalis.Result.AspNetCore package to get started. Then, apply the [TranslateResultToActionResult] attribute to any actions or controllers that you want to automatically translate from Result types to ActionResult types.

About

A result abstraction that can be mapped to HTTP response codes if needed.

Topics

Resources

Code of conduct

Contributing

Stars

1.0k stars

Watchers

12 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Ardalis.Result - NuGetNuGetBuild Status

Ardails.Result.AspNetCore - NuGetNuGetArdails.Result.FluentValidation - NuGetNuGet

Follow @ardalisFollow @nimblepros

Result

A result abstraction that can be mapped to HTTP response codes if needed.

Docs

Docs are located in the /docs folder and available online at result.ardalis.com Please add issues for new docs requests and pull requests for docs issues are welcome!

Learn More

What Problem Does This Address?

Many methods on service need to return some kind of value. For instance, they may be looking up some data and returning a set of results or a single object. They might be creating something, persisting it, and then returning it. Typically, such methods are implemented like this:

publicCustomerGetCustomer(intcustomerId){// more logicreturncustomer;}publicCustomerCreateCustomer(stringfirstName,stringlastName){// more logicreturncustomer;}

This works great as long as we're only concerned with the happy path. But what happens if there are multiple failure modes, not all of which make sense to be handled by exceptions?

  • What happens if customerId is not found?
  • What happens if required lastName is not provided?
  • What happens if the current user doesn't have permission to create new customers?

The standard way to address these concerns is with exceptions. Maybe you throw a different exception for each different failure mode, and the calling code is then required to have multiple catch blocks designed for each type of failure. This makes life painful for the consumer, and results in a lot of exceptions for things that aren't necessarily exceptional. Like this:

[HttpGet]publicasyncTask<ActionResult<CustomerDTO>>GetCustomer(intcustomerId){try{varcustomer=_repository.GetById(customerId);varcustomerDTO=CustomerDTO.MapFrom(customer);returnOk(customerDTO);}catch(NullReferenceExceptionex){returnNotFound();}catch(Exceptionex){returnnewStatusCodeResult(StatusCodes.Status500InternalServerError);}}

Another approach is to return a Tuple of the expected result along with other things, like a status code and additional failure mode metadata. While tuples can be great for individual, flexible responses, they're not as good for having a single, standard, reusable approach to a problem.

The Result pattern provides a standard, reusable way to return both success as well as multiple kinds of non-success responses from .NET services in a way that can easily be mapped to API response types. Although the Ardalis.Result package has no dependencies on ASP.NET Core and can be used from any .NET Core application, the Ardalis.Result.AspNetCore companion package includes resources to enhance the use of this pattern within ASP.NET Core web API applications.

Sample Usage

Creating a Result

The sample folder includes some examples of how to use the project. Here are a couple of simple uses.

Imagine the snippet below is defined in a domain service that retrieves WeatherForecasts. When compared to the approach described above, this approach uses a result to handle common failure scenarios like missing data denoted as NotFound and or input validation errors denoted as Invalid. If execution is successful, the result will contain the random data generated by the final return statement.

publicResult<IEnumerable<WeatherForecast>>GetForecast(ForecastRequestDtomodel){if(model.PostalCode=="NotFound")returnResult<IEnumerable<WeatherForecast>>.NotFound();// validate modelif(model.PostalCode.Length>10){returnResult<IEnumerable<WeatherForecast>>.Invalid(newList<ValidationError>{newValidationError{Identifier=nameof(model.PostalCode),ErrorMessage="PostalCode cannot exceed 10 characters."}});}varrng=newRandom();returnnewResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1,5).Select(index =>newWeatherForecast{Date=DateTime.Now.AddDays(index),TemperatureC=rng.Next(-20,55),Summary=Summaries[rng.Next(Summaries.Length)]}).ToArray());}

Translating Results to ActionResults

Continuing with the domain service example from the previous section, it's important to show that the domain service doesn't know about ActionResult or other MVC/etc types. But since it is using a Result<T> abstraction, it can return results that are easily mapped to HTTP status codes. Note that the method above returns a Result<IEnumerable<WeatherForecast> but in some cases, it might need to return an Invalid result, or a NotFound result. Otherwise, it returns a Success result with the actual returned value (just like an API would return an HTTP 200 and the actual result of the API call).

You can apply the [TranslateResultToActionResult] attribute to an API Endpoint (or controller action if you still use those things) and it will automatically translate the Result<T> return type of the method to an ActionResult<T> appropriately based on the Result type.

[TranslateResultToActionResult][HttpPost("Create")]publicResult<IEnumerable<WeatherForecast>>CreateForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetForecast(model);}

Alternatively, you can use the ToActionResult helper method within an endpoint to achieve the same thing:

[HttpPost("/Forecast/New")]publicoverrideActionResult<IEnumerable<WeatherForecast>>Handle(ForecastRequestDtorequest){returnthis.ToActionResult(_weatherService.GetForecast(request));// alternately// return _weatherService.GetForecast(request).ToActionResult(this);}

Translating Results to Minimal API Results

Similar to how the ToActionResult extension method translates Ardalis.Results to ActionResults, the ToMinimalApiResult translates results to the new Microsoft.AspNetCore.Http.ResultsIResult types in .NET 6+. The following code snippet demonstrates how one might use the domain service that returns a Result<IEnumerable<WeatherForecast>> and convert to a Microsoft.AspNetCore.Http.Results instance.

app.MapPost("/Forecast/New",(ForecastRequestDtorequest,WeatherServiceweatherService)=>{returnweatherService.GetForecast(request).ToMinimalApiResult();}).WithName("NewWeatherForecast");

The full Minimal API sample can be found in the sample folder.

Mapping Results From One Type to Another

A common use case is to map between domain entities to API response types usually represented as DTOs. You can map a result containing a domain entity to a Result containing a DTO by using the Map method. The following example calls the method _weatherService.GetSingleForecast which returns a Result<WeatherForecast> which is then converted to a Result<WeatherForecastSummaryDto> by the call to Map. Then, the result is converted to an ActionResult<WeatherForecastSummaryDto> using the ToActionResult helper method.

[HttpPost("Summary")]publicActionResult<WeatherForecastSummaryDto>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetSingleForecast(model).Map(wf =>newWeatherForecastSummaryDto(wf.Date,wf.Summary)).ToActionResult(this);}

Chaining Result Operations with Bind and BindAsync

You can chain together multiple operations that return Result<T> using the Bind and BindAsync methods. This allows you to perform a sequence of operations where each operation can fail, and if any operation fails, the subsequent operations will not be executed. This can be useful for eliminating checking for success after each Result operation with a more readable and maintainable chain of operations.

[HttpPost("Summary")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){varresult1=await_weatherService.GetSingleForecast(model);if(!result1.IsSuccess){returnresult1.ToActionResult(this);}varresult2=await_weatherService.GetForecastDetailsAsync(result1.Value.Id);returnresult2.Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

With the Bind and BindAsync methods, you can simplify this code by chaining the operations together. The following example demonstrates how to use BindAsync to achieve the same result in a more concise manner:

[HttpPost("BindedForecast")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecastWithBind([FromBody]ForecastRequestDtomodel){returnawait_weatherService.GetSingleForecast(model).BindAsync(wf =>_weatherService.GetForecastDetailsAsync(wf.Id)).Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

In this example, if GetSingleForecast returns a failure result, GetForecastDetailsAsync will not be called, and the failure will propagate through the chain. If all operations succeed, the final result will be mapped to a WeatherForecastSummaryDto and returned as an ActionResult.

ASP.NET API Metadata

By default, Asp Net Core and API Explorer know nothing about [TranslateResultToActionResult] and what it is doing. To reflect [TranslateResultToActionResult] behavior in metadata generated by API Explorer (which is then used by tools like Swashbuckle, NSwag etc.), you can use ResultConvention:

services.AddControllers(mvcOptions =>mvcOptions.AddDefaultResultConvention());

This will add [ProducesResponseType] for every known ResultStatus to every endpoint marked with [TranslateResultToActionResult]. To customize ResultConvention behavior, one may use the AddResultConvention method:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap()));

This code is functionally equivalent to the previous example.

From here you can modify the ResultStatus to HttpStatusCode mapping

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).For(ResultStatus.Error,HttpStatusCode.InternalServerError)));

ResultConvention will add [ProducesResponseType] for every result status configured in ResultStatusMap. AddDefaultMap() maps every known ResultType, so if you want to exclude certain ResultType from being listed (e.g. your app doesn't have authentication and authorization, and you don't want 401 and 403 to be listed as SupportedResponseType) you can do this:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).Remove(ResultStatus.Forbidden).Remove(ResultStatus.Unauthorized)));

Alternatively, you can specify which (failure) result statuses are expected from a certain endpoint:

[TranslateResultToActionResult()][ExpectedFailures(ResultStatus.NotFound,ResultStatus.Invalid)][HttpDelete("Remove/{id}")]publicResultRemovePerson(intid){// Method logic}

!!! If you list a certain Result status in ExpectedFailures, it must be configured in ResultConvention on startup.

Another configurable feature is what part of the Result object is returned in case of specific failure:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Error,HttpStatusCode.BadRequest, resultStatusOptions =>resultStatusOptions.With((ctrlr,result)=>string.Join("\r\n",result.ValidationErrors)))));

Using Results with FluentValidation

We can use Ardalis.Result.FluentValidation on a service with FluentValidation like that:

publicasyncTask<Result<BlogCategory>>UpdateAsync(BlogCategoryblogCategory){if(Guid.Empty==blogCategory.BlogCategoryId)returnResult<BlogCategory>.NotFound();varvalidator=newBlogCategoryValidator();varvalidation=awaitvalidator.ValidateAsync(blogCategory);if(!validation.IsValid){returnResult<BlogCategory>.Invalid(validation.AsErrors());}varitemToUpdate=(awaitGetByIdAsync(blogCategory.BlogCategoryId)).Value;if(itemToUpdate==null){returnResult<BlogCategory>.NotFound();}itemToUpdate.Update(blogCategory.Name,blogCategory.ParentId);returnResult<BlogCategory>.Success(await_blogCategoryRepository.UpdateAsync(itemToUpdate));}

Getting Started

If you're building an ASP.NET Core Web API you can simply install the Ardalis.Result.AspNetCore package to get started. Then, apply the [TranslateResultToActionResult] attribute to any actions or controllers that you want to automatically translate from Result types to ActionResult types.

About

A result abstraction that can be mapped to HTTP response codes if needed.

Topics

Resources

Code of conduct

Contributing

Stars

1.0k stars

Watchers

12 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Ardalis.Result - NuGetNuGetBuild Status

Ardails.Result.AspNetCore - NuGetNuGetArdails.Result.FluentValidation - NuGetNuGet

Follow @ardalisFollow @nimblepros

Result

A result abstraction that can be mapped to HTTP response codes if needed.

Docs

Docs are located in the /docs folder and available online at result.ardalis.com Please add issues for new docs requests and pull requests for docs issues are welcome!

Learn More

What Problem Does This Address?

Many methods on service need to return some kind of value. For instance, they may be looking up some data and returning a set of results or a single object. They might be creating something, persisting it, and then returning it. Typically, such methods are implemented like this:

publicCustomerGetCustomer(intcustomerId){// more logicreturncustomer;}publicCustomerCreateCustomer(stringfirstName,stringlastName){// more logicreturncustomer;}

This works great as long as we're only concerned with the happy path. But what happens if there are multiple failure modes, not all of which make sense to be handled by exceptions?

  • What happens if customerId is not found?
  • What happens if required lastName is not provided?
  • What happens if the current user doesn't have permission to create new customers?

The standard way to address these concerns is with exceptions. Maybe you throw a different exception for each different failure mode, and the calling code is then required to have multiple catch blocks designed for each type of failure. This makes life painful for the consumer, and results in a lot of exceptions for things that aren't necessarily exceptional. Like this:

[HttpGet]publicasyncTask<ActionResult<CustomerDTO>>GetCustomer(intcustomerId){try{varcustomer=_repository.GetById(customerId);varcustomerDTO=CustomerDTO.MapFrom(customer);returnOk(customerDTO);}catch(NullReferenceExceptionex){returnNotFound();}catch(Exceptionex){returnnewStatusCodeResult(StatusCodes.Status500InternalServerError);}}

Another approach is to return a Tuple of the expected result along with other things, like a status code and additional failure mode metadata. While tuples can be great for individual, flexible responses, they're not as good for having a single, standard, reusable approach to a problem.

The Result pattern provides a standard, reusable way to return both success as well as multiple kinds of non-success responses from .NET services in a way that can easily be mapped to API response types. Although the Ardalis.Result package has no dependencies on ASP.NET Core and can be used from any .NET Core application, the Ardalis.Result.AspNetCore companion package includes resources to enhance the use of this pattern within ASP.NET Core web API applications.

Sample Usage

Creating a Result

The sample folder includes some examples of how to use the project. Here are a couple of simple uses.

Imagine the snippet below is defined in a domain service that retrieves WeatherForecasts. When compared to the approach described above, this approach uses a result to handle common failure scenarios like missing data denoted as NotFound and or input validation errors denoted as Invalid. If execution is successful, the result will contain the random data generated by the final return statement.

publicResult<IEnumerable<WeatherForecast>>GetForecast(ForecastRequestDtomodel){if(model.PostalCode=="NotFound")returnResult<IEnumerable<WeatherForecast>>.NotFound();// validate modelif(model.PostalCode.Length>10){returnResult<IEnumerable<WeatherForecast>>.Invalid(newList<ValidationError>{newValidationError{Identifier=nameof(model.PostalCode),ErrorMessage="PostalCode cannot exceed 10 characters."}});}varrng=newRandom();returnnewResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1,5).Select(index =>newWeatherForecast{Date=DateTime.Now.AddDays(index),TemperatureC=rng.Next(-20,55),Summary=Summaries[rng.Next(Summaries.Length)]}).ToArray());}

Translating Results to ActionResults

Continuing with the domain service example from the previous section, it's important to show that the domain service doesn't know about ActionResult or other MVC/etc types. But since it is using a Result<T> abstraction, it can return results that are easily mapped to HTTP status codes. Note that the method above returns a Result<IEnumerable<WeatherForecast> but in some cases, it might need to return an Invalid result, or a NotFound result. Otherwise, it returns a Success result with the actual returned value (just like an API would return an HTTP 200 and the actual result of the API call).

You can apply the [TranslateResultToActionResult] attribute to an API Endpoint (or controller action if you still use those things) and it will automatically translate the Result<T> return type of the method to an ActionResult<T> appropriately based on the Result type.

[TranslateResultToActionResult][HttpPost("Create")]publicResult<IEnumerable<WeatherForecast>>CreateForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetForecast(model);}

Alternatively, you can use the ToActionResult helper method within an endpoint to achieve the same thing:

[HttpPost("/Forecast/New")]publicoverrideActionResult<IEnumerable<WeatherForecast>>Handle(ForecastRequestDtorequest){returnthis.ToActionResult(_weatherService.GetForecast(request));// alternately// return _weatherService.GetForecast(request).ToActionResult(this);}

Translating Results to Minimal API Results

Similar to how the ToActionResult extension method translates Ardalis.Results to ActionResults, the ToMinimalApiResult translates results to the new Microsoft.AspNetCore.Http.ResultsIResult types in .NET 6+. The following code snippet demonstrates how one might use the domain service that returns a Result<IEnumerable<WeatherForecast>> and convert to a Microsoft.AspNetCore.Http.Results instance.

app.MapPost("/Forecast/New",(ForecastRequestDtorequest,WeatherServiceweatherService)=>{returnweatherService.GetForecast(request).ToMinimalApiResult();}).WithName("NewWeatherForecast");

The full Minimal API sample can be found in the sample folder.

Mapping Results From One Type to Another

A common use case is to map between domain entities to API response types usually represented as DTOs. You can map a result containing a domain entity to a Result containing a DTO by using the Map method. The following example calls the method _weatherService.GetSingleForecast which returns a Result<WeatherForecast> which is then converted to a Result<WeatherForecastSummaryDto> by the call to Map. Then, the result is converted to an ActionResult<WeatherForecastSummaryDto> using the ToActionResult helper method.

[HttpPost("Summary")]publicActionResult<WeatherForecastSummaryDto>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetSingleForecast(model).Map(wf =>newWeatherForecastSummaryDto(wf.Date,wf.Summary)).ToActionResult(this);}

Chaining Result Operations with Bind and BindAsync

You can chain together multiple operations that return Result<T> using the Bind and BindAsync methods. This allows you to perform a sequence of operations where each operation can fail, and if any operation fails, the subsequent operations will not be executed. This can be useful for eliminating checking for success after each Result operation with a more readable and maintainable chain of operations.

[HttpPost("Summary")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){varresult1=await_weatherService.GetSingleForecast(model);if(!result1.IsSuccess){returnresult1.ToActionResult(this);}varresult2=await_weatherService.GetForecastDetailsAsync(result1.Value.Id);returnresult2.Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

With the Bind and BindAsync methods, you can simplify this code by chaining the operations together. The following example demonstrates how to use BindAsync to achieve the same result in a more concise manner:

[HttpPost("BindedForecast")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecastWithBind([FromBody]ForecastRequestDtomodel){returnawait_weatherService.GetSingleForecast(model).BindAsync(wf =>_weatherService.GetForecastDetailsAsync(wf.Id)).Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

In this example, if GetSingleForecast returns a failure result, GetForecastDetailsAsync will not be called, and the failure will propagate through the chain. If all operations succeed, the final result will be mapped to a WeatherForecastSummaryDto and returned as an ActionResult.

ASP.NET API Metadata

By default, Asp Net Core and API Explorer know nothing about [TranslateResultToActionResult] and what it is doing. To reflect [TranslateResultToActionResult] behavior in metadata generated by API Explorer (which is then used by tools like Swashbuckle, NSwag etc.), you can use ResultConvention:

services.AddControllers(mvcOptions =>mvcOptions.AddDefaultResultConvention());

This will add [ProducesResponseType] for every known ResultStatus to every endpoint marked with [TranslateResultToActionResult]. To customize ResultConvention behavior, one may use the AddResultConvention method:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap()));

This code is functionally equivalent to the previous example.

From here you can modify the ResultStatus to HttpStatusCode mapping

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).For(ResultStatus.Error,HttpStatusCode.InternalServerError)));

ResultConvention will add [ProducesResponseType] for every result status configured in ResultStatusMap. AddDefaultMap() maps every known ResultType, so if you want to exclude certain ResultType from being listed (e.g. your app doesn't have authentication and authorization, and you don't want 401 and 403 to be listed as SupportedResponseType) you can do this:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).Remove(ResultStatus.Forbidden).Remove(ResultStatus.Unauthorized)));

Alternatively, you can specify which (failure) result statuses are expected from a certain endpoint:

[TranslateResultToActionResult()][ExpectedFailures(ResultStatus.NotFound,ResultStatus.Invalid)][HttpDelete("Remove/{id}")]publicResultRemovePerson(intid){// Method logic}

!!! If you list a certain Result status in ExpectedFailures, it must be configured in ResultConvention on startup.

Another configurable feature is what part of the Result object is returned in case of specific failure:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Error,HttpStatusCode.BadRequest, resultStatusOptions =>resultStatusOptions.With((ctrlr,result)=>string.Join("\r\n",result.ValidationErrors)))));

Using Results with FluentValidation

We can use Ardalis.Result.FluentValidation on a service with FluentValidation like that:

publicasyncTask<Result<BlogCategory>>UpdateAsync(BlogCategoryblogCategory){if(Guid.Empty==blogCategory.BlogCategoryId)returnResult<BlogCategory>.NotFound();varvalidator=newBlogCategoryValidator();varvalidation=awaitvalidator.ValidateAsync(blogCategory);if(!validation.IsValid){returnResult<BlogCategory>.Invalid(validation.AsErrors());}varitemToUpdate=(awaitGetByIdAsync(blogCategory.BlogCategoryId)).Value;if(itemToUpdate==null){returnResult<BlogCategory>.NotFound();}itemToUpdate.Update(blogCategory.Name,blogCategory.ParentId);returnResult<BlogCategory>.Success(await_blogCategoryRepository.UpdateAsync(itemToUpdate));}

Getting Started

If you're building an ASP.NET Core Web API you can simply install the Ardalis.Result.AspNetCore package to get started. Then, apply the [TranslateResultToActionResult] attribute to any actions or controllers that you want to automatically translate from Result types to ActionResult types.

About

A result abstraction that can be mapped to HTTP response codes if needed.

Topics

Resources

Code of conduct

Contributing

Stars

1.0k stars

Watchers

12 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Ardalis.Result - NuGetNuGetBuild Status

Ardails.Result.AspNetCore - NuGetNuGetArdails.Result.FluentValidation - NuGetNuGet

Follow @ardalisFollow @nimblepros

Result

A result abstraction that can be mapped to HTTP response codes if needed.

Docs

Docs are located in the /docs folder and available online at result.ardalis.com Please add issues for new docs requests and pull requests for docs issues are welcome!

Learn More

What Problem Does This Address?

Many methods on service need to return some kind of value. For instance, they may be looking up some data and returning a set of results or a single object. They might be creating something, persisting it, and then returning it. Typically, such methods are implemented like this:

publicCustomerGetCustomer(intcustomerId){// more logicreturncustomer;}publicCustomerCreateCustomer(stringfirstName,stringlastName){// more logicreturncustomer;}

This works great as long as we're only concerned with the happy path. But what happens if there are multiple failure modes, not all of which make sense to be handled by exceptions?

  • What happens if customerId is not found?
  • What happens if required lastName is not provided?
  • What happens if the current user doesn't have permission to create new customers?

The standard way to address these concerns is with exceptions. Maybe you throw a different exception for each different failure mode, and the calling code is then required to have multiple catch blocks designed for each type of failure. This makes life painful for the consumer, and results in a lot of exceptions for things that aren't necessarily exceptional. Like this:

[HttpGet]publicasyncTask<ActionResult<CustomerDTO>>GetCustomer(intcustomerId){try{varcustomer=_repository.GetById(customerId);varcustomerDTO=CustomerDTO.MapFrom(customer);returnOk(customerDTO);}catch(NullReferenceExceptionex){returnNotFound();}catch(Exceptionex){returnnewStatusCodeResult(StatusCodes.Status500InternalServerError);}}

Another approach is to return a Tuple of the expected result along with other things, like a status code and additional failure mode metadata. While tuples can be great for individual, flexible responses, they're not as good for having a single, standard, reusable approach to a problem.

The Result pattern provides a standard, reusable way to return both success as well as multiple kinds of non-success responses from .NET services in a way that can easily be mapped to API response types. Although the Ardalis.Result package has no dependencies on ASP.NET Core and can be used from any .NET Core application, the Ardalis.Result.AspNetCore companion package includes resources to enhance the use of this pattern within ASP.NET Core web API applications.

Sample Usage

Creating a Result

The sample folder includes some examples of how to use the project. Here are a couple of simple uses.

Imagine the snippet below is defined in a domain service that retrieves WeatherForecasts. When compared to the approach described above, this approach uses a result to handle common failure scenarios like missing data denoted as NotFound and or input validation errors denoted as Invalid. If execution is successful, the result will contain the random data generated by the final return statement.

publicResult<IEnumerable<WeatherForecast>>GetForecast(ForecastRequestDtomodel){if(model.PostalCode=="NotFound")returnResult<IEnumerable<WeatherForecast>>.NotFound();// validate modelif(model.PostalCode.Length>10){returnResult<IEnumerable<WeatherForecast>>.Invalid(newList<ValidationError>{newValidationError{Identifier=nameof(model.PostalCode),ErrorMessage="PostalCode cannot exceed 10 characters."}});}varrng=newRandom();returnnewResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1,5).Select(index =>newWeatherForecast{Date=DateTime.Now.AddDays(index),TemperatureC=rng.Next(-20,55),Summary=Summaries[rng.Next(Summaries.Length)]}).ToArray());}

Translating Results to ActionResults

Continuing with the domain service example from the previous section, it's important to show that the domain service doesn't know about ActionResult or other MVC/etc types. But since it is using a Result<T> abstraction, it can return results that are easily mapped to HTTP status codes. Note that the method above returns a Result<IEnumerable<WeatherForecast> but in some cases, it might need to return an Invalid result, or a NotFound result. Otherwise, it returns a Success result with the actual returned value (just like an API would return an HTTP 200 and the actual result of the API call).

You can apply the [TranslateResultToActionResult] attribute to an API Endpoint (or controller action if you still use those things) and it will automatically translate the Result<T> return type of the method to an ActionResult<T> appropriately based on the Result type.

[TranslateResultToActionResult][HttpPost("Create")]publicResult<IEnumerable<WeatherForecast>>CreateForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetForecast(model);}

Alternatively, you can use the ToActionResult helper method within an endpoint to achieve the same thing:

[HttpPost("/Forecast/New")]publicoverrideActionResult<IEnumerable<WeatherForecast>>Handle(ForecastRequestDtorequest){returnthis.ToActionResult(_weatherService.GetForecast(request));// alternately// return _weatherService.GetForecast(request).ToActionResult(this);}

Translating Results to Minimal API Results

Similar to how the ToActionResult extension method translates Ardalis.Results to ActionResults, the ToMinimalApiResult translates results to the new Microsoft.AspNetCore.Http.ResultsIResult types in .NET 6+. The following code snippet demonstrates how one might use the domain service that returns a Result<IEnumerable<WeatherForecast>> and convert to a Microsoft.AspNetCore.Http.Results instance.

app.MapPost("/Forecast/New",(ForecastRequestDtorequest,WeatherServiceweatherService)=>{returnweatherService.GetForecast(request).ToMinimalApiResult();}).WithName("NewWeatherForecast");

The full Minimal API sample can be found in the sample folder.

Mapping Results From One Type to Another

A common use case is to map between domain entities to API response types usually represented as DTOs. You can map a result containing a domain entity to a Result containing a DTO by using the Map method. The following example calls the method _weatherService.GetSingleForecast which returns a Result<WeatherForecast> which is then converted to a Result<WeatherForecastSummaryDto> by the call to Map. Then, the result is converted to an ActionResult<WeatherForecastSummaryDto> using the ToActionResult helper method.

[HttpPost("Summary")]publicActionResult<WeatherForecastSummaryDto>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetSingleForecast(model).Map(wf =>newWeatherForecastSummaryDto(wf.Date,wf.Summary)).ToActionResult(this);}

Chaining Result Operations with Bind and BindAsync

You can chain together multiple operations that return Result<T> using the Bind and BindAsync methods. This allows you to perform a sequence of operations where each operation can fail, and if any operation fails, the subsequent operations will not be executed. This can be useful for eliminating checking for success after each Result operation with a more readable and maintainable chain of operations.

[HttpPost("Summary")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){varresult1=await_weatherService.GetSingleForecast(model);if(!result1.IsSuccess){returnresult1.ToActionResult(this);}varresult2=await_weatherService.GetForecastDetailsAsync(result1.Value.Id);returnresult2.Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

With the Bind and BindAsync methods, you can simplify this code by chaining the operations together. The following example demonstrates how to use BindAsync to achieve the same result in a more concise manner:

[HttpPost("BindedForecast")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecastWithBind([FromBody]ForecastRequestDtomodel){returnawait_weatherService.GetSingleForecast(model).BindAsync(wf =>_weatherService.GetForecastDetailsAsync(wf.Id)).Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

In this example, if GetSingleForecast returns a failure result, GetForecastDetailsAsync will not be called, and the failure will propagate through the chain. If all operations succeed, the final result will be mapped to a WeatherForecastSummaryDto and returned as an ActionResult.

ASP.NET API Metadata

By default, Asp Net Core and API Explorer know nothing about [TranslateResultToActionResult] and what it is doing. To reflect [TranslateResultToActionResult] behavior in metadata generated by API Explorer (which is then used by tools like Swashbuckle, NSwag etc.), you can use ResultConvention:

services.AddControllers(mvcOptions =>mvcOptions.AddDefaultResultConvention());

This will add [ProducesResponseType] for every known ResultStatus to every endpoint marked with [TranslateResultToActionResult]. To customize ResultConvention behavior, one may use the AddResultConvention method:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap()));

This code is functionally equivalent to the previous example.

From here you can modify the ResultStatus to HttpStatusCode mapping

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).For(ResultStatus.Error,HttpStatusCode.InternalServerError)));

ResultConvention will add [ProducesResponseType] for every result status configured in ResultStatusMap. AddDefaultMap() maps every known ResultType, so if you want to exclude certain ResultType from being listed (e.g. your app doesn't have authentication and authorization, and you don't want 401 and 403 to be listed as SupportedResponseType) you can do this:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).Remove(ResultStatus.Forbidden).Remove(ResultStatus.Unauthorized)));

Alternatively, you can specify which (failure) result statuses are expected from a certain endpoint:

[TranslateResultToActionResult()][ExpectedFailures(ResultStatus.NotFound,ResultStatus.Invalid)][HttpDelete("Remove/{id}")]publicResultRemovePerson(intid){// Method logic}

!!! If you list a certain Result status in ExpectedFailures, it must be configured in ResultConvention on startup.

Another configurable feature is what part of the Result object is returned in case of specific failure:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Error,HttpStatusCode.BadRequest, resultStatusOptions =>resultStatusOptions.With((ctrlr,result)=>string.Join("\r\n",result.ValidationErrors)))));

Using Results with FluentValidation

We can use Ardalis.Result.FluentValidation on a service with FluentValidation like that:

publicasyncTask<Result<BlogCategory>>UpdateAsync(BlogCategoryblogCategory){if(Guid.Empty==blogCategory.BlogCategoryId)returnResult<BlogCategory>.NotFound();varvalidator=newBlogCategoryValidator();varvalidation=awaitvalidator.ValidateAsync(blogCategory);if(!validation.IsValid){returnResult<BlogCategory>.Invalid(validation.AsErrors());}varitemToUpdate=(awaitGetByIdAsync(blogCategory.BlogCategoryId)).Value;if(itemToUpdate==null){returnResult<BlogCategory>.NotFound();}itemToUpdate.Update(blogCategory.Name,blogCategory.ParentId);returnResult<BlogCategory>.Success(await_blogCategoryRepository.UpdateAsync(itemToUpdate));}

Getting Started

If you're building an ASP.NET Core Web API you can simply install the Ardalis.Result.AspNetCore package to get started. Then, apply the [TranslateResultToActionResult] attribute to any actions or controllers that you want to automatically translate from Result types to ActionResult types.

About

A result abstraction that can be mapped to HTTP response codes if needed.

Topics

Resources

Code of conduct

Contributing

Stars

1.0k stars

Watchers

12 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Ardalis.Result - NuGetNuGetBuild Status

Ardails.Result.AspNetCore - NuGetNuGetArdails.Result.FluentValidation - NuGetNuGet

Follow @ardalisFollow @nimblepros

Result

A result abstraction that can be mapped to HTTP response codes if needed.

Docs

Docs are located in the /docs folder and available online at result.ardalis.com Please add issues for new docs requests and pull requests for docs issues are welcome!

Learn More

What Problem Does This Address?

Many methods on service need to return some kind of value. For instance, they may be looking up some data and returning a set of results or a single object. They might be creating something, persisting it, and then returning it. Typically, such methods are implemented like this:

publicCustomerGetCustomer(intcustomerId){// more logicreturncustomer;}publicCustomerCreateCustomer(stringfirstName,stringlastName){// more logicreturncustomer;}

This works great as long as we're only concerned with the happy path. But what happens if there are multiple failure modes, not all of which make sense to be handled by exceptions?

  • What happens if customerId is not found?
  • What happens if required lastName is not provided?
  • What happens if the current user doesn't have permission to create new customers?

The standard way to address these concerns is with exceptions. Maybe you throw a different exception for each different failure mode, and the calling code is then required to have multiple catch blocks designed for each type of failure. This makes life painful for the consumer, and results in a lot of exceptions for things that aren't necessarily exceptional. Like this:

[HttpGet]publicasyncTask<ActionResult<CustomerDTO>>GetCustomer(intcustomerId){try{varcustomer=_repository.GetById(customerId);varcustomerDTO=CustomerDTO.MapFrom(customer);returnOk(customerDTO);}catch(NullReferenceExceptionex){returnNotFound();}catch(Exceptionex){returnnewStatusCodeResult(StatusCodes.Status500InternalServerError);}}

Another approach is to return a Tuple of the expected result along with other things, like a status code and additional failure mode metadata. While tuples can be great for individual, flexible responses, they're not as good for having a single, standard, reusable approach to a problem.

The Result pattern provides a standard, reusable way to return both success as well as multiple kinds of non-success responses from .NET services in a way that can easily be mapped to API response types. Although the Ardalis.Result package has no dependencies on ASP.NET Core and can be used from any .NET Core application, the Ardalis.Result.AspNetCore companion package includes resources to enhance the use of this pattern within ASP.NET Core web API applications.

Sample Usage

Creating a Result

The sample folder includes some examples of how to use the project. Here are a couple of simple uses.

Imagine the snippet below is defined in a domain service that retrieves WeatherForecasts. When compared to the approach described above, this approach uses a result to handle common failure scenarios like missing data denoted as NotFound and or input validation errors denoted as Invalid. If execution is successful, the result will contain the random data generated by the final return statement.

publicResult<IEnumerable<WeatherForecast>>GetForecast(ForecastRequestDtomodel){if(model.PostalCode=="NotFound")returnResult<IEnumerable<WeatherForecast>>.NotFound();// validate modelif(model.PostalCode.Length>10){returnResult<IEnumerable<WeatherForecast>>.Invalid(newList<ValidationError>{newValidationError{Identifier=nameof(model.PostalCode),ErrorMessage="PostalCode cannot exceed 10 characters."}});}varrng=newRandom();returnnewResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1,5).Select(index =>newWeatherForecast{Date=DateTime.Now.AddDays(index),TemperatureC=rng.Next(-20,55),Summary=Summaries[rng.Next(Summaries.Length)]}).ToArray());}

Translating Results to ActionResults

Continuing with the domain service example from the previous section, it's important to show that the domain service doesn't know about ActionResult or other MVC/etc types. But since it is using a Result<T> abstraction, it can return results that are easily mapped to HTTP status codes. Note that the method above returns a Result<IEnumerable<WeatherForecast> but in some cases, it might need to return an Invalid result, or a NotFound result. Otherwise, it returns a Success result with the actual returned value (just like an API would return an HTTP 200 and the actual result of the API call).

You can apply the [TranslateResultToActionResult] attribute to an API Endpoint (or controller action if you still use those things) and it will automatically translate the Result<T> return type of the method to an ActionResult<T> appropriately based on the Result type.

[TranslateResultToActionResult][HttpPost("Create")]publicResult<IEnumerable<WeatherForecast>>CreateForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetForecast(model);}

Alternatively, you can use the ToActionResult helper method within an endpoint to achieve the same thing:

[HttpPost("/Forecast/New")]publicoverrideActionResult<IEnumerable<WeatherForecast>>Handle(ForecastRequestDtorequest){returnthis.ToActionResult(_weatherService.GetForecast(request));// alternately// return _weatherService.GetForecast(request).ToActionResult(this);}

Translating Results to Minimal API Results

Similar to how the ToActionResult extension method translates Ardalis.Results to ActionResults, the ToMinimalApiResult translates results to the new Microsoft.AspNetCore.Http.ResultsIResult types in .NET 6+. The following code snippet demonstrates how one might use the domain service that returns a Result<IEnumerable<WeatherForecast>> and convert to a Microsoft.AspNetCore.Http.Results instance.

app.MapPost("/Forecast/New",(ForecastRequestDtorequest,WeatherServiceweatherService)=>{returnweatherService.GetForecast(request).ToMinimalApiResult();}).WithName("NewWeatherForecast");

The full Minimal API sample can be found in the sample folder.

Mapping Results From One Type to Another

A common use case is to map between domain entities to API response types usually represented as DTOs. You can map a result containing a domain entity to a Result containing a DTO by using the Map method. The following example calls the method _weatherService.GetSingleForecast which returns a Result<WeatherForecast> which is then converted to a Result<WeatherForecastSummaryDto> by the call to Map. Then, the result is converted to an ActionResult<WeatherForecastSummaryDto> using the ToActionResult helper method.

[HttpPost("Summary")]publicActionResult<WeatherForecastSummaryDto>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetSingleForecast(model).Map(wf =>newWeatherForecastSummaryDto(wf.Date,wf.Summary)).ToActionResult(this);}

Chaining Result Operations with Bind and BindAsync

You can chain together multiple operations that return Result<T> using the Bind and BindAsync methods. This allows you to perform a sequence of operations where each operation can fail, and if any operation fails, the subsequent operations will not be executed. This can be useful for eliminating checking for success after each Result operation with a more readable and maintainable chain of operations.

[HttpPost("Summary")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){varresult1=await_weatherService.GetSingleForecast(model);if(!result1.IsSuccess){returnresult1.ToActionResult(this);}varresult2=await_weatherService.GetForecastDetailsAsync(result1.Value.Id);returnresult2.Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

With the Bind and BindAsync methods, you can simplify this code by chaining the operations together. The following example demonstrates how to use BindAsync to achieve the same result in a more concise manner:

[HttpPost("BindedForecast")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecastWithBind([FromBody]ForecastRequestDtomodel){returnawait_weatherService.GetSingleForecast(model).BindAsync(wf =>_weatherService.GetForecastDetailsAsync(wf.Id)).Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

In this example, if GetSingleForecast returns a failure result, GetForecastDetailsAsync will not be called, and the failure will propagate through the chain. If all operations succeed, the final result will be mapped to a WeatherForecastSummaryDto and returned as an ActionResult.

ASP.NET API Metadata

By default, Asp Net Core and API Explorer know nothing about [TranslateResultToActionResult] and what it is doing. To reflect [TranslateResultToActionResult] behavior in metadata generated by API Explorer (which is then used by tools like Swashbuckle, NSwag etc.), you can use ResultConvention:

services.AddControllers(mvcOptions =>mvcOptions.AddDefaultResultConvention());

This will add [ProducesResponseType] for every known ResultStatus to every endpoint marked with [TranslateResultToActionResult]. To customize ResultConvention behavior, one may use the AddResultConvention method:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap()));

This code is functionally equivalent to the previous example.

From here you can modify the ResultStatus to HttpStatusCode mapping

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).For(ResultStatus.Error,HttpStatusCode.InternalServerError)));

ResultConvention will add [ProducesResponseType] for every result status configured in ResultStatusMap. AddDefaultMap() maps every known ResultType, so if you want to exclude certain ResultType from being listed (e.g. your app doesn't have authentication and authorization, and you don't want 401 and 403 to be listed as SupportedResponseType) you can do this:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).Remove(ResultStatus.Forbidden).Remove(ResultStatus.Unauthorized)));

Alternatively, you can specify which (failure) result statuses are expected from a certain endpoint:

[TranslateResultToActionResult()][ExpectedFailures(ResultStatus.NotFound,ResultStatus.Invalid)][HttpDelete("Remove/{id}")]publicResultRemovePerson(intid){// Method logic}

!!! If you list a certain Result status in ExpectedFailures, it must be configured in ResultConvention on startup.

Another configurable feature is what part of the Result object is returned in case of specific failure:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Error,HttpStatusCode.BadRequest, resultStatusOptions =>resultStatusOptions.With((ctrlr,result)=>string.Join("\r\n",result.ValidationErrors)))));

Using Results with FluentValidation

We can use Ardalis.Result.FluentValidation on a service with FluentValidation like that:

publicasyncTask<Result<BlogCategory>>UpdateAsync(BlogCategoryblogCategory){if(Guid.Empty==blogCategory.BlogCategoryId)returnResult<BlogCategory>.NotFound();varvalidator=newBlogCategoryValidator();varvalidation=awaitvalidator.ValidateAsync(blogCategory);if(!validation.IsValid){returnResult<BlogCategory>.Invalid(validation.AsErrors());}varitemToUpdate=(awaitGetByIdAsync(blogCategory.BlogCategoryId)).Value;if(itemToUpdate==null){returnResult<BlogCategory>.NotFound();}itemToUpdate.Update(blogCategory.Name,blogCategory.ParentId);returnResult<BlogCategory>.Success(await_blogCategoryRepository.UpdateAsync(itemToUpdate));}

Getting Started

If you're building an ASP.NET Core Web API you can simply install the Ardalis.Result.AspNetCore package to get started. Then, apply the [TranslateResultToActionResult] attribute to any actions or controllers that you want to automatically translate from Result types to ActionResult types.

About

A result abstraction that can be mapped to HTTP response codes if needed.

Topics

Resources

Code of conduct

Contributing

Stars

1.0k stars

Watchers

12 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Ardalis.Result - NuGetNuGetBuild Status

Ardails.Result.AspNetCore - NuGetNuGetArdails.Result.FluentValidation - NuGetNuGet

Follow @ardalisFollow @nimblepros

Result

A result abstraction that can be mapped to HTTP response codes if needed.

Docs

Docs are located in the /docs folder and available online at result.ardalis.com Please add issues for new docs requests and pull requests for docs issues are welcome!

Learn More

What Problem Does This Address?

Many methods on service need to return some kind of value. For instance, they may be looking up some data and returning a set of results or a single object. They might be creating something, persisting it, and then returning it. Typically, such methods are implemented like this:

publicCustomerGetCustomer(intcustomerId){// more logicreturncustomer;}publicCustomerCreateCustomer(stringfirstName,stringlastName){// more logicreturncustomer;}

This works great as long as we're only concerned with the happy path. But what happens if there are multiple failure modes, not all of which make sense to be handled by exceptions?

  • What happens if customerId is not found?
  • What happens if required lastName is not provided?
  • What happens if the current user doesn't have permission to create new customers?

The standard way to address these concerns is with exceptions. Maybe you throw a different exception for each different failure mode, and the calling code is then required to have multiple catch blocks designed for each type of failure. This makes life painful for the consumer, and results in a lot of exceptions for things that aren't necessarily exceptional. Like this:

[HttpGet]publicasyncTask<ActionResult<CustomerDTO>>GetCustomer(intcustomerId){try{varcustomer=_repository.GetById(customerId);varcustomerDTO=CustomerDTO.MapFrom(customer);returnOk(customerDTO);}catch(NullReferenceExceptionex){returnNotFound();}catch(Exceptionex){returnnewStatusCodeResult(StatusCodes.Status500InternalServerError);}}

Another approach is to return a Tuple of the expected result along with other things, like a status code and additional failure mode metadata. While tuples can be great for individual, flexible responses, they're not as good for having a single, standard, reusable approach to a problem.

The Result pattern provides a standard, reusable way to return both success as well as multiple kinds of non-success responses from .NET services in a way that can easily be mapped to API response types. Although the Ardalis.Result package has no dependencies on ASP.NET Core and can be used from any .NET Core application, the Ardalis.Result.AspNetCore companion package includes resources to enhance the use of this pattern within ASP.NET Core web API applications.

Sample Usage

Creating a Result

The sample folder includes some examples of how to use the project. Here are a couple of simple uses.

Imagine the snippet below is defined in a domain service that retrieves WeatherForecasts. When compared to the approach described above, this approach uses a result to handle common failure scenarios like missing data denoted as NotFound and or input validation errors denoted as Invalid. If execution is successful, the result will contain the random data generated by the final return statement.

publicResult<IEnumerable<WeatherForecast>>GetForecast(ForecastRequestDtomodel){if(model.PostalCode=="NotFound")returnResult<IEnumerable<WeatherForecast>>.NotFound();// validate modelif(model.PostalCode.Length>10){returnResult<IEnumerable<WeatherForecast>>.Invalid(newList<ValidationError>{newValidationError{Identifier=nameof(model.PostalCode),ErrorMessage="PostalCode cannot exceed 10 characters."}});}varrng=newRandom();returnnewResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1,5).Select(index =>newWeatherForecast{Date=DateTime.Now.AddDays(index),TemperatureC=rng.Next(-20,55),Summary=Summaries[rng.Next(Summaries.Length)]}).ToArray());}

Translating Results to ActionResults

Continuing with the domain service example from the previous section, it's important to show that the domain service doesn't know about ActionResult or other MVC/etc types. But since it is using a Result<T> abstraction, it can return results that are easily mapped to HTTP status codes. Note that the method above returns a Result<IEnumerable<WeatherForecast> but in some cases, it might need to return an Invalid result, or a NotFound result. Otherwise, it returns a Success result with the actual returned value (just like an API would return an HTTP 200 and the actual result of the API call).

You can apply the [TranslateResultToActionResult] attribute to an API Endpoint (or controller action if you still use those things) and it will automatically translate the Result<T> return type of the method to an ActionResult<T> appropriately based on the Result type.

[TranslateResultToActionResult][HttpPost("Create")]publicResult<IEnumerable<WeatherForecast>>CreateForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetForecast(model);}

Alternatively, you can use the ToActionResult helper method within an endpoint to achieve the same thing:

[HttpPost("/Forecast/New")]publicoverrideActionResult<IEnumerable<WeatherForecast>>Handle(ForecastRequestDtorequest){returnthis.ToActionResult(_weatherService.GetForecast(request));// alternately// return _weatherService.GetForecast(request).ToActionResult(this);}

Translating Results to Minimal API Results

Similar to how the ToActionResult extension method translates Ardalis.Results to ActionResults, the ToMinimalApiResult translates results to the new Microsoft.AspNetCore.Http.ResultsIResult types in .NET 6+. The following code snippet demonstrates how one might use the domain service that returns a Result<IEnumerable<WeatherForecast>> and convert to a Microsoft.AspNetCore.Http.Results instance.

app.MapPost("/Forecast/New",(ForecastRequestDtorequest,WeatherServiceweatherService)=>{returnweatherService.GetForecast(request).ToMinimalApiResult();}).WithName("NewWeatherForecast");

The full Minimal API sample can be found in the sample folder.

Mapping Results From One Type to Another

A common use case is to map between domain entities to API response types usually represented as DTOs. You can map a result containing a domain entity to a Result containing a DTO by using the Map method. The following example calls the method _weatherService.GetSingleForecast which returns a Result<WeatherForecast> which is then converted to a Result<WeatherForecastSummaryDto> by the call to Map. Then, the result is converted to an ActionResult<WeatherForecastSummaryDto> using the ToActionResult helper method.

[HttpPost("Summary")]publicActionResult<WeatherForecastSummaryDto>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetSingleForecast(model).Map(wf =>newWeatherForecastSummaryDto(wf.Date,wf.Summary)).ToActionResult(this);}

Chaining Result Operations with Bind and BindAsync

You can chain together multiple operations that return Result<T> using the Bind and BindAsync methods. This allows you to perform a sequence of operations where each operation can fail, and if any operation fails, the subsequent operations will not be executed. This can be useful for eliminating checking for success after each Result operation with a more readable and maintainable chain of operations.

[HttpPost("Summary")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){varresult1=await_weatherService.GetSingleForecast(model);if(!result1.IsSuccess){returnresult1.ToActionResult(this);}varresult2=await_weatherService.GetForecastDetailsAsync(result1.Value.Id);returnresult2.Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

With the Bind and BindAsync methods, you can simplify this code by chaining the operations together. The following example demonstrates how to use BindAsync to achieve the same result in a more concise manner:

[HttpPost("BindedForecast")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecastWithBind([FromBody]ForecastRequestDtomodel){returnawait_weatherService.GetSingleForecast(model).BindAsync(wf =>_weatherService.GetForecastDetailsAsync(wf.Id)).Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

In this example, if GetSingleForecast returns a failure result, GetForecastDetailsAsync will not be called, and the failure will propagate through the chain. If all operations succeed, the final result will be mapped to a WeatherForecastSummaryDto and returned as an ActionResult.

ASP.NET API Metadata

By default, Asp Net Core and API Explorer know nothing about [TranslateResultToActionResult] and what it is doing. To reflect [TranslateResultToActionResult] behavior in metadata generated by API Explorer (which is then used by tools like Swashbuckle, NSwag etc.), you can use ResultConvention:

services.AddControllers(mvcOptions =>mvcOptions.AddDefaultResultConvention());

This will add [ProducesResponseType] for every known ResultStatus to every endpoint marked with [TranslateResultToActionResult]. To customize ResultConvention behavior, one may use the AddResultConvention method:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap()));

This code is functionally equivalent to the previous example.

From here you can modify the ResultStatus to HttpStatusCode mapping

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).For(ResultStatus.Error,HttpStatusCode.InternalServerError)));

ResultConvention will add [ProducesResponseType] for every result status configured in ResultStatusMap. AddDefaultMap() maps every known ResultType, so if you want to exclude certain ResultType from being listed (e.g. your app doesn't have authentication and authorization, and you don't want 401 and 403 to be listed as SupportedResponseType) you can do this:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).Remove(ResultStatus.Forbidden).Remove(ResultStatus.Unauthorized)));

Alternatively, you can specify which (failure) result statuses are expected from a certain endpoint:

[TranslateResultToActionResult()][ExpectedFailures(ResultStatus.NotFound,ResultStatus.Invalid)][HttpDelete("Remove/{id}")]publicResultRemovePerson(intid){// Method logic}

!!! If you list a certain Result status in ExpectedFailures, it must be configured in ResultConvention on startup.

Another configurable feature is what part of the Result object is returned in case of specific failure:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Error,HttpStatusCode.BadRequest, resultStatusOptions =>resultStatusOptions.With((ctrlr,result)=>string.Join("\r\n",result.ValidationErrors)))));

Using Results with FluentValidation

We can use Ardalis.Result.FluentValidation on a service with FluentValidation like that:

publicasyncTask<Result<BlogCategory>>UpdateAsync(BlogCategoryblogCategory){if(Guid.Empty==blogCategory.BlogCategoryId)returnResult<BlogCategory>.NotFound();varvalidator=newBlogCategoryValidator();varvalidation=awaitvalidator.ValidateAsync(blogCategory);if(!validation.IsValid){returnResult<BlogCategory>.Invalid(validation.AsErrors());}varitemToUpdate=(awaitGetByIdAsync(blogCategory.BlogCategoryId)).Value;if(itemToUpdate==null){returnResult<BlogCategory>.NotFound();}itemToUpdate.Update(blogCategory.Name,blogCategory.ParentId);returnResult<BlogCategory>.Success(await_blogCategoryRepository.UpdateAsync(itemToUpdate));}

Getting Started

If you're building an ASP.NET Core Web API you can simply install the Ardalis.Result.AspNetCore package to get started. Then, apply the [TranslateResultToActionResult] attribute to any actions or controllers that you want to automatically translate from Result types to ActionResult types.

About

A result abstraction that can be mapped to HTTP response codes if needed.

Topics

Resources

Code of conduct

Contributing

Stars

1.0k stars

Watchers

12 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Ardalis.Result - NuGetNuGetBuild Status

Ardails.Result.AspNetCore - NuGetNuGetArdails.Result.FluentValidation - NuGetNuGet

Follow @ardalisFollow @nimblepros

Result

A result abstraction that can be mapped to HTTP response codes if needed.

Docs

Docs are located in the /docs folder and available online at result.ardalis.com Please add issues for new docs requests and pull requests for docs issues are welcome!

Learn More

What Problem Does This Address?

Many methods on service need to return some kind of value. For instance, they may be looking up some data and returning a set of results or a single object. They might be creating something, persisting it, and then returning it. Typically, such methods are implemented like this:

publicCustomerGetCustomer(intcustomerId){// more logicreturncustomer;}publicCustomerCreateCustomer(stringfirstName,stringlastName){// more logicreturncustomer;}

This works great as long as we're only concerned with the happy path. But what happens if there are multiple failure modes, not all of which make sense to be handled by exceptions?

  • What happens if customerId is not found?
  • What happens if required lastName is not provided?
  • What happens if the current user doesn't have permission to create new customers?

The standard way to address these concerns is with exceptions. Maybe you throw a different exception for each different failure mode, and the calling code is then required to have multiple catch blocks designed for each type of failure. This makes life painful for the consumer, and results in a lot of exceptions for things that aren't necessarily exceptional. Like this:

[HttpGet]publicasyncTask<ActionResult<CustomerDTO>>GetCustomer(intcustomerId){try{varcustomer=_repository.GetById(customerId);varcustomerDTO=CustomerDTO.MapFrom(customer);returnOk(customerDTO);}catch(NullReferenceExceptionex){returnNotFound();}catch(Exceptionex){returnnewStatusCodeResult(StatusCodes.Status500InternalServerError);}}

Another approach is to return a Tuple of the expected result along with other things, like a status code and additional failure mode metadata. While tuples can be great for individual, flexible responses, they're not as good for having a single, standard, reusable approach to a problem.

The Result pattern provides a standard, reusable way to return both success as well as multiple kinds of non-success responses from .NET services in a way that can easily be mapped to API response types. Although the Ardalis.Result package has no dependencies on ASP.NET Core and can be used from any .NET Core application, the Ardalis.Result.AspNetCore companion package includes resources to enhance the use of this pattern within ASP.NET Core web API applications.

Sample Usage

Creating a Result

The sample folder includes some examples of how to use the project. Here are a couple of simple uses.

Imagine the snippet below is defined in a domain service that retrieves WeatherForecasts. When compared to the approach described above, this approach uses a result to handle common failure scenarios like missing data denoted as NotFound and or input validation errors denoted as Invalid. If execution is successful, the result will contain the random data generated by the final return statement.

publicResult<IEnumerable<WeatherForecast>>GetForecast(ForecastRequestDtomodel){if(model.PostalCode=="NotFound")returnResult<IEnumerable<WeatherForecast>>.NotFound();// validate modelif(model.PostalCode.Length>10){returnResult<IEnumerable<WeatherForecast>>.Invalid(newList<ValidationError>{newValidationError{Identifier=nameof(model.PostalCode),ErrorMessage="PostalCode cannot exceed 10 characters."}});}varrng=newRandom();returnnewResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1,5).Select(index =>newWeatherForecast{Date=DateTime.Now.AddDays(index),TemperatureC=rng.Next(-20,55),Summary=Summaries[rng.Next(Summaries.Length)]}).ToArray());}

Translating Results to ActionResults

Continuing with the domain service example from the previous section, it's important to show that the domain service doesn't know about ActionResult or other MVC/etc types. But since it is using a Result<T> abstraction, it can return results that are easily mapped to HTTP status codes. Note that the method above returns a Result<IEnumerable<WeatherForecast> but in some cases, it might need to return an Invalid result, or a NotFound result. Otherwise, it returns a Success result with the actual returned value (just like an API would return an HTTP 200 and the actual result of the API call).

You can apply the [TranslateResultToActionResult] attribute to an API Endpoint (or controller action if you still use those things) and it will automatically translate the Result<T> return type of the method to an ActionResult<T> appropriately based on the Result type.

[TranslateResultToActionResult][HttpPost("Create")]publicResult<IEnumerable<WeatherForecast>>CreateForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetForecast(model);}

Alternatively, you can use the ToActionResult helper method within an endpoint to achieve the same thing:

[HttpPost("/Forecast/New")]publicoverrideActionResult<IEnumerable<WeatherForecast>>Handle(ForecastRequestDtorequest){returnthis.ToActionResult(_weatherService.GetForecast(request));// alternately// return _weatherService.GetForecast(request).ToActionResult(this);}

Translating Results to Minimal API Results

Similar to how the ToActionResult extension method translates Ardalis.Results to ActionResults, the ToMinimalApiResult translates results to the new Microsoft.AspNetCore.Http.ResultsIResult types in .NET 6+. The following code snippet demonstrates how one might use the domain service that returns a Result<IEnumerable<WeatherForecast>> and convert to a Microsoft.AspNetCore.Http.Results instance.

app.MapPost("/Forecast/New",(ForecastRequestDtorequest,WeatherServiceweatherService)=>{returnweatherService.GetForecast(request).ToMinimalApiResult();}).WithName("NewWeatherForecast");

The full Minimal API sample can be found in the sample folder.

Mapping Results From One Type to Another

A common use case is to map between domain entities to API response types usually represented as DTOs. You can map a result containing a domain entity to a Result containing a DTO by using the Map method. The following example calls the method _weatherService.GetSingleForecast which returns a Result<WeatherForecast> which is then converted to a Result<WeatherForecastSummaryDto> by the call to Map. Then, the result is converted to an ActionResult<WeatherForecastSummaryDto> using the ToActionResult helper method.

[HttpPost("Summary")]publicActionResult<WeatherForecastSummaryDto>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){return_weatherService.GetSingleForecast(model).Map(wf =>newWeatherForecastSummaryDto(wf.Date,wf.Summary)).ToActionResult(this);}

Chaining Result Operations with Bind and BindAsync

You can chain together multiple operations that return Result<T> using the Bind and BindAsync methods. This allows you to perform a sequence of operations where each operation can fail, and if any operation fails, the subsequent operations will not be executed. This can be useful for eliminating checking for success after each Result operation with a more readable and maintainable chain of operations.

[HttpPost("Summary")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecast([FromBody]ForecastRequestDtomodel){varresult1=await_weatherService.GetSingleForecast(model);if(!result1.IsSuccess){returnresult1.ToActionResult(this);}varresult2=await_weatherService.GetForecastDetailsAsync(result1.Value.Id);returnresult2.Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

With the Bind and BindAsync methods, you can simplify this code by chaining the operations together. The following example demonstrates how to use BindAsync to achieve the same result in a more concise manner:

[HttpPost("BindedForecast")]publicasyncTask<ActionResult<WeatherForecastSummaryDto>>CreateSummaryForecastWithBind([FromBody]ForecastRequestDtomodel){returnawait_weatherService.GetSingleForecast(model).BindAsync(wf =>_weatherService.GetForecastDetailsAsync(wf.Id)).Map(details =>newWeatherForecastSummaryDto(details.Date,details.Summary)).ToActionResult(this);}

In this example, if GetSingleForecast returns a failure result, GetForecastDetailsAsync will not be called, and the failure will propagate through the chain. If all operations succeed, the final result will be mapped to a WeatherForecastSummaryDto and returned as an ActionResult.

ASP.NET API Metadata

By default, Asp Net Core and API Explorer know nothing about [TranslateResultToActionResult] and what it is doing. To reflect [TranslateResultToActionResult] behavior in metadata generated by API Explorer (which is then used by tools like Swashbuckle, NSwag etc.), you can use ResultConvention:

services.AddControllers(mvcOptions =>mvcOptions.AddDefaultResultConvention());

This will add [ProducesResponseType] for every known ResultStatus to every endpoint marked with [TranslateResultToActionResult]. To customize ResultConvention behavior, one may use the AddResultConvention method:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap()));

This code is functionally equivalent to the previous example.

From here you can modify the ResultStatus to HttpStatusCode mapping

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).For(ResultStatus.Error,HttpStatusCode.InternalServerError)));

ResultConvention will add [ProducesResponseType] for every result status configured in ResultStatusMap. AddDefaultMap() maps every known ResultType, so if you want to exclude certain ResultType from being listed (e.g. your app doesn't have authentication and authorization, and you don't want 401 and 403 to be listed as SupportedResponseType) you can do this:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Ok,HttpStatusCode.OK, resultStatusOptions =>resultStatusOptions.For("POST",HttpStatusCode.Created).For("DELETE",HttpStatusCode.NoContent)).Remove(ResultStatus.Forbidden).Remove(ResultStatus.Unauthorized)));

Alternatively, you can specify which (failure) result statuses are expected from a certain endpoint:

[TranslateResultToActionResult()][ExpectedFailures(ResultStatus.NotFound,ResultStatus.Invalid)][HttpDelete("Remove/{id}")]publicResultRemovePerson(intid){// Method logic}

!!! If you list a certain Result status in ExpectedFailures, it must be configured in ResultConvention on startup.

Another configurable feature is what part of the Result object is returned in case of specific failure:

services.AddControllers(mvcOptions =>mvcOptions.AddResultConvention(resultStatusMap =>resultStatusMap.AddDefaultMap().For(ResultStatus.Error,HttpStatusCode.BadRequest, resultStatusOptions =>resultStatusOptions.With((ctrlr,result)=>string.Join("\r\n",result.ValidationErrors)))));

Using Results with FluentValidation

We can use Ardalis.Result.FluentValidation on a service with FluentValidation like that:

publicasyncTask<Result<BlogCategory>>UpdateAsync(BlogCategoryblogCategory){if(Guid.Empty==blogCategory.BlogCategoryId)returnResult<BlogCategory>.NotFound();varvalidator=newBlogCategoryValidator();varvalidation=awaitvalidator.ValidateAsync(blogCategory);if(!validation.IsValid){returnResult<BlogCategory>.Invalid(validation.AsErrors());}varitemToUpdate=(awaitGetByIdAsync(blogCategory.BlogCategoryId)).Value;if(itemToUpdate==null){returnResult<BlogCategory>.NotFound();}itemToUpdate.Update(blogCategory.Name,blogCategory.ParentId);returnResult<BlogCategory>.Success(await_blogCategoryRepository.UpdateAsync(itemToUpdate));}

Getting Started

If you're building an ASP.NET Core Web API you can simply install the Ardalis.Result.AspNetCore package to get started. Then, apply the [TranslateResultToActionResult] attribute to any actions or controllers that you want to automatically translate from Result types to ActionResult types.

About

A result abstraction that can be mapped to HTTP response codes if needed.

Topics

Resources

Code of conduct

Contributing

Stars

1.0k stars

Watchers

12 watching

Forks

Releases

Packages

Used by

Contributors

Languages