Skip to content

Commit 2df40df

Browse files
Support AOT compatibility where possible
1 parent 8baf3e0 commit 2df40df

File tree

8 files changed

+109
-35
lines changed

8 files changed

+109
-35
lines changed

src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
<PackageTags>Asp;AspNet;AspNetCore;Versioning</PackageTags>
1111
</PropertyGroup>
1212

13+
<PropertyGroup Condition=" '$(TargetFramework)' == '$(DefaultTargetFramework)' ">
14+
<IsAotCompatible>true</IsAotCompatible>
15+
</PropertyGroup>
16+
1317
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.0' ">
1418
<Compile Remove="netstandard2.0\**\*.cs;net#.0\**\*.cs" />
1519
<None Include="netstandard2.0\**\*.cs;net#.0\**\*.cs" />

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<AssemblyTitle>ASP.NET Core API Versioning</AssemblyTitle>
99
<Description>A service API versioning library for Microsoft ASP.NET Core.</Description>
1010
<PackageTags>Asp;AspNet;AspNetCore;Versioning</PackageTags>
11+
<IsAotCompatible>true</IsAotCompatible>
1112
</PropertyGroup>
1213

1314
<ItemGroup>

src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

+44-23
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace Microsoft.Extensions.DependencyInjection;
66
using Asp.Versioning.ApiExplorer;
77
using Asp.Versioning.Routing;
88
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Http.Json;
910
using Microsoft.AspNetCore.Routing;
1011
using Microsoft.Extensions.DependencyInjection.Extensions;
1112
using Microsoft.Extensions.Options;
@@ -90,26 +91,7 @@ private static void AddApiVersioningServices( IServiceCollection services )
9091
services.TryAddEnumerable( Singleton<IApiVersionMetadataCollationProvider, EndpointApiVersionMetadataCollationProvider>() );
9192
services.Replace( WithLinkGeneratorDecorator( services ) );
9293
TryAddProblemDetailsRfc7231Compliance( services );
93-
}
94-
95-
// REF: https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceDescriptor.cs#L125
96-
private static Type GetImplementationType( this ServiceDescriptor descriptor )
97-
{
98-
if ( descriptor.ImplementationType != null )
99-
{
100-
return descriptor.ImplementationType;
101-
}
102-
else if ( descriptor.ImplementationInstance != null )
103-
{
104-
return descriptor.ImplementationInstance.GetType();
105-
}
106-
else if ( descriptor.ImplementationFactory != null )
107-
{
108-
var typeArguments = descriptor.ImplementationFactory.GetType().GenericTypeArguments;
109-
return typeArguments[1];
110-
}
111-
112-
throw new InvalidOperationException();
94+
TryAddErrorObjectJsonOptions( services );
11395
}
11496

