Skip to content

Move schema store into service and add schema keys #56084

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 5, 2024
Merged
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 @@ -64,7 +64,8 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services)
private static IServiceCollection AddOpenApiCore(this IServiceCollection services, string documentName)
{
services.AddEndpointsApiExplorer();
services.AddKeyedSingleton<OpenApiComponentService>(documentName);
services.AddKeyedSingleton<OpenApiSchemaService>(documentName);
services.AddKeyedSingleton<OpenApiSchemaStore>(documentName);
services.AddKeyedSingleton<OpenApiDocumentService>(documentName);
// Required for build-time generation
services.AddSingleton<IDocumentProvider, OpenApiDocumentProvider>();
Expand Down
2 changes: 1 addition & 1 deletion src/OpenApi/src/Services/OpenApiDocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal sealed class OpenApiDocumentService(
IServiceProvider serviceProvider)
{
private readonly OpenApiOptions _options = optionsMonitor.Get(documentName);
private readonly OpenApiComponentService _componentService = serviceProvider.GetRequiredKeyedService<OpenApiComponentService>(documentName);
private readonly OpenApiSchemaService _componentService = serviceProvider.GetRequiredKeyedService<OpenApiSchemaService>(documentName);
private readonly IOpenApiDocumentTransformer _scrubExtensionsTransformer = new ScrubExtensionsTransformer();

private static readonly OpenApiEncoding _defaultFormEncoding = new OpenApiEncoding { Style = ParameterStyle.Form, Explode = true };
Expand Down
12 changes: 12 additions & 0 deletions src/OpenApi/src/Services/Schemas/OpenApiSchemaKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;

namespace Microsoft.AspNetCore.OpenApi;

/// <summary>
/// Represents a unique identifier that is used to store and retrieve
/// JSON schemas associated with a given property.
/// </summary>
internal record struct OpenApiSchemaKey(Type Type, ParameterInfo? ParameterInfo);
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.IO.Pipelines;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using JsonSchemaMapper;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;

Expand All @@ -22,21 +21,9 @@ namespace Microsoft.AspNetCore.OpenApi;
/// an OpenAPI document. In particular, this is the API that is used to
/// interact with the JSON schemas that are managed by a given OpenAPI document.
/// </summary>
internal sealed class OpenApiComponentService(IOptions<JsonOptions> jsonOptions)
internal sealed class OpenApiSchemaService([ServiceKey] string documentName, IOptions<JsonOptions> jsonOptions, IServiceProvider serviceProvider)
{
private readonly ConcurrentDictionary<(Type, ParameterInfo?), JsonObject> _schemas = new()
{
// Pre-populate OpenAPI schemas for well-defined types in ASP.NET Core.
[(typeof(IFormFile), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
[(typeof(IFormFileCollection), null)] = new JsonObject
{
["type"] = "array",
["items"] = new JsonObject { ["type"] = "string", ["format"] = "binary" }
},
[(typeof(Stream), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
[(typeof(PipeReader), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
};

private readonly OpenApiSchemaStore _schemaStore = serviceProvider.GetRequiredKeyedService<OpenApiSchemaStore>(documentName);
private readonly JsonSerializerOptions _jsonSerializerOptions = jsonOptions.Value.SerializerOptions;
private readonly JsonSchemaMapperConfiguration _configuration = new()
{
Expand Down Expand Up @@ -74,8 +61,8 @@ internal OpenApiSchema GetOrCreateSchema(Type type, ApiParameterDescription? par
{
var key = parameterDescription?.ParameterDescriptor is IParameterInfoParameterDescriptor parameterInfoDescription
&& parameterDescription.ModelMetadata.PropertyName is null
? (type, parameterInfoDescription.ParameterInfo) : (type, null);
var schemaAsJsonObject = _schemas.GetOrAdd(key, CreateSchema);
? new OpenApiSchemaKey(type, parameterInfoDescription.ParameterInfo) : new OpenApiSchemaKey(type, null);
var schemaAsJsonObject = _schemaStore.GetOrAdd(key, CreateSchema);
if (parameterDescription is not null)
{
schemaAsJsonObject.ApplyParameterInfo(parameterDescription);
Expand All @@ -84,7 +71,7 @@ internal OpenApiSchema GetOrCreateSchema(Type type, ApiParameterDescription? par
return deserializedSchema != null ? deserializedSchema.Schema : new OpenApiSchema();
}

private JsonObject CreateSchema((Type Type, ParameterInfo? ParameterInfo) key)
private JsonObject CreateSchema(OpenApiSchemaKey key)
=> key.ParameterInfo is not null
? JsonSchemaMapper.JsonSchemaMapper.GetJsonSchema(_jsonSerializerOptions, key.ParameterInfo, _configuration)
: JsonSchemaMapper.JsonSchemaMapper.GetJsonSchema(_jsonSerializerOptions, key.Type, _configuration);
Expand Down
40 changes: 40 additions & 0 deletions src/OpenApi/src/Services/Schemas/OpenApiSchemaStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.IO.Pipelines;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.OpenApi;

/// <summary>
/// Stores schemas generated by the JsonSchemaMapper for a
/// given OpenAPI document for later resolution.
/// </summary>
internal sealed class OpenApiSchemaStore
{
private readonly ConcurrentDictionary<OpenApiSchemaKey, JsonObject> _schemas = new()
{
// Pre-populate OpenAPI schemas for well-defined types in ASP.NET Core.
[new OpenApiSchemaKey(typeof(IFormFile), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
[new OpenApiSchemaKey(typeof(IFormFileCollection), null)] = new JsonObject
{
["type"] = "array",
["items"] = new JsonObject { ["type"] = "string", ["format"] = "binary" }
},
[new OpenApiSchemaKey(typeof(Stream), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
[new OpenApiSchemaKey(typeof(PipeReader), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
};

/// <summary>
/// Resolves the JSON schema for the given type and parameter description.
/// </summary>
/// <param name="key">The key associated with the generated schema.</param>
/// <param name="valueFactory">A function used to generated the JSON object representing the schema.</param>
/// <returns>A <see cref="JsonObject" /> representing the JSON schema associated with the key.</returns>
public JsonObject GetOrAdd(OpenApiSchemaKey key, Func<OpenApiSchemaKey, JsonObject> valueFactory)
{
return _schemas.GetOrAdd(key, valueFactory);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void AddOpenApi_WithDocumentName_RegistersServices()
var serviceProvider = services.BuildServiceProvider();

// Assert
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiComponentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiSchemaService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiDocumentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(IDocumentProvider) && sd.Lifetime == ServiceLifetime.Singleton);
var options = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenApiOptions>>();
Expand Down Expand Up @@ -69,7 +69,7 @@ public void AddOpenApi_WithDocumentNameAndConfigureOptions_RegistersServices()
var serviceProvider = services.BuildServiceProvider();

// Assert
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiComponentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiSchemaService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiDocumentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(IDocumentProvider) && sd.Lifetime == ServiceLifetime.Singleton);
var options = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenApiOptions>>();
Expand Down Expand Up @@ -102,7 +102,7 @@ public void AddOpenApi_WithoutDocumentName_RegistersServices()
var serviceProvider = services.BuildServiceProvider();

// Assert
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiComponentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiSchemaService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiDocumentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(IDocumentProvider) && sd.Lifetime == ServiceLifetime.Singleton);
var options = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenApiOptions>>();
Expand Down Expand Up @@ -135,7 +135,7 @@ public void AddOpenApi_WithConfigureOptions_RegistersServices()
var serviceProvider = services.BuildServiceProvider();

// Assert
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiComponentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiSchemaService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiDocumentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(IDocumentProvider) && sd.Lifetime == ServiceLifetime.Singleton);
var options = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenApiOptions>>();
Expand All @@ -157,7 +157,7 @@ public void AddOpenApi_WithDuplicateDocumentNames_UsesLastRegistration()
var serviceProvider = services.BuildServiceProvider();

// Assert
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiComponentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiSchemaService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiDocumentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(IDocumentProvider) && sd.Lifetime == ServiceLifetime.Singleton);
var options = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenApiOptions>>();
Expand All @@ -181,7 +181,7 @@ public void AddOpenApi_WithDuplicateDocumentNames_UsesLastRegistration_ValidateO
var serviceProvider = services.BuildServiceProvider();

// Assert
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiComponentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiSchemaService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiDocumentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
Assert.Contains(services, sd => sd.ServiceType == typeof(IDocumentProvider) && sd.Lifetime == ServiceLifetime.Singleton);
var options = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenApiOptions>>();
Expand Down
35 changes: 21 additions & 14 deletions src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
Expand All @@ -18,7 +15,6 @@
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
using Moq;
using static Microsoft.AspNetCore.OpenApi.Tests.OpenApiOperationGeneratorTests;
Expand Down Expand Up @@ -217,27 +213,38 @@ private class TestServiceProvider : IServiceProvider, IKeyedServiceProvider
public static TestServiceProvider Instance { get; } = new TestServiceProvider();
private IKeyedServiceProvider _serviceProvider;
internal OpenApiDocumentService TestDocumentService { get; set; }
internal OpenApiComponentService TestComponentService { get; set; } = new OpenApiComponentService(Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()));
internal OpenApiSchemaStore TestSchemaStoreService { get; } = new OpenApiSchemaStore();
private OpenApiSchemaService _testSchemaService;

public void SetInternalServiceProvider(IServiceCollection serviceCollection)
{
serviceCollection.AddKeyedSingleton<OpenApiSchemaStore>("Test");
_serviceProvider = serviceCollection.BuildServiceProvider();
_testSchemaService = new OpenApiSchemaService(
"Test",
Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()),
_serviceProvider
);
}

public object GetKeyedService(Type serviceType, object serviceKey)
{

if (serviceType == typeof(OpenApiDocumentService))
{
return TestDocumentService;
}
if (serviceType == typeof(OpenApiComponentService))
if (serviceType == typeof(OpenApiSchemaService))
{
return TestComponentService;
return _testSchemaService;
}

if (serviceType == typeof(OpenApiComponentService))
if (serviceType == typeof(OpenApiSchemaService))
{
return _testSchemaService;
}
if (serviceType == typeof(OpenApiSchemaStore))
{
return TestComponentService;
return TestSchemaStoreService;
}

return _serviceProvider.GetKeyedService(serviceType, serviceKey);
Expand All @@ -249,14 +256,14 @@ public object GetRequiredKeyedService(Type serviceType, object serviceKey)
{
return TestDocumentService;
}
if (serviceType == typeof(OpenApiComponentService))
if (serviceType == typeof(OpenApiSchemaService))
{
return TestComponentService;
return _testSchemaService;
}

if (serviceType == typeof(OpenApiComponentService))
if (serviceType == typeof(OpenApiSchemaService))
{
return TestComponentService;
return _testSchemaService;
}

return _serviceProvider.GetRequiredKeyedService(serviceType, serviceKey);
Expand Down
Loading