Code Monkey home page Code Monkey logo

result's Introduction

Ardalis.Result - NuGet NuGet Build Status

Ardails.Result.AspNetCore - NuGet NuGet   Ardails.Result.FluentValidation - NuGet NuGet

Follow @ardalis   Follow @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:

public Customer GetCustomer(int customerId)
{
  // more logic
  return customer;
}

public Customer CreateCustomer(string firstName, string lastName)
{
  // more logic
  return customer;
}

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]
public async Task<ActionResult<CustomerDTO>> GetCustomer(int customerId)
{
  try
  {
    var customer = _repository.GetById(customerId);
    
    var customerDTO = CustomerDTO.MapFrom(customer);
    
    return Ok(customerDTO);
  }
  catch (NullReferenceException ex)
  {
    return NotFound();
  }
  catch (Exception ex)
  {
    return new StatusCodeResult(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.

public Result<IEnumerable<WeatherForecast>> GetForecast(ForecastRequestDto model)
{
    if (model.PostalCode == "NotFound") return Result<IEnumerable<WeatherForecast>>.NotFound();

    // validate model
    if (model.PostalCode.Length > 10)
    {
        return Result<IEnumerable<WeatherForecast>>.Invalid(new List<ValidationError> {
            new ValidationError
            {
                Identifier = nameof(model.PostalCode),
                ErrorMessage = "PostalCode cannot exceed 10 characters." 
            }
        });
    }

    var rng = new Random();
    return new Result<IEnumerable<WeatherForecast>>(Enumerable.Range(1, 5)
        .Select(index => new WeatherForecast
        {
            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")]
public Result<IEnumerable<WeatherForecast>> CreateForecast([FromBody]ForecastRequestDto model)
{
    return _weatherService.GetForecast(model);
}

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

[HttpPost("/Forecast/New")]
public override ActionResult<IEnumerable<WeatherForecast>> Handle(ForecastRequestDto request)
{
    return this.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.Results IResult 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", (ForecastRequestDto request, WeatherService weatherService) =>
{
    return weatherService.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")]
public ActionResult<WeatherForecastSummaryDto> CreateSummaryForecast([FromBody] ForecastRequestDto model)
{
    return _weatherService.GetSingleForecast(model)
        .Map(wf => new WeatherForecastSummaryDto(wf.Date, wf.Summary))
        .ToActionResult(this);
}

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}")]
public Result RemovePerson(int id)
{
    // 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:

public async Task<Result<BlogCategory>> UpdateAsync(BlogCategory blogCategory)
{
    if (Guid.Empty == blogCategory.BlogCategoryId) return Result<BlogCategory>.NotFound();

    var validator = new BlogCategoryValidator();
    var validation = await validator.ValidateAsync(blogCategory);
    if (!validation.IsValid)
    {
        return Result<BlogCategory>.Invalid(validation.AsErrors());
    }

    var itemToUpdate = (await GetByIdAsync(blogCategory.BlogCategoryId)).Value;
    if (itemToUpdate == null)
    {
        return Result<BlogCategory>.NotFound();
    }

    itemToUpdate.Update(blogCategory.Name, blogCategory.ParentId);

    return Result<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.

result's People

Contributors

almostengr avatar ardalis avatar artem-romanenia avatar berv63 avatar buti1021 avatar csteele86 avatar dadyarri avatar dalmazox avatar david-szabo97 avatar dependabot[bot] avatar doriangreen avatar enochmtzr avatar ewerton avatar feliperomero3 avatar fil-at-werma avatar happi-cat avatar iliyanang avatar ilyabelitser avatar kylemcmaster avatar mischaweerwag avatar pedroduarte0 avatar radekwojpl2 avatar randomuser99 avatar sadukie avatar scottjferguson avatar shadynagy avatar shahabganji avatar simongeering avatar sunecko avatar thomas-mullaly avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

result's Issues

New Feature: Add support for void returns

Currently, Result enforces that it is of a specific type. This does not allow for times when no value is returned from a function (i.e., typically a delete or an update). Having this would allow us to still utilize the useful features of Result. From an API perspective, this would then become an HTTP 201 No Content.

Pagination option

I am not sure but, what if we have a paged info property in Result class?

public class Result
{
    // ...
    public PagedInfo<T> PagedInfo { get; set; } = null;
    // ...
}

public class PagedInfo<T>
{
    public long PageNumber { get; set; }
    public long PageSize { get; set; }
    public long TotalPages { get; set; }
    public long TotalRecords { get; set; }
}

I think it covers paging scenarios too.

Use with DTOs

Given my aggregates can return a Result of T where T is an entity how do I translate Result of T into a Result of DTO in my API?

I'm trying to incorporate the use of both Result types and avoiding leaking entities

Invalid() returns 404 rather than 400

This is an odd one... pretty simple manual FluentValidation then using the ToActionResult extension method with .Invalid()...

When I use the nuget package, the result is 404, if i copy the extension class from this repo into my project I get the expected 400 with the validation errors json...

DTOValidator validator = new();
ValidationResult valResult = validator.Validate(dto);
if (!valResult.IsValid)
{
    return this.ToActionResult(Result<bool>.Invalid(valResult.AsErrors()));
}

possibly related to #74 but the old code looks like it should return a 400 as well... could be something in my middleware but I'd expect the 'source code in my project' to generate the same result.

Add next page token to PagedResult

In many cases a token for the next page is used, but neither PagedResult or PagedInfo includes this.
This could be solved by adding a string property to either PagedResult or PagedInfo.

I'm glad to do a PR with proposed changes.

Add a new Extension method on Result instead of Controller to map to an ActionResult

Basically instead of:

public override ActionResult<IEnumerable<WeatherForecast>> Handle(ForecastRequestDto request)
{
    return this.ToActionResult(_weatherService.GetForecast(request));
}

do this:

public override ActionResult<IEnumerable<WeatherForecast>> Handle(ForecastRequestDto request)
{
     return _weatherService.GetForecast(request).ToActionResult(this);
}

Consider adding an implicit cast from T to Result<T>

Consider adding an implicit cast from T to Result with an OK status. Since pretty much every use case involves the OK code it would be beneficial to be able to reduce the boilerplate code by simply returning the T value instead of using the Result constructor or the Success static method. There is the potential confusion of someone returning errors as a OK result because they didn't realize how the implicit cast works but that will probably be detected immediately and the user will be able to adapt.

TranslateResultToActionResult doesn't give PagedInfo

While using TranslateResultToActionResult to return responses in the form of ActionResults, there is no way for the caller to know the details of the pagination.

The reason is because the ToActionResult method in the ResultExtensions class just cares about the "result.GetValue()" and ignores the Pagination details which were fed by the upstream services as shown below:

return Result<IEnumerable<StoryDto>>.Success(pagedStoryDtos).ToPagedResult(pagedInfo);

Is there a way we can modify this behavior so that the caller can see the pagination details too?

Release 3.3.1 doesn't have latest ResultStatus - missing Unauthorised

The release v3.3.1 has the following definition of ResultStatus:

namespace Ardalis.Result
{
    public enum ResultStatus
    {
        Ok,
        Error,
        Forbidden,
        Invalid,
        NotFound
    }
}

Whereas the source code has an additional entry of Unauthorized which is needed for handling incorrect password kind of cases. Can you please update the release so that it has the correct Enum?

NotFound override to return a body

We'd like to use the result.NotFound() but designate the difference between 404 (route doesnt exist) and 404 (resource doesnt exist) by returning an error in the body.

Latest Ardalis.Result.AspNetCore not published?

Thanks for the lib it's really great.
There are many change that are in github but not published to nuget, could latest Ardalis.Result.AspNetCore be pushed?

See, the one in nuget is very old:
image

Not all result statuses are covered in ToActionResult

Currently we're checking for NotFound/Invalid explicitly in ToActionResult.
And finally we're returning Ok w/o checking if its really ok.
So any other status will be returned as 200 (and thats not correct I think)

Just see link and code below

            if (result.Status == ResultStatus.NotFound) return controller.NotFound();
            if (result.Status == ResultStatus.Invalid)
            {
                foreach (var error in result.ValidationErrors)
                {
                    // TODO: Fix after updating to 3.0.0
                    controller.ModelState.AddModelError(error.Identifier, error.ErrorMessage);
                }
                return controller.BadRequest(controller.ModelState);
            }

            return controller.Ok(result.Value);

PS: status has Error/Forbidden which aren't covered by if/then above.
PPS: So Forbidden will be treated as 200.


Assume should be like:

// first point (ok)
if (result.Status == ResultStatus.Ok) return controller.Ok(result.Value);
// custom responses
if (result.Status == ResultStatus.NotFound) return controller.NotFound();
if (result.Status == ResultStatus.Invalid) 
{
     /*  some codee here */ 
    return controller.BadRequest(controller.ModelState);
}

if (result.Status == ResultStatus.Forbidden) { /* some code here*/ }
if (result.Status == ResultStatus.Error } { /* some code here */ }

// default for not ok responses: can be NotSupported or smth
throw new  NotSupportedException();

Adding additional Result types for additional scenarios

I've started using Result<T> in a lot more places than I originally thought, and I've found some cases where I believe a few more options would be helpful.

As an example, I want to return the following:

  • An ResultStatus.Unchanged carrying no value in the event nothing is modified
  • A ResultStatus.Conflict perhaps with a payload of what EF Core has available for database values, current values, etc.
  • A ResultStatus.AlreadyExists with no payload
  • A ResultStatus.DetailedError with coded results instead of strings.
  • A ResultStatus.Success with no payload

What I've been using it for is to trap the exceptions in the repository and return only Result(s) which I handle in the endpoint.

I don't know, this might be a terrible idea, but this is a tiny bit like a discriminated union where I'm getting varied results from the same call. I did this in Dart and it was pretty handy.

If there is no interest, or someone's already working on it, I'll drop it. Perhaps clone the repo and just keep the project separate.

I've never contributed to open source projects before, so this is new to me, but if other people find it useful, I'm happy to contribute something.

Please advise. Thanks.

Add simple IsSuccess property to Result

I want to check to see if a result is successful or not sometimes if I pass a JsonResult of Result back to my Razor Page to be consumed by javascript.

I will add to the project along with tests and submit my very first github pull request!

Look into options for a FluentAPI or Builder pattern implementation

As discussed on stream, it would be handy to have a FluentAPI to create Result instances in an easier and more readable fashion, when adding validation errors for example.

This will need some consideration in the design and a POC.
The name suggested on the stream for it was ResultBuilder

Add sample showing how to use api response

In certain use cases a result may produce a type from Core but an ApiEndpoint may return a web response.

This works:

  public override async Task<ActionResult<fileResponse>> HandleAsync(int fileId, CancellationToken cancellationToken = default)
  {
    var result = await _mediator.Send(new fileRestoreCommand(fileId));

    if (result.Status == ResultStatus.NotFound)
    {
      return NotFound();
    }

    var file = result.Value;

    var response = new fileResponse(
      id: file.Id,
      description: file.Description);

    return Ok(response);
  }

This does not work since Result has a type from core and ActionResult wants a type from web

    if (!result.IsSuccess)
    {
      return this.ToActionResult(result);
    }

Support non-generic Result

That way, it gets less awkward when I still want to use ToActionResult, but on an endpoint without a response.

The use-case for this is that I still want validation and proper error responses.

ToResultAction() not showing as an option for a variable of type Result

For some reason, I am unable to use ToActionResult() on a Result type in a controller class that inherits from ControllerBase. It's not an option in Intellisense.

But it's available on the controller instance if I use this syntax: this.ToActionResult(test).
I am referencing Ardalis.Result.AspNetCore in the file.

What am I missing?

result

[Question] What would be a good way to return CreatedAtAction 201 result?

Hello,
thank you for the good library and good ideas from concepts!

What would be an elegant / good way to solve this problem. I could not find anything in the calculation.

[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<ActionResult<InventoryInspectionDoneDTO>> Create([FromBody] InventoryInspectionDoneCreateDTO dto)
{
       var resp = (await _inventoryInspectionsDoneService.CreateAsync(dto)).Map(x => 
       _mapper.Map<InventoryInspectionDoneDTO>(x));
       if (resp.IsSuccess)
          {
              return CreatedAtAction(nameof(GetById), new { id = resp.Value.Id }, resp.Value);
          }

   return this.ToActionResult(resp);
}

One idea would be write a custom extension but maybe there is chance to include it in this library.

Thanks for any ideas!

Update README to show typical use cases

The sample has several ways to use the library, but these should be called out in the README file as well. Should be broken up into just using the Result object and its helpers, and then separately the integration with AspNetCore for Actions/Endpoints.

ValidationErrors

Could IResult.ValidationErrors be Dictionary<string, string[]> to allow multiple errors for the same key?

FluentValidation dependency conflict

The FluentValidation dependency in the Ardalis.Result.FluentValidation.csproj specifies that you must have version >= 9.x, but with version 10.x, there was a breaking change that prevents this from working: https://docs.fluentvalidation.net/en/latest/upgrading-to-10.html#validationresult-errors-type-change. Here is where the code will break if you try using version 10.x of FluentValidation with Ardalis.Result:

foreach (var valFailure in valResult.Errors)

Here is the exception:

Message: 
    Test method <redacted test name> threw exception: 
    System.MissingMethodException: Method not found: 'System.Collections.Generic.IList`1<FluentValidation.Results.ValidationFailure> FluentValidation.Results.ValidationResult.get_Errors()'.
  Stack Trace: 
    FluentValidationResultExtensions.AsErrors(ValidationResult valResult)

System.NotSupportedException: Serialization and deserialization of 'System.Type' instances are not supported. Path: $.ValueType.

When return result it get exception :
System.NotSupportedException: Serialization and deserialization of 'System.Type' instances are not supported. Path: $.ValueType.
---> System.NotSupportedException: Serialization and deserialization of 'System.Type' instances are not supported.
at System.Text.Json.Serialization.Converters.UnsupportedTypeConverter1.Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonConverter1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.Metadata.JsonPropertyInfo1.GetMemberAndWriteJson(Object obj, WriteStack& state, Utf8JsonWriter writer) at System.Text.Json.Serialization.Converters.ObjectDefaultConverter1.OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonConverter1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)


public Result AddUser(AddUserRequest addUserRequest)
{
try
{
return Result.Success("test");
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.