Skip to content

Commit d9261d7

Browse files
committed
Injectable schema generators, remove building of intermediate service provider, remove ISchemaRepositoryAccessor, remove OAS caching (no measurable gain)
1 parent ebf4f10 commit d9261d7

15 files changed

+385
-388
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using JsonApiDotNetCore.Middleware;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Options;
5+
6+
namespace JsonApiDotNetCore.OpenApi;
7+
8+
internal sealed class ConfigureMvcOptions : IConfigureOptions<MvcOptions>
9+
{
10+
private readonly IControllerResourceMapping _controllerResourceMapping;
11+
private readonly IJsonApiRoutingConvention _jsonApiRoutingConvention;
12+
13+
public ConfigureMvcOptions(IControllerResourceMapping controllerResourceMapping, IJsonApiRoutingConvention jsonApiRoutingConvention)
14+
{
15+
ArgumentGuard.NotNull(controllerResourceMapping);
16+
ArgumentGuard.NotNull(jsonApiRoutingConvention);
17+
18+
_controllerResourceMapping = controllerResourceMapping;
19+
_jsonApiRoutingConvention = jsonApiRoutingConvention;
20+
}
21+
22+
public void Configure(MvcOptions options)
23+
{
24+
AddSwashbuckleCliCompatibility(options);
25+
AddOpenApiEndpointConvention(options);
26+
}
27+
28+
private void AddSwashbuckleCliCompatibility(MvcOptions options)
29+
{
30+
if (!options.Conventions.Any(convention => convention is IJsonApiRoutingConvention))
31+
{
32+
// See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1957 for why this is needed.
33+
options.Conventions.Insert(0, _jsonApiRoutingConvention);
34+
}
35+
}
36+
37+
private void AddOpenApiEndpointConvention(MvcOptions options)
38+
{
39+
var convention = new OpenApiEndpointConvention(_controllerResourceMapping);
40+
options.Conventions.Add(convention);
41+
}
42+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using System.Reflection;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Middleware;
4+
using JsonApiDotNetCore.OpenApi.SwaggerComponents;
5+
using Microsoft.AspNetCore.Mvc.ApiExplorer;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Options;
8+
using Swashbuckle.AspNetCore.SwaggerGen;
9+
10+
namespace JsonApiDotNetCore.OpenApi;
11+
12+
internal sealed class ConfigureSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
13+
{
14+
private readonly IControllerResourceMapping _controllerResourceMapping;
15+
private readonly JsonApiOperationIdSelector _operationIdSelector;
16+
private readonly JsonApiSchemaIdSelector _schemaIdSelector;
17+
18+
public ConfigureSwaggerGenOptions(IControllerResourceMapping controllerResourceMapping, JsonApiOperationIdSelector operationIdSelector,
19+
JsonApiSchemaIdSelector schemaIdSelector)
20+
{
21+
ArgumentGuard.NotNull(controllerResourceMapping);
22+
ArgumentGuard.NotNull(operationIdSelector);
23+
ArgumentGuard.NotNull(schemaIdSelector);
24+
25+
_controllerResourceMapping = controllerResourceMapping;
26+
_operationIdSelector = operationIdSelector;
27+
_schemaIdSelector = schemaIdSelector;
28+
}
29+
30+
public void Configure(SwaggerGenOptions options)
31+
{
32+
options.SupportNonNullableReferenceTypes();
33+
options.UseAllOfToExtendReferenceSchemas();
34+
35+
SetOperationInfo(options, _controllerResourceMapping);
36+
SetSchemaIdSelector(options);
37+
38+
options.DocumentFilter<EndpointOrderingFilter>();
39+
options.OperationFilter<JsonApiOperationDocumentationFilter>();
40+
}
41+
42+
private void SetOperationInfo(SwaggerGenOptions swaggerGenOptions, IControllerResourceMapping controllerResourceMapping)
43+
{
44+
swaggerGenOptions.TagActionsBy(description => GetOperationTags(description, controllerResourceMapping));
45+
swaggerGenOptions.CustomOperationIds(_operationIdSelector.GetOperationId);
46+
}
47+
48+
private static IList<string> GetOperationTags(ApiDescription description, IControllerResourceMapping controllerResourceMapping)
49+
{
50+
MethodInfo actionMethod = description.ActionDescriptor.GetActionMethod();
51+
ResourceType? resourceType = controllerResourceMapping.GetResourceTypeForController(actionMethod.ReflectedType);
52+
53+
if (resourceType == null)
54+
{
55+
throw new NotSupportedException("Only JsonApiDotNetCore endpoints are supported.");
56+
}
57+
58+
return [resourceType.PublicName];
59+
}
60+
61+
private void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions)
62+
{
63+
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.GetSchemaId);
64+
}
65+
}

