A simple result pattern package to achieve API/Services/Handlers consistent response. Common result statuses, Error/NotFound/Invalid, and Success are handled and encapsulated in this package.
$ dotnet add package Ghanavats.ResultPatternConsistent and Flexible Result Handling for Developers to have better control over responses.
The Ghanavats.ResultPattern NuGet package offers a powerful and adaptable approach to returning results from methods and handlers.
Built on a well-known pattern, this implementation provides flexibility for developers to design and extend their own result-handling strategies.
Ghanavats Result Pattern supports both generic and non-generic scenarios. If it's more convenient, use it without specifying a type, otherwise use the generic variant.
Result.Success(), Result.Error("..."),
or Result.Invalid(...) without specifying a type when no return value is needed.ProblemDetails object for better error representation.Dictionary<string, string[]>. We are returning ValidationProblemDetails object for better error representation.ProblemDetails object for better error representation.Let's say you have a method to create a User via a repository. You want to use Result Pattern for flow control.
public Result<User> CreateUser(CreateUserRequest request)
{
if (string.IsNullOrWhiteSpace(request.Email))
{
return Result<User>.Invalid(validationResult); // where validationResult is a FluentValidation ValidationResult instance
// Optionally, you can also do:
// return Result<User>.Invalid(new Dictionary<string, string[]>
}
if (_userRepository.Exists(request.Email))
{
return Result<User>.Error("User already exists.");
// Or optionally, you can also specify an error kind:
// return Result<User>.Error("User already exists.", ErrorKind.BusinessRule);
}
var user = new User(request.Email);
_userRepository.Add(user);
return Result<User>.Success(user, "User created successfully.");
}
Expected Output for Result.Error (Also similar for Result.NotFound)
{
"status": 500,
"type": "https://datatracker.ietf.org/doc/html/rfc9110#section-15.6.1",
"title": "Unknown. There has been a problem with your request.",
"detail": "Sample error message.",
"instance": "/users/add",
"traceId": "Trace Id"
}
Expected Output for Result.Invalid
{
"status": 400,
"title": "One or more validation errors occurred.",
"detail": "See the errors property for details.",
"errors": {
"Email": [
"Email is required.",
"Email must be a valid email address."
]
},
"instance": "/users/add",
"traceId": "Trace Id"
}
Now, let's say the caller consumes CreateUser and needs to check the result:
var result = userService.CreateUser(request);
if(result.IsSuccess())
{
// take care of the logic here
}
if(result.IsError())
{
// Do what you need to do
}
The Aggregate method provides a simple and consistent way to group multiple Result instances (such as from multiple internal service calls or business rule validations) into a summarised collection of outcomes by status (e.g. Error, Invalid).
var results = new[]
{
Result.Ok(),
Result.Error("Database connection failed."),
Result.Invalid(validationResult), // where validationResult is a FluentValidation ValidationResult instance
};
var aggregated = Result.Aggregate(results);
Output (simplified):
[
{
"Status": "Error",
"Messages": ["Database connection failed."],
"ValidationErrorsPair": null
},
{
"Status": "Invalid",
"Messages": [],
"ValidationErrorsPair": {
"Property1": ["Error message 1"]
}
}
]
The Mapping feature in Ghanavats.ResultPattern allows you to seamlessly convert Result or Result objects into framework-specific HTTP responses in ASP.NET Core. It removes boilerplate code from your controllers and minimal APIs by providing ready-to-use extension methods that map your result status to the correct HTTP status code and response format.
For Minimal APIs
IResult ToResult(this Result result)
IResult ToResult<T>(this Result<T> result)
ValueTask<IResult> ToResultAsync(this Result result)
ValueTask<IResult> ToResultAsync<T>(this Result<T> result)
For MVC Controllers
IActionResult ToActionResult(this Result result, ControllerBase controller)
IActionResult ToActionResult<T>(this Result<T> result, ControllerBase controller)
ValueTask<IActionResult> ToActionResultAsync(this Result result, ControllerBase controller)
ValueTask<IActionResult> ToActionResultAsync<T>(this Result<T> result, ControllerBase controller)
Minimal API – synchronous mapping
app.MapPost("/users", (SomeRequest req, ISomeService svc) =>
{
var result = svc.DoSomething(req);
return result.ToResult(); // Converts Result to IResult
});
Minimal API – async mapping
app.MapPost("/users", async (SomeRequest req, ISomeService svc) =>
{
var result = await svc.DoSomethingAsync(req);
return await result.ToResultAsync(); // Converts Result<T> to IResult
});
MVC Controller – synchronous mapping
[HttpPost]
public IActionResult CreateUser(CreateUserRequest req)
{
var result = _svc.CreateUser(req);
return result.ToActionResult(this); // Converts Result to IActionResult
}
MVC Controller – async mapping
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserRequest req)
{
var result = await _svc.CreateUserAsync(req);
return await result.ToActionResultAsync(this); // Converts Result<T> to IActionResult
}
Note: You don't have to
awaitthe mapping. For Minimal API, you can do:return result.ToResultAsync();Or for MVC Controller action, you can do:
return result.ToActionResultAsync(this);