Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ public void apply_to_should_process_odataX2Dlike_api_description()

model.SetAnnotationValue( model, new ApiVersionAnnotation( ApiVersion.Default ) );

#pragma warning disable CA1825 // Avoid zero-length array allocations
return new()
{
ActionDescriptor = new ControllerActionDescriptor()
Expand All @@ -643,6 +644,7 @@ public void apply_to_should_process_odataX2Dlike_api_description()
},
Properties = { [typeof( ApiVersion )] = ApiVersion.Default },
};
#pragma warning restore CA1825 // Avoid zero-length array allocations
}

private static ApiDescription NewApiDescription( Type controllerType ) =>
Expand All @@ -652,6 +654,7 @@ private static ApiDescription NewApiDescription( Type controllerType, Type respo
{
model.SetAnnotationValue( model, new ApiVersionAnnotation( ApiVersion.Default ) );

#pragma warning disable CA1825 // Avoid zero-length array allocations
return new()
{
ActionDescriptor = new ControllerActionDescriptor()
Expand All @@ -674,6 +677,7 @@ private static ApiDescription NewApiDescription( Type controllerType, Type respo
},
Properties = { [typeof( ApiVersion )] = ApiVersion.Default },
};
#pragma warning restore CA1825 // Avoid zero-length array allocations
}

#pragma warning disable IDE0060 // Remove unused parameter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public void applies_to_endpoints_should_return_true_for_service_document()
var paramSource = Mock.Of<IApiVersionParameterSource>();
var options = Options.Create( new ApiVersioningOptions() );
var policy = new DefaultMetadataMatcherPolicy( paramSource, options );
#pragma warning disable CA1825 // Avoid zero-length array allocations
var metadata = new ODataRoutingMetadata( string.Empty, EdmCoreModel.Instance, [] );
#pragma warning restore CA1825 // Avoid zero-length array allocations
var items = new object[] { metadata };
var endpoints = new Endpoint[] { new( Limbo, new( items ), default ) };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ namespace Microsoft.AspNetCore.Builder;

using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using Asp.Versioning.OpenApi.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

/// <summary>
/// Provides extension methods for <see cref="IEndpointConventionBuilder"/>.
Expand Down Expand Up @@ -44,12 +44,11 @@ private static void ApplyApiVersioning( EndpointBuilder builder )

private static Task InterceptRequestServices( HttpContext context, RequestDelegate action )
{
if ( context.RequestServices is not KeyedServiceContainer requestServices )
if ( context.RequestServices is not AggregateKeyedServiceProvider )
{
requestServices = context.RequestServices.GetRequiredService<KeyedServiceContainer>();
context.RequestServices = context.RequestServices.GetRequiredService<IHost>().Services;
}

context.RequestServices = requestServices;
return action( context );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Asp.Versioning.OpenApi.Configuration;

using Asp.Versioning.ApiExplorer;
using Asp.Versioning.OpenApi.Reflection;
using Asp.Versioning.OpenApi.Transformers;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -48,7 +47,6 @@ private static void Configure( VersionedOpenApiOptions versionedOptions, XmlComm
var options = versionedOptions.Document;
var apiExplorer = new ApiExplorerTransformer( versionedOptions );

options.SetDocumentName( versionedOptions.Description.GroupName );
options.AddDocumentTransformer( apiExplorer );
options.AddSchemaTransformer( apiExplorer );
options.AddOperationTransformer( apiExplorer );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

#pragma warning disable IDE0130

namespace Microsoft.Extensions.DependencyInjection;

using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;

internal sealed class AggregateKeyedServiceProvider : IKeyedServiceProvider, IDisposable
{
private readonly IServiceCollection services;
private readonly SemaphoreSlim semaphore = new SemaphoreSlim( 1, 1 );
private readonly IServiceProvider originalServiceProvider;
private IServiceProvider activeServiceProvider;
private bool initialized;
private int? initializingThreadId;

public AggregateKeyedServiceProvider( IServiceProvider serviceProvider, IServiceCollection services )
{
this.services = services;
originalServiceProvider = serviceProvider;
activeServiceProvider = serviceProvider;
var lifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStarted.Register( () => EnsureInitialized( true ) );
}

private IServiceProvider ServiceProvider
{
get
{
EnsureInitialized( false );
return activeServiceProvider;
}
}

private void EnsureInitialized( bool isReady )
{
// If already initialized, we can return immediately.
if ( initialized )
{
return;
}

if ( initializingThreadId.HasValue && Environment.CurrentManagedThreadId == initializingThreadId.Value )
{
return;
}

// If a "ready" call entered this call already, ensure that other calls will be blocked until we fully initialize.
semaphore.Wait();
try
{

Check warning

Code scanning / CodeQL

Constant condition Warning

Condition is always false because of
access to field initialized
.
if ( initialized || !isReady )
{
return;
}

initializingThreadId = Environment.CurrentManagedThreadId;
var provider = activeServiceProvider.GetRequiredService<IApiVersionDescriptionProvider>();

var collection = new ServiceCollection();
foreach ( var descriptor in services )
{
collection.Add( descriptor );
}

var descriptions = provider.ApiVersionDescriptions;

for ( var i = 0; i < descriptions.Count; i++ )
{
var description = descriptions[i];
collection.AddOpenApi( description.GroupName );
}

activeServiceProvider = collection.BuildServiceProvider();
initialized = true;
initializingThreadId = null;
}
finally
{
semaphore.Release();
}
}

public object? GetKeyedService( Type serviceType, object? serviceKey )
{
return ServiceProvider.GetKeyedService( serviceType, serviceKey );
}

public object GetRequiredKeyedService( Type serviceType, object? serviceKey )
{
return ServiceProvider.GetRequiredKeyedService( serviceType, serviceKey );
}

public object? GetService( Type serviceType )
=> originalServiceProvider.GetService( serviceType ) ?? ServiceProvider.GetService( serviceType );

public void Dispose()
{
semaphore.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@
namespace Microsoft.Extensions.DependencyInjection;

using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using Asp.Versioning.OpenApi;
using Asp.Versioning.OpenApi.Configuration;
using Asp.Versioning.OpenApi.Reflection;
using Asp.Versioning.OpenApi.Transformers;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Reflection;
using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
using EM = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions;
Expand Down Expand Up @@ -64,18 +62,74 @@

var services = builder.Services;

services.AddTransient( NewRequestServices );
services.Add( Singleton( Type.IDocumentProvider, ResolveDocumentProvider ) );
services.Add( GetDocumentProviderDescriptor() );

var hostDescriptor = services.Single(
s => !s.IsKeyedService &&
s.ServiceType == typeof( IHost ) &&
s.Lifetime == ServiceLifetime.Singleton &&
s.ImplementationInstance is null &&
s.ImplementationType is null &&
s.ImplementationFactory is not null );
var hostDescriptorIndex = services.IndexOf( hostDescriptor );

builder.Services[hostDescriptorIndex] = CreateHostWrapperDescriptor( services, hostDescriptor.ImplementationFactory! );

services.AddSingleton<VersionedOpenApiOptionsFactory>();
services.TryAddEnumerable( Transient<IPostConfigureOptions<OpenApiOptions>, ConfigureOpenApiOptions>() );
services.TryAdd( Singleton<IOptionsFactory<VersionedOpenApiOptions>>( EM.GetRequiredService<VersionedOpenApiOptionsFactory> ) );
services.AddTransient( sp => new XmlCommentsFile( assemblies, sp.GetRequiredService<IHostEnvironment>() ) );
services.TryAddTransient( sp => new XmlCommentsTransformer( sp.GetRequiredService<XmlCommentsFile>() ) );
}

private static ServiceDescriptor GetDocumentProviderDescriptor()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddOpenApi();
foreach ( var descriptor in serviceCollection )
{
if ( descriptor.ServiceType.FullName == "Microsoft.Extensions.ApiDescriptions.IDocumentProvider" )
{
return descriptor;
}

Check warning

Code scanning / CodeQL

Erroneous class compare Warning

Erroneous class compare.
}

if ( GetJsonConfiguration() is { } descriptor )
throw new UnreachableException();
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
Comment on lines +92 to +98
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Youssef1313 Overall, it's looking solid to me. I'll see if I can't pull down these changes and test it out tonight. Thanks for the assist.


private static ServiceDescriptor CreateHostWrapperDescriptor( IServiceCollection serviceCollection, Func<IServiceProvider, object> hostFactory )
{
Func<IServiceProvider, object> updatedHostFactory = serviceProvider =>
{
var originalHost = (IHost) hostFactory( serviceProvider );
return new OpenApiHost(originalHost, NewRequestServices(serviceProvider, serviceCollection));
};

return new ServiceDescriptor( typeof( IHost ), updatedHostFactory, ServiceLifetime.Singleton );
}

private sealed class OpenApiHost : IHost
{
private readonly IHost originalHost;
private readonly IServiceProvider customServiceProvider;

public OpenApiHost( IHost originalHost, IServiceProvider customServiceProvider )
{
services.TryAddEnumerable( descriptor );
this.originalHost = originalHost;
this.customServiceProvider = customServiceProvider;
}

public IServiceProvider Services
=> customServiceProvider;

public void Dispose()
=> originalHost.Dispose();

public Task StartAsync( CancellationToken cancellationToken = default )
=> originalHost.StartAsync( cancellationToken );

public Task StopAsync( CancellationToken cancellationToken = default )
=> originalHost.StopAsync( cancellationToken );
}

// NOTE: The calling assembly must be captured at the call site that invokes AddOpenApi. In 99% of the cases that
Expand All @@ -94,55 +148,8 @@
return [.. assemblies];
}

// HACK: the json configuration is internal; this approach negates the use of reflection
// REF: https://github.com/dotnet/aspnetcore/blob/08a9fc2c3864d99759ab3d71cfda868d852bfc4b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs#L121
private static ServiceDescriptor? GetJsonConfiguration()
{
var services = new ServiceCollection();
services.AddOpenApi( "*" );
return services.SingleOrDefault( sd => sd.ServiceType == typeof( IConfigureOptions<JsonOptions> ) );
}

private static object ResolveDocumentProvider( IServiceProvider provider ) =>
provider.GetRequiredService<KeyedServiceContainer>().GetRequiredService( Type.IDocumentProvider );

[UnconditionalSuppressMessage( "ILLink", "IL3050" )]
private static KeyedServiceContainer NewRequestServices( IServiceProvider services )
private static AggregateKeyedServiceProvider NewRequestServices( IServiceProvider services, IServiceCollection parentServiceCollection )
{
var provider = services.GetRequiredService<IApiVersionDescriptionProvider>();
var container = new KeyedServiceContainer( services );
var type = typeof( IOpenApiDocumentProvider );
var descriptions = provider.ApiVersionDescriptions;
var names = new List<string>( descriptions.Count );

for ( var i = 0; i < descriptions.Count; i++ )
{
var description = descriptions[i];

// REF: https://github.com/dotnet/aspnetcore/blob/319e87fd950a99f3baae2aa79db3d4fb68783d85/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs#L64
#pragma warning disable CA1308 // Normalize strings to uppercase
var key = description.GroupName.ToLowerInvariant();
#pragma warning restore CA1308

names.Add( key );
container.AddService( Type.OpenApiSchemaService, key, Class.OpenApiSchemaService.New );
container.AddService( Type.OpenApiDocumentService, key, Class.OpenApiDocumentService.New );
container.AddService( type, key, ( sp, k ) => sp.GetRequiredKeyedService( Type.OpenApiDocumentService, k ) );
}

if ( names.Count > 0 )
{
var array = Array.CreateInstance( Type.NamedService, names.Count );

for ( var i = 0; i < names.Count; i++ )
{
array.SetValue( Class.NamedService.New( names[i] ), i );
}

container.AddService( Type.IDocumentProvider, Class.OpenApiDocumentProvider.New );
container.AddService( Type.IEnumerableOfNamedService, array );
}

return container;
return new AggregateKeyedServiceProvider( services, parentServiceCollection );
}
}
Loading
Loading