Skip to content

Commit 789c54e

Browse files
Support error object backward compatibility. Related to #1019
1 parent 28d5999 commit 789c54e

File tree

17 files changed

+648
-30
lines changed

17 files changed

+648
-30
lines changed

examples/AspNetCore/WebApi/BasicExample/Controllers/ValuesController.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
[Route( "api/[controller]" )]
88
public class ValuesController : ControllerBase
99
{
10+
public ValuesController(IEnumerable<IProblemDetailsWriter> problemDetailsWriters )
11+
{
12+
13+
}
14+
1015
// GET api/values?api-version=1.0
1116
[HttpGet]
1217
public string Get( ApiVersion apiVersion ) => $"Controller = {GetType().Name}\nVersion = {apiVersion}";

examples/AspNetCore/WebApi/BasicExample/Program.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
[assembly: Microsoft.AspNetCore.Mvc.ApiController]
1+
using Asp.Versioning;
2+
using Microsoft.Extensions.DependencyInjection.Extensions;
3+
4+
[assembly: Microsoft.AspNetCore.Mvc.ApiController]
25

36
var builder = WebApplication.CreateBuilder( args );
47

58
// Add services to the container.
69

10+
711
builder.Services.AddControllers();
12+
builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton<IProblemDetailsWriter, ErrorObjectWriter>() );
813
builder.Services.AddProblemDetails();
914
builder.Services.AddApiVersioning(
1015
options =>
@@ -15,6 +20,8 @@
1520
} )
1621
.AddMvc();
1722

23+
//builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton<IProblemDetailsWriter, ErrorObjectWriter>() );
24+
1825
var app = builder.Build();
1926

2027
// Configure the HTTP request pipeline.

examples/AspNetCore/WebApi/MinimalApiExample/Program.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.Extensions.DependencyInjection.Extensions;
5+
16
var builder = WebApplication.CreateBuilder( args );
27

38
// Add services to the container.
49

510
builder.Services.AddProblemDetails();
11+
builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton<IProblemDetailsWriter, ErrorObjectWriter>() );
612

713
// enable api versioning and return the headers
814
// "api-supported-versions" and "api-deprecated-versions"
@@ -20,7 +26,7 @@
2026
var forecast = app.NewVersionedApi();
2127