src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@ internal sealed class JsonApiOperationIdSelector
3535
};
3636

3737
private readonly IControllerResourceMapping _controllerResourceMapping;
38-
private readonly JsonNamingPolicy? _namingPolicy;
38+
private readonly IJsonApiOptions _options;
3939

40-
public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceMapping, JsonNamingPolicy? namingPolicy)
40+
public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options)
4141
{
4242
ArgumentGuard.NotNull(controllerResourceMapping);
43+
ArgumentGuard.NotNull(options);
4344

4445
_controllerResourceMapping = controllerResourceMapping;
45-
_namingPolicy = namingPolicy;
46+
_options = options;
4647
}
4748

4849
public string GetOperationId(ApiDescription endpoint)
@@ -122,6 +123,7 @@ private string ApplyTemplate(string operationIdTemplate, ResourceType resourceTy
122123
// @formatter:wrap_before_first_method_call true restore
123124
// @formatter:wrap_chained_method_calls restore
124125

125-
return _namingPolicy != null ? _namingPolicy.ConvertName(pascalCaseOperationId) : pascalCaseOperationId;
126+
JsonNamingPolicy? namingPolicy = _options.SerializerOptions.PropertyNamingPolicy;
127+
return namingPolicy != null ? namingPolicy.ConvertName(pascalCaseOperationId) : pascalCaseOperationId;
126128
}
127129
}

src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,16 @@ internal sealed class JsonApiSchemaIdSelector
3838
[typeof(ResourceIdentifier<>)] = "[ResourceName] Identifier"
3939
};
4040

41-
private readonly JsonNamingPolicy? _namingPolicy;
4241
private readonly IResourceGraph _resourceGraph;
42+
private readonly IJsonApiOptions _options;
4343

44-
public JsonApiSchemaIdSelector(JsonNamingPolicy? namingPolicy, IResourceGraph resourceGraph)
44+
public JsonApiSchemaIdSelector(IResourceGraph resourceGraph, IJsonApiOptions options)
4545
{
4646
ArgumentGuard.NotNull(resourceGraph);
47+
ArgumentGuard.NotNull(options);
4748

48-
_namingPolicy = namingPolicy;
4949
_resourceGraph = resourceGraph;
50+
_options = options;
5051
}
5152

5253
public string GetSchemaId(Type type)
@@ -60,23 +61,19 @@ public string GetSchemaId(Type type)
6061
return resourceType.PublicName.Singularize();
6162
}
6263

64+
JsonNamingPolicy? namingPolicy = _options.SerializerOptions.PropertyNamingPolicy;
65+
6366
if (type.IsConstructedGenericType && OpenTypeToSchemaTemplateMap.ContainsKey(type.GetGenericTypeDefinition()))
6467
{
6568
Type openType = type.GetGenericTypeDefinition();
6669
Type resourceClrType = type.GetGenericArguments().First();
67-
resourceType = _resourceGraph.FindResourceType(resourceClrType);
68-
69-
if (resourceType == null)
70-
{
71-
throw new UnreachableCodeException();
72-
}
70+
resourceType = _resourceGraph.GetResourceType(resourceClrType);
7371

7472
string pascalCaseSchemaId = OpenTypeToSchemaTemplateMap[openType].Replace("[ResourceName]", resourceType.PublicName.Singularize()).ToPascalCase();
75-
76-
return _namingPolicy != null ? _namingPolicy.ConvertName(pascalCaseSchemaId) : pascalCaseSchemaId;
73+
return namingPolicy != null ? namingPolicy.ConvertName(pascalCaseSchemaId) : pascalCaseSchemaId;
7774
}
7875

