Skip to content

Commit a19a67c

Browse files
authored
Add extension methods for mapping Result<TData> to IActionResult. (#7)
1 parent 42d6b5f commit a19a67c

File tree

8 files changed

+865
-3
lines changed

8 files changed

+865
-3
lines changed

src/Winton.DomainModelling.AspNetCore/DomainExceptionFilter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace Winton.DomainModelling.AspNetCore
1111
/// <summary>
1212
/// An exception filter for converting <see cref="DomainException" />s to <see cref="IActionResult" />s.
1313
/// </summary>
14+
[Obsolete("Prefer returning Result types from the domain and convert to an ActionResult using the extensions provided in this library.", false)]
1415
public sealed class DomainExceptionFilter : IExceptionFilter
1516
{
1617
private readonly Func<DomainException, ErrorResponse, IActionResult> _exceptionMapper;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Winton. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Mvc;
7+
8+
namespace Winton.DomainModelling.AspNetCore
9+
{
10+
internal static class ErrorExtensions
11+
{
12+
internal static ActionResult ToActionResult(this Error error, Func<Error, ProblemDetails> selectProblemDetails)
13+
{
14+
ProblemDetails problemDetails = selectProblemDetails?.Invoke(error) ?? CreateDefaultProblemDetails(error);
15+
return new ObjectResult(problemDetails)
16+
{
17+
StatusCode = problemDetails.Status
18+
};
19+
}
20+
21+
private static ProblemDetails CreateDefaultProblemDetails(Error error)
22+
{
23+
int GetStatusCode()
24+
{
25+
switch (error)
26+
{
27+
case UnauthorizedError _:
28+
return StatusCodes.Status401Unauthorized;
29+
case NotFoundError _:
30+
return StatusCodes.Status404NotFound;
31+
default:
32+
return StatusCodes.Status400BadRequest;
33+
}
34+
}
35+
36+
int statusCode = GetStatusCode();
37+
return new ProblemDetails
38+
{
39+
Detail = error.Detail,
40+
Status = statusCode,
41+
Title = error.Title,
42+
Type = $"https://httpstatuses.com/{statusCode}"
43+
};
44+
}
45+
}
46+
}

src/Winton.DomainModelling.AspNetCore/ErrorResponse.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
// Copyright (c) Winton. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
33

4+
using System;
5+
46
namespace Winton.DomainModelling.AspNetCore
57
{
68
/// <summary>
79
/// A response that represents an error.
810
/// </summary>
11+
[Obsolete("Prefer returning Result types from the domain and convert to an ActionResult using the extensions provided in this library.", false)]
912
public struct ErrorResponse
1013
{
1114
/// <summary>
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
// Copyright (c) Winton. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Mvc;
7+
8+
namespace Winton.DomainModelling.AspNetCore
9+
{
10+
/// <summary>
11+
/// Extension methods for converting <see cref="Result{TData}"/> types into <see cref="IActionResult"/> types.
12+
/// </summary>
13+
public static class ResultExtensions
14+
{
15+
/// <summary>
16+
/// Converts a <see cref="Result{Unit}"/> to an <see cref="IActionResult"/>.
17+
/// </summary>
18+
/// <param name="result">
19+
/// The result that this extension method is invoked on.
20+
/// </param>
21+
/// <returns>
22+
/// If this result is a success then a <see cref="NoContentResult"/> is returned;
23+
/// otherwise it is converted to the appropriate 4xx response with Problem Details
24+
/// containing information about the error.
25+
/// </returns>
26+
public static IActionResult ToActionResult(this Result<Unit> result)
27+
{
28+
return result.ToActionResult(null as Func<Error, ProblemDetails>);
29+
}
30+
31+
/// <summary>
32+
/// Converts a <see cref="Result{Unit}"/> to an <see cref="IActionResult"/>.
33+
/// </summary>
34+
/// <param name="result">
35+
/// The result that this extension method is invoked on.
36+
/// </param>
37+
/// <param name="onError">
38+
/// The function that is invoked if this <paramref name="result"/> is a <see cref="Failure{TData}"/>.
39+
/// It is responsible for mapping the <see cref="Error"/> to <see cref="ProblemDetails"/>.
40+
/// If this function returns <code>null</code> then the default error mapping conventions are used.
41+
/// This therefore provides a way to customize the error mapping from <see cref="Error"/> to <see cref="ProblemDetails"/>.
42+
/// </param>
43+
/// <returns>
44+
/// If this result is a success then a <see cref="NoContentResult"/> is returned;
45+
/// otherwise it is converted to an error response containing <see cref="ProblemDetails"/>
46+
/// in the response body.
47+
/// </returns>
48+
public static IActionResult ToActionResult(
49+
this Result<Unit> result,
50+
Func<Error, ProblemDetails> onError)
51+
{
52+
return result.Match(_ => new NoContentResult(), error => error.ToActionResult(onError));
53+
}
54+
55+
/// <summary>
56+
/// Converts a <see cref="Task{Result}"/> of a <see cref="Result{Unit}"/> to an <see cref="IActionResult"/>.
57+
/// </summary>
58+
/// <param name="resultTask">
59+
/// The asynchronous result that this extension method is invoked on.
60+
/// </param>
61+
/// <returns>
62+
/// If this result is a success then a <see cref="NoContentResult"/> is returned;
63+
/// otherwise it is converted to the appropriate 4xx response containing <see cref="ProblemDetails"/>
64+
/// in the response body.
65+
/// </returns>
66+
public static async Task<IActionResult> ToActionResult(this Task<Result<Unit>> resultTask)
67+
{
68+
return (await resultTask).ToActionResult();
69+
}
70+
71+
/// <summary>
72+
/// Converts a <see cref="Task{Result}"/> of a <see cref="Result{Unit}"/> to an <see cref="IActionResult"/>.
73+
/// </summary>
74+
/// <param name="resultTask">
75+
/// The asynchronous result that this extension method is invoked on.
76+
/// </param>
77+
/// <param name="onError">
78+
/// The function that is invoked if this <paramref name="resultTask"/> is a <see cref="Failure{TData}"/>.
79+
/// It is responsible for mapping the <see cref="Error"/> to <see cref="ProblemDetails"/>.
80+
/// If this function returns <code>null</code> then the default error mapping conventions are used.
81+
/// This therefore provides a way to customize the error mapping from <see cref="Error"/> to <see cref="ProblemDetails"/>.
82+
/// </param>
83+
/// <returns>
84+
/// If this result is a success then a <see cref="NoContentResult"/> is returned;
85+
/// otherwise it is converted to an error response containing <see cref="ProblemDetails"/>
86+
/// in the response body.
87+
/// </returns>
88+
public static async Task<IActionResult> ToActionResult(
89+
this Task<Result<Unit>> resultTask,
90+
Func<Error, ProblemDetails> onError)
91+
{
92+
return (await resultTask).ToActionResult(onError);
93+
}
94+
95+
/// <summary>
96+
/// Converts a <see cref="Result{TData}"/> to an <see cref="ActionResult{TData}"/>.
97+
/// </summary>
98+
/// <typeparam name="TData">
99+
/// The type of data encapsulated by the result.
100+
/// </typeparam>
101+
/// <param name="result">
102+
/// The result that this extension method is invoked on.
103+
/// </param>
104+
/// <returns>
105+
/// If this result is a success then an <see cref="ActionResult{TData}"/> is returned;
106+
/// otherwise it is converted to the appropriate 4xx response containing <see cref="ProblemDetails"/>
107+
/// in the response body.
108+
/// </returns>
109+
public static ActionResult<TData> ToActionResult<TData>(this Result<TData> result)
110+
{
111+
return result.ToActionResult(null as Func<Error, ProblemDetails>);
112+
}
113+
114+
/// <summary>
115+
/// Converts a <see cref="Result{TData}"/> to an <see cref="ActionResult{TData}"/>.
116+
/// </summary>
117+
/// <typeparam name="TData">
118+
/// The type of data encapsulated by the result.
119+
/// </typeparam>
120+
/// <param name="result">
121+
/// The result that this extension method is invoked on.
122+
/// </param>
123+
/// <param name="onError">
124+
/// The function that is invoked if this <paramref name="result"/> is a <see cref="Failure{TData}"/>.
125+
/// It is responsible for mapping the <see cref="Error"/> to <see cref="ProblemDetails"/>.
126+
/// If this function returns <code>null</code> then the default error mapping conventions are used.
127+
/// This therefore provides a way to customize the error mapping from <see cref="Error"/> to <see cref="ProblemDetails"/>.
128+
/// </param>
129+
/// <returns>
130+
/// If this result is a success then an <see cref="ActionResult{TData}"/> is returned;
131+
/// otherwise it is converted to an error response containing <see cref="ProblemDetails"/>
132+
/// in the response body.
133+
/// </returns>
134+
public static ActionResult<TData> ToActionResult<TData>(
135+
this Result<TData> result,
136+
Func<Error, ProblemDetails> onError)
137+
{
138+
return result.Match(data => new ActionResult<TData>(data), error => error.ToActionResult(onError));
139+
}
140+
141+
/// <summary>
142+
/// Asynchronously converts a <see cref="Task{Result}"/> of a <see cref="Result{TData}"/>
143+
/// to an <see cref="ActionResult{TData}"/>.
144+
/// </summary>
145+
/// <typeparam name="TData">
146+
/// The type of data encapsulated by the result.
147+
/// </typeparam>
148+
/// <param name="resultTask">
149+
/// The asynchronous result that this extension method is invoked on.
150+
/// </param>
151+
/// <returns>
152+
/// If this result is a success then an <see cref="ActionResult{TData}"/> is returned;
153+
/// otherwise it is converted to the appropriate 4xx response containing <see cref="ProblemDetails"/>
154+
/// in the response body.
155+
/// </returns>
156+
public static async Task<ActionResult<TData>> ToActionResult<TData>(this Task<Result<TData>> resultTask)
157+
{
158+
return (await resultTask).ToActionResult();
159+
}
160+
161+
/// <summary>
162+
/// Asynchronously converts a <see cref="Task{Result}"/> of a <see cref="Result{TData}"/>
163+
/// to an <see cref="ActionResult{TData}"/>.
164+
/// </summary>
165+
/// <typeparam name="TData">
166+
/// The type of data encapsulated by the result.
167+
/// </typeparam>
168+
/// <param name="resultTask">
169+
/// The asynchronous result that this extension method is invoked on.
170+
/// </param>
171+
/// <param name="onError">
172+
/// The function that is invoked if this <paramref name="resultTask"/> is a <see cref="Failure{TData}"/>.
173+
/// It is responsible for mapping the <see cref="Error"/> to <see cref="ProblemDetails"/>.
174+
/// If this function returns <code>null</code> then the default error mapping conventions are used.
175+
/// This therefore provides a way to customize the error mapping from <see cref="Error"/> to <see cref="ProblemDetails"/>.
176+
/// </param>
177+
/// <returns>
178+
/// If this result is a success then an <see cref="ActionResult{TData}"/> is returned;
179+
/// otherwise it is converted to an error response containing <see cref="ProblemDetails"/>
180+
/// in the response body.
181+
/// </returns>
182+
public static async Task<ActionResult<TData>> ToActionResult<TData>(
183+
this Task<Result<TData>> resultTask,
184+
Func<Error, ProblemDetails> onError)
185+
{
186+
return (await resultTask).ToActionResult(onError);
187+
}
188+
189+
/// <summary>
190+
/// Converts a <see cref="Result{TData}"/> to an <see cref="IActionResult"/>.
191+
/// </summary>
192+
/// <typeparam name="TData">
193+
/// The type of data encapsulated by the result.
194+
/// </typeparam>
195+
/// <param name="result">
196+
/// The result that this extension method is invoked on.
197+
/// </param>
198+
/// <param name="onSuccess">
199+
/// The function that is invoked if this <paramref name="result"/> is a <see cref="Success{TData}"/>.
200+
/// It is invoked to map the data to an <see cref="IActionResult"/>.
201+
/// </param>
202+
/// <returns>
203+
/// If this result is a success the result of <paramref name="onSuccess"/> is returned;
204+
/// otherwise it is converted to the appropriate 4xx response containing <see cref="ProblemDetails"/>
205+
/// in the response body.
206+
/// </returns>
207+
public static IActionResult ToActionResult<TData>(
208+
this Result<TData> result,
209+
Func<TData, IActionResult> onSuccess)
210+
{
211+
return result.ToActionResult(onSuccess, null);
212+
}
213+
214+
/// <summary>
215+
/// Converts a <see cref="Result{TData}"/> to an <see cref="IActionResult"/>.
216+
/// </summary>
217+
/// <typeparam name="TData">
218+
/// The type of data encapsulated by the result.
219+
/// </typeparam>
220+
/// <param name="result">
221+
/// The result that this extension method is invoked on.
222+
/// </param>
223+
/// <param name="onSuccess">
224+
/// The function that is invoked if this <paramref name="result"/> is a <see cref="Success{TData}"/>.
225+
/// It is invoked to map the data to an <see cref="IActionResult"/>.
226+
/// </param>
227+
/// <param name="onError">
228+
/// The function that is invoked if this <paramref name="result"/> is a <see cref="Failure{TData}"/>.
229+
/// It is invoked to map the <see cref="Error"/> to <see cref="ProblemDetails"/>.
230+
/// If this function returns <code>null</code> then the default error mapping conventions are used.
231+
/// This therefore provides a way to customize the error mapping from <see cref="Error"/> to <see cref="ProblemDetails"/>.
232+
/// </param>
233+
/// <returns>
234+
/// If this result is a success the result of <paramref name="onSuccess"/> is returned;
235+
/// otherwise it is converted to an error response containing <see cref="ProblemDetails"/>
236+
/// in the response body.
237+
/// </returns>
238+
public static IActionResult ToActionResult<TData>(
239+
this Result<TData> result,
240+
Func<TData, IActionResult> onSuccess,
241+
Func<Error, ProblemDetails> onError)
242+
{
243+
return result.Match(onSuccess, error => error.ToActionResult(onError));
244+
}
245+
246+
/// <summary>
247+
/// Asynchronously converts a <see cref="Task{Result}"/> of a <see cref="Result{TData}"/>
248+
/// to an <see cref="IActionResult"/>.
249+
/// </summary>
250+
/// <typeparam name="TData">
251+
/// The type of data encapsulated by the result.
252+
/// </typeparam>
253+
/// <param name="resultTask">
254+
/// The asynchronous result that this extension method is invoked on.
255+
/// </param>
256+
/// <param name="onSuccess">
257+
/// The function that is invoked if this <paramref name="resultTask"/> is a <see cref="Success{TData}"/>.
258+
/// It is invoked to map the data to an <see cref="IActionResult"/>.
259+
/// </param>
260+
/// <returns>
261+
/// If this result is a success the result of <paramref name="onSuccess"/> is returned;
262+
/// otherwise it is converted to the appropriate 4xx response containing <see cref="ProblemDetails"/>
263+
/// in the response body.
264+
/// </returns>
265+
public static async Task<IActionResult> ToActionResult<TData>(
266+
this Task<Result<TData>> resultTask,
267+
Func<TData, IActionResult> onSuccess)
268+
{
269+
return (await resultTask).ToActionResult(onSuccess);
270+
}
271+
272+
/// <summary>
273+
/// Asynchronously converts a <see cref="Task{Result}"/> of a <see cref="Result{TData}"/>
274+
/// to an <see cref="IActionResult"/>.
275+
/// </summary>
276+
/// <typeparam name="TData">
277+
/// The type of data encapsulated by the result.
278+
/// </typeparam>
279+
/// <param name="resultTask">
280+
/// The asynchronous result that this extension method is invoked on.
281+
/// </param>
282+
/// <param name="onSuccess">
283+
/// The function that is invoked if this <paramref name="resultTask"/> is a <see cref="Success{TData}"/>.
284+
/// It is invoked to map the data to an <see cref="IActionResult"/>.
285+
/// </param>
286+
/// <param name="onError">
287+
/// The function that is invoked if this <paramref name="resultTask"/> is a <see cref="Failure{TData}"/>.
288+
/// It is invoked to map the <see cref="Error"/> to <see cref="ProblemDetails"/>.
289+
/// If this function returns <code>null</code> then the default error mapping conventions are used.
290+
/// This therefore provides a way to customize the error mapping from <see cref="Error"/> to <see cref="ProblemDetails"/>.
291+
/// </param>
292+
/// <returns>
293+
/// If this result is a success the result of <paramref name="onSuccess"/> is returned;
294+
/// otherwise it is converted to an error response containing <see cref="ProblemDetails"/>
295+
/// in the response body.
296+
/// </returns>
297+
public static async Task<IActionResult> ToActionResult<TData>(
298+
this Task<Result<TData>> resultTask,
299+
Func<TData, IActionResult> onSuccess,
300+
Func<Error, ProblemDetails> onError)
301+
{
302+
return (await resultTask).ToActionResult(onSuccess, onError);
303+
}
304+
}
305+
}

src/Winton.DomainModelling.AspNetCore/Winton.DomainModelling.AspNetCore.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@
2727

2828
<ItemGroup>
2929
<AdditionalFiles Include="../../stylecop.json" />
30-
<None Include="../../LICENSE" Pack="true" PackagePath=""/>
30+
<None Include="../../LICENSE" Pack="true" PackagePath="" />
3131
</ItemGroup>
3232

3333
<ItemGroup>
34-
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.0.0" />
34+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.1.3" />
3535
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="All" />
36-
<PackageReference Include="Winton.DomainModelling.Abstractions" Version="1.1.0" />
36+
<PackageReference Include="Winton.DomainModelling.Abstractions" Version="1.2.0-master0011" />
3737
</ItemGroup>
3838

3939
</Project>

0 commit comments

Comments
 (0)