Skip to content

Commit e5a84b0

Browse files
Back port error object support. Related to #1019
1 parent c462195 commit e5a84b0

File tree

10 files changed

+277
-9
lines changed

10 files changed

+277
-9
lines changed
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;
5+
6+
using Microsoft.Extensions.DependencyInjection;
7+
8+
public class InteropFixture : MinimalApiFixture
9+
{
10+
protected override void OnConfigureServices( IServiceCollection services )
11+
{
12+
services.AddSingleton<IProblemDetailsFactory, ErrorObjectFactory>();
13+
base.OnConfigureServices( services );
14+
}
15+
}

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MinimalApiFixture.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22

3-
namespace Asp.Versioning;
3+
namespace Asp.Versioning.Http;
44

55
using Asp.Versioning.Conventions;
66
using Microsoft.AspNetCore.Builder;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace given_a_versioned_minimal_API;
4+
5+
using Asp.Versioning;
6+
using Asp.Versioning.Http;
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 = "The HTTP resource that matches the request URI " +
44+
"'http://localhost/api/values' does not support " +
45+
"the API version '3.0'.",
46+
},
47+
},
48+
} );
49+
}
50+
51+
public when_error_objects_are_enabled( InteropFixture fixture ) : base( fixture ) { }
52+
}
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.Mvc.UsingAttributes;
5+
6+
using Microsoft.Extensions.DependencyInjection;
7+
8+
public class InteropFixture : BasicFixture
9+
{
10+
protected override void OnConfigureServices( IServiceCollection services )
11+
{
12+
services.AddSingleton<IProblemDetailsFactory, ErrorObjectFactory>();
13+
base.OnConfigureServices( services );
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace given_a_versioned_Controller;
4+
5+
using Asp.Versioning;
6+
using Asp.Versioning.Mvc.UsingAttributes;
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 = "The HTTP resource that matches the request URI " +
44+
"'http://localhost/api/values' does not support " +
45+
"the API version '3.0'.",
46+
},
47+
},
48+
} );
49+
}
50+
51+
public when_error_objects_are_enabled( InteropFixture fixture ) : base( fixture ) { }
52+
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.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>6.4.0</VersionPrefix>
5-
<AssemblyVersion>6.4.0.0</AssemblyVersion>
4+
<VersionPrefix>6.5.0</VersionPrefix>
5+
<AssemblyVersion>6.5.0.0</AssemblyVersion>
66
<TargetFrameworks>net6.0;netcoreapp3.1</TargetFrameworks>
77
<RootNamespace>Asp.Versioning</RootNamespace>
88
<AssemblyTitle>ASP.NET Core API Versioning</AssemblyTitle>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning;
4+
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Mvc;
7+
8+
/// <summary>
9+
/// Represents a factory that creates problem details formatted for error objects in responses.
10+
/// </summary>
11+
/// <remarks>This enables backward compatibility by converting <see cref="ProblemDetails"/> into Error Objects that
12+
/// conform to the <a ref="https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#7102-error-condition-responses">Error Responses</a>
13+
/// in the Microsoft REST API Guidelines and
14+
/// <a ref="https://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#_Toc38457793">OData Error Responses</a>.</remarks>
15+
[CLSCompliant( false )]
16+
public sealed class ErrorObjectFactory : IProblemDetailsFactory
17+
{
18+
private readonly IProblemDetailsFactory inner;
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="ErrorObjectFactory"/> class.
22+
/// </summary>
23+
public ErrorObjectFactory() : this( new DefaultProblemDetailsFactory() ) { }
24+
25+
private ErrorObjectFactory( IProblemDetailsFactory inner ) => this.inner = inner;
26+
27+
/// <summary>
28+
/// Creates and returns a new factory that decorates another factory.
29+
/// </summary>
30+
/// <param name="decorated">The inner, decorated factory instance.</param>
31+
/// <returns>A new <see cref="ErrorObjectFactory"/>.</returns>
32+
public static ErrorObjectFactory Decorate( IProblemDetailsFactory decorated ) => new( decorated );
33+
34+
/// <inheritdoc />
35+
public ProblemDetails CreateProblemDetails(
36+
HttpRequest request,
37+
int? statusCode = null,
38+
string? title = null,
39+
string? type = null,
40+
string? detail = null,
41+
string? instance = null )
42+
{
43+
var problemDetails = inner.CreateProblemDetails(
44+
request ?? throw new ArgumentNullException( nameof( request ) ),
45+
statusCode,
46+
title,
47+
type,
48+
detail,
49+
instance );
50+
51+
if ( IsSupported( problemDetails ) )
52+
{
53+
var response = request.HttpContext.Response;
54+
response.OnStarting( ChangeContentType, response );
55+
return ToErrorObject( problemDetails );
56+
}
57+
58+
return problemDetails;
59+
}
60+
61+
private static bool IsSupported( ProblemDetails problemDetails )
62+
{
63+
var type = problemDetails.Type;
64+
65+
return type == ProblemDetailsDefaults.Unsupported.Type ||
66+
type == ProblemDetailsDefaults.Unspecified.Type ||
67+
type == ProblemDetailsDefaults.Invalid.Type ||
68+
type == ProblemDetailsDefaults.Ambiguous.Type;
69+
}
70+
71+
private static ProblemDetails ToErrorObject( ProblemDetails problemDetails )
72+
{
73+
var error = new Dictionary<string, object>( capacity: 4 );
74+
var errorObject = new ProblemDetails()
75+
{
76+
Extensions =
77+
{
78+
[nameof( error )] = error,
79+
},
80+
};
81+
82+
if ( !string.IsNullOrEmpty( problemDetails.Title ) )
83+
{
84+
error["message"] = problemDetails.Title;
85+
}
86+
87+
if ( problemDetails.Extensions.TryGetValue( "code", out var value ) && value is string code )
88+
{
89+
error["code"] = code;
90+
}
91+
92+
if ( !string.IsNullOrEmpty( problemDetails.Instance ) )
93+
{
94+
error["target"] = problemDetails.Instance;
95+
}
96+
97+
if ( !string.IsNullOrEmpty( problemDetails.Detail ) )
98+
{
99+
error["innerError"] = new Dictionary<string, object>( capacity: 1 )
100+
{
101+
["message"] = problemDetails.Detail,
102+
};
103+
}
104+
105+
return errorObject;
106+
}
107+
108+
private static Task ChangeContentType( object state )
109+
{
110+
var response = (HttpResponse) state;
111+
response.ContentType = "application/json";
112+
return Task.CompletedTask;
113+
}
114+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-

1+
Add backward compatibility for error objects

src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22

3+
// Ignore Spelling: Mvc
34
namespace Microsoft.Extensions.DependencyInjection;
45

56
using Asp.Versioning;
@@ -10,6 +11,7 @@ namespace Microsoft.Extensions.DependencyInjection;
1011
using Microsoft.AspNetCore.Mvc;
1112
using Microsoft.AspNetCore.Mvc.Abstractions;
1213
using Microsoft.AspNetCore.Mvc.ApplicationModels;
14+
using Microsoft.AspNetCore.Mvc.Infrastructure;
1315
using Microsoft.AspNetCore.Mvc.Routing;
1416
using Microsoft.Extensions.DependencyInjection.Extensions;
1517
using Microsoft.Extensions.Options;
@@ -76,7 +78,18 @@ private static void AddServices( IServiceCollection services )
7678
services.TryAddEnumerable( Transient<IApiControllerSpecification, ApiBehaviorSpecification>() );
7779
services.TryAddEnumerable( Singleton<IApiVersionMetadataCollationProvider, ActionApiVersionMetadataCollationProvider>() );
7880
services.Replace( WithUrlHelperFactoryDecorator( services ) );
79-
services.TryReplace( typeof( DefaultProblemDetailsFactory ), Singleton<IProblemDetailsFactory, MvcProblemDetailsFactory>() );
81+
82+
if ( !services.TryReplace(
83+
typeof( DefaultProblemDetailsFactory ),
84+
Singleton<IProblemDetailsFactory, MvcProblemDetailsFactory>() ) )
85+
{
86+
services.TryReplace(
87+
typeof( ErrorObjectFactory ),
88+
Singleton<IProblemDetailsFactory, ErrorObjectFactory>(
89+
sp => ErrorObjectFactory.Decorate(
90+
new MvcProblemDetailsFactory(
91+
sp.GetRequiredService<ProblemDetailsFactory>() ) ) ) );
92+
}
8093
}
8194