7976
// Used for a fixed set of types, such as JsonApiObject, LinksInResourceCollectionDocument etc.
80-
return _namingPolicy != null ? _namingPolicy.ConvertName(type.Name) : type.Name;
77+
return namingPolicy != null ? namingPolicy.ConvertName(type.Name) : type.Name;
8178
}
8279
}

src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,15 @@ namespace JsonApiDotNetCore.OpenApi;
99

1010
internal sealed class ResourceFieldValidationMetadataProvider
1111
{
12-
private readonly bool _validateModelState;
13-
private readonly NullabilityInfoContext _nullabilityContext = new();
12+
private readonly IJsonApiOptions _options;
1413
private readonly IModelMetadataProvider _modelMetadataProvider;
1514

1615
public ResourceFieldValidationMetadataProvider(IJsonApiOptions options, IModelMetadataProvider modelMetadataProvider)
1716
{
1817
ArgumentGuard.NotNull(options);
1918
ArgumentGuard.NotNull(modelMetadataProvider);
2019

21-
_validateModelState = options.ValidateModelState;
20+
_options = options;
2221
_modelMetadataProvider = modelMetadataProvider;
2322
}
2423

@@ -33,12 +32,13 @@ public bool IsNullable(ResourceFieldAttribute field)
3332

3433
bool hasRequiredAttribute = field.Property.HasAttribute<RequiredAttribute>();
3534

36-
if (_validateModelState && hasRequiredAttribute)
35+
if (_options.ValidateModelState && hasRequiredAttribute)
3736
{
3837
return false;
3938
}
4039

41-
NullabilityInfo nullabilityInfo = _nullabilityContext.Create(field.Property);
40+
NullabilityInfoContext nullabilityContext = new();
41+
NullabilityInfo nullabilityInfo = nullabilityContext.Create(field.Property);
4242
return nullabilityInfo.ReadState != NullabilityState.NotNull;
4343
}
4444

@@ -48,7 +48,7 @@ public bool IsRequired(ResourceFieldAttribute field)
4848

4949
bool hasRequiredAttribute = field.Property.HasAttribute<RequiredAttribute>();
5050

51-
if (!_validateModelState)
51+
if (!_options.ValidateModelState)
5252
{
5353
return hasRequiredAttribute;
5454
}
@@ -58,7 +58,8 @@ public bool IsRequired(ResourceFieldAttribute field)
5858
return false;
5959
}
6060

61-
NullabilityInfo nullabilityInfo = _nullabilityContext.Create(field.Property);
61+
NullabilityInfoContext nullabilityContext = new();
62+
NullabilityInfo nullabilityInfo = nullabilityContext.Create(field.Property);
6263
bool isRequiredValueType = field.Property.PropertyType.IsValueType && hasRequiredAttribute && nullabilityInfo.ReadState == NullabilityState.NotNull;
6364

6465
if (isRequiredValueType)
Lines changed: 26 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
using System.Reflection;
2-
using System.Text.Json;
3-
using JsonApiDotNetCore.Configuration;
41
using JsonApiDotNetCore.Middleware;
52
using JsonApiDotNetCore.OpenApi.SwaggerComponents;
3+
using Microsoft.AspNetCore.Mvc;
64
using Microsoft.AspNetCore.Mvc.ApiExplorer;
75
using Microsoft.AspNetCore.Mvc.Infrastructure;
86
using Microsoft.Extensions.DependencyInjection;
97
using Microsoft.Extensions.DependencyInjection.Extensions;
10-
using Swashbuckle.AspNetCore.Swagger;
8+
using Microsoft.Extensions.Options;
119
using Swashbuckle.AspNetCore.SwaggerGen;
1210

