DomainResult 2.0.0
See the version list below for details.
dotnet add package DomainResult --version 2.0.0
NuGet\Install-Package DomainResult -Version 2.0.0
<PackageReference Include="DomainResult" Version="2.0.0" />
paket add DomainResult --version 2.0.0
#r "nuget: DomainResult, 2.0.0"
// Install DomainResult as a Cake Addin #addin nuget:?package=DomainResult&version=2.0.0 // Install DomainResult as a Cake Tool #tool nuget:?package=DomainResult&version=2.0.0
DomainResult
NuGet for decoupling domain operation results from ActionResult-based types of ASP.NET Web API
Two tiny NuGet packages addressing challenges in the ASP.NET Web API realm posed by separation of the Domain Layer (aka Business Layer) from the Application Layer:
- eliminating dependency on Microsoft.AspNetCore.Mvc (and IActionResult in particular) in the Domain Layer (usually a separate project);
- mapping various of responses from the Domain Layer to appropriate ActionResult-related type.
Content:
- Basic use-case
- Quick start
- 'DomainResult.Common' package. Returning result from Domain Layer method
- 'DomainResult' package. Conversion to ActionResult
- Custom Problem Details output
- Alternative solutions
Basic use-case
For a Domain Layer method like this:
public async Task<(InvoiceResponseDto, IDomainResult)> GetInvoice(int invoiceId)
{
if (invoiceId < 0)
// Returns a validation error
return IDomainResult.Failed<InvoiceResponseDto>("Try harder");
var invoice = await DataContext.Invoices.FindAsync(invoiceId);
if (invoice == null)
// Returns a Not Found response
return IDomainResult.NotFound<InvoiceResponseDto>();
// Returns the invoice
return IDomainResult.Success(invoice);
}
or if you're against ValueTuple or static methods on interfaces (added in C# 8), then a more traditional method signature:
public async Task<IDomainResult<InvoiceResponseDto>> GetInvoice(int invoiceId)
{
if (invoiceId < 0)
// Returns a validation error
return DomainResult.Failed<InvoiceResponseDto>("Try harder");
var invoice = await DataContext.Invoices.FindAsync(invoiceId);
if (invoice == null)
// Returns a Not Found response
return DomainResult.NotFound<InvoiceResponseDto>();
// Returns the invoice
return DomainResult.Success(invoice);
}
The Web API controller method would look like:
[ProducesResponseType(typeof(InvoiceResponseDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetInvoice()
{
return _service.GetInvoice().ToActionResult();
}
or leverage ActionResult<T>
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<ActionResult<InvoiceResponseDto>> GetInvoice()
{
return _service.GetInvoice().ToActionResultOfT();
}
The above returns:
- HTTP code
200 OK
along with an instance ofInvoiceResponseDto
on successful executions. - Non-2xx codes wrapped in ProblemDetails (as per RFC 7807):
- HTTP code
400 Bad Request
with a message "Try harder" when the invoice ID < 1 (the HTTP code can be configured to422 Unprocessable Entity
). - HTTP code
404 Not Found
for incorrect invoice IDs.
- HTTP code
Quick start
- Install DomainResult NuGet package for the Web API project.
- Install DomainResult.Common NuGet package for the Domain Layer (aka Business Layer) projects. If the Domain Layer is inside the Web API project, then skip this step.
- Follow the documentation below,
samples
in the repo and common sense.
The library targets .NET Core 3.1
, .NET 5.0
and .NET 6.0
.
'DomainResult.Common' package. Returning result from Domain Layer method
A tiny package with no dependency on Microsoft.AspNetCore.*
namespaces that provides:
- data types for returning from domain operations (wraps up the returned value and adds operation status with error messages if applicable);
- extension methods to effortlessly form the desired response.
It's built around IDomainResult
interface that has 3 properties:
IReadOnlyCollection<string> Errors { get; } // Collection of error messages if any
bool IsSuccess { get; } // Flag, whether the current status is successful or not
DomainOperationStatus Status { get; } // Current status of the domain operation: Success, Failed, NotFound, Unauthorized, etc.
And IDomainResult<T>
interface that also adds
// Value returned by the domain operation
T Value { get; }
It has 50+ static extension methods to return a successful or unsuccessful result from the domain method with one of the following types:
Returned type | Returned type wrapped in Task |
---|---|
IDomainResult |
Task<IDomainResult> |
IDomainResult<T> |
Task<IDomainResult<T>> |
(T, IDomainResult) |
Task<(T, IDomainResult)> |
Examples:
// Successful result with no value
IDomainResult res = IDomainResult.Success(); // res.Status is 'Success'
// Successful result with an int
(value, state) = IDomainResult.Success(10); // value = 10; state.Status is 'Success'
// The same but wrapped in a task
var res = IDomainResult.SuccessTask(10); // res is Task<(int, IDomainResult)>
// Implicit convertion
IDomainResult<int> res = 10; // res.Value = 10; res.Status is 'Success'
// Error message
IDomainResult res = IDomainResult.Failed("Ahh!"); // res.Status is 'Failed' and res.Errors = new []{ "Ahh!" }
// Error when expected an int
(value, state) = IDomainResult.Failed<int>("Ahh!"); // value = 0, state.Status is 'Failed' and state.Errors = new []{ "Ahh!" }
// 'Not Found' acts like the errors
(value, state) = IDomainResult.NotFound<int>(); // value = 0, state.Status is 'NotFound'
Task<(int val, IDomainResult state)> res = IDomainResult.NotFoundTask<int>(); // value = 0, state.Status is 'NotFound'
// 'Unauthorized' response
(value, state) = IDomainResult.Unauthorized<int>(); // value = 0, state.Status is 'Unauthorized'
Notes:
- The
Task
suffix on the extension methods indicates that the returned type is wrapped in aTask
(e.g.SuccessTask()
,FailedTask()
,NotFoundTask()
,UnauthorizedTask()
). - The
Failed()
andNotFound()
methods take as input parameters:string
,string[]
.Failed()
can also take ValidationResult.
'DomainResult' package. Conversion to ActionResult
Converts a IDomainResult
-based object to various ActionResult
-based types providing 20+ static extension methods.
Returned type | Returned type wrapped in Task |
Extension methods |
---|---|---|
IActionResult |
Task<IActionResult> |
ToActionResult() <br>ToCustomActionResult() |
ActionResult<T> |
Task<ActionResult<T>> |
ToActionResultOfT() <br>ToCustomActionResultOfT() |
<sub><sup>Note: DomainResult
package has dependency on Microsoft.AspNetCore.*
namespace and DomainResult.Common
package.</sup></sub>
The mapping rules are built around IDomainResult.Status
:
IDomainResult.Status |
Returned ActionResult -based type |
---|---|
Success |
If no value is returned then 204 NoContent , otherwise - 200 OK <br>Supports custom codes (e.g. 201 Created ) |
NotFound |
HTTP code 404 NotFound (default) |
Failed |
HTTP code 400 (default) or can be configured to 422 or any other code |
Unauthorized |
HTTP code 403 Forbidden (default) |
Examples:
// Returns `IActionResult` with HTTP code `204 NoContent` on success
IDomainResult.ToActionResult();
// The same as above, but returns `Task<IActionResult>` with no need in 'await'
Task<IDomainResult>.ToActionResult();
// Returns `IActionResult` with HTTP code `200 Ok` along with the value
IDomainResult<T>.ToActionResult();
(T, IDomainResult).ToActionResult();
// As above, but returns `Task<IActionResult>` with no need in 'await'
Task<IDomainResult<T>>.ToActionResult();
Task<(T, IDomainResult)>.ToActionResult();
// Returns `ActionResult<T>` with HTTP code `200 Ok` along with the value
IDomainResult<T>.ToActionResultOfT();
(T, IDomainResult).ToActionResultOfT();
// As above, but returns `Task<ActionResult<T>>` with no need in 'await'
Task<IDomainResult<T>>.ToActionResultOfT();
Task<(T, IDomainResult)>.ToActionResultOfT();
Custom Problem Details output
There is a way to tune the Problem Details output case-by-case.
Custom ActionResult response for 2xx HTTP codes
When returning a standard 200
or 204
HTTP code is not enough, there are extension methods to knock yourself out - ToCustomActionResult()
and ToCustomActionResultOfT
.
Example of returning 201 Created along with a location header field pointing to the created resource (as per RFC7231):
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public ActionResult<int> CreateItem(CreateItemDto dto)
{
// Service method for creating an item and returning its ID.
// Can return any of the IDomainResult types (e.g. (int, IDomainResult, IDomainResult<int>, Task<...>, etc).
var result = _service.CreateItem(dto);
// Custom conversion of the successful response only. For others, it returns standard 4xx HTTP codes
return result.ToCustomActionResultOfT(
// On success returns '201 Created' with a link to '/{id}' route in HTTP headers
val => CreatedAtAction(nameof(GetById), new { id = val }, val)
);
}
// Returns an entity by ID
[HttpGet("{id}")]
public IActionResult GetById([FromRoute] int id)
{
...
}
It works with any of extensions in Microsoft.AspNetCore.Mvc.ControllerBase
. Here are some:
- AcceptedAtAction and AcceptedAtRoute for HTTP code 202 Accepted;
- File or PhysicalFile for returning
200 OK
with the specifiedContent-Type
, and the specified file name; - Redirect, RedirectToRoute, RedirectToAction for returning 302 Found with various details.
Custom error handling
The default HTTP codes for statuses Failed
, NotFound
and Unauthorized
are defined in public static properties of ActionResultConventions
with default values:
// The HTTP code to return for client request error
int ErrorHttpCode { get; set; } = 400;
// The 'title' property of the returned JSON on HTTP code 400
string ErrorProblemDetailsTitle { get; set; } = "Bad Request";
// The HTTP code to return when a record not found
int NotFoundHttpCode { get; set; } = 404;
// The 'title' property of the returned JSON on HTTP code 404
string NotFoundProblemDetailsTitle { get; set; }= "Not Found";
// The HTTP code to return when access to a record is forbidden
int UnauthorizedHttpCode { get; set; } = 403;
// The 'title' property of the returned JSON on HTTP code 403
string UnauthorizedProblemDetailsTitle { get; set; }= "Unauthorized access";
Feel free to change them (hmm... remember they're static, with all the pros and cons). The reasons you may want it:
The extension methods also support custom response in case of the IDomainResult.Status
being Failed
, NotFound
or Unauthorized
:
[HttpGet("[action]")]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public Task<ActionResult<int>> GetFailedWithCustomStatusAndMessage()
{
var res = _service.GetFailedWithNoMessage();
return res.ToActionResultOfT(
(problemDetails, state) =>
{
if (state.Errors?.Any() == true)
return;
problemDetails.Status = 422; // Replace the default 400 code
problemDetails.Title = "D'oh!"; // Replace the default 'Bad Request' title
problemDetails.Detail = "I wish devs put more efforts into it..."; // Custom message
});
}
Alternative solutions
The problem solved here is not unique, so how does DomainResult stand out?
Why not FluentResults?
FluentResults is a great tool for indicating success or failure in the returned object. But there are different objectives:
- FluentResults provides a generalised container for returning results and potential errors;
- DomainResult is focused on a more specialised case when the Domain Logic is consumed by Web API.
Hence, DomainResult provides out-of-the-box:
- Specialised extension methods (like
IDomainResult.NotFound()
that in FluentResult would be indistinctive from other errors) - Supports various ways of conversions to
ActionResult
(returning Problem Details in case of error), functionality that is not available in FluentResults and quite weak in the other NuGets extending FluentResults.
Why not Hellang's ProblemDetails?
Hellang.Middleware.ProblemDetails is another good one, where you can map exceptions to problem details.
In this case, the difference is ideological - "throwing exception" vs "returning a faulty status" for the sad path of execution in the business logic.
Main distinctive features of DomainResult are
- Allows simpler nested calls of the domain logic (no exceptions handlers when severity of their "sad" path is not exception-worthy).
- Provides a predefined set of responses for main execution paths ("bad request", "not found", etc.). Works out-of-the-box.
- Has an option to tune each output independently.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 is compatible. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp3.1 is compatible. |
-
.NETCoreApp 3.1
- DomainResult.Common (>= 2.0.0)
-
net5.0
- DomainResult.Common (>= 2.0.0)
-
net6.0
- DomainResult.Common (>= 2.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on DomainResult:
Repository | Stars |
---|---|
ravendb/samples-yabt
"Yet Another Bug Tracker" solution sample for RavenDB and .NET with Angular UI
|