Skip to content

Commit

Permalink
Merge branch 'main' into dev/css/refactor-ver-prov-factory
Browse files Browse the repository at this point in the history
  • Loading branch information
commonsensesoftware authored Mar 26, 2024
2 parents a227703 + 42607c5 commit 5f3f0ed
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )
else
{
UpdateModelTypes( result, matched );
UpdateFunctionCollectionParameters( result, matched );
}
}

Expand Down Expand Up @@ -456,6 +457,75 @@ private void UpdateModelTypes( ApiDescription description, IODataRoutingMetadata
}
}

private static void UpdateFunctionCollectionParameters( ApiDescription description, IODataRoutingMetadata metadata )
{
var parameters = description.ParameterDescriptions;

if ( parameters.Count == 0 )
{
return;
}

var function = default( IEdmFunction );
var mapping = default( IDictionary<string, string> );

for ( var i = 0; i < metadata.Template.Count; i++ )
{
var segment = metadata.Template[i];

if ( segment is FunctionSegmentTemplate func )
{
function = func.Function;
mapping = func.ParameterMappings;
break;
}
else if ( segment is FunctionImportSegmentTemplate import )
{
function = import.FunctionImport.Function;
mapping = import.ParameterMappings;
break;
}
}

if ( function is null || mapping is null )
{
return;
}

var name = default( string );

foreach ( var parameter in function.Parameters )
{
if ( parameter.Type.IsCollection() &&
mapping.TryGetValue( parameter.Name, out name ) &&
parameters.SingleOrDefault( p => p.Name == name ) is { } param )
{
param.Source = BindingSource.Path;
break;
}
}

var path = description.RelativePath;

if ( string.IsNullOrEmpty( name ) || string.IsNullOrEmpty( path ) )
{
return;
}

var span = name.AsSpan();
Span<char> oldValue = stackalloc char[name.Length + 2];
Span<char> newValue = stackalloc char[name.Length + 4];

newValue[1] = oldValue[0] = '{';
newValue[^2] = oldValue[^1] = '}';
newValue[0] = '[';
newValue[^1] = ']';
span.CopyTo( oldValue.Slice( 1, name.Length ) );
span.CopyTo( newValue.Slice( 2, name.Length ) );

description.RelativePath = path.Replace( oldValue.ToString(), newValue.ToString(), Ordinal );
}

private sealed class ApiDescriptionComparer : IEqualityComparer<ApiDescription>
{
private readonly IEqualityComparer<string?> comparer = StringComparer.OrdinalIgnoreCase;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,10 @@ private static int ODataOrder() =>
new ODataApiDescriptionProvider(
new StubModelMetadataProvider(),
new StubModelTypeBuilder(),
new OptionsFactory<ODataOptions>(
Enumerable.Empty<IConfigureOptions<ODataOptions>>(),
Enumerable.Empty<IPostConfigureOptions<ODataOptions>>() ),
new OptionsFactory<ODataOptions>( [], [] ),
Opts.Create(
new ODataApiExplorerOptions(
new(
new StubODataApiVersionCollectionProvider(),
Enumerable.Empty<IModelConfiguration>() ) ) ) ).Order;
new( new StubODataApiVersionCollectionProvider(), [] ) ) ) ).Order;