1311
namespace JsonApiDotNetCore.OpenApi;
@@ -23,14 +21,15 @@ public static void AddOpenApi(this IServiceCollection services, IMvcCoreBuilder
2321
ArgumentGuard.NotNull(mvcBuilder);
2422

2523
AddCustomApiExplorer(services, mvcBuilder);
26-
2724
AddCustomSwaggerComponents(services);
25+
AddSwaggerGenerator(services);
26+
27+
services.AddTransient<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>();
2828

29-
using ServiceProvider provider = services.BuildServiceProvider();
30-
using IServiceScope scope = provider.CreateScope();
31-
AddSwaggerGenerator(scope, services, setupSwaggerGenAction);
32-
AddSwashbuckleCliCompatibility(scope, mvcBuilder);
33-
AddOpenApiEndpointConvention(scope, mvcBuilder);
29+
if (setupSwaggerGenAction != null)
30+
{
31+
services.Configure(setupSwaggerGenAction);
32+
}
3433
}
3534

3635
private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBuilder mvcBuilder)
@@ -44,96 +43,41 @@ private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBu
4443
var apiDescriptionProviders = provider.GetRequiredService<IEnumerable<IApiDescriptionProvider>>();
4544
var resourceFieldValidationMetadataProvider = provider.GetRequiredService<ResourceFieldValidationMetadataProvider>();
4645

47-
JsonApiActionDescriptorCollectionProvider descriptorCollectionProviderWrapper =
46+
JsonApiActionDescriptorCollectionProvider jsonApiActionDescriptorCollectionProvider =
4847
new(controllerResourceMapping, actionDescriptorCollectionProvider, resourceFieldValidationMetadataProvider);
4948

50-
return new ApiDescriptionGroupCollectionProvider(descriptorCollectionProviderWrapper, apiDescriptionProviders);
49+
return new ApiDescriptionGroupCollectionProvider(jsonApiActionDescriptorCollectionProvider, apiDescriptionProviders);
5150
});
5251

5352
mvcBuilder.AddApiExplorer();
5453

5554
mvcBuilder.AddMvcOptions(options => options.InputFormatters.Add(new JsonApiRequestFormatMetadataProvider()));
5655
}
5756

