-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMvcResultExtensions.cs
More file actions
157 lines (153 loc) · 10.5 KB
/
MvcResultExtensions.cs
File metadata and controls
157 lines (153 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
using System.Net;
using F23.Hateoas;
using F23.Kernel.Results;
using Microsoft.AspNetCore.Mvc;
using UnauthorizedResult = F23.Kernel.Results.UnauthorizedResult;
namespace F23.Kernel.AspNetCore;
/// <summary>
/// Provides extension methods for converting <see cref="Result"/> objects to ASP.NET Core MVC <see cref="IActionResult"/> objects.
/// </summary>
public static class MvcResultExtensions
{
/// <summary>
/// Converts a <see cref="Result"/> into an appropriate <see cref="IActionResult"/>
/// that represents the result to be sent in an HTTP response.
/// </summary>
/// <param name="result">The <see cref="Result"/> to be converted.</param>
/// <param name="useProblemDetails">Whether to use a RFC7807 problem details body for a failure response. The default is <c>true</c>.</param>
/// <returns>
/// An <see cref="IActionResult"/> representing the HTTP response:
/// <list type="bullet">
/// <item><see cref="NoContentResult"/> for <see cref="SuccessResult"/>.</item>
/// <item><see cref="ObjectResult"/> with <see cref="ProblemDetails"/> for any non-successful result if <paramref name="useProblemDetails"/> is <c>true</c>.</item>
/// <item><see cref="NotFoundResult"/> for a <see cref="PreconditionFailedResult"/> indicating <see cref="PreconditionFailedReason.NotFound"/> if <paramref name="useProblemDetails"/> is <c>false</c>.</item>
/// <item><see cref="StatusCodeResult"/> with HTTP status code 412 for a <see cref="PreconditionFailedResult"/> indicating <see cref="PreconditionFailedReason.ConcurrencyMismatch"/> if <paramref name="useProblemDetails"/> is <c>false</c>.</item>
/// <item><see cref="ConflictResult"/> for a <see cref="PreconditionFailedResult"/> indicating <see cref="PreconditionFailedReason.Conflict"/> if <paramref name="useProblemDetails"/> is <c>false</c>.</item>
/// <item><see cref="BadRequestObjectResult"/> with model state populated for a <see cref="ValidationFailedResult"/> if <paramref name="useProblemDetails"/> is <c>false</c>.</item>
/// <item><see cref="ForbidResult"/> in case of an <see cref="UnauthorizedResult"/> if <paramref name="useProblemDetails"/> is <c>false</c>.</item>
/// </list>
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the <paramref name="result"/> does not match any known result types.
/// </exception>
public static IActionResult ToActionResult(this Result result, bool useProblemDetails = true)
=> result switch
{
SuccessResult
=> new NoContentResult(),
AggregateResult { IsSuccess: true }
=> new NoContentResult(),
AggregateResult { IsSuccess: false, Results.Count: > 0 } aggregateResult
=> aggregateResult.Results.First(i => !i.IsSuccess).ToActionResult(useProblemDetails),
PreconditionFailedResult { Reason: PreconditionFailedReason.NotFound } when useProblemDetails
=> result.ToProblemDetailsResult(HttpStatusCode.NotFound),
PreconditionFailedResult { Reason: PreconditionFailedReason.NotFound } when !useProblemDetails
=> new NotFoundResult(),
PreconditionFailedResult { Reason: PreconditionFailedReason.ConcurrencyMismatch } when useProblemDetails
=> result.ToProblemDetailsResult(HttpStatusCode.PreconditionFailed),
PreconditionFailedResult { Reason: PreconditionFailedReason.ConcurrencyMismatch } when !useProblemDetails
=> new StatusCodeResult((int) HttpStatusCode.PreconditionFailed),
PreconditionFailedResult { Reason: PreconditionFailedReason.Conflict } when useProblemDetails
=> result.ToProblemDetailsResult(HttpStatusCode.Conflict),
PreconditionFailedResult { Reason: PreconditionFailedReason.Conflict } when !useProblemDetails
=> new ConflictResult(),
ValidationFailedResult validationFailed when useProblemDetails
=> new ObjectResult(new ValidationProblemDetails(validationFailed.Errors.ToModelState())
{
Title = result.Message,
Status = (int) HttpStatusCode.BadRequest,
})
{
StatusCode = (int) HttpStatusCode.BadRequest,
},
ValidationFailedResult validationFailed when !useProblemDetails
=> new BadRequestObjectResult(validationFailed.Errors.ToModelState()),
UnauthorizedResult when useProblemDetails
=> result.ToProblemDetailsResult(HttpStatusCode.Forbidden),
UnauthorizedResult when !useProblemDetails
=> new ForbidResult(),
_ => throw new ArgumentOutOfRangeException(nameof(result))
};
/// <summary>
/// Converts a <see cref="Result{T}"/> into an appropriate <see cref="IActionResult"/>
/// that represents the result to be sent in an HTTP response.
/// </summary>
/// <typeparam name="T">The type of the value contained in the result, if successful.</typeparam>
/// <param name="result">The result instance to convert.</param>
/// <param name="successMap">
/// An optional function to map a successful result to a custom <see cref="IActionResult"/>.
/// If not provided, a default mapping is applied.
/// </param>
/// <param name="useProblemDetails">Whether to use a RFC7807 problem details body for a failure response. The default is <c>true</c>.</param>
/// <returns>
/// An <see cref="IActionResult"/> representing the HTTP response:
/// <list type="bullet">
/// <item>The result of <paramref name="successMap"/>, if specified, for a <see cref="SuccessResult{T}"/>.</item>
/// <item><see cref="OkObjectResult"/> for a <see cref="SuccessResult{T}"/>, when <paramref name="successMap"/> is not specified.</item>
/// <item><see cref="ObjectResult"/> with <see cref="ProblemDetails"/> for any non-successful result if <paramref name="useProblemDetails"/> is <c>true</c>.</item>
/// <item><see cref="NotFoundResult"/> for a <see cref="PreconditionFailedResult{T}"/> indicating <see cref="PreconditionFailedReason.NotFound"/> if <paramref name="useProblemDetails"/> is <c>false</c>.</item>
/// <item><see cref="StatusCodeResult"/> with HTTP status code 412 for a <see cref="PreconditionFailedResult{T}"/> indicating <see cref="PreconditionFailedReason.ConcurrencyMismatch"/> if <paramref name="useProblemDetails"/> is <c>false</c>.</item>
/// <item><see cref="ConflictResult"/> for a <see cref="PreconditionFailedResult{T}"/> indicating <see cref="PreconditionFailedReason.Conflict"/> if <paramref name="useProblemDetails"/> is <c>false</c>.</item>
/// <item><see cref="BadRequestObjectResult"/> with model state populated for a <see cref="ValidationFailedResult{T}"/> if <paramref name="useProblemDetails"/> is <c>false</c>.</item>
/// <item><see cref="ForbidResult"/> in case of an <see cref="UnauthorizedResult{T}"/> if <paramref name="useProblemDetails"/> is <c>false</c>.</item>
/// </list>
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the <paramref name="result"/> does not match any known result types.
/// </exception>
public static IActionResult ToActionResult<T>(this Result<T> result, Func<T, IActionResult>? successMap = null, bool useProblemDetails = true)
=> result switch
{
SuccessResult<T> success when successMap != null
=> successMap(success.Value),
SuccessResult<T> success
=> new OkObjectResult(new HypermediaResponse(success.Value!)), // [!]: TODO: Fix nullability issue in F23.Hateoas
PreconditionFailedResult<T> { Reason: PreconditionFailedReason.NotFound } when useProblemDetails
=> result.ToProblemDetailsResult(HttpStatusCode.NotFound),
PreconditionFailedResult<T> { Reason: PreconditionFailedReason.NotFound } when !useProblemDetails
=> new NotFoundResult(),
PreconditionFailedResult<T> { Reason: PreconditionFailedReason.ConcurrencyMismatch } when useProblemDetails
=> result.ToProblemDetailsResult(HttpStatusCode.PreconditionFailed),
PreconditionFailedResult<T> { Reason: PreconditionFailedReason.ConcurrencyMismatch } when !useProblemDetails
=> new StatusCodeResult((int) HttpStatusCode.PreconditionFailed),
PreconditionFailedResult<T> { Reason: PreconditionFailedReason.Conflict } when useProblemDetails
=> result.ToProblemDetailsResult(HttpStatusCode.Conflict),
PreconditionFailedResult<T> { Reason: PreconditionFailedReason.Conflict } when !useProblemDetails
=> new ConflictResult(),
ValidationFailedResult<T> validationFailed when useProblemDetails
=> new ObjectResult(new ValidationProblemDetails(validationFailed.Errors.ToModelState())
{
Title = result.Message,
Status = (int) HttpStatusCode.BadRequest,
})
{
StatusCode = (int) HttpStatusCode.BadRequest,
},
ValidationFailedResult<T> validationFailed when !useProblemDetails
=> new BadRequestObjectResult(validationFailed.Errors.ToModelState()),
UnauthorizedResult<T> when useProblemDetails
=> result.ToProblemDetailsResult(HttpStatusCode.Forbidden),
UnauthorizedResult<T> when !useProblemDetails
=> new ForbidResult(),
_ => throw new ArgumentOutOfRangeException(nameof(result))
};
/// <summary>
/// Converts a <see cref="Result"/> into a <see cref="ObjectResult"/> containing <see cref="ProblemDetails"/>.
/// </summary>
/// <param name="result">The <see cref="Result"/> to be converted.</param>
/// <param name="statusCode">The HTTP status code to be set in the <see cref="ProblemDetails"/>.</param>
/// <returns>A <see cref="ObjectResult"/> containing the <see cref="ProblemDetails"/>.</returns>
/// <remarks>
/// This is primarily an internal API, intended to be used from <see cref="ToActionResult"/> or <see cref="ToActionResult{T}"/>.
/// However, it might have some desired use cases where direct usage is appropriate, so it is made public.
/// </remarks>
public static IActionResult ToProblemDetailsResult(this Result result, HttpStatusCode statusCode)
=> new ObjectResult(new ProblemDetails
{
Title = result.Message,
Status = (int)statusCode,
})
{
StatusCode = (int)statusCode,
};
}