Skip to content

Commit 8adff2c

Browse files
authored
Move schema store into service and add schema keys (#56084)
1 parent d831a50 commit 8adff2c

10 files changed

+88
-41
lines changed

src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services)
6464
private static IServiceCollection AddOpenApiCore(this IServiceCollection services, string documentName)
6565
{
6666
services.AddEndpointsApiExplorer();
67-
services.AddKeyedSingleton<OpenApiComponentService>(documentName);
67+
services.AddKeyedSingleton<OpenApiSchemaService>(documentName);
68+
services.AddKeyedSingleton<OpenApiSchemaStore>(documentName);
6869
services.AddKeyedSingleton<OpenApiDocumentService>(documentName);
6970
// Required for build-time generation
7071
services.AddSingleton<IDocumentProvider, OpenApiDocumentProvider>();

src/OpenApi/src/Services/OpenApiDocumentService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ internal sealed class OpenApiDocumentService(
3030
IServiceProvider serviceProvider)
3131
{
3232
private readonly OpenApiOptions _options = optionsMonitor.Get(documentName);
33-
private readonly OpenApiComponentService _componentService = serviceProvider.GetRequiredKeyedService<OpenApiComponentService>(documentName);
33+
private readonly OpenApiSchemaService _componentService = serviceProvider.GetRequiredKeyedService<OpenApiSchemaService>(documentName);
3434
private readonly IOpenApiDocumentTransformer _scrubExtensionsTransformer = new ScrubExtensionsTransformer();
3535

3636
private static readonly OpenApiEncoding _defaultFormEncoding = new OpenApiEncoding { Style = ParameterStyle.Form, Explode = true };
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Reflection;
5+
6+
namespace Microsoft.AspNetCore.OpenApi;
7+
8+
/// <summary>
9+
/// Represents a unique identifier that is used to store and retrieve
10+
/// JSON schemas associated with a given property.
11+
/// </summary>
12+
internal record struct OpenApiSchemaKey(Type Type, ParameterInfo? ParameterInfo);

src/OpenApi/src/Services/OpenApiComponentService.cs renamed to src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.Concurrent;
54
using System.ComponentModel.DataAnnotations;
65
using System.IO.Pipelines;
7-
using System.Reflection;
86
using System.Text.Json;
97
using System.Text.Json.Nodes;
108
using JsonSchemaMapper;
119
using Microsoft.AspNetCore.Http;
1210
using Microsoft.AspNetCore.Http.Json;
1311
using Microsoft.AspNetCore.Mvc.ApiExplorer;
1412
using Microsoft.AspNetCore.Mvc.Infrastructure;
13+
using Microsoft.Extensions.DependencyInjection;
1514
using Microsoft.Extensions.Options;
1615
using Microsoft.OpenApi.Models;
1716

@@ -22,21 +21,9 @@ namespace Microsoft.AspNetCore.OpenApi;
2221
/// an OpenAPI document. In particular, this is the API that is used to
2322
/// interact with the JSON schemas that are managed by a given OpenAPI document.
2423
/// </summary>
25-
internal sealed class OpenApiComponentService(IOptions<JsonOptions> jsonOptions)
24+
internal sealed class OpenApiSchemaService([ServiceKey] string documentName, IOptions<JsonOptions> jsonOptions, IServiceProvider serviceProvider)
2625
{
27-
private readonly ConcurrentDictionary<(Type, ParameterInfo?), JsonObject> _schemas = new()
28-
{
29-
// Pre-populate OpenAPI schemas for well-defined types in ASP.NET Core.
30-
[(typeof(IFormFile), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
31-
[(typeof(IFormFileCollection), null)] = new JsonObject
32-
{
33-
["type"] = "array",
34-
["items"] = new JsonObject { ["type"] = "string", ["format"] = "binary" }
35-
},
36-
[(typeof(Stream), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
37-
[(typeof(PipeReader), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
38-
};
39-
26+
private readonly OpenApiSchemaStore _schemaStore = serviceProvider.GetRequiredKeyedService<OpenApiSchemaStore>(documentName);
4027
private readonly JsonSerializerOptions _jsonSerializerOptions = jsonOptions.Value.SerializerOptions;
4128
private readonly JsonSchemaMapperConfiguration _configuration = new()
4229
{
@@ -74,8 +61,8 @@ internal OpenApiSchema GetOrCreateSchema(Type type, ApiParameterDescription? par
7461
{
7562
var key = parameterDescription?.ParameterDescriptor is IParameterInfoParameterDescriptor parameterInfoDescription
7663
&& parameterDescription.ModelMetadata.PropertyName is null
77-
? (type, parameterInfoDescription.ParameterInfo) : (type, null);
78-
var schemaAsJsonObject = _schemas.GetOrAdd(key, CreateSchema);
64+
? new OpenApiSchemaKey(type, parameterInfoDescription.ParameterInfo) : new OpenApiSchemaKey(type, null);
65+
var schemaAsJsonObject = _schemaStore.GetOrAdd(key, CreateSchema);
7966
if (parameterDescription is not null)
8067
{
8168
schemaAsJsonObject.ApplyParameterInfo(parameterDescription);
@@ -84,7 +71,7 @@ internal OpenApiSchema GetOrCreateSchema(Type type, ApiParameterDescription? par
8471
return deserializedSchema != null ? deserializedSchema.Schema : new OpenApiSchema();
8572
}
8673

87-
private JsonObject CreateSchema((Type Type, ParameterInfo? ParameterInfo) key)
74+
private JsonObject CreateSchema(OpenApiSchemaKey key)
8875
=> key.ParameterInfo is not null
8976
? JsonSchemaMapper.JsonSchemaMapper.GetJsonSchema(_jsonSerializerOptions, key.ParameterInfo, _configuration)
9077
: JsonSchemaMapper.JsonSchemaMapper.GetJsonSchema(_jsonSerializerOptions, key.Type, _configuration);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Concurrent;
5+
using System.IO.Pipelines;
6+
using System.Text.Json.Nodes;
7+
using Microsoft.AspNetCore.Http;
8+
9+
namespace Microsoft.AspNetCore.OpenApi;
10+
11+
/// <summary>
12+
/// Stores schemas generated by the JsonSchemaMapper for a
13+
/// given OpenAPI document for later resolution.
14+
/// </summary>
15+
internal sealed class OpenApiSchemaStore
16+
{
17+
private readonly ConcurrentDictionary<OpenApiSchemaKey, JsonObject> _schemas = new()
18+
{
19+
// Pre-populate OpenAPI schemas for well-defined types in ASP.NET Core.
20+
[new OpenApiSchemaKey(typeof(IFormFile), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
21+
[new OpenApiSchemaKey(typeof(IFormFileCollection), null)] = new JsonObject
22+
{
23+
["type"] = "array",
24+
["items"] = new JsonObject { ["type"] = "string", ["format"] = "binary" }
25+
},
26+
[new OpenApiSchemaKey(typeof(Stream), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
27+
[new OpenApiSchemaKey(typeof(PipeReader), null)] = new JsonObject { ["type"] = "string", ["format"] = "binary" },
28+
};
29+
30+
/// <summary>
31+
/// Resolves the JSON schema for the given type and parameter description.
32+
/// </summary>
33+
/// <param name="key">The key associated with the generated schema.</param>
34+
/// <param name="valueFactory">A function used to generated the JSON object representing the schema.</param>
35+
/// <returns>A <see cref="JsonObject" /> representing the JSON schema associated with the key.</returns>
36+
public JsonObject GetOrAdd(OpenApiSchemaKey key, Func<OpenApiSchemaKey, JsonObject> valueFactory)
37+
{
38+
return _schemas.GetOrAdd(key, valueFactory);
39+
}
40+
}

src/OpenApi/test/Extensions/OpenApiServiceCollectionExtensionsTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void AddOpenApi_WithDocumentName_RegistersServices()
3535
var serviceProvider = services.BuildServiceProvider();
3636

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

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

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

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

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

183183
// Assert
184-
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiComponentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
184+
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiSchemaService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
185185
Assert.Contains(services, sd => sd.ServiceType == typeof(OpenApiDocumentService) && sd.Lifetime == ServiceLifetime.Singleton && (string)sd.ServiceKey == documentName);
186186
Assert.Contains(services, sd => sd.ServiceType == typeof(IDocumentProvider) && sd.Lifetime == ServiceLifetime.Singleton);
187187
var options = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenApiOptions>>();

src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Reflection;
5-
using System.Text.Json;
65
using Microsoft.AspNetCore.Builder;
7-
using Microsoft.AspNetCore.Http;
8-
using Microsoft.AspNetCore.Http.Json;
96
using Microsoft.AspNetCore.Mvc;
107
using Microsoft.AspNetCore.Mvc.Abstractions;
118
using Microsoft.AspNetCore.Mvc.ActionConstraints;
@@ -18,7 +15,6 @@
1815
using Microsoft.AspNetCore.Routing.Constraints;
1916
using Microsoft.Extensions.DependencyInjection;
2017
using Microsoft.Extensions.Options;
21-
using Microsoft.OpenApi.Extensions;
2218
using Microsoft.OpenApi.Models;
2319
using Moq;
2420
using static Microsoft.AspNetCore.OpenApi.Tests.OpenApiOperationGeneratorTests;
@@ -217,27 +213,38 @@ private class TestServiceProvider : IServiceProvider, IKeyedServiceProvider
217213
public static TestServiceProvider Instance { get; } = new TestServiceProvider();
218214
private IKeyedServiceProvider _serviceProvider;
219215
internal OpenApiDocumentService TestDocumentService { get; set; }
220-
internal OpenApiComponentService TestComponentService { get; set; } = new OpenApiComponentService(Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()));
216+
internal OpenApiSchemaStore TestSchemaStoreService { get; } = new OpenApiSchemaStore();
217+
private OpenApiSchemaService _testSchemaService;
221218

222219
public void SetInternalServiceProvider(IServiceCollection serviceCollection)
223220
{
221+
serviceCollection.AddKeyedSingleton<OpenApiSchemaStore>("Test");
224222
_serviceProvider = serviceCollection.BuildServiceProvider();
223+
_testSchemaService = new OpenApiSchemaService(
224+
"Test",
225+
Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()),
226+
_serviceProvider
227+
);
225228
}
226229

227230
public object GetKeyedService(Type serviceType, object serviceKey)
228231
{
232+
229233
if (serviceType == typeof(OpenApiDocumentService))
230234
{
231235
return TestDocumentService;
232236
}
233-
if (serviceType == typeof(OpenApiComponentService))
237+
if (serviceType == typeof(OpenApiSchemaService))
234238
{
235-
return TestComponentService;
239+
return _testSchemaService;
236240
}
237-
238-
if (serviceType == typeof(OpenApiComponentService))
241+
if (serviceType == typeof(OpenApiSchemaService))
242+
{
243+
return _testSchemaService;
244+
}
245+
if (serviceType == typeof(OpenApiSchemaStore))
239246
{
240-
return TestComponentService;
247+
return TestSchemaStoreService;
241248
}
242249

243250
return _serviceProvider.GetKeyedService(serviceType, serviceKey);
@@ -249,14 +256,14 @@ public object GetRequiredKeyedService(Type serviceType, object serviceKey)
249256
{
250257
return TestDocumentService;
251258
}
252-
if (serviceType == typeof(OpenApiComponentService))
259+
if (serviceType == typeof(OpenApiSchemaService))
253260
{
254-
return TestComponentService;
261+
return _testSchemaService;
255262
}
256263

257-
if (serviceType == typeof(OpenApiComponentService))
264+
if (serviceType == typeof(OpenApiSchemaService))
258265
{
259-
return TestComponentService;
266+
return _testSchemaService;
260267
}
261268

262269
return _serviceProvider.GetRequiredKeyedService(serviceType, serviceKey);

0 commit comments

Comments
 (0)