[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ namespace Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.Routing;
using System.Diagnostics.CodeAnalysis;

Check failure on line 11 in src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze

Using directives should be ordered alphabetically by the namespaces (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1210.md)

Check failure on line 11 in src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze

Using directives should be ordered alphabetically by the namespaces (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1210.md)

Check failure on line 11 in src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze

Using directives should be ordered alphabetically by the namespaces (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1210.md)

Check failure on line 11 in src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze

Using directives should be ordered alphabetically by the namespaces (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1210.md)
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using static DynamicallyAccessedMemberTypes;

Check failure on line 14 in src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze

Using directive for type 'System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes' should be qualified (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1135.md)

Check failure on line 14 in src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze

Using directive for type 'System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes' should be qualified (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1135.md)

Check failure on line 14 in src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze

Using directive for type 'System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes' should be qualified (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1135.md)

Check failure on line 14 in src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs

View workflow job for this annotation

GitHub Actions / Analyze

Using directive for type 'System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes' should be qualified (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1135.md)
using static ServiceDescriptor;

/// <summary>
Expand Down Expand Up @@ -74,6 +76,65 @@ public static IApiVersioningBuilder EnableApiVersionBinding( this IApiVersioning
return builder;
}

/// <summary>
/// Adds error object support in problem details.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection">services</see> available in the application.</param>
/// <param name="setup">The <see cref="JsonOptions">JSON options</see> setup <see cref="Action{T}"/> to perform, if any.</param>
/// <returns>The original <paramref name="services"/>.</returns>
/// <remarks>
/// <para>
/// This method is only intended to provide backward compatibility with previous library versions by converting
/// <see cref="Microsoft.AspNetCore.Mvc.ProblemDetails"/> into Error Objects that conform to the
/// <a ref="https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#7102-error-condition-responses">Error Responses</a>
/// in the Microsoft REST API Guidelines and
/// <a ref="https://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#_Toc38457793">OData Error Responses</a>.
/// </para>
/// <para>
/// This method should be called before <see cref="ProblemDetailsServiceCollectionExtensions.AddProblemDetails(IServiceCollection)"/>.
/// </para>
/// </remarks>
public static IServiceCollection AddErrorObjects( this IServiceCollection services, Action<JsonOptions>? setup = default ) =>
AddErrorObjects<ErrorObjectWriter>( services, setup );

/// <summary>
/// Adds error object support in problem details.
/// </summary>
/// <typeparam name="TWriter">The type of <see cref="ErrorObjectWriter"/>.</typeparam>
/// <param name="services">The <see cref="IServiceCollection">services</see> available in the application.</param>
/// <param name="setup">The <see cref="JsonOptions">JSON options</see> setup <see cref="Action{T}"/> to perform, if any.</param>
/// <returns>The original <paramref name="services"/>.</returns>
/// <remarks>
/// <para>
/// This method is only intended to provide backward compatibility with previous library versions by converting
/// <see cref="Microsoft.AspNetCore.Mvc.ProblemDetails"/> into Error Objects that conform to the
/// <a ref="https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#7102-error-condition-responses">Error Responses</a>
/// in the Microsoft REST API Guidelines and
/// <a ref="https://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#_Toc38457793">OData Error Responses</a>.
/// </para>
/// <para>
/// This method should be called before <see cref="ProblemDetailsServiceCollectionExtensions.AddProblemDetails(IServiceCollection)"/>.
/// </para>
/// </remarks>
public static IServiceCollection AddErrorObjects<[DynamicallyAccessedMembers( PublicConstructors )] TWriter>(
this IServiceCollection services,
Action<JsonOptions>? setup = default )
where TWriter : ErrorObjectWriter
{
ArgumentNullException.ThrowIfNull( services );

services.TryAddEnumerable( Singleton<IProblemDetailsWriter, TWriter>() );
services.Configure( setup ?? DefaultErrorObjectJsonConfig );

// TODO: remove with TryAddErrorObjectJsonOptions in 9.0+
services.AddTransient<ErrorObjectsAdded>();

return services;
}

private static void DefaultErrorObjectJsonConfig( JsonOptions options ) =>
options.SerializerOptions.TypeInfoResolverChain.Insert( 0, ErrorObjectWriter.ErrorObjectJsonContext.Default );

private static void AddApiVersioningServices( IServiceCollection services )
{
ArgumentNullException.ThrowIfNull( services );
Expand Down Expand Up @@ -179,23 +240,48 @@ static Rfc7231ProblemDetailsWriter NewProblemDetailsWriter( IServiceProvider ser
new( (IProblemDetailsWriter) serviceProvider.GetRequiredService( decoratedType ) );
}

// TODO: retain for 8.1.x back-compat, but remove in 9.0+ in favor of AddErrorObjects for perf
private static void TryAddErrorObjectJsonOptions( IServiceCollection services )
{
var serviceType = typeof( IProblemDetailsWriter );
var implementationType = typeof( ErrorObjectWriter );
var markerType = typeof( ErrorObjectsAdded );
var hasErrorObjects = false;
var hasErrorObjectsJsonConfig = false;

for ( var i = 0; i < services.Count; i++ )
{
var service = services[i];

// inheritance is intentionally not considered here because it will require a user-defined
// JsonSerlizerContext and IConfigureOptions<JsonOptions>
if ( service.ServiceType == serviceType &&
service.ImplementationType == implementationType )
if ( !hasErrorObjects &&
service.ServiceType == serviceType &&
implementationType.IsAssignableFrom( service.ImplementationType ) )
{
services.TryAddEnumerable( Singleton<IConfigureOptions<JsonOptions>, ErrorObjectJsonOptionsSetup>() );
return;
hasErrorObjects = true;

if ( hasErrorObjectsJsonConfig )
{
break;
}
}
else if ( service.ServiceType == markerType )
{
hasErrorObjectsJsonConfig = true;

if ( hasErrorObjects )
{
break;
}
}
}

if ( hasErrorObjects && !hasErrorObjectsJsonConfig )
{
services.Configure<JsonOptions>( DefaultErrorObjectJsonConfig );
}
}

// TEMP: this is a marker class to test whether Error Objects have been explicitly added. remove in 9.0+
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
private sealed class ErrorObjectsAdded { }
}

This file was deleted.

27 changes: 20 additions & 7 deletions src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

// Ignore Spelling: Serializer
namespace Asp.Versioning;

using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -30,6 +31,19 @@ public partial class ErrorObjectWriter : IProblemDetailsWriter
public ErrorObjectWriter( IOptions<JsonOptions> options ) =>
this.options = ( options ?? throw new ArgumentNullException( nameof( options ) ) ).Value.SerializerOptions;

/// <summary>
/// Gets the associated, default <see cref="JsonSerializerContext"/>.
/// </summary>
/// <value>The associated, default <see cref="JsonSerializerContext"/>.</value>
public static JsonSerializerContext DefaultJsonSerializerContext => ErrorObjectJsonContext.Default;

/// <summary>
/// Creates and returns a new <see cref="JsonSerializerContext"/> associated with the writer.
/// </summary>
/// <param name="options">The <see cref="JsonSerializerOptions">JSON serializer options</see> to use.</param>
/// <returns>A new <see cref="JsonSerializerContext"/>.</returns>
public static JsonSerializerContext NewJsonSerializerContext( JsonSerializerOptions options ) => new ErrorObjectJsonContext( options );

/// <inheritdoc />
public virtual bool CanWrite( ProblemDetailsContext context )
{
Expand Down Expand Up @@ -89,6 +103,7 @@ internal ErrorObject( ProblemDetails problemDetails ) =>
/// </summary>
protected internal readonly partial struct ErrorDetail
{
private const string CodeProperty = "code";
private readonly ProblemDetails problemDetails;
private readonly InnerError? innerError;
private readonly Dictionary<string, object> extensions = [];
Expand All @@ -103,23 +118,21 @@ internal ErrorDetail( ProblemDetails problemDetails )
/// Gets or sets one of a server-defined set of error codes.
/// </summary>
/// <value>A server-defined error code.</value>
[JsonPropertyName( "code" )]
[JsonPropertyName( CodeProperty )]
[JsonIgnore( Condition = WhenWritingNull )]
public string? Code
{
get => problemDetails.Extensions.TryGetValue( "code", out var value ) &&
value is string code ?
code :
default;
get => problemDetails.Extensions.TryGetValue( CodeProperty, out var value ) &&
value is string code ? code : default;
set
{
if ( value is null )
{
problemDetails.Extensions.Remove( "code" );
problemDetails.Extensions.Remove( CodeProperty );
}
else
{
problemDetails.Extensions["code"] = value;
problemDetails.Extensions[CodeProperty] = value;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,14 @@ public static bool TryUpdateRelativePathAndRemoveApiVersionParameter( this ApiDe
return false;
}

var token = '{' + parameter.Name + '}';
Span<char> token = stackalloc char[parameter.Name.Length + 2];

token[0] = '{';
token[^1] = '}';
parameter.Name.AsSpan().CopyTo( token.Slice( 1, parameter.Name.Length ) );

var value = apiVersion.ToString( options.SubstitutionFormat, CultureInfo.InvariantCulture );
var newRelativePath = relativePath.Replace( token, value, StringComparison.Ordinal );
var newRelativePath = relativePath.Replace( token.ToString(), value, StringComparison.Ordinal );

if ( relativePath == newRelativePath )
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
using System.Runtime.CompilerServices;
using static Asp.Versioning.ApiVersionParameterLocation;
using static System.Linq.Enumerable;
using static System.StringComparison;
Expand Down Expand Up @@ -304,7 +305,7 @@ routeInfo.Constraints is IEnumerable<IRouteConstraint> constraints &&
continue;
}

var token = $"{parameter.Name}:{constraintName}";
var token = FormatToken( parameter.Name, constraintName );

parameterDescription.Name = parameter.Name;
description.RelativePath = relativePath.Replace( token, parameter.Name, Ordinal );
Expand Down Expand Up @@ -375,7 +376,7 @@ routeInfo.Constraints is IEnumerable<IRouteConstraint> constraints &&
},
Source = BindingSource.Path,
};
var token = $"{parameter.Name}:{constraintName}";
var token = FormatToken( parameter.Name!, constraintName! );

description.RelativePath = relativePath.Replace( token, parameter.Name, Ordinal );
description.ParameterDescriptions.Insert( 0, result );
Expand Down Expand Up @@ -457,4 +458,18 @@ private static bool FirstParameterIsOptional(

return apiVersion == defaultApiVersion;
}

[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static string FormatToken( ReadOnlySpan<char> parameterName, ReadOnlySpan<char> constraintName )
{
var left = parameterName.Length;
var right = constraintName.Length;
Span<char> token = stackalloc char[left + right + 1];

parameterName.CopyTo( token[..left] );
token[left] = ':';
constraintName.CopyTo( token.Slice( left + 1, right ) );

return token.ToString();
}
}
Loading

0 comments on commit 5f3f0ed

Please sign in to comment.