11597
private static ServiceDescriptor WithLinkGeneratorDecorator( IServiceCollection services )
@@ -127,12 +109,31 @@ private static ServiceDescriptor WithLinkGeneratorDecorator( IServiceCollection
127109

128110
if ( factory == null )
129111
{
130-
var decoratedType = descriptor.GetImplementationType();
131-
var decoratorType = typeof( ApiVersionLinkGenerator<> ).MakeGenericType( decoratedType );
112+
// REF: https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs#L96
113+
// REF: https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceDescriptor.cs#L292
114+
var decoratedType = descriptor switch
115+
{
116+
{ ImplementationType: var type } when type is not null => type,
117+
{ ImplementationInstance: var instance } when instance is not null => instance.GetType(),
118+
_ => throw new InvalidOperationException(),
119+
};
132120

133121
services.Replace( Describe( decoratedType, decoratedType, lifetime ) );
134122

135-
return Describe( typeof( LinkGenerator ), decoratorType, lifetime );
123+
LinkGenerator NewFactory( IServiceProvider serviceProvider )
124+
{
125+
var instance = (LinkGenerator) serviceProvider.GetRequiredService( decoratedType! );
126+
var source = serviceProvider.GetRequiredService<IApiVersionParameterSource>();
127+
128+
if ( source.VersionsByUrl() )
129+
{
130+
instance = new ApiVersionLinkGenerator( instance );
131+
}
132+
133+
return instance;
134+
}
135+
136+
return Describe( typeof( LinkGenerator ), NewFactory, lifetime );
136137
}
137138
else
138139
{
@@ -177,4 +178,24 @@ static bool IsDefaultProblemDetailsWriter( ServiceDescriptor serviceDescriptor )
177178
static Rfc7231ProblemDetailsWriter NewProblemDetailsWriter( IServiceProvider serviceProvider, Type decoratedType ) =>
178179
new( (IProblemDetailsWriter) serviceProvider.GetRequiredService( decoratedType ) );
179180
}
181+
182+
private static void TryAddErrorObjectJsonOptions( IServiceCollection services )
183+
{
184+
var serviceType = typeof( IProblemDetailsWriter );
185+
var implementationType = typeof( ErrorObjectWriter );
186+
187+
for ( var i = 0; i < services.Count; i++ )
188+
{
189+
var service = services[i];
190+
191+
// inheritance is intentionally not considered here because it will require a user-defined
192+
// JsonSerlizerContext and IConfigureOptions<JsonOptions>
193+
if ( service.ServiceType == serviceType &&
194+
service.ImplementationType == implementationType )
195+
{
196+
services.TryAddEnumerable( Singleton<IConfigureOptions<JsonOptions>, ErrorObjectJsonOptionsSetup>() );
197+
return;
198+
}
199+
}
200+
}
180201
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
4+
5+
namespace Asp.Versioning;
6+
7+
using Microsoft.AspNetCore.Http.Json;
8+
using Microsoft.Extensions.Options;
9+
10+
/// <summary>
11+
/// Adds the ErrorObjectJsonContext to the current JsonSerializerOptions.
12+
///
13+
/// This allows for consistent serialization behavior for ErrorObject regardless if the
14+
/// default reflection-based serializer is used and makes it trim/NativeAOT compatible.
15+
/// </summary>
16+
internal sealed class ErrorObjectJsonOptionsSetup : IConfigureOptions<JsonOptions>
17+
{
18+
// Always insert the ErrorObjectJsonContext to the beginning of the chain at the time this Configure
19+
// is invoked. This JsonTypeInfoResolver will be before the default reflection-based resolver, and
20+
// before any other resolvers currently added. If apps need to customize serialization, they can
21+
// prepend a custom ErrorObject resolver to the chain in an IConfigureOptions<JsonOptions> registered.
22+
public void Configure( JsonOptions options ) =>
23+
options.SerializerOptions.TypeInfoResolverChain.Insert( 0, new ErrorObjectWriter.ErrorObjectJsonContext() );
24+
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs

+23-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ namespace Asp.Versioning;
44

55
using Microsoft.AspNetCore.Http;
66
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.Extensions.Options;
8+
using System.Text.Json;
79
using System.Text.Json.Serialization;
810
using static System.Text.Json.Serialization.JsonIgnoreCondition;
11+
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
912

1013
/// <summary>
1114
/// Represents a problem details writer that outputs error objects in responses.
@@ -15,8 +18,18 @@ namespace Asp.Versioning;
1518
/// in the Microsoft REST API Guidelines and
1619
/// <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>
1720
[CLSCompliant( false )]
18-
public class ErrorObjectWriter : IProblemDetailsWriter
21+
public partial class ErrorObjectWriter : IProblemDetailsWriter
1922
{
23+
private readonly JsonSerializerOptions options;
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="ErrorObjectWriter"/> class.
27+
/// </summary>
28+
/// <param name="options">The current <see cref="JsonOptions">JSON options</see>.</param>
29+
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <c>null</c>.</exception>
30+
public ErrorObjectWriter( IOptions<JsonOptions> options ) =>
31+
this.options = ( options ?? throw new ArgumentNullException( nameof( options ) ) ).Value.SerializerOptions;
32+
2033
/// <inheritdoc />
2134
public virtual bool CanWrite( ProblemDetailsContext context )
2235
{
@@ -40,7 +53,7 @@ public virtual ValueTask WriteAsync( ProblemDetailsContext context )
4053

4154
OnBeforeWrite( context, ref obj );
4255

43-
return new( response.WriteAsJsonAsync( obj ) );
56+
return new( response.WriteAsJsonAsync( obj, options.GetTypeInfo( obj.GetType() ) ) );
4457
}
4558

4659
/// <summary>
@@ -58,7 +71,7 @@ protected virtual void OnBeforeWrite( ProblemDetailsContext context, ref ErrorOb
5871
/// <summary>
5972
/// Represents an error object.
6073
/// </summary>
61-
protected readonly struct ErrorObject
74+
protected internal readonly partial struct ErrorObject
6275
{
6376
internal ErrorObject( ProblemDetails problemDetails ) =>
6477
Error = new( problemDetails );
@@ -74,7 +87,7 @@ internal ErrorObject( ProblemDetails problemDetails ) =>
7487
/// <summary>
7588
/// Represents the error detail.
7689
/// </summary>
77-
protected readonly struct ErrorDetail
90+
protected internal readonly partial struct ErrorDetail
7891
{
7992
private readonly ProblemDetails problemDetails;
8093
private readonly InnerError? innerError;
@@ -154,7 +167,7 @@ public string? Target
154167
/// <summary>
155168
/// Represents an inner error.
156169
/// </summary>
157-
protected readonly struct InnerError
170+
protected internal readonly partial struct InnerError
158171
{
159172
private readonly ProblemDetails problemDetails;
160173
private readonly Dictionary<string, object> extensions = [];
@@ -181,4 +194,9 @@ public string? Message
181194
[JsonExtensionData]
182195
public IDictionary<string, object> Extensions => extensions;
183196
}
197+
198+
[JsonSerializable( typeof( ErrorObject ) )]
199+
internal sealed partial class ErrorObjectJsonContext : JsonSerializerContext
200+
{
201+
}
184202
}

src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersioningRouteOptionsSetup.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public virtual void PostConfigure( string? name, RouteOptions options )
2424
{
2525
ArgumentNullException.ThrowIfNull( options );
2626

27-
var key = versioningOptions.Value.RouteConstraintName;
28-
options.ConstraintMap.Add( key, typeof( ApiVersionRouteConstraint ) );
27+
var token = versioningOptions.Value.RouteConstraintName;
28+
options.SetParameterPolicy<ApiVersionRouteConstraint>( token );
2929
}
3030
}

src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/ErrorObjectWriterTest.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Asp.Versioning;
44

55
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Http.Json;
7+
using Microsoft.Extensions.Options;
68
using System.Text.Json;
79
using System.Threading.Tasks;
810

@@ -16,7 +18,7 @@ public class ErrorObjectWriterTest
1618
public void can_write_should_be_true_for_api_versioning_problem_types( string type )
1719
{
1820
// arrange
19-
var writer = new ErrorObjectWriter();
21+
var writer = new ErrorObjectWriter( Options.Create( new JsonOptions() ) );
2022
var context = new ProblemDetailsContext()
2123
{
2224
HttpContext = new DefaultHttpContext(),
@@ -38,7 +40,7 @@ public void can_write_should_be_false_for_other_problem_types()
3840
{
3941
// arrange
4042
const string BadRequest = "https://tools.ietf.org/html/rfc7231#section-6.5.1";
41-
var writer = new ErrorObjectWriter();
43+
var writer = new ErrorObjectWriter( Options.Create( new JsonOptions() ) );
4244
var context = new ProblemDetailsContext()
4345
{
4446
HttpContext = new DefaultHttpContext(),
@@ -73,14 +75,14 @@ public async Task write_async_should_output_expected_json()
7375
},
7476
};
7577

76-
var writer = new ErrorObjectWriter();
78+
var writer = new ErrorObjectWriter( Options.Create( new JsonOptions() ) );
7779
using var stream = new MemoryStream();
7880
var response = new Mock<HttpResponse>() { CallBase = true };
7981
var httpContext = new Mock<HttpContext>() { CallBase = true };
8082

8183
response.SetupGet( r => r.Body ).Returns( stream );
8284
response.SetupProperty( r => r.ContentType );
83-
response.SetupGet(r => r.HttpContext).Returns(() => httpContext.Object );
85+
response.SetupGet( r => r.HttpContext ).Returns( () => httpContext.Object );
8486
httpContext.SetupGet( c => c.Response ).Returns( response.Object );
8587

8688
var context = new ProblemDetailsContext()

src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
<PackageTags>Asp;AspNet;AspNetCore;Versioning;Http</PackageTags>
1111
</PropertyGroup>
1212

13+
<PropertyGroup Condition=" '$(TargetFramework)' == '$(DefaultTargetFramework)' ">
14+
<IsAotCompatible>true</IsAotCompatible>
15+
</PropertyGroup>
16+
1317
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.1' ">
1418
<Compile Remove="net#.0\**\*.cs" />
1519
<None Include="net#.0\**\*.cs" />
@@ -44,5 +48,5 @@
4448
<ItemGroup>
4549
<ProjectReference Include="..\..\..\Abstractions\src\Asp.Versioning.Abstractions\Asp.Versioning.Abstractions.csproj" />
4650
</ItemGroup>
47-
51+
4852
</Project>

0 commit comments

Comments
 (0)