Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 11e4166

Browse files
committed
Add support for API error response type based on RFC 7807
1 parent d6fd641 commit 11e4166

File tree

11 files changed

+407
-4
lines changed

11 files changed

+407
-4
lines changed

samples/MvcSandbox/Controllers/HomeController.cs

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

4+
using System.ComponentModel.DataAnnotations;
45
using Microsoft.AspNetCore.Mvc;
56

67
namespace MvcSandbox.Controllers
@@ -14,5 +15,58 @@ public IActionResult Index()
1415
{
1516
return View();
1617
}
18+
19+
[ProblemErrorPolicy]
20+
[HttpGet("/test1")]
21+
public ActionResult<Person> PostTest([FromQuery] int id)
22+
{
23+
if (id <= 0)
24+
{
25+
return NotFound();
26+
}
27+
28+
return new Person { Id = id };
29+
}
30+
31+
[ProblemErrorPolicy]
32+
[HttpPost("/test2")]
33+
public ActionResult<Person> ModelStateTest([FromBody] Person person)
34+
{
35+
if (!ModelState.IsValid)
36+
{
37+
return BadRequest(ModelState);
38+
}
39+
40+
return person;
41+
}
42+
43+
[HttpPost("/problem")]
44+
public ActionResult<Person> Problem(int cooks)
45+
{
46+
if (cooks > 10)
47+
{
48+
return new Problem
49+
{
50+
Title = "Too many cooks",
51+
Status = Microsoft.AspNetCore.Http.StatusCodes.Status429TooManyRequests,
52+
["items-being-cooked"] = new[]
53+
{
54+
"Pancakes",
55+
"Donuts",
56+
"Waffles"
57+
}
58+
};
59+
}
60+
61+
return new Person { Id = 1 };
62+
}
63+
64+
public class Person
65+
{
66+
public int Id { get; set; }
67+
68+
[Required]
69+
public string Name { get; set; }
70+
}
1771
}
1872
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
2+
{
3+
public class ActionResultApiDescriptionProvider : IApiDescriptionProvider
4+
{
5+
public int Order => -1000 + 10;
6+
7+
public void OnProvidersExecuted(ApiDescriptionProviderContext context)
8+
{
9+
10+
}
11+
12+
public void OnProvidersExecuting(ApiDescriptionProviderContext context)
13+
{
14+
for (var i = 0; i < context.Results.Count; i++)
15+
{
16+
var apiDescription = context.Results[i];
17+
foreach (var responseType in apiDescription.SupportedResponseTypes)
18+
{
19+
if (responseType.Type.IsGenericType && typeof(ActionResult<>).IsAssignableFrom(responseType.Type.GetGenericTypeDefinition()))
20+
{
21+
responseType.Type = responseType.Type.GetGenericArguments()[0];
22+
}
23+
}
24+
}
25+
}
26+
}
27+
}
28+