8295
private static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor )
@@ -130,18 +143,24 @@ IUrlHelperFactory NewFactory( IServiceProvider serviceProvider )
130143
return new DecoratedServiceDescriptor( typeof( IUrlHelperFactory ), NewFactory, lifetime );
131144
}
132145

133-
private static void TryReplace( this IServiceCollection services, Type implementationType, ServiceDescriptor descriptor )
146+
private static bool TryReplace( this IServiceCollection services, Type implementationType, ServiceDescriptor descriptor )
134147
{
135148
for ( var i = 0; i < services.Count; i++ )
136149
{
137150
var service = services[i];
151+
var match = service.ServiceType == descriptor.ServiceType &&
152+
( service.ImplementationType == implementationType ||
153+
service.ImplementationInstance?.GetType() == implementationType ||
154+
service.ImplementationFactory?.Method.ReturnType == implementationType );
138155

139-
if ( service.ServiceType == descriptor.ServiceType && descriptor.ImplementationType == implementationType )
156+
if ( match )
140157
{
141158
services[i] = descriptor;
142-
return;
159+
return true;
143160
}
144161
}
162+
163+
return false;
145164
}
146165

147166
private sealed class DecoratedServiceDescriptor : ServiceDescriptor

src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/MvcProblemDetailsFactory.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22

3+
// Ignore Spelling: Mvc
34
#pragma warning disable CA1812
45

56
namespace Asp.Versioning;
@@ -24,7 +25,7 @@ public ProblemDetails CreateProblemDetails(
2425
{
2526
var httpContext = request.HttpContext;
2627
var problemDetails = factory.CreateProblemDetails( httpContext, statusCode, title, type, detail, instance );
27-
DefaultProblemDetailsFactory.ApplyExtensions(problemDetails );
28+
DefaultProblemDetailsFactory.ApplyExtensions( problemDetails );
2829
return problemDetails;
2930
}
3031
}

0 commit comments

Comments
 (0)