58-
private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection services, Action<SwaggerGenOptions>? setupSwaggerGenAction)
59-
{
60-
var controllerResourceMapping = scope.ServiceProvider.GetRequiredService<IControllerResourceMapping>();
61-
var resourceGraph = scope.ServiceProvider.GetRequiredService<IResourceGraph>();
62-
var jsonApiOptions = scope.ServiceProvider.GetRequiredService<IJsonApiOptions>();
63-
JsonNamingPolicy? namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy;
64-
65-
AddSchemaGenerator(services);
66-
67-
services.AddSwaggerGen(swaggerGenOptions =>
68-
{
69-
swaggerGenOptions.SupportNonNullableReferenceTypes();
70-
SetOperationInfo(swaggerGenOptions, controllerResourceMapping, namingPolicy);
71-
SetSchemaIdSelector(swaggerGenOptions, resourceGraph, namingPolicy);
72-
swaggerGenOptions.DocumentFilter<EndpointOrderingFilter>();
73-
swaggerGenOptions.UseAllOfToExtendReferenceSchemas();
74-
swaggerGenOptions.OperationFilter<JsonApiOperationDocumentationFilter>();
75-
76-
setupSwaggerGenAction?.Invoke(swaggerGenOptions);
77-
});
78-
}
79-
80-
private static void AddSchemaGenerator(IServiceCollection services)
81-
{
82-
services.TryAddSingleton<SchemaGenerator>();
83-
services.TryAddSingleton<ISchemaGenerator, JsonApiSchemaGenerator>();
84-
}
85-
86-
private static void SetOperationInfo(SwaggerGenOptions swaggerGenOptions, IControllerResourceMapping controllerResourceMapping,
87-
JsonNamingPolicy? namingPolicy)
88-
{
89-
swaggerGenOptions.TagActionsBy(description => GetOperationTags(description, controllerResourceMapping));
90-
91-
JsonApiOperationIdSelector jsonApiOperationIdSelector = new(controllerResourceMapping, namingPolicy);
92-
swaggerGenOptions.CustomOperationIds(jsonApiOperationIdSelector.GetOperationId);
93-
}
94-
95-
private static IList<string> GetOperationTags(ApiDescription description, IControllerResourceMapping controllerResourceMapping)
96-
{
97-
MethodInfo actionMethod = description.ActionDescriptor.GetActionMethod();
98-
ResourceType? resourceType = controllerResourceMapping.GetResourceTypeForController(actionMethod.ReflectedType);
99-
100-
if (resourceType == null)
101-
{
102-
throw new NotSupportedException("Only JsonApiDotNetCore endpoints are supported.");
103-
}
104-
105-
return new[]
106-
{
107-
resourceType.PublicName
108-
};
109-
}
110-
111-
private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceGraph resourceGraph, JsonNamingPolicy? namingPolicy)
112-
{
113-
JsonApiSchemaIdSelector jsonApiSchemaIdSelector = new(namingPolicy, resourceGraph);
114-
115-
swaggerGenOptions.CustomSchemaIds(type => jsonApiSchemaIdSelector.GetSchemaId(type));
116-
}
117-
11857
private static void AddCustomSwaggerComponents(IServiceCollection services)
11958
{
120-
services.TryAddSingleton<SwaggerGenerator>();
121-
services.TryAddSingleton<ISwaggerProvider, CachingSwaggerProvider>();
122-
12359
services.TryAddSingleton<ISerializerDataContractResolver, JsonApiDataContractResolver>();
60+
services.TryAddSingleton<ResourceDocumentationReader>();
61+
services.TryAddSingleton<JsonApiOperationIdSelector>();
62+
services.TryAddSingleton<JsonApiSchemaIdSelector>();
12463
}
12564

126-
private static void AddSwashbuckleCliCompatibility(IServiceScope scope, IMvcCoreBuilder mvcBuilder)
65+
private static void AddSwaggerGenerator(IServiceCollection services)
12766
{
128-
// See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1957 for why this is needed.
129-
var routingConvention = scope.ServiceProvider.GetRequiredService<IJsonApiRoutingConvention>();
130-
mvcBuilder.AddMvcOptions(options => options.Conventions.Insert(0, routingConvention));
67+
AddSchemaGenerators(services);
68+
69+
services.AddSwaggerGen();
70+
services.AddSingleton<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerGenOptions>();
13171
}
13272

133-
private static void AddOpenApiEndpointConvention(IServiceScope scope, IMvcCoreBuilder mvcBuilder)
73+
private static void AddSchemaGenerators(IServiceCollection services)
13474
{
135-
var controllerResourceMapping = scope.ServiceProvider.GetRequiredService<IControllerResourceMapping>();
75+
services.TryAddSingleton<SchemaGenerator>();
76+
services.TryAddSingleton<ISchemaGenerator, JsonApiSchemaGenerator>();
13677

137-
mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention(controllerResourceMapping)));
78+
services.TryAddSingleton<DocumentSchemaGenerator>();
79+
services.TryAddSingleton<ResourceTypeSchemaGenerator>();
80+
services.TryAddSingleton<ResourceIdentifierSchemaGenerator>();
81+
services.TryAddSingleton<ResourceDataSchemaGenerator>();
13882
}
13983
}

0 commit comments

Comments
 (0)