src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,7 @@ private IList<ApiParameterDescription> GetParameters(ApiParameterContext context
193193
parameter.Source == BindingSource.ModelBinding ||
194194
parameter.Source == BindingSource.Custom)
195195
{
196-
ApiParameterRouteInfo routeInfo;
197-
if (routeParameters.TryGetValue(parameter.Name, out routeInfo))
196+
if (routeParameters.TryGetValue(parameter.Name, out var routeInfo))
198197
{
199198
parameter.RouteInfo = routeInfo;
200199
routeParameters.Remove(parameter.Name);
@@ -322,8 +321,7 @@ private IReadOnlyList<ApiRequestFormat> GetRequestFormats(
322321
{
323322
foreach (var formatter in _inputFormatters)
324323
{
325-
var requestFormatMetadataProvider = formatter as IApiRequestFormatMetadataProvider;
326-
if (requestFormatMetadataProvider != null)
324+
if (formatter is IApiRequestFormatMetadataProvider requestFormatMetadataProvider)
327325
{
328326
var supportedTypes = requestFormatMetadataProvider.GetSupportedContentTypes(contentType, type);
329327

src/Microsoft.AspNetCore.Mvc.ApiExplorer/DependencyInjection/MvcApiExplorerMvcCoreBuilderExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ internal static void AddApiExplorerServices(IServiceCollection services)
2626
services.TryAddSingleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
2727
services.TryAddEnumerable(
2828
ServiceDescriptor.Transient<IApiDescriptionProvider, DefaultApiDescriptionProvider>());
29+
services.TryAddEnumerable(
30+
ServiceDescriptor.Transient<IApiDescriptionProvider, ActionResultApiDescriptionProvider>());
31+
services.TryAddEnumerable(
32+
ServiceDescriptor.Transient<IApiDescriptionProvider, ErrorPolicyApiDescriptorProvider>());
2933
}
3034
}
3135
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Mvc.Abstractions;
6+
using Microsoft.AspNetCore.Mvc.ModelBinding;
7+
8+
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
9+
{
10+
public class ErrorPolicyApiDescriptorProvider : IApiDescriptionProvider
11+
{
12+
private readonly IModelMetadataProvider _metadataProvider;
13+
14+
public ErrorPolicyApiDescriptorProvider(IModelMetadataProvider modelMetadataProvider)
15+
{
16+
_metadataProvider = modelMetadataProvider;
17+
}
18+
19+
public int Order => -1000 + 10;
20+
21+
public void OnProvidersExecuted(ApiDescriptionProviderContext context)
22+
{
23+
24+
}
25+
26+
public void OnProvidersExecuting(ApiDescriptionProviderContext context)
27+
{
28+
for (var i = 0; i < context.Results.Count; i++)
29+
{
30+
var apiDescription = context.Results[i];
31+
var policy = FindErrorPolicy(apiDescription.ActionDescriptor);
32+
if (policy != null)
33+
{
34+
var errorPolicyContext = new ErrorPolicyContext(_metadataProvider) { Description = apiDescription };
35+
policy.Apply(errorPolicyContext);
36+
37+
context.Results[i] = errorPolicyContext.Description;
38+
}
39+
}
40+
}
41+
42+
private IErrorPolicy FindErrorPolicy(ActionDescriptor action)
43+
{
44+
for (var i = action.FilterDescriptors.Count - 1; i >= 0; i--)
45+
{
46+
if (action.FilterDescriptors[i].Filter is IErrorPolicy policy)
47+
{
48+
return policy;
49+
}
50+
}
51+
52+
return null;
53+
}
54+
}
55+
}

src/Microsoft.AspNetCore.Mvc.Core/ActionResultOfT.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ public static implicit operator ActionResult<TValue>(ActionResult result)
5050
return new ActionResult<TValue>(result);
5151
}
5252

53+
public static implicit operator ActionResult<TValue>(Problem problem)
54+
{
55+
var badRequestResultObject = new BadRequestObjectResult(problem);
56+
if (problem.Status != null)
57+
{
58+
badRequestResultObject.StatusCode = problem.Status;
59+
}
60+
61+
return new ActionResult<TValue>(badRequestResultObject);
62+
}
63+
5364
IActionResult IConvertToActionResult.Convert()
5465
{
5566
return Result ?? new ObjectResult(Value)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Mvc.ModelBinding;
5+
6+
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
7+
{
8+
public class ErrorPolicyContext
9+
{
10+
public ErrorPolicyContext(IModelMetadataProvider metadataProvider)
11+
{
12+
MetadataProvider = metadataProvider;
13+
}
14+
15+
public ApiDescription Description { get; set; }
16+
17+
public IModelMetadataProvider MetadataProvider { get; }
18+
}
19+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
5+
{
6+
public interface IErrorPolicy
7+
{
8+
void Apply(ErrorPolicyContext context);
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace Microsoft.AspNetCore.Mvc.Core
6+
{
7+
class ErrorPolicy
8+
{
9+
}
10+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.AspNetCore.Mvc
8+
{
9+
/// <summary>
10+
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
11+
/// </summary>
12+
public class Problem : Dictionary<string, object>
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="SerializableError"/> class.
16+
/// </summary>
17+
public Problem()
18+
: base(StringComparer.OrdinalIgnoreCase)
19+
{
20+
}
21+
22+
/// <summary>
23+
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when
24+
/// dereferenced, it provide human-readable documentation for the problem type
25+
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
26+
/// "about:blank".
27+
/// </summary>
28+
public string Type
29+
{
30+
get => GetValue<string>(nameof(Type));
31+
set => Add(nameof(Type), value);
32+
}
33+
34+
/// <summary>
35+
/// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence
36+
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
37+
/// see[RFC7231], Section 3.4).
38+
/// </summary>
39+
public string Title
40+
{
41+
get => GetValue<string>(nameof(Title));
42+
set => Add(nameof(Title), value);
43+
}
44+
45+
/// <summary>
46+
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
47+
/// </summary>
48+
public int? Status
49+
{
50+
get => GetValue<int?>(nameof(Status));
51+
set => Add(nameof(Status), value);
52+
}
53+
54+
/// <summary>
55+
/// A human-readable explanation specific to this occurrence of the problem.
56+
/// </summary>
57+
public string Detail
58+
{
59+
get => GetValue<string>(nameof(Detail));
60+
set => Add(nameof(Detail), value);
61+
}
62+
63+
/// <summary>
64+
/// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced.
65+
/// </summary>
66+
public string Instance
67+
{
68+
get => GetValue<string>(nameof(Instance));
69+
set => Add(nameof(Instance), value);
70+
}
71+
72+
private TValue GetValue<TValue>(string key)
73+
{
74+
if (TryGetValue(key, out var value))
75+
{
76+
return (TValue)value;
77+
}
78+
79+
return default(TValue);
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)