2228
// GET /weatherforecast?api-version=1.0
23-
forecast.MapGet( "/weatherforecast", () =>
29+
forecast.MapGet( "/weatherforecast", ([FromServices] IEnumerable<IProblemDetailsWriter> writers) =>
2430
{
2531
return Enumerable.Range( 1, 5 ).Select( index =>
2632
new WeatherForecast
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
// Ignore Spelling: Interop
4+
namespace Asp.Versioning.Http.Basic;
5+
6+
using System.Web.Http;
7+
8+
public class InteropFixture : BasicFixture
9+
{
10+
protected override void OnConfigure( HttpConfiguration configuration )
11+
{
12+
configuration.ConvertProblemDetailsToErrorObject();
13+
base.OnConfigure( configuration );
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace given_a_versioned_ApiController;
4+
5+
using Asp.Versioning;
6+
using Asp.Versioning.Http.Basic;
7+
8+
public class when_error_objects_are_enabled : AcceptanceTest, IClassFixture<InteropFixture>
9+
{
10+
[Fact]
11+
public async Task then_the_response_should_not_be_problem_details()
12+
{
13+
// arrange
14+
var example = new
15+
{
16+
error = new
17+
{
18+
code = default( string ),
19+
message = default( string ),
20+
target = default( string ),
21+
innerError = new
22+
{
23+
message = default( string ),
24+
},
25+
},
26+
};
27+
28+
// act
29+
var response = await GetAsync( "api/values?api-version=3.0" );
30+
var error = await response.Content.ReadAsExampleAsync( example );
31+
32+
// assert
33+
response.Content.Headers.ContentType.MediaType.Should().Be( "application/json" );
34+
error.Should().BeEquivalentTo(
35+
new
36+
{
37+
error = new
38+
{
39+
code = "UnsupportedApiVersion",
40+
message = "Unsupported API version",
41+
innerError = new
42+
{
43+
message = "No route providing a controller name with API version '3.0' " +
44+
"was found to match request URI 'http://localhost/api/values'.",
45+
},
46+
},
47+
} );
48+
}
49+
50+
public when_error_objects_are_enabled( InteropFixture fixture ) : base( fixture ) { }
51+
}

src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<VersionPrefix>7.0.1</VersionPrefix>
5-
<AssemblyVersion>7.0.0.0</AssemblyVersion>
4+
<VersionPrefix>7.1.0</VersionPrefix>
5+
<AssemblyVersion>7.1.0.0</AssemblyVersion>
66
<TargetFrameworks>net45;net472</TargetFrameworks>
77
<AssemblyTitle>ASP.NET Web API Versioning</AssemblyTitle>
88
<Description>A service API versioning library for Microsoft ASP.NET Web API.</Description>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
// REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs
4+
namespace Asp.Versioning;
5+
6+
using Newtonsoft.Json;
7+
using static Asp.Versioning.ProblemDetailsDefaults;
8+
using static Newtonsoft.Json.NullValueHandling;
9+
10+
internal sealed class ErrorObjectFactory : IProblemDetailsFactory
11+
{
12+
public ProblemDetails CreateProblemDetails(
13+
HttpRequestMessage request,
14+
int? statusCode = null,
15+
string? title = null,
16+
string? type = null,
17+
string? detail = null,
18+
string? instance = null )
19+
{
20+
var status = statusCode ?? 500;
21+
ErrorObject? problem;
22+
23+
if ( type == Ambiguous.Type )
24+
{
25+
problem = NewError( title, instance );
26+
problem.Error.Code = Ambiguous.Code;
27+
}
28+
else if ( type == Invalid.Type )
29+
{
30+
problem = NewError( title, instance );
31+
problem.Error.Code = Invalid.Code;
32+
return ProblemDetailsFactory.AddInvalidExtensions( request, status, problem, ApplyMessage );
33+
}
34+
else if ( type == Unspecified.Type )
35+
{
36+
problem = NewError( title, instance );
37+
problem.Error.Code = Unspecified.Code;
38+
}
39+
else if ( type == Unsupported.Type )
40+
{
41+
problem = NewError( title, instance );
42+
problem.Error.Code = Unsupported.Code;
43+
return ProblemDetailsFactory.AddUnsupportedExtensions( request, status, problem, ApplyMessage );
44+
}
45+
46+
return ProblemDetailsFactory.Default.CreateProblemDetails(
47+
request,
48+
statusCode,
49+
title,
50+
type,
51+
detail,
52+
instance );
53+
}
54+
55+
private static ErrorObject NewError( string? message, string? target ) =>
56+
new()
57+
{
58+
Error =
59+
{
60+
Message = message,
61+
Target = target,
62+
},
63+
};
64+
65+
private static void ApplyMessage( ErrorObject obj, string message ) =>
66+
obj.Error.InnerError = new() { Message = message };
67+
68+
private sealed class ErrorObject : ProblemDetails
69+
{
70+
[JsonProperty( "error" )]
71+
public ErrorDetail Error { get; } = new();
72+
}
73+
74+
private sealed class ErrorDetail
75+
{
76+
[JsonProperty( "code", NullValueHandling = Ignore )]
77+
public string? Code { get; set; }
78+
79+
[JsonProperty( "message", NullValueHandling = Ignore )]
80+
public string? Message { get; set; }
81+
82+
[JsonProperty( "target", NullValueHandling = Ignore )]
83+
public string? Target { get; set; }
84+
85+
[JsonProperty( "innerError", NullValueHandling = Ignore )]
86+
public InnerError? InnerError { get; set; }
87+
}
88+
89+
private sealed class InnerError
90+
{
91+
[JsonProperty( "message", NullValueHandling = Ignore )]
92+
public string? Message { get; set; }
93+
}
94+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
// REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs
4+
namespace Asp.Versioning.Formatting;
5+
6+
using System.IO;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Net.Http.Formatting;
10+
using System.Net.Http.Headers;
11+
using System.Text;
12+
using System.Threading;
13+
using System.Threading.Tasks;
14+
using static Asp.Versioning.ProblemDetailsDefaults;
15+
16+
/// <summary>
17+
/// Represents a media type formatter for problem details based on https://tools.ietf.org/html/rfc7807.
18+
/// </summary>
19+
public class ProblemDetailsMediaTypeFormatter : MediaTypeFormatter
20+
{
21+
private readonly JsonMediaTypeFormatter json;
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="ProblemDetailsMediaTypeFormatter"/> class.
25+
/// </summary>
26+
public ProblemDetailsMediaTypeFormatter() : this( new() ) { }
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="ProblemDetailsMediaTypeFormatter"/> class.
30+
/// </summary>
31+
/// <param name="formatter">The existing instance to derive from.</param>
32+
public ProblemDetailsMediaTypeFormatter( JsonMediaTypeFormatter formatter )
33+
: base( formatter )
34+
{
35+
json = formatter;
36+
SupportedEncodings.Add( new UTF8Encoding( encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true ) );
37+
SupportedEncodings.Add( new UnicodeEncoding( bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true ) );
38+
SupportedMediaTypes.Add( DefaultMediaType );
39+
}
40+
41+
/// <summary>
42+
/// Gets the default media type.
43+
/// </summary>
44+
/// <value>Returns the media type for application/problem+json.</value>
45+
public static MediaTypeHeaderValue DefaultMediaType { get; } = MediaTypeHeaderValue.Parse( MediaType.Json );
46+
47+
/// <inheritdoc />
48+
public override bool CanReadType( Type type ) => false;
49+
50+
/// <inheritdoc />
51+
public override bool CanWriteType( Type type ) => typeof( ProblemDetails ).IsAssignableFrom( type );
52+
53+
/// <inheritdoc />
54+
public override Task WriteToStreamAsync(
55+
Type type,
56+
object value,
57+
Stream writeStream,
58+
HttpContent content,
59+
TransportContext transportContext,
60+
CancellationToken cancellationToken ) =>
61+
json.WriteToStreamAsync( type, value, writeStream, content, transportContext, cancellationToken );
62+
63+
/// <inheritdoc />
64+
public override void SetDefaultContentHeaders( Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType )
65+
{
66+
mediaType.MediaType = DefaultMediaType.MediaType;
67+
base.SetDefaultContentHeaders( type, headers, mediaType );
68+
}
69+
}

src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetailsFactory.cs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ namespace Asp.Versioning;
1010

1111
internal sealed class ProblemDetailsFactory : IProblemDetailsFactory
1212
{
13-
private static ProblemDetailsFactory? @default;
13+
private static IProblemDetailsFactory? @default;
1414

15-
public static IProblemDetailsFactory Default => @default ??= new();
15+
public static IProblemDetailsFactory Default
16+
{
17+
get => @default ??= new ProblemDetailsFactory();
18+
set => @default = value;
19+
}
1620

1721
public ProblemDetails CreateProblemDetails(
1822
HttpRequestMessage request,
@@ -40,7 +44,7 @@ public ProblemDetails CreateProblemDetails(
4044
else if ( type == Invalid.Type )
4145
{
4246
problemDetails.Code = Invalid.Code;
43-
return AddInvalidExtensions( request, status, problemDetails );
47+
return AddInvalidExtensions( request, status, problemDetails, ApplyMessage );
4448
}
4549
else if ( type == Unspecified.Type )
4650
{
@@ -49,13 +53,17 @@ public ProblemDetails CreateProblemDetails(
4953
else if ( type == Unsupported.Type )
5054
{
5155
problemDetails.Code = Unsupported.Code;
52-
return AddUnsupportedExtensions( request, status, problemDetails );
56+
return AddUnsupportedExtensions( request, status, problemDetails, ApplyMessage );
5357
}
5458

5559
return problemDetails;
5660
}
5761

58-
private static ProblemDetailsEx AddInvalidExtensions( HttpRequestMessage request, int status, ProblemDetailsEx problemDetails )
62+
internal static T AddInvalidExtensions<T>(
63+
HttpRequestMessage request,
64+
int status,
65+
T problemDetails,
66+
Action<T, string> applyMessage ) where T : ProblemDetails
5967
{
6068
if ( status != 400 || !request.ShouldIncludeErrorDetail() )
6169
{
@@ -64,14 +72,18 @@ private static ProblemDetailsEx AddInvalidExtensions( HttpRequestMessage request
6472

6573
var safeUrl = request.RequestUri.SafeFullPath();
6674
var requestedVersion = request.ApiVersionProperties().RawRequestedApiVersion;
67-
var error = string.Format( CurrentCulture, SR.VersionedControllerNameNotFound, safeUrl, requestedVersion );
75+
var message = string.Format( CurrentCulture, SR.VersionedControllerNameNotFound, safeUrl, requestedVersion );
6876

69-
problemDetails.Error = error;
77+
applyMessage( problemDetails, message );
7078

7179
return problemDetails;
7280
}
7381

74-
private static ProblemDetailsEx AddUnsupportedExtensions( HttpRequestMessage request, int status, ProblemDetailsEx problemDetails )
82+
internal static T AddUnsupportedExtensions<T>(
83+
HttpRequestMessage request,
84+
int status,
85+
T problemDetails,
86+
Action<T, string> applyMessage ) where T : ProblemDetails
7587
{
7688
if ( !request.ShouldIncludeErrorDetail() )
7789
{
@@ -95,14 +107,17 @@ private static ProblemDetailsEx AddUnsupportedExtensions( HttpRequestMessage req
95107

96108
var safeUrl = request.RequestUri.SafeFullPath();
97109
var requestedMethod = request.Method;
98-
var version = request.GetRequestedApiVersion()?.ToString() ?? "(null)";
99-
var error = string.Format( CurrentCulture, messageFormat, safeUrl, version, requestedMethod );
110+
var version = request.ApiVersionProperties().RawRequestedApiVersion ?? "(null)";
111+
var message = string.Format( CurrentCulture, messageFormat, safeUrl, version, requestedMethod );
100112

101-
problemDetails.Error = error;
113+
applyMessage( problemDetails, message );
102114

103115
return problemDetails;
104116
}
105117

118+
private static void ApplyMessage( ProblemDetailsEx problemDetails, string message ) =>
119+
problemDetails.Error = message;
120+
106121
private sealed class ProblemDetailsEx : ProblemDetails
107122
{
108123
[JsonProperty( "code", NullValueHandling = Ignore )]

0 commit comments

Comments
 (0)