diff --git a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsDocumentFilter.cs b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsDocumentFilter.cs index 3d883a81ab..8af6b04c08 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsDocumentFilter.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsDocumentFilter.cs @@ -2,47 +2,46 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +public class AnnotationsDocumentFilter : IDocumentFilter { - public class AnnotationsDocumentFilter : IDocumentFilter + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { - public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) - { - if (swaggerDoc.Tags == null) - swaggerDoc.Tags = new List(); + if (swaggerDoc.Tags == null) + swaggerDoc.Tags = new List(); - // Collect (unique) controller names and custom attributes in a dictionary - var controllerNamesAndAttributes = context.ApiDescriptions - .Select(apiDesc => apiDesc.ActionDescriptor as ControllerActionDescriptor) - .Where(actionDesc => actionDesc != null) - .GroupBy(actionDesc => actionDesc.ControllerName) - .Select(group => new KeyValuePair>(group.Key, group.First().ControllerTypeInfo.GetCustomAttributes(true))); + // Collect (unique) controller names and custom attributes in a dictionary + var controllerNamesAndAttributes = context.ApiDescriptions + .Select(apiDesc => apiDesc.ActionDescriptor as ControllerActionDescriptor) + .Where(actionDesc => actionDesc != null) + .GroupBy(actionDesc => actionDesc.ControllerName) + .Select(group => new KeyValuePair>(group.Key, group.First().ControllerTypeInfo.GetCustomAttributes(true))); - foreach (var entry in controllerNamesAndAttributes) - { - ApplySwaggerTagAttribute(swaggerDoc, entry.Key, entry.Value); - } + foreach (var entry in controllerNamesAndAttributes) + { + ApplySwaggerTagAttribute(swaggerDoc, entry.Key, entry.Value); } + } - private void ApplySwaggerTagAttribute( - OpenApiDocument swaggerDoc, - string controllerName, - IEnumerable customAttributes) - { - var swaggerTagAttribute = customAttributes - .OfType() - .FirstOrDefault(); + private void ApplySwaggerTagAttribute( + OpenApiDocument swaggerDoc, + string controllerName, + IEnumerable customAttributes) + { + var swaggerTagAttribute = customAttributes + .OfType() + .FirstOrDefault(); - if (swaggerTagAttribute == null) return; + if (swaggerTagAttribute == null) return; - swaggerDoc.Tags.Add(new OpenApiTag - { - Name = controllerName, - Description = swaggerTagAttribute.Description, - ExternalDocs = (swaggerTagAttribute.ExternalDocsUrl != null) - ? new OpenApiExternalDocs { Url = new Uri(swaggerTagAttribute.ExternalDocsUrl) } - : null - }); - } + swaggerDoc.Tags.Add(new OpenApiTag + { + Name = controllerName, + Description = swaggerTagAttribute.Description, + ExternalDocs = (swaggerTagAttribute.ExternalDocsUrl != null) + ? new OpenApiExternalDocs { Url = new Uri(swaggerTagAttribute.ExternalDocsUrl) } + : null + }); } } diff --git a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsOperationFilter.cs b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsOperationFilter.cs index 9aee151d82..b0bded559f 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsOperationFilter.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsOperationFilter.cs @@ -1,123 +1,122 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +public class AnnotationsOperationFilter : IOperationFilter { - public class AnnotationsOperationFilter : IOperationFilter + public void Apply(OpenApiOperation operation, OperationFilterContext context) { - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - IEnumerable controllerAttributes = []; - IEnumerable actionAttributes = []; - IEnumerable metadataAttributes = []; + IEnumerable controllerAttributes = []; + IEnumerable actionAttributes = []; + IEnumerable metadataAttributes = []; - if (context.MethodInfo != null) - { - controllerAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true); - actionAttributes = context.MethodInfo.GetCustomAttributes(true); - } + if (context.MethodInfo != null) + { + controllerAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true); + actionAttributes = context.MethodInfo.GetCustomAttributes(true); + } -#if NET6_0_OR_GREATER - if (context.ApiDescription?.ActionDescriptor?.EndpointMetadata != null) - { - metadataAttributes = context.ApiDescription.ActionDescriptor.EndpointMetadata; - } +#if NET + if (context.ApiDescription?.ActionDescriptor?.EndpointMetadata != null) + { + metadataAttributes = context.ApiDescription.ActionDescriptor.EndpointMetadata; + } #endif - // NOTE: When controller and action attributes are applicable, action attributes should take priority. - // Hence, why they're at the end of the list (i.e. last one wins). - // Distinct() is applied due to an ASP.NET Core issue: https://github.com/dotnet/aspnetcore/issues/34199. - var allAttributes = controllerAttributes - .Union(actionAttributes) - .Union(metadataAttributes) - .Distinct(); - - var actionAndEndpointAttributes = actionAttributes - .Union(metadataAttributes) - .Distinct(); - - ApplySwaggerOperationAttribute(operation, actionAndEndpointAttributes); - ApplySwaggerOperationFilterAttributes(operation, context, allAttributes); - ApplySwaggerResponseAttributes(operation, context, allAttributes); - } + // NOTE: When controller and action attributes are applicable, action attributes should take priority. + // Hence, why they're at the end of the list (i.e. last one wins). + // Distinct() is applied due to an ASP.NET Core issue: https://github.com/dotnet/aspnetcore/issues/34199. + var allAttributes = controllerAttributes + .Union(actionAttributes) + .Union(metadataAttributes) + .Distinct(); + + var actionAndEndpointAttributes = actionAttributes + .Union(metadataAttributes) + .Distinct(); + + ApplySwaggerOperationAttribute(operation, actionAndEndpointAttributes); + ApplySwaggerOperationFilterAttributes(operation, context, allAttributes); + ApplySwaggerResponseAttributes(operation, context, allAttributes); + } - private static void ApplySwaggerOperationAttribute( - OpenApiOperation operation, - IEnumerable actionAttributes) - { - var swaggerOperationAttribute = actionAttributes - .OfType() - .FirstOrDefault(); + private static void ApplySwaggerOperationAttribute( + OpenApiOperation operation, + IEnumerable actionAttributes) + { + var swaggerOperationAttribute = actionAttributes + .OfType() + .FirstOrDefault(); - if (swaggerOperationAttribute == null) return; + if (swaggerOperationAttribute == null) return; - if (swaggerOperationAttribute.Summary != null) - operation.Summary = swaggerOperationAttribute.Summary; + if (swaggerOperationAttribute.Summary != null) + operation.Summary = swaggerOperationAttribute.Summary; - if (swaggerOperationAttribute.Description != null) - operation.Description = swaggerOperationAttribute.Description; + if (swaggerOperationAttribute.Description != null) + operation.Description = swaggerOperationAttribute.Description; - if (swaggerOperationAttribute.OperationId != null) - operation.OperationId = swaggerOperationAttribute.OperationId; + if (swaggerOperationAttribute.OperationId != null) + operation.OperationId = swaggerOperationAttribute.OperationId; - if (swaggerOperationAttribute.Tags != null) - { - operation.Tags = [.. swaggerOperationAttribute.Tags.Select(tagName => new OpenApiTag { Name = tagName })]; - } + if (swaggerOperationAttribute.Tags != null) + { + operation.Tags = [.. swaggerOperationAttribute.Tags.Select(tagName => new OpenApiTag { Name = tagName })]; } + } - public static void ApplySwaggerOperationFilterAttributes( - OpenApiOperation operation, - OperationFilterContext context, - IEnumerable controllerAndActionAttributes) - { - var swaggerOperationFilterAttributes = controllerAndActionAttributes - .OfType(); + public static void ApplySwaggerOperationFilterAttributes( + OpenApiOperation operation, + OperationFilterContext context, + IEnumerable controllerAndActionAttributes) + { + var swaggerOperationFilterAttributes = controllerAndActionAttributes + .OfType(); - foreach (var swaggerOperationFilterAttribute in swaggerOperationFilterAttributes) - { - var filter = (IOperationFilter)Activator.CreateInstance(swaggerOperationFilterAttribute.FilterType); - filter.Apply(operation, context); - } + foreach (var swaggerOperationFilterAttribute in swaggerOperationFilterAttributes) + { + var filter = (IOperationFilter)Activator.CreateInstance(swaggerOperationFilterAttribute.FilterType); + filter.Apply(operation, context); } + } - private static void ApplySwaggerResponseAttributes( - OpenApiOperation operation, - OperationFilterContext context, - IEnumerable controllerAndActionAttributes) + private static void ApplySwaggerResponseAttributes( + OpenApiOperation operation, + OperationFilterContext context, + IEnumerable controllerAndActionAttributes) + { + var swaggerResponseAttributes = controllerAndActionAttributes.OfType(); + + foreach (var swaggerResponseAttribute in swaggerResponseAttributes) { - var swaggerResponseAttributes = controllerAndActionAttributes.OfType(); + var statusCode = swaggerResponseAttribute.StatusCode.ToString(); - foreach (var swaggerResponseAttribute in swaggerResponseAttributes) - { - var statusCode = swaggerResponseAttribute.StatusCode.ToString(); + operation.Responses ??= []; - operation.Responses ??= []; + if (!operation.Responses.TryGetValue(statusCode, out OpenApiResponse response)) + { + response = new OpenApiResponse(); + } - if (!operation.Responses.TryGetValue(statusCode, out OpenApiResponse response)) - { - response = new OpenApiResponse(); - } + if (swaggerResponseAttribute.Description != null) + { + response.Description = swaggerResponseAttribute.Description; + } - if (swaggerResponseAttribute.Description != null) - { - response.Description = swaggerResponseAttribute.Description; - } + operation.Responses[statusCode] = response; - operation.Responses[statusCode] = response; + if (swaggerResponseAttribute.ContentTypes != null) + { + response.Content.Clear(); - if (swaggerResponseAttribute.ContentTypes != null) + foreach (var contentType in swaggerResponseAttribute.ContentTypes) { - response.Content.Clear(); - - foreach (var contentType in swaggerResponseAttribute.ContentTypes) - { - var schema = (swaggerResponseAttribute.Type != null && swaggerResponseAttribute.Type != typeof(void)) - ? context.SchemaGenerator.GenerateSchema(swaggerResponseAttribute.Type, context.SchemaRepository) - : null; + var schema = (swaggerResponseAttribute.Type != null && swaggerResponseAttribute.Type != typeof(void)) + ? context.SchemaGenerator.GenerateSchema(swaggerResponseAttribute.Type, context.SchemaRepository) + : null; - response.Content.Add(contentType, new OpenApiMediaType { Schema = schema }); - } + response.Content.Add(contentType, new OpenApiMediaType { Schema = schema }); } } } diff --git a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsParameterFilter.cs b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsParameterFilter.cs index 59e9394a24..e2741c5606 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsParameterFilter.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsParameterFilter.cs @@ -2,47 +2,46 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +public class AnnotationsParameterFilter : IParameterFilter { - public class AnnotationsParameterFilter : IParameterFilter + public void Apply(OpenApiParameter parameter, ParameterFilterContext context) { - public void Apply(OpenApiParameter parameter, ParameterFilterContext context) + if (context.PropertyInfo != null) { - if (context.PropertyInfo != null) - { - ApplyPropertyAnnotations(parameter, context.PropertyInfo); - } - else if (context.ParameterInfo != null) - { - ApplyParamAnnotations(parameter, context.ParameterInfo); - } + ApplyPropertyAnnotations(parameter, context.PropertyInfo); } - - private void ApplyPropertyAnnotations(OpenApiParameter parameter, PropertyInfo propertyInfo) + else if (context.ParameterInfo != null) { - var swaggerParameterAttribute = propertyInfo.GetCustomAttributes() - .FirstOrDefault(); - - if (swaggerParameterAttribute != null) - ApplySwaggerParameterAttribute(parameter, swaggerParameterAttribute); + ApplyParamAnnotations(parameter, context.ParameterInfo); } + } - private void ApplyParamAnnotations(OpenApiParameter parameter, ParameterInfo parameterInfo) - { + private void ApplyPropertyAnnotations(OpenApiParameter parameter, PropertyInfo propertyInfo) + { + var swaggerParameterAttribute = propertyInfo.GetCustomAttributes() + .FirstOrDefault(); - var swaggerParameterAttribute = parameterInfo.GetCustomAttribute(); + if (swaggerParameterAttribute != null) + ApplySwaggerParameterAttribute(parameter, swaggerParameterAttribute); + } - if (swaggerParameterAttribute != null) - ApplySwaggerParameterAttribute(parameter, swaggerParameterAttribute); - } + private void ApplyParamAnnotations(OpenApiParameter parameter, ParameterInfo parameterInfo) + { - private void ApplySwaggerParameterAttribute(OpenApiParameter parameter, SwaggerParameterAttribute swaggerParameterAttribute) - { - if (swaggerParameterAttribute.Description != null) - parameter.Description = swaggerParameterAttribute.Description; + var swaggerParameterAttribute = parameterInfo.GetCustomAttribute(); - if (swaggerParameterAttribute.RequiredFlag.HasValue) - parameter.Required = swaggerParameterAttribute.RequiredFlag.Value; - } + if (swaggerParameterAttribute != null) + ApplySwaggerParameterAttribute(parameter, swaggerParameterAttribute); + } + + private void ApplySwaggerParameterAttribute(OpenApiParameter parameter, SwaggerParameterAttribute swaggerParameterAttribute) + { + if (swaggerParameterAttribute.Description != null) + parameter.Description = swaggerParameterAttribute.Description; + + if (swaggerParameterAttribute.RequiredFlag.HasValue) + parameter.Required = swaggerParameterAttribute.RequiredFlag.Value; } } diff --git a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsRequestBodyFilter.cs b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsRequestBodyFilter.cs index d9d585efd8..89b7175f29 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsRequestBodyFilter.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsRequestBodyFilter.cs @@ -2,55 +2,54 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +public class AnnotationsRequestBodyFilter : IRequestBodyFilter { - public class AnnotationsRequestBodyFilter : IRequestBodyFilter + public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context) { - public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context) + var bodyParameterDescription = context.BodyParameterDescription; + + if (bodyParameterDescription == null) return; + + var propertyInfo = bodyParameterDescription.PropertyInfo(); + if (propertyInfo != null) { - var bodyParameterDescription = context.BodyParameterDescription; - - if (bodyParameterDescription == null) return; - - var propertyInfo = bodyParameterDescription.PropertyInfo(); - if (propertyInfo != null) - { - ApplyPropertyAnnotations(requestBody, propertyInfo); - return; - } - - var parameterInfo = bodyParameterDescription.ParameterInfo(); - if (parameterInfo != null) - { - ApplyParamAnnotations(requestBody, parameterInfo); - return; - } + ApplyPropertyAnnotations(requestBody, propertyInfo); + return; } - private void ApplyPropertyAnnotations(OpenApiRequestBody parameter, PropertyInfo propertyInfo) + var parameterInfo = bodyParameterDescription.ParameterInfo(); + if (parameterInfo != null) { - var swaggerRequestBodyAttribute = propertyInfo.GetCustomAttributes() - .FirstOrDefault(); - - if (swaggerRequestBodyAttribute != null) - ApplySwaggerRequestBodyAttribute(parameter, swaggerRequestBodyAttribute); + ApplyParamAnnotations(requestBody, parameterInfo); + return; } + } - private void ApplyParamAnnotations(OpenApiRequestBody requestBody, ParameterInfo parameterInfo) - { - var swaggerRequestBodyAttribute = parameterInfo.GetCustomAttribute(); + private void ApplyPropertyAnnotations(OpenApiRequestBody parameter, PropertyInfo propertyInfo) + { + var swaggerRequestBodyAttribute = propertyInfo.GetCustomAttributes() + .FirstOrDefault(); - if (swaggerRequestBodyAttribute != null) - ApplySwaggerRequestBodyAttribute(requestBody, swaggerRequestBodyAttribute); - } + if (swaggerRequestBodyAttribute != null) + ApplySwaggerRequestBodyAttribute(parameter, swaggerRequestBodyAttribute); + } - private void ApplySwaggerRequestBodyAttribute(OpenApiRequestBody parameter, SwaggerRequestBodyAttribute swaggerRequestBodyAttribute) - { - if (swaggerRequestBodyAttribute.Description != null) - parameter.Description = swaggerRequestBodyAttribute.Description; + private void ApplyParamAnnotations(OpenApiRequestBody requestBody, ParameterInfo parameterInfo) + { + var swaggerRequestBodyAttribute = parameterInfo.GetCustomAttribute(); - if (swaggerRequestBodyAttribute.RequiredFlag.HasValue) - parameter.Required = swaggerRequestBodyAttribute.RequiredFlag.Value; - } + if (swaggerRequestBodyAttribute != null) + ApplySwaggerRequestBodyAttribute(requestBody, swaggerRequestBodyAttribute); + } + + private void ApplySwaggerRequestBodyAttribute(OpenApiRequestBody parameter, SwaggerRequestBodyAttribute swaggerRequestBodyAttribute) + { + if (swaggerRequestBodyAttribute.Description != null) + parameter.Description = swaggerRequestBodyAttribute.Description; + + if (swaggerRequestBodyAttribute.RequiredFlag.HasValue) + parameter.Required = swaggerRequestBodyAttribute.RequiredFlag.Value; } } diff --git a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSchemaFilter.cs b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSchemaFilter.cs index 175926dc93..3aa24c5fad 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSchemaFilter.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSchemaFilter.cs @@ -3,96 +3,95 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +public class AnnotationsSchemaFilter : ISchemaFilter { - public class AnnotationsSchemaFilter : ISchemaFilter + private readonly IServiceProvider _serviceProvider; + + public AnnotationsSchemaFilter(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public void Apply(OpenApiSchema schema, SchemaFilterContext context) { - private readonly IServiceProvider _serviceProvider; + ApplyTypeAnnotations(schema, context); + + // NOTE: It's possible for both MemberInfo and ParameterInfo to have non-null values - i.e. when the schema is for a property + // within a class that is bound to a parameter. In this case, the MemberInfo should take precedence. - public AnnotationsSchemaFilter(IServiceProvider serviceProvider) + if (context.MemberInfo != null) { - _serviceProvider = serviceProvider; + ApplyMemberAnnotations(schema, context.MemberInfo); } - - public void Apply(OpenApiSchema schema, SchemaFilterContext context) + else if (context.ParameterInfo != null) { - ApplyTypeAnnotations(schema, context); - - // NOTE: It's possible for both MemberInfo and ParameterInfo to have non-null values - i.e. when the schema is for a property - // within a class that is bound to a parameter. In this case, the MemberInfo should take precedence. - - if (context.MemberInfo != null) - { - ApplyMemberAnnotations(schema, context.MemberInfo); - } - else if (context.ParameterInfo != null) - { - ApplyParamAnnotations(schema, context.ParameterInfo); - } + ApplyParamAnnotations(schema, context.ParameterInfo); } + } - private void ApplyTypeAnnotations(OpenApiSchema schema, SchemaFilterContext context) - { - var schemaAttribute = context.Type.GetCustomAttributes() - .FirstOrDefault(); + private void ApplyTypeAnnotations(OpenApiSchema schema, SchemaFilterContext context) + { + var schemaAttribute = context.Type.GetCustomAttributes() + .FirstOrDefault(); - if (schemaAttribute != null) - ApplySchemaAttribute(schema, schemaAttribute); + if (schemaAttribute != null) + ApplySchemaAttribute(schema, schemaAttribute); - var schemaFilterAttribute = context.Type.GetCustomAttributes() - .FirstOrDefault(); + var schemaFilterAttribute = context.Type.GetCustomAttributes() + .FirstOrDefault(); - if (schemaFilterAttribute != null) - { - var filter = (ISchemaFilter)ActivatorUtilities.CreateInstance( - _serviceProvider, - schemaFilterAttribute.Type, - schemaFilterAttribute.Arguments); + if (schemaFilterAttribute != null) + { + var filter = (ISchemaFilter)ActivatorUtilities.CreateInstance( + _serviceProvider, + schemaFilterAttribute.Type, + schemaFilterAttribute.Arguments); - filter.Apply(schema, context); - } + filter.Apply(schema, context); } + } - private void ApplyParamAnnotations(OpenApiSchema schema, ParameterInfo parameterInfo) - { - var schemaAttribute = parameterInfo.GetCustomAttributes() - .FirstOrDefault(); + private void ApplyParamAnnotations(OpenApiSchema schema, ParameterInfo parameterInfo) + { + var schemaAttribute = parameterInfo.GetCustomAttributes() + .FirstOrDefault(); - if (schemaAttribute != null) - ApplySchemaAttribute(schema, schemaAttribute); - } + if (schemaAttribute != null) + ApplySchemaAttribute(schema, schemaAttribute); + } - private void ApplyMemberAnnotations(OpenApiSchema schema, MemberInfo memberInfo) - { - var schemaAttribute = memberInfo.GetCustomAttributes() - .FirstOrDefault(); + private void ApplyMemberAnnotations(OpenApiSchema schema, MemberInfo memberInfo) + { + var schemaAttribute = memberInfo.GetCustomAttributes() + .FirstOrDefault(); - if (schemaAttribute != null) - ApplySchemaAttribute(schema, schemaAttribute); - } + if (schemaAttribute != null) + ApplySchemaAttribute(schema, schemaAttribute); + } - private void ApplySchemaAttribute(OpenApiSchema schema, SwaggerSchemaAttribute schemaAttribute) - { - if (schemaAttribute.Description != null) - schema.Description = schemaAttribute.Description; + private void ApplySchemaAttribute(OpenApiSchema schema, SwaggerSchemaAttribute schemaAttribute) + { + if (schemaAttribute.Description != null) + schema.Description = schemaAttribute.Description; - if (schemaAttribute.Format != null) - schema.Format = schemaAttribute.Format; + if (schemaAttribute.Format != null) + schema.Format = schemaAttribute.Format; - if (schemaAttribute.ReadOnlyFlag.HasValue) - schema.ReadOnly = schemaAttribute.ReadOnlyFlag.Value; + if (schemaAttribute.ReadOnlyFlag.HasValue) + schema.ReadOnly = schemaAttribute.ReadOnlyFlag.Value; - if (schemaAttribute.WriteOnlyFlag.HasValue) - schema.WriteOnly = schemaAttribute.WriteOnlyFlag.Value; + if (schemaAttribute.WriteOnlyFlag.HasValue) + schema.WriteOnly = schemaAttribute.WriteOnlyFlag.Value; - if (schemaAttribute.NullableFlag.HasValue) - schema.Nullable = schemaAttribute.NullableFlag.Value; + if (schemaAttribute.NullableFlag.HasValue) + schema.Nullable = schemaAttribute.NullableFlag.Value; - if (schemaAttribute.Required != null) - schema.Required = new SortedSet(schemaAttribute.Required); + if (schemaAttribute.Required != null) + schema.Required = new SortedSet(schemaAttribute.Required); - if (schemaAttribute.Title != null) - schema.Title = schemaAttribute.Title; - } + if (schemaAttribute.Title != null) + schema.Title = schemaAttribute.Title; } } diff --git a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSwaggerGenOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSwaggerGenOptionsExtensions.cs index cd5fdfe19f..ad2f222a2f 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSwaggerGenOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSwaggerGenOptionsExtensions.cs @@ -1,157 +1,156 @@ using System.Text.Json.Serialization; -using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.Annotations; +using Swashbuckle.AspNetCore.SwaggerGen; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class AnnotationsSwaggerGenOptionsExtensions { - public static class AnnotationsSwaggerGenOptionsExtensions + /// + /// Enables Swagger annotations (SwaggerOperationAttribute, SwaggerParameterAttribute etc.) + /// + /// + /// Enables SwaggerSubType attribute for inheritance + /// Enables SwaggerSubType and SwaggerDiscriminator attributes for polymorphism + public static void EnableAnnotations( + this SwaggerGenOptions options, + bool enableAnnotationsForInheritance, + bool enableAnnotationsForPolymorphism) { - /// - /// Enables Swagger annotations (SwaggerOperationAttribute, SwaggerParameterAttribute etc.) - /// - /// - /// Enables SwaggerSubType attribute for inheritance - /// Enables SwaggerSubType and SwaggerDiscriminator attributes for polymorphism - public static void EnableAnnotations( - this SwaggerGenOptions options, - bool enableAnnotationsForInheritance, - bool enableAnnotationsForPolymorphism) + options.SchemaFilter(); + options.ParameterFilter(); + options.RequestBodyFilter(); + options.OperationFilter(); + options.DocumentFilter(); + + if (enableAnnotationsForInheritance || enableAnnotationsForPolymorphism) { - options.SchemaFilter(); - options.ParameterFilter(); - options.RequestBodyFilter(); - options.OperationFilter(); - options.DocumentFilter(); + options.SelectSubTypesUsing(AnnotationsSubTypesSelector); + options.SelectDiscriminatorNameUsing(AnnotationsDiscriminatorNameSelector); + options.SelectDiscriminatorValueUsing(AnnotationsDiscriminatorValueSelector); - if (enableAnnotationsForInheritance || enableAnnotationsForPolymorphism) + if (enableAnnotationsForInheritance) { - options.SelectSubTypesUsing(AnnotationsSubTypesSelector); - options.SelectDiscriminatorNameUsing(AnnotationsDiscriminatorNameSelector); - options.SelectDiscriminatorValueUsing(AnnotationsDiscriminatorValueSelector); - - if (enableAnnotationsForInheritance) - { - options.UseAllOfForInheritance(); - } - - if (enableAnnotationsForPolymorphism) - { - options.UseOneOfForPolymorphism(); - } + options.UseAllOfForInheritance(); } - } - /// - /// Enables Swagger annotations (SwaggerOperationAttribute, SwaggerParameterAttribute etc.) - /// - /// - public static void EnableAnnotations(this SwaggerGenOptions options) - { - options.EnableAnnotations( - enableAnnotationsForPolymorphism: false, - enableAnnotationsForInheritance: false); + if (enableAnnotationsForPolymorphism) + { + options.UseOneOfForPolymorphism(); + } } + } - private static IEnumerable AnnotationsSubTypesSelector(Type type) - { - var subTypeAttributes = type.GetCustomAttributes(false) - .OfType(); + /// + /// Enables Swagger annotations (SwaggerOperationAttribute, SwaggerParameterAttribute etc.) + /// + /// + public static void EnableAnnotations(this SwaggerGenOptions options) + { + options.EnableAnnotations( + enableAnnotationsForPolymorphism: false, + enableAnnotationsForInheritance: false); + } - if (subTypeAttributes.Any()) - { - return subTypeAttributes.Select(attr => attr.SubType); - } + private static IEnumerable AnnotationsSubTypesSelector(Type type) + { + var subTypeAttributes = type.GetCustomAttributes(false) + .OfType(); + + if (subTypeAttributes.Any()) + { + return subTypeAttributes.Select(attr => attr.SubType); + } #pragma warning disable CS0618 // Type or member is obsolete - var obsoleteAttribute = type.GetCustomAttributes(false) - .OfType() - .FirstOrDefault(); + var obsoleteAttribute = type.GetCustomAttributes(false) + .OfType() + .FirstOrDefault(); #pragma warning restore CS0618 // Type or member is obsolete - if (obsoleteAttribute != null) - { - return obsoleteAttribute.SubTypes; - } + if (obsoleteAttribute != null) + { + return obsoleteAttribute.SubTypes; + } -#if NET7_0_OR_GREATER - var jsonDerivedTypeAttributes = type.GetCustomAttributes(false) - .OfType() - .ToList(); +#if NET + var jsonDerivedTypeAttributes = type.GetCustomAttributes(false) + .OfType() + .ToList(); - if (jsonDerivedTypeAttributes.Count > 0) - { - return jsonDerivedTypeAttributes.Select(attr => attr.DerivedType); - } + if (jsonDerivedTypeAttributes.Count > 0) + { + return jsonDerivedTypeAttributes.Select(attr => attr.DerivedType); + } #endif - return Enumerable.Empty(); - } + return Enumerable.Empty(); + } - private static string AnnotationsDiscriminatorNameSelector(Type baseType) - { - var discriminatorAttribute = baseType.GetCustomAttributes(false) - .OfType() - .FirstOrDefault(); + private static string AnnotationsDiscriminatorNameSelector(Type baseType) + { + var discriminatorAttribute = baseType.GetCustomAttributes(false) + .OfType() + .FirstOrDefault(); - if (discriminatorAttribute != null) - { - return discriminatorAttribute.PropertyName; - } + if (discriminatorAttribute != null) + { + return discriminatorAttribute.PropertyName; + } #pragma warning disable CS0618 // Type or member is obsolete - var obsoleteAttribute = baseType.GetCustomAttributes(false) - .OfType() - .FirstOrDefault(); + var obsoleteAttribute = baseType.GetCustomAttributes(false) + .OfType() + .FirstOrDefault(); #pragma warning restore CS0618 // Type or member is obsolete - if (obsoleteAttribute != null) - { - return obsoleteAttribute.Discriminator; - } + if (obsoleteAttribute != null) + { + return obsoleteAttribute.Discriminator; + } -#if NET7_0_OR_GREATER - var jsonPolymorphicAttributes = baseType.GetCustomAttributes(false) - .OfType() - .FirstOrDefault(); +#if NET + var jsonPolymorphicAttributes = baseType.GetCustomAttributes(false) + .OfType() + .FirstOrDefault(); - if (jsonPolymorphicAttributes != null) - { - return jsonPolymorphicAttributes.TypeDiscriminatorPropertyName; - } + if (jsonPolymorphicAttributes != null) + { + return jsonPolymorphicAttributes.TypeDiscriminatorPropertyName; + } #endif - return null; - } + return null; + } - private static string AnnotationsDiscriminatorValueSelector(Type subType) + private static string AnnotationsDiscriminatorValueSelector(Type subType) + { + var baseType = subType.BaseType; + while (baseType != null) { - var baseType = subType.BaseType; - while (baseType != null) + var subTypeAttribute = baseType.GetCustomAttributes(false) + .OfType() + .FirstOrDefault(attr => attr.SubType == subType); + + if (subTypeAttribute != null) { - var subTypeAttribute = baseType.GetCustomAttributes(false) - .OfType() - .FirstOrDefault(attr => attr.SubType == subType); - - if (subTypeAttribute != null) - { - return subTypeAttribute.DiscriminatorValue; - } - -#if NET7_0_OR_GREATER - var jsonDerivedTypeAttributes = baseType.GetCustomAttributes(false) - .OfType() - .FirstOrDefault(attr => attr.DerivedType == subType); - - if (jsonDerivedTypeAttributes is { TypeDiscriminator: string discriminator }) - { - return discriminator; - } -#endif + return subTypeAttribute.DiscriminatorValue; + } + +#if NET + var jsonDerivedTypeAttributes = baseType.GetCustomAttributes(false) + .OfType() + .FirstOrDefault(attr => attr.DerivedType == subType); - baseType = baseType.BaseType; + if (jsonDerivedTypeAttributes is { TypeDiscriminator: string discriminator }) + { + return discriminator; } +#endif - return null; + baseType = baseType.BaseType; } + + return null; } } diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerDiscriminatorAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerDiscriminatorAttribute.cs index c268a6f24d..b342e19310 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerDiscriminatorAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerDiscriminatorAttribute.cs @@ -1,13 +1,12 @@ -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, AllowMultiple = false)] +public class SwaggerDiscriminatorAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, AllowMultiple = false)] - public class SwaggerDiscriminatorAttribute : Attribute + public SwaggerDiscriminatorAttribute(string propertyName) { - public SwaggerDiscriminatorAttribute(string propertyName) - { - PropertyName = propertyName; - } - - public string PropertyName { get; set; } + PropertyName = propertyName; } -} \ No newline at end of file + + public string PropertyName { get; set; } +} diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerOperationAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerOperationAttribute.cs index 98df69251b..167931367f 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerOperationAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerOperationAttribute.cs @@ -1,39 +1,38 @@ -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +/// +/// Enriches Operation metadata for a given action method +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class SwaggerOperationAttribute : Attribute { - /// - /// Enriches Operation metadata for a given action method - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class SwaggerOperationAttribute : Attribute + public SwaggerOperationAttribute(string summary = null, string description = null) { - public SwaggerOperationAttribute(string summary = null, string description = null) - { - Summary = summary; - Description = description; - } + Summary = summary; + Description = description; + } - /// - /// A short summary of what the operation does. For maximum readability in the swagger-ui, - /// this field SHOULD be less than 120 characters. - /// - public string Summary { get; set; } + /// + /// A short summary of what the operation does. For maximum readability in the swagger-ui, + /// this field SHOULD be less than 120 characters. + /// + public string Summary { get; set; } - /// - /// A verbose explanation of the operation behavior. GFM syntax can be used for rich text representation. - /// - public string Description { get; set; } + /// + /// A verbose explanation of the operation behavior. GFM syntax can be used for rich text representation. + /// + public string Description { get; set; } - /// - /// Unique string used to identify the operation. The id MUST be unique among all operations described - /// in the API. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, - /// it is recommended to follow common programming naming conventions. - /// - public string OperationId { get; set; } + /// + /// Unique string used to identify the operation. The id MUST be unique among all operations described + /// in the API. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, + /// it is recommended to follow common programming naming conventions. + /// + public string OperationId { get; set; } - /// - /// A list of tags for API documentation control. Tags can be used for logical grouping of operations - /// by resources or any other qualifier. - /// - public string[] Tags { get; set; } - } -} \ No newline at end of file + /// + /// A list of tags for API documentation control. Tags can be used for logical grouping of operations + /// by resources or any other qualifier. + /// + public string[] Tags { get; set; } +} diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerOperationFilterAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerOperationFilterAttribute.cs index 7ed4e008bc..ebdab9374d 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerOperationFilterAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerOperationFilterAttribute.cs @@ -1,13 +1,12 @@ -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public class SwaggerOperationFilterAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] - public class SwaggerOperationFilterAttribute : Attribute + public SwaggerOperationFilterAttribute(Type filterType) { - public SwaggerOperationFilterAttribute(Type filterType) - { - FilterType = filterType; - } - - public Type FilterType { get; private set; } + FilterType = filterType; } -} \ No newline at end of file + + public Type FilterType { get; private set; } +} diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerParameterAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerParameterAttribute.cs index 1a1e3cab4b..b491a89c84 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerParameterAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerParameterAttribute.cs @@ -1,32 +1,31 @@ -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +/// +/// Enriches Parameter metadata for "path", "query" or "header" bound parameters or properties +/// +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false)] +public class SwaggerParameterAttribute : Attribute { - /// - /// Enriches Parameter metadata for "path", "query" or "header" bound parameters or properties - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false)] - public class SwaggerParameterAttribute : Attribute + public SwaggerParameterAttribute(string description = null) { - public SwaggerParameterAttribute(string description = null) - { - Description = description; - } - - /// - /// A brief description of the parameter. This could contain examples of use. - /// GFM syntax can be used for rich text representation - /// - public string Description { get; set; } + Description = description; + } - /// - /// Determines whether the parameter is mandatory. If the parameter is in "path", - /// it will be required by default as Swagger does not allow optional path parameters - /// - public bool Required - { - get { throw new InvalidOperationException($"Use {nameof(RequiredFlag)} instead"); } - set { RequiredFlag = value; } - } + /// + /// A brief description of the parameter. This could contain examples of use. + /// GFM syntax can be used for rich text representation + /// + public string Description { get; set; } - internal bool? RequiredFlag { get; set; } + /// + /// Determines whether the parameter is mandatory. If the parameter is in "path", + /// it will be required by default as Swagger does not allow optional path parameters + /// + public bool Required + { + get { throw new InvalidOperationException($"Use {nameof(RequiredFlag)} instead"); } + set { RequiredFlag = value; } } -} \ No newline at end of file + + internal bool? RequiredFlag { get; set; } +} diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerRequestBodyAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerRequestBodyAttribute.cs index 714eea1903..e221f7873c 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerRequestBodyAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerRequestBodyAttribute.cs @@ -1,32 +1,31 @@ -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +/// +/// Enriches RequestBody metadata for "body" bound parameters or properties +/// +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false)] +public class SwaggerRequestBodyAttribute : Attribute { - /// - /// Enriches RequestBody metadata for "body" bound parameters or properties - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false)] - public class SwaggerRequestBodyAttribute : Attribute + public SwaggerRequestBodyAttribute(string description = null) { - public SwaggerRequestBodyAttribute(string description = null) - { - Description = description; - } - - /// - /// A brief description of the requestBody. This could contain examples of use. - /// GFM syntax can be used for rich text representation - /// - public string Description { get; set; } + Description = description; + } - /// - /// Determines whether the requestBody is mandatory. If the parameter is in "path", - /// it will be required by default as Swagger does not allow optional path parameters - /// - public bool Required - { - get { throw new InvalidOperationException($"Use {nameof(RequiredFlag)} instead"); } - set { RequiredFlag = value; } - } + /// + /// A brief description of the requestBody. This could contain examples of use. + /// GFM syntax can be used for rich text representation + /// + public string Description { get; set; } - internal bool? RequiredFlag { get; set; } + /// + /// Determines whether the requestBody is mandatory. If the parameter is in "path", + /// it will be required by default as Swagger does not allow optional path parameters + /// + public bool Required + { + get { throw new InvalidOperationException($"Use {nameof(RequiredFlag)} instead"); } + set { RequiredFlag = value; } } -} \ No newline at end of file + + internal bool? RequiredFlag { get; set; } +} diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerResponseAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerResponseAttribute.cs index dd7e7c2e68..641f78e87e 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerResponseAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerResponseAttribute.cs @@ -1,34 +1,33 @@ using Microsoft.AspNetCore.Mvc; -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +/// +/// Adds or enriches Response metadata for a given action method +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +public class SwaggerResponseAttribute : ProducesResponseTypeAttribute { - /// - /// Adds or enriches Response metadata for a given action method - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class SwaggerResponseAttribute : ProducesResponseTypeAttribute + public SwaggerResponseAttribute(int statusCode, string description = null, Type type = null) + : base(type ?? typeof(void), statusCode) { - public SwaggerResponseAttribute(int statusCode, string description = null, Type type = null) - : base(type ?? typeof(void), statusCode) - { - Description = description; - } + Description = description; + } - public SwaggerResponseAttribute(int statusCode, string description = null, Type type = null, params string[] contentTypes) - : base(type ?? typeof(void), statusCode) - { - Description = description; - ContentTypes = contentTypes; - } + public SwaggerResponseAttribute(int statusCode, string description = null, Type type = null, params string[] contentTypes) + : base(type ?? typeof(void), statusCode) + { + Description = description; + ContentTypes = contentTypes; + } - /// - /// A short description of the response. GFM syntax can be used for rich text representation. - /// - public string Description { get; set; } + /// + /// A short description of the response. GFM syntax can be used for rich text representation. + /// + public string Description { get; set; } - /// - /// A collection of MIME types that the response can be produced with. - /// - public string[] ContentTypes { get; set; } - } + /// + /// A collection of MIME types that the response can be produced with. + /// + public string[] ContentTypes { get; set; } } diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerSchemaAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerSchemaAttribute.cs index f4f0e5bcab..3ca1ec53f2 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerSchemaAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerSchemaAttribute.cs @@ -1,49 +1,48 @@ -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +[AttributeUsage( + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Parameter | + AttributeTargets.Property | + AttributeTargets.Enum, + AllowMultiple = false)] +public class SwaggerSchemaAttribute : Attribute { - [AttributeUsage( - AttributeTargets.Class | - AttributeTargets.Struct | - AttributeTargets.Parameter | - AttributeTargets.Property | - AttributeTargets.Enum, - AllowMultiple = false)] - public class SwaggerSchemaAttribute : Attribute + public SwaggerSchemaAttribute(string description = null) { - public SwaggerSchemaAttribute(string description = null) - { - Description = description; - } + Description = description; + } - public string Description { get; set; } + public string Description { get; set; } - public string Format { get; set; } + public string Format { get; set; } - public bool ReadOnly - { - get { throw new InvalidOperationException($"Use {nameof(ReadOnlyFlag)} instead"); } - set { ReadOnlyFlag = value; } - } + public bool ReadOnly + { + get { throw new InvalidOperationException($"Use {nameof(ReadOnlyFlag)} instead"); } + set { ReadOnlyFlag = value; } + } - public bool WriteOnly - { - get { throw new InvalidOperationException($"Use {nameof(WriteOnlyFlag)} instead"); } - set { WriteOnlyFlag = value; } - } + public bool WriteOnly + { + get { throw new InvalidOperationException($"Use {nameof(WriteOnlyFlag)} instead"); } + set { WriteOnlyFlag = value; } + } - public bool Nullable - { - get { throw new InvalidOperationException($"Use {nameof(NullableFlag)} instead"); } - set { NullableFlag = value; } - } + public bool Nullable + { + get { throw new InvalidOperationException($"Use {nameof(NullableFlag)} instead"); } + set { NullableFlag = value; } + } - public string[] Required { get; set; } + public string[] Required { get; set; } - public string Title { get; set; } + public string Title { get; set; } - internal bool? ReadOnlyFlag { get; private set; } + internal bool? ReadOnlyFlag { get; private set; } - internal bool? WriteOnlyFlag { get; private set; } + internal bool? WriteOnlyFlag { get; private set; } - internal bool? NullableFlag { get; private set; } - } + internal bool? NullableFlag { get; private set; } } diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerSchemaFilterAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerSchemaFilterAttribute.cs index 91e13db747..de31dd25d6 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerSchemaFilterAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerSchemaFilterAttribute.cs @@ -1,20 +1,19 @@ -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +[AttributeUsage( + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Enum, + AllowMultiple = false)] +public class SwaggerSchemaFilterAttribute : Attribute { - [AttributeUsage( - AttributeTargets.Class | - AttributeTargets.Struct | - AttributeTargets.Enum, - AllowMultiple = false)] - public class SwaggerSchemaFilterAttribute : Attribute + public SwaggerSchemaFilterAttribute(Type type) { - public SwaggerSchemaFilterAttribute(Type type) - { - Type = type; - Arguments = new object[]{ }; - } + Type = type; + Arguments = new object[]{ }; + } - public Type Type { get; private set; } + public Type Type { get; private set; } - public object[] Arguments { get; set; } - } -} \ No newline at end of file + public object[] Arguments { get; set; } +} diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerSubTypeAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerSubTypeAttribute.cs index 1e93ab328d..b5689344d2 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerSubTypeAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerSubTypeAttribute.cs @@ -1,15 +1,14 @@ -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, AllowMultiple = true)] +public class SwaggerSubTypeAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, AllowMultiple = true)] - public class SwaggerSubTypeAttribute : Attribute + public SwaggerSubTypeAttribute(Type subType) { - public SwaggerSubTypeAttribute(Type subType) - { - SubType = subType; - } + SubType = subType; + } - public Type SubType { get; set; } + public Type SubType { get; set; } - public string DiscriminatorValue { get; set; } - } -} \ No newline at end of file + public string DiscriminatorValue { get; set; } +} diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerSubTypesAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerSubTypesAttribute.cs index be55a5f348..e45eae32e4 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerSubTypesAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerSubTypesAttribute.cs @@ -1,16 +1,15 @@ -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +[Obsolete("Use multiple SwaggerSubType attributes instead")] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, AllowMultiple = false)] +public class SwaggerSubTypesAttribute : Attribute { - [Obsolete("Use multiple SwaggerSubType attributes instead")] - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, AllowMultiple = false)] - public class SwaggerSubTypesAttribute : Attribute + public SwaggerSubTypesAttribute(params Type[] subTypes) { - public SwaggerSubTypesAttribute(params Type[] subTypes) - { - SubTypes = subTypes; - } + SubTypes = subTypes; + } - public IEnumerable SubTypes { get; } + public IEnumerable SubTypes { get; } - public string Discriminator { get; set; } - } -} \ No newline at end of file + public string Discriminator { get; set; } +} diff --git a/src/Swashbuckle.AspNetCore.Annotations/SwaggerTagAttribute.cs b/src/Swashbuckle.AspNetCore.Annotations/SwaggerTagAttribute.cs index fd0a10b37a..02bf644706 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/SwaggerTagAttribute.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/SwaggerTagAttribute.cs @@ -1,29 +1,28 @@ -namespace Swashbuckle.AspNetCore.Annotations +namespace Swashbuckle.AspNetCore.Annotations; + +/// +/// Adds Tag metadata for a given controller (i.e. the controller name tag) +/// +/// +/// Don't use this attribute if you're tagging Operations with something other than controller name +/// e.g. if you're customizing the tagging behavior with TagActionsBy. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public class SwaggerTagAttribute : Attribute { - /// - /// Adds Tag metadata for a given controller (i.e. the controller name tag) - /// - /// - /// Don't use this attribute if you're tagging Operations with something other than controller name - /// e.g. if you're customizing the tagging behavior with TagActionsBy. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class SwaggerTagAttribute : Attribute + public SwaggerTagAttribute(string description = null, string externalDocsUrl = null) { - public SwaggerTagAttribute(string description = null, string externalDocsUrl = null) - { - Description = description; - ExternalDocsUrl = externalDocsUrl; - } + Description = description; + ExternalDocsUrl = externalDocsUrl; + } - /// - /// A short description for the tag. GFM syntax can be used for rich text representation. - /// - public string Description { get; } + /// + /// A short description for the tag. GFM syntax can be used for rich text representation. + /// + public string Description { get; } - /// - /// A URL for additional external documentation. Value MUST be in the format of a URL. - /// - public string ExternalDocsUrl { get; } - } -} \ No newline at end of file + /// + /// A URL for additional external documentation. Value MUST be in the format of a URL. + /// + public string ExternalDocsUrl { get; } +} diff --git a/src/Swashbuckle.AspNetCore.ApiTesting.Xunit/ApiTestFixture.cs b/src/Swashbuckle.AspNetCore.ApiTesting.Xunit/ApiTestFixture.cs index cc27276026..1d1bbc7012 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting.Xunit/ApiTestFixture.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting.Xunit/ApiTestFixture.cs @@ -2,39 +2,38 @@ using Microsoft.OpenApi.Models; using Xunit; -namespace Swashbuckle.AspNetCore.ApiTesting.Xunit +namespace Swashbuckle.AspNetCore.ApiTesting.Xunit; + +[Collection("ApiTests")] +public class ApiTestFixture : + IClassFixture> where TEntryPoint : class { - [Collection("ApiTests")] - public class ApiTestFixture : - IClassFixture> where TEntryPoint : class - { - private readonly ApiTestRunnerBase _apiTestRunner; - private readonly WebApplicationFactory _webAppFactory; - private readonly string _documentName; + private readonly ApiTestRunnerBase _apiTestRunner; + private readonly WebApplicationFactory _webAppFactory; + private readonly string _documentName; - public ApiTestFixture( - ApiTestRunnerBase apiTestRunner, - WebApplicationFactory webAppFactory, - string documentName) - { - _apiTestRunner = apiTestRunner; - _webAppFactory = webAppFactory; - _documentName = documentName; - } + public ApiTestFixture( + ApiTestRunnerBase apiTestRunner, + WebApplicationFactory webAppFactory, + string documentName) + { + _apiTestRunner = apiTestRunner; + _webAppFactory = webAppFactory; + _documentName = documentName; + } - public void Describe(string pathTemplate, OperationType operationType, OpenApiOperation operationSpec) - { - _apiTestRunner.ConfigureOperation(_documentName, pathTemplate, operationType, operationSpec); - } + public void Describe(string pathTemplate, OperationType operationType, OpenApiOperation operationSpec) + { + _apiTestRunner.ConfigureOperation(_documentName, pathTemplate, operationType, operationSpec); + } - public async Task TestAsync(string operationId, string expectedStatusCode, HttpRequestMessage request) - { - await _apiTestRunner.TestAsync( - _documentName, - operationId, - expectedStatusCode, - request, - _webAppFactory.CreateClient()); - } + public async Task TestAsync(string operationId, string expectedStatusCode, HttpRequestMessage request) + { + await _apiTestRunner.TestAsync( + _documentName, + operationId, + expectedStatusCode, + request, + _webAppFactory.CreateClient()); } -} \ No newline at end of file +} diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerBase.cs b/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerBase.cs index d74fc6242b..7b409bf21e 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerBase.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerBase.cs @@ -1,84 +1,83 @@ using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public abstract class ApiTestRunnerBase : IDisposable { - public abstract class ApiTestRunnerBase : IDisposable + private readonly ApiTestRunnerOptions _options; + private readonly RequestValidator _requestValidator; + private readonly ResponseValidator _responseValidator; + + protected ApiTestRunnerBase() { - private readonly ApiTestRunnerOptions _options; - private readonly RequestValidator _requestValidator; - private readonly ResponseValidator _responseValidator; + _options = new ApiTestRunnerOptions(); + _requestValidator = new RequestValidator(_options.ContentValidators); + _responseValidator = new ResponseValidator(_options.ContentValidators); + } - protected ApiTestRunnerBase() - { - _options = new ApiTestRunnerOptions(); - _requestValidator = new RequestValidator(_options.ContentValidators); - _responseValidator = new ResponseValidator(_options.ContentValidators); - } + public void Configure(Action setupAction) + { + setupAction(_options); + } - public void Configure(Action setupAction) - { - setupAction(_options); - } + public void ConfigureOperation( + string documentName, + string pathTemplate, + OperationType operationType, + OpenApiOperation operation) + { + var openApiDocument = _options.GetOpenApiDocument(documentName); + + if (openApiDocument.Paths == null) + openApiDocument.Paths = new OpenApiPaths(); - public void ConfigureOperation( - string documentName, - string pathTemplate, - OperationType operationType, - OpenApiOperation operation) + if (!openApiDocument.Paths.TryGetValue(pathTemplate, out OpenApiPathItem pathItem)) { - var openApiDocument = _options.GetOpenApiDocument(documentName); + pathItem = new OpenApiPathItem(); + openApiDocument.Paths.Add(pathTemplate, pathItem); + } - if (openApiDocument.Paths == null) - openApiDocument.Paths = new OpenApiPaths(); + pathItem.AddOperation(operationType, operation); + } - if (!openApiDocument.Paths.TryGetValue(pathTemplate, out OpenApiPathItem pathItem)) - { - pathItem = new OpenApiPathItem(); - openApiDocument.Paths.Add(pathTemplate, pathItem); - } + public async Task TestAsync( + string documentName, + string operationId, + string expectedStatusCode, + HttpRequestMessage request, + HttpClient httpClient) + { + var openApiDocument = _options.GetOpenApiDocument(documentName); + if (!openApiDocument.TryFindOperationById(operationId, out string pathTemplate, out OperationType operationType)) + throw new InvalidOperationException($"Operation with id '{operationId}' not found in OpenAPI document '{documentName}'"); - pathItem.AddOperation(operationType, operation); - } + if (expectedStatusCode.StartsWith("2")) + _requestValidator.Validate(request, openApiDocument, pathTemplate, operationType); - public async Task TestAsync( - string documentName, - string operationId, - string expectedStatusCode, - HttpRequestMessage request, - HttpClient httpClient) - { - var openApiDocument = _options.GetOpenApiDocument(documentName); - if (!openApiDocument.TryFindOperationById(operationId, out string pathTemplate, out OperationType operationType)) - throw new InvalidOperationException($"Operation with id '{operationId}' not found in OpenAPI document '{documentName}'"); + var response = await httpClient.SendAsync(request); - if (expectedStatusCode.StartsWith("2")) - _requestValidator.Validate(request, openApiDocument, pathTemplate, operationType); + _responseValidator.Validate(response, openApiDocument, pathTemplate, operationType, expectedStatusCode); + } - var response = await httpClient.SendAsync(request); + public void Dispose() + { + if (!_options.GenerateOpenApiFiles) return; - _responseValidator.Validate(response, openApiDocument, pathTemplate, operationType, expectedStatusCode); - } + if (_options.FileOutputRoot == null) + throw new Exception("GenerateOpenApiFiles set but FileOutputRoot is null"); - public void Dispose() + foreach (var entry in _options.OpenApiDocs) { - if (!_options.GenerateOpenApiFiles) return; - - if (_options.FileOutputRoot == null) - throw new Exception("GenerateOpenApiFiles set but FileOutputRoot is null"); + var outputDir = Path.Combine(_options.FileOutputRoot, entry.Key); + Directory.CreateDirectory(outputDir); - foreach (var entry in _options.OpenApiDocs) + using (var streamWriter = new StreamWriter(Path.Combine(outputDir, "openapi.json"))) { - var outputDir = Path.Combine(_options.FileOutputRoot, entry.Key); - Directory.CreateDirectory(outputDir); - - using (var streamWriter = new StreamWriter(Path.Combine(outputDir, "openapi.json"))) - { - var openApiJsonWriter = new OpenApiJsonWriter(streamWriter); - entry.Value.SerializeAsV3(openApiJsonWriter); - streamWriter.Close(); - } + var openApiJsonWriter = new OpenApiJsonWriter(streamWriter); + entry.Value.SerializeAsV3(openApiJsonWriter); + streamWriter.Close(); } } } -} \ No newline at end of file +} diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerOptions.cs b/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerOptions.cs index 97eec67708..e3bd6a0755 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerOptions.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerOptions.cs @@ -1,23 +1,22 @@ using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class ApiTestRunnerOptions { - public class ApiTestRunnerOptions + public ApiTestRunnerOptions() { - public ApiTestRunnerOptions() - { - OpenApiDocs = new Dictionary(); - ContentValidators = new List { new JsonContentValidator() }; - GenerateOpenApiFiles = false; - FileOutputRoot = null; - } + OpenApiDocs = new Dictionary(); + ContentValidators = new List { new JsonContentValidator() }; + GenerateOpenApiFiles = false; + FileOutputRoot = null; + } - public Dictionary OpenApiDocs { get; } + public Dictionary OpenApiDocs { get; } - public List ContentValidators { get; } + public List ContentValidators { get; } - public bool GenerateOpenApiFiles { get; set; } + public bool GenerateOpenApiFiles { get; set; } - public string FileOutputRoot { get; set; } - } -} \ No newline at end of file + public string FileOutputRoot { get; set; } +} diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerOptionsExtensions.cs index eaa95c813b..d99c6dce55 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/ApiTestRunnerOptionsExtensions.cs @@ -1,26 +1,25 @@ using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public static class ApiTestRunnerOptionsExtensions { - public static class ApiTestRunnerOptionsExtensions + public static void AddOpenApiFile(this ApiTestRunnerOptions options, string documentName, string filePath) { - public static void AddOpenApiFile(this ApiTestRunnerOptions options, string documentName, string filePath) - { - using var fileStream = File.OpenRead(filePath); + using var fileStream = File.OpenRead(filePath); - var openApiDocument = new OpenApiStreamReader().Read(fileStream, out var diagnostic); - options.OpenApiDocs.Add(documentName, openApiDocument); - } + var openApiDocument = new OpenApiStreamReader().Read(fileStream, out var diagnostic); + options.OpenApiDocs.Add(documentName, openApiDocument); + } - public static OpenApiDocument GetOpenApiDocument(this ApiTestRunnerOptions options, string documentName) + public static OpenApiDocument GetOpenApiDocument(this ApiTestRunnerOptions options, string documentName) + { + if (!options.OpenApiDocs.TryGetValue(documentName, out OpenApiDocument document)) { - if (!options.OpenApiDocs.TryGetValue(documentName, out OpenApiDocument document)) - { - throw new InvalidOperationException($"Document with name '{documentName}' not found"); - } - - return document; + throw new InvalidOperationException($"Document with name '{documentName}' not found"); } + + return document; } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/ContentDoesNotMatchSpecException.cs b/src/Swashbuckle.AspNetCore.ApiTesting/ContentDoesNotMatchSpecException.cs new file mode 100644 index 0000000000..c92ee90e23 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.ApiTesting/ContentDoesNotMatchSpecException.cs @@ -0,0 +1,3 @@ +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class ContentDoesNotMatchSpecException(string message) : Exception(message); diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/HttpHeadersExtensions.cs b/src/Swashbuckle.AspNetCore.ApiTesting/HttpHeadersExtensions.cs index 264b437c44..67865955a5 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/HttpHeadersExtensions.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/HttpHeadersExtensions.cs @@ -1,18 +1,17 @@ using System.Collections.Specialized; using System.Net.Http.Headers; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public static class HttpHeadersExtensions { - public static class HttpHeadersExtensions + internal static NameValueCollection ToNameValueCollection(this HttpHeaders httpHeaders) { - internal static NameValueCollection ToNameValueCollection(this HttpHeaders httpHeaders) + var headerNameValues = new NameValueCollection(); + foreach (var entry in httpHeaders) { - var headerNameValues = new NameValueCollection(); - foreach (var entry in httpHeaders) - { - headerNameValues.Add(entry.Key, string.Join(",", entry.Value)); - } - return headerNameValues; + headerNameValues.Add(entry.Key, string.Join(",", entry.Value)); } + return headerNameValues; } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/IContentValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/IContentValidator.cs index abd1705f90..220270962b 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/IContentValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/IContentValidator.cs @@ -1,18 +1,10 @@ using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.ApiTesting -{ - public interface IContentValidator - { - bool CanValidate(string mediaType); +namespace Swashbuckle.AspNetCore.ApiTesting; - void Validate(OpenApiMediaType mediaTypeSpec, OpenApiDocument openApiDocument, HttpContent content); - } +public interface IContentValidator +{ + bool CanValidate(string mediaType); - public class ContentDoesNotMatchSpecException : Exception - { - public ContentDoesNotMatchSpecException(string message) - : base(message) - { } - } + void Validate(OpenApiMediaType mediaTypeSpec, OpenApiDocument openApiDocument, HttpContent content); } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonContentValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonContentValidator.cs index fa168b39ca..34f8af5a02 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonContentValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonContentValidator.cs @@ -1,26 +1,25 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonContentValidator : IContentValidator { - public class JsonContentValidator : IContentValidator - { - private readonly JsonValidator _jsonValidator = new(); + private readonly JsonValidator _jsonValidator = new(); - public bool CanValidate(string mediaType) => mediaType.Contains("json"); + public bool CanValidate(string mediaType) => mediaType.Contains("json"); - public void Validate(OpenApiMediaType mediaTypeSpec, OpenApiDocument openApiDocument, HttpContent content) + public void Validate(OpenApiMediaType mediaTypeSpec, OpenApiDocument openApiDocument, HttpContent content) + { + if (mediaTypeSpec?.Schema == null) { - if (mediaTypeSpec?.Schema == null) - { - return; - } + return; + } - var instance = JToken.Parse(content.ReadAsStringAsync().Result); - if (!_jsonValidator.Validate(mediaTypeSpec.Schema, openApiDocument, instance, out IEnumerable errorMessages)) - { - throw new ContentDoesNotMatchSpecException(string.Join(Environment.NewLine, errorMessages)); - } + var instance = JToken.Parse(content.ReadAsStringAsync().Result); + if (!_jsonValidator.Validate(mediaTypeSpec.Schema, openApiDocument, instance, out IEnumerable errorMessages)) + { + throw new ContentDoesNotMatchSpecException(string.Join(Environment.NewLine, errorMessages)); } } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/IJsonValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/IJsonValidator.cs index e9970f5bee..22874ec0d3 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/IJsonValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/IJsonValidator.cs @@ -1,16 +1,15 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public interface IJsonValidator { - public interface IJsonValidator - { - bool CanValidate(OpenApiSchema schema); + bool CanValidate(OpenApiSchema schema); - bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages); - } -} \ No newline at end of file + bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages); +} diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonAllOfValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonAllOfValidator.cs index bfa1e4a66f..397253266b 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonAllOfValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonAllOfValidator.cs @@ -1,34 +1,33 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonAllOfValidator(JsonValidator jsonValidator) : IJsonValidator { - public class JsonAllOfValidator(JsonValidator jsonValidator) : IJsonValidator - { - private readonly JsonValidator _jsonValidator = jsonValidator; + private readonly JsonValidator _jsonValidator = jsonValidator; - public bool CanValidate(OpenApiSchema schema) => schema.AllOf != null && schema.AllOf.Any(); + public bool CanValidate(OpenApiSchema schema) => schema.AllOf != null && schema.AllOf.Any(); - public bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages) - { - var errorMessagesList = new List(); + public bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages) + { + var errorMessagesList = new List(); - var allOfArray = schema.AllOf.ToArray(); + var allOfArray = schema.AllOf.ToArray(); - for (int i = 0; i < allOfArray.Length; i++) + for (int i = 0; i < allOfArray.Length; i++) + { + if (!_jsonValidator.Validate(allOfArray[i], openApiDocument, instance, out IEnumerable subErrorMessages)) { - if (!_jsonValidator.Validate(allOfArray[i], openApiDocument, instance, out IEnumerable subErrorMessages)) - { - errorMessagesList.AddRange(subErrorMessages.Select(msg => $"{msg} (allOf[{i}])")); - } + errorMessagesList.AddRange(subErrorMessages.Select(msg => $"{msg} (allOf[{i}])")); } - - errorMessages = errorMessagesList; - return !errorMessages.Any(); } + + errorMessages = errorMessagesList; + return !errorMessages.Any(); } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonAnyOfValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonAnyOfValidator.cs index 790eee785a..ceb7f239a9 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonAnyOfValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonAnyOfValidator.cs @@ -1,37 +1,36 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonAnyOfValidator(JsonValidator jsonValidator) : IJsonValidator { - public class JsonAnyOfValidator(JsonValidator jsonValidator) : IJsonValidator - { - private readonly JsonValidator _jsonValidator = jsonValidator; + private readonly JsonValidator _jsonValidator = jsonValidator; - public bool CanValidate(OpenApiSchema schema) => schema.AnyOf != null && schema.AnyOf.Any(); + public bool CanValidate(OpenApiSchema schema) => schema.AnyOf != null && schema.AnyOf.Any(); - public bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages) - { - var errorMessagesList = new List(); + public bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages) + { + var errorMessagesList = new List(); - var anyOfArray = schema.AnyOf.ToArray(); + var anyOfArray = schema.AnyOf.ToArray(); - for (int i = 0; i < anyOfArray.Length; i++) + for (int i = 0; i < anyOfArray.Length; i++) + { + if (_jsonValidator.Validate(anyOfArray[i], openApiDocument, instance, out IEnumerable subErrorMessages)) { - if (_jsonValidator.Validate(anyOfArray[i], openApiDocument, instance, out IEnumerable subErrorMessages)) - { - errorMessages = []; - return true; - } - - errorMessagesList.AddRange(subErrorMessages.Select(msg => $"{msg} (anyOf[{i}])")); + errorMessages = []; + return true; } - errorMessages = errorMessagesList; - return !errorMessages.Any(); + errorMessagesList.AddRange(subErrorMessages.Select(msg => $"{msg} (anyOf[{i}])")); } + + errorMessages = errorMessagesList; + return !errorMessages.Any(); } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonArrayValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonArrayValidator.cs index 0b2bead1ea..db681f9961 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonArrayValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonArrayValidator.cs @@ -1,61 +1,60 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonArrayValidator(IJsonValidator jsonValidator) : IJsonValidator { - public class JsonArrayValidator(IJsonValidator jsonValidator) : IJsonValidator - { - private readonly IJsonValidator _jsonValidator = jsonValidator; + private readonly IJsonValidator _jsonValidator = jsonValidator; - public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.Array; + public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.Array; - public bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages) + public bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages) + { + if (instance.Type != JTokenType.Array) { - if (instance.Type != JTokenType.Array) - { - errorMessages = [$"Path: {instance.Path}. Instance is not of type 'array'"]; - return false; - } + errorMessages = [$"Path: {instance.Path}. Instance is not of type 'array'"]; + return false; + } - var arrayInstance = (JArray)instance; - var errorMessagesList = new List(); + var arrayInstance = (JArray)instance; + var errorMessagesList = new List(); - // items - if (schema.Items != null) + // items + if (schema.Items != null) + { + foreach (var itemInstance in arrayInstance) { - foreach (var itemInstance in arrayInstance) + if (!_jsonValidator.Validate(schema.Items, openApiDocument, itemInstance, out IEnumerable itemErrorMessages)) { - if (!_jsonValidator.Validate(schema.Items, openApiDocument, itemInstance, out IEnumerable itemErrorMessages)) - { - errorMessagesList.AddRange(itemErrorMessages); - } + errorMessagesList.AddRange(itemErrorMessages); } } + } - // maxItems - if (schema.MaxItems.HasValue && (arrayInstance.Count > schema.MaxItems.Value)) - { - errorMessagesList.Add($"Path: {instance.Path}. Array size is greater than maxItems"); - } - - // minItems - if (schema.MinItems.HasValue && (arrayInstance.Count < schema.MinItems.Value)) - { - errorMessagesList.Add($"Path: {instance.Path}. Array size is less than minItems"); - } + // maxItems + if (schema.MaxItems.HasValue && (arrayInstance.Count > schema.MaxItems.Value)) + { + errorMessagesList.Add($"Path: {instance.Path}. Array size is greater than maxItems"); + } - // uniqueItems - if (schema.UniqueItems.HasValue && (arrayInstance.Count != arrayInstance.Distinct().Count())) - { - errorMessagesList.Add($"Path: {instance.Path}. Array does not contain uniqueItems"); - } + // minItems + if (schema.MinItems.HasValue && (arrayInstance.Count < schema.MinItems.Value)) + { + errorMessagesList.Add($"Path: {instance.Path}. Array size is less than minItems"); + } - errorMessages = errorMessagesList; - return !errorMessages.Any(); + // uniqueItems + if (schema.UniqueItems.HasValue && (arrayInstance.Count != arrayInstance.Distinct().Count())) + { + errorMessagesList.Add($"Path: {instance.Path}. Array does not contain uniqueItems"); } + + errorMessages = errorMessagesList; + return !errorMessages.Any(); } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonBooleanValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonBooleanValidator.cs index 9920c4f5e1..9e06b42ac3 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonBooleanValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonBooleanValidator.cs @@ -1,26 +1,25 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonBooleanValidator : IJsonValidator { - public class JsonBooleanValidator : IJsonValidator - { - public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.Boolean; + public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.Boolean; - public bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages) + public bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages) + { + if (instance.Type != JTokenType.Boolean) { - if (instance.Type != JTokenType.Boolean) - { - errorMessages = [$"Path: {instance.Path}. Instance is not of type 'boolean'"]; - return false; - } - - errorMessages = []; - return true; + errorMessages = [$"Path: {instance.Path}. Instance is not of type 'boolean'"]; + return false; } + + errorMessages = []; + return true; } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonNullValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonNullValidator.cs index 83a0078c32..8ddf15b53c 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonNullValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonNullValidator.cs @@ -1,26 +1,25 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonNullValidator : IJsonValidator { - public class JsonNullValidator : IJsonValidator - { - public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.Null; + public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.Null; - public bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages) + public bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages) + { + if (instance.Type != JTokenType.Null) { - if (instance.Type != JTokenType.Null) - { - errorMessages = [$"Path: {instance.Path}. Instance is not of type 'null'"]; - return false; - } - - errorMessages = []; - return true; + errorMessages = [$"Path: {instance.Path}. Instance is not of type 'null'"]; + return false; } + + errorMessages = []; + return true; } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonNumberValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonNumberValidator.cs index c7850015d1..9101fb5010 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonNumberValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonNumberValidator.cs @@ -1,65 +1,64 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonNumberValidator : IJsonValidator { - public class JsonNumberValidator : IJsonValidator + public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.Number; + + public bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages) { - public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.Number; + if (instance.Type is not JTokenType.Float and not JTokenType.Integer) + { + errorMessages = [$"Path: {instance.Path}. Instance is not of type 'number'"]; + return false; + } + + var numberValue = instance.Value(); + var errorMessagesList = new List(); - public bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages) + // multipleOf + if (schema.MultipleOf.HasValue && ((numberValue % schema.MultipleOf.Value) != 0)) { - if (instance.Type is not JTokenType.Float and not JTokenType.Integer) - { - errorMessages = [$"Path: {instance.Path}. Instance is not of type 'number'"]; - return false; - } + errorMessagesList.Add($"Path: {instance.Path}. Number is not evenly divisible by multipleOf"); + } - var numberValue = instance.Value(); - var errorMessagesList = new List(); + // maximum & exclusiveMaximum + if (schema.Maximum.HasValue) + { + var exclusiveMaximum = schema.ExclusiveMaximum ?? false; - // multipleOf - if (schema.MultipleOf.HasValue && ((numberValue % schema.MultipleOf.Value) != 0)) + if (exclusiveMaximum && (numberValue >= schema.Maximum.Value)) { - errorMessagesList.Add($"Path: {instance.Path}. Number is not evenly divisible by multipleOf"); + errorMessagesList.Add($"Path: {instance.Path}. Number is greater than, or equal to, maximum"); } - - // maximum & exclusiveMaximum - if (schema.Maximum.HasValue) + else if (numberValue > schema.Maximum.Value) { - var exclusiveMaximum = schema.ExclusiveMaximum ?? false; - - if (exclusiveMaximum && (numberValue >= schema.Maximum.Value)) - { - errorMessagesList.Add($"Path: {instance.Path}. Number is greater than, or equal to, maximum"); - } - else if (numberValue > schema.Maximum.Value) - { - errorMessagesList.Add($"Path: {instance.Path}. Number is greater than maximum"); - } + errorMessagesList.Add($"Path: {instance.Path}. Number is greater than maximum"); } + } - // minimum & exclusiveMinimum - if (schema.Minimum.HasValue) - { - var exclusiveMinimum = schema.ExclusiveMinimum ?? false; + // minimum & exclusiveMinimum + if (schema.Minimum.HasValue) + { + var exclusiveMinimum = schema.ExclusiveMinimum ?? false; - if (exclusiveMinimum && (numberValue <= schema.Minimum.Value)) - { - errorMessagesList.Add($"Path: {instance.Path}. Number is less than, or equal to, minimum"); - } - else if (numberValue < schema.Minimum.Value) - { - errorMessagesList.Add($"Path: {instance.Path}. Number is less than minimum"); - } + if (exclusiveMinimum && (numberValue <= schema.Minimum.Value)) + { + errorMessagesList.Add($"Path: {instance.Path}. Number is less than, or equal to, minimum"); + } + else if (numberValue < schema.Minimum.Value) + { + errorMessagesList.Add($"Path: {instance.Path}. Number is less than minimum"); } - - errorMessages = errorMessagesList; - return !errorMessages.Any(); } + + errorMessages = errorMessagesList; + return !errorMessages.Any(); } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonObjectValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonObjectValidator.cs index da4ebf5d46..3b357609c8 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonObjectValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonObjectValidator.cs @@ -1,78 +1,77 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonObjectValidator(IJsonValidator jsonValidator) : IJsonValidator { - public class JsonObjectValidator(IJsonValidator jsonValidator) : IJsonValidator - { - private readonly IJsonValidator _jsonValidator = jsonValidator; + private readonly IJsonValidator _jsonValidator = jsonValidator; - public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.Object; + public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.Object; - public bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages) + public bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages) + { + if (instance.Type != JTokenType.Object) { - if (instance.Type != JTokenType.Object) - { - errorMessages = [$"Path: {instance.Path}. Instance is not of type 'object'"]; - return false; - } + errorMessages = [$"Path: {instance.Path}. Instance is not of type 'object'"]; + return false; + } - var jObject = (JObject)instance; - var properties = jObject.Properties(); - var errorMessagesList = new List(); + var jObject = (JObject)instance; + var properties = jObject.Properties(); + var errorMessagesList = new List(); - // maxProperties - if (schema.MaxProperties.HasValue && properties.Count() > schema.MaxProperties.Value) - { - errorMessagesList.Add($"Path: {instance.Path}. Number of properties is greater than maxProperties"); - } + // maxProperties + if (schema.MaxProperties.HasValue && properties.Count() > schema.MaxProperties.Value) + { + errorMessagesList.Add($"Path: {instance.Path}. Number of properties is greater than maxProperties"); + } - // minProperties - if (schema.MinProperties.HasValue && properties.Count() < schema.MinProperties.Value) - { - errorMessagesList.Add($"Path: {instance.Path}. Number of properties is less than minProperties"); - } + // minProperties + if (schema.MinProperties.HasValue && properties.Count() < schema.MinProperties.Value) + { + errorMessagesList.Add($"Path: {instance.Path}. Number of properties is less than minProperties"); + } - // required - if (schema.Required != null && schema.Required.Except(properties.Select(p => p.Name)).Any()) - { - errorMessagesList.Add($"Path: {instance.Path}. Required property(s) not present"); - } + // required + if (schema.Required != null && schema.Required.Except(properties.Select(p => p.Name)).Any()) + { + errorMessagesList.Add($"Path: {instance.Path}. Required property(s) not present"); + } - foreach (var property in properties) - { - // properties - IEnumerable propertyErrorMessages; + foreach (var property in properties) + { + // properties + IEnumerable propertyErrorMessages; - if (schema.Properties != null && schema.Properties.TryGetValue(property.Name, out OpenApiSchema propertySchema)) + if (schema.Properties != null && schema.Properties.TryGetValue(property.Name, out OpenApiSchema propertySchema)) + { + if (!_jsonValidator.Validate(propertySchema, openApiDocument, property.Value, out propertyErrorMessages)) { - if (!_jsonValidator.Validate(propertySchema, openApiDocument, property.Value, out propertyErrorMessages)) - { - errorMessagesList.AddRange(propertyErrorMessages); - } - - continue; + errorMessagesList.AddRange(propertyErrorMessages); } - if (!schema.AdditionalPropertiesAllowed) - { - errorMessagesList.Add($"Path: {instance.Path}. Additional properties not allowed"); - } + continue; + } - // additionalProperties - if (schema.AdditionalProperties != null && - !_jsonValidator.Validate(schema.AdditionalProperties, openApiDocument, property.Value, out propertyErrorMessages)) - { - errorMessagesList.AddRange(propertyErrorMessages); - } + if (!schema.AdditionalPropertiesAllowed) + { + errorMessagesList.Add($"Path: {instance.Path}. Additional properties not allowed"); } - errorMessages = errorMessagesList; - return !errorMessages.Any(); + // additionalProperties + if (schema.AdditionalProperties != null && + !_jsonValidator.Validate(schema.AdditionalProperties, openApiDocument, property.Value, out propertyErrorMessages)) + { + errorMessagesList.AddRange(propertyErrorMessages); + } } + + errorMessages = errorMessagesList; + return !errorMessages.Any(); } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonOneOfValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonOneOfValidator.cs index 7203e9a230..3b1ae31fe5 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonOneOfValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonOneOfValidator.cs @@ -1,51 +1,50 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonOneOfValidator(JsonValidator jsonValidator) : IJsonValidator { - public class JsonOneOfValidator(JsonValidator jsonValidator) : IJsonValidator - { - private readonly JsonValidator _jsonValidator = jsonValidator; + private readonly JsonValidator _jsonValidator = jsonValidator; - public bool CanValidate(OpenApiSchema schema) => schema.OneOf != null && schema.OneOf.Any(); + public bool CanValidate(OpenApiSchema schema) => schema.OneOf != null && schema.OneOf.Any(); - public bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages) - { - var errorMessagesList = new List(); + public bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages) + { + var errorMessagesList = new List(); - var oneOfArray = schema.OneOf.ToArray(); + var oneOfArray = schema.OneOf.ToArray(); - int matched = 0; - for (int i = 0; i < oneOfArray.Length; i++) + int matched = 0; + for (int i = 0; i < oneOfArray.Length; i++) + { + if (_jsonValidator.Validate(oneOfArray[i], openApiDocument, instance, out IEnumerable subErrorMessages)) { - if (_jsonValidator.Validate(oneOfArray[i], openApiDocument, instance, out IEnumerable subErrorMessages)) - { - matched++; - } - else - { - errorMessagesList.AddRange(subErrorMessages.Select(msg => $"{msg} (oneOf[{i}])")); - } + matched++; } - - if (matched == 0) + else { - errorMessages = errorMessagesList; - return false; + errorMessagesList.AddRange(subErrorMessages.Select(msg => $"{msg} (oneOf[{i}])")); } + } - if (matched > 1) - { - errorMessages = [$"Path: {instance.Path}. Instance matches multiple schemas in oneOf array"]; - return false; - } + if (matched == 0) + { + errorMessages = errorMessagesList; + return false; + } - errorMessages = []; - return true; + if (matched > 1) + { + errorMessages = [$"Path: {instance.Path}. Instance matches multiple schemas in oneOf array"]; + return false; } + + errorMessages = []; + return true; } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonStringValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonStringValidator.cs index b43ac911ae..f044526ac5 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonStringValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonStringValidator.cs @@ -2,47 +2,46 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonStringValidator : IJsonValidator { - public class JsonStringValidator : IJsonValidator + public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.String; + + public bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages) { - public bool CanValidate(OpenApiSchema schema) => schema.Type == JsonSchemaTypes.String; + if (instance.Type is not JTokenType.Date and not JTokenType.Guid and not JTokenType.String) + { + errorMessages = [$"Path: {instance.Path}. Instance is not of type 'string'"]; + return false; + } + + var stringValue = instance.Value(); + var errorMessagesList = new List(); + + // maxLength + if (schema.MaxLength.HasValue && (stringValue.Length > schema.MaxLength.Value)) + { + errorMessagesList.Add($"Path: {instance.Path}. String length is greater than maxLength"); + } + + // minLength + if (schema.MinLength.HasValue && (stringValue.Length < schema.MinLength.Value)) + { + errorMessagesList.Add($"Path: {instance.Path}. String length is less than minLength"); + } - public bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages) + // pattern + if ((schema.Pattern != null) && !Regex.IsMatch(stringValue, schema.Pattern)) { - if (instance.Type is not JTokenType.Date and not JTokenType.Guid and not JTokenType.String) - { - errorMessages = [$"Path: {instance.Path}. Instance is not of type 'string'"]; - return false; - } - - var stringValue = instance.Value(); - var errorMessagesList = new List(); - - // maxLength - if (schema.MaxLength.HasValue && (stringValue.Length > schema.MaxLength.Value)) - { - errorMessagesList.Add($"Path: {instance.Path}. String length is greater than maxLength"); - } - - // minLength - if (schema.MinLength.HasValue && (stringValue.Length < schema.MinLength.Value)) - { - errorMessagesList.Add($"Path: {instance.Path}. String length is less than minLength"); - } - - // pattern - if ((schema.Pattern != null) && !Regex.IsMatch(stringValue, schema.Pattern)) - { - errorMessagesList.Add($"Path: {instance.Path}. String does not match pattern"); - } - - errorMessages = errorMessagesList; - return !errorMessages.Any(); + errorMessagesList.Add($"Path: {instance.Path}. String does not match pattern"); } + + errorMessages = errorMessagesList; + return !errorMessages.Any(); } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonValidator.cs index e71294d535..1fc1ee02dd 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/JsonValidation/JsonValidator.cs @@ -1,57 +1,56 @@ using Microsoft.OpenApi.Models; using Newtonsoft.Json.Linq; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class JsonValidator : IJsonValidator { - public class JsonValidator : IJsonValidator - { - private readonly IEnumerable _subValidators; + private readonly IEnumerable _subValidators; - public JsonValidator() - { - _subValidators = - [ - new JsonNullValidator(), - new JsonBooleanValidator(), - new JsonObjectValidator(this), - new JsonArrayValidator(this), - new JsonNumberValidator(), - new JsonStringValidator(), - new JsonAllOfValidator(this), - new JsonAnyOfValidator(this), - new JsonOneOfValidator(this), - ]; - } + public JsonValidator() + { + _subValidators = + [ + new JsonNullValidator(), + new JsonBooleanValidator(), + new JsonObjectValidator(this), + new JsonArrayValidator(this), + new JsonNumberValidator(), + new JsonStringValidator(), + new JsonAllOfValidator(this), + new JsonAnyOfValidator(this), + new JsonOneOfValidator(this), + ]; + } - public bool CanValidate(OpenApiSchema schema) => true; + public bool CanValidate(OpenApiSchema schema) => true; - public bool Validate( - OpenApiSchema schema, - OpenApiDocument openApiDocument, - JToken instance, - out IEnumerable errorMessages) - { - schema = schema.Reference != null - ? (OpenApiSchema)openApiDocument.ResolveReference(schema.Reference) - : schema; + public bool Validate( + OpenApiSchema schema, + OpenApiDocument openApiDocument, + JToken instance, + out IEnumerable errorMessages) + { + schema = schema.Reference != null + ? (OpenApiSchema)openApiDocument.ResolveReference(schema.Reference) + : schema; - var errorMessagesList = new List(); + var errorMessagesList = new List(); - foreach (var subValidator in _subValidators) + foreach (var subValidator in _subValidators) + { + if (!subValidator.CanValidate(schema)) { - if (!subValidator.CanValidate(schema)) - { - continue; - } - - if (!subValidator.Validate(schema, openApiDocument, instance, out IEnumerable subErrorMessages)) - { - errorMessagesList.AddRange(subErrorMessages); - } + continue; } - errorMessages = errorMessagesList; - return !errorMessages.Any(); + if (!subValidator.Validate(schema, openApiDocument, instance, out IEnumerable subErrorMessages)) + { + errorMessagesList.AddRange(subErrorMessages); + } } + + errorMessages = errorMessagesList; + return !errorMessages.Any(); } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/OpenApiDocumentExtensions.cs b/src/Swashbuckle.AspNetCore.ApiTesting/OpenApiDocumentExtensions.cs index a100b76079..914cc93d1e 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/OpenApiDocumentExtensions.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/OpenApiDocumentExtensions.cs @@ -1,48 +1,47 @@ using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public static class OpenApiDocumentExtensions { - public static class OpenApiDocumentExtensions + internal static bool TryFindOperationById( + this OpenApiDocument openApiDocument, + string operationId, + out string pathTemplate, + out OperationType operationType) { - internal static bool TryFindOperationById( - this OpenApiDocument openApiDocument, - string operationId, - out string pathTemplate, - out OperationType operationType) + foreach (var pathEntry in openApiDocument.Paths ?? new OpenApiPaths()) { - foreach (var pathEntry in openApiDocument.Paths ?? new OpenApiPaths()) - { - var pathItem = pathEntry.Value; + var pathItem = pathEntry.Value; - foreach (var operationEntry in pathItem.Operations) + foreach (var operationEntry in pathItem.Operations) + { + if (operationEntry.Value.OperationId == operationId) { - if (operationEntry.Value.OperationId == operationId) - { - pathTemplate = pathEntry.Key; - operationType = operationEntry.Key; - return true; - } + pathTemplate = pathEntry.Key; + operationType = operationEntry.Key; + return true; } } - - pathTemplate = null; - operationType = default(OperationType); - return false; } - internal static OpenApiOperation GetOperationByPathAndType( - this OpenApiDocument openApiDocument, - string pathTemplate, - OperationType operationType, - out OpenApiPathItem pathSpec) - { - if (openApiDocument.Paths.TryGetValue(pathTemplate, out pathSpec)) - { - if (pathSpec.Operations.TryGetValue(operationType, out var type)) - return type; - } + pathTemplate = null; + operationType = default(OperationType); + return false; + } - throw new InvalidOperationException($"Operation with path '{pathTemplate}' and type '{operationType}' not found"); + internal static OpenApiOperation GetOperationByPathAndType( + this OpenApiDocument openApiDocument, + string pathTemplate, + OperationType operationType, + out OpenApiPathItem pathSpec) + { + if (openApiDocument.Paths.TryGetValue(pathTemplate, out pathSpec)) + { + if (pathSpec.Operations.TryGetValue(operationType, out var type)) + return type; } + + throw new InvalidOperationException($"Operation with path '{pathTemplate}' and type '{operationType}' not found"); } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/OpenApiSchemaExtensions.cs b/src/Swashbuckle.AspNetCore.ApiTesting/OpenApiSchemaExtensions.cs index 554dc3529d..7c0c953412 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/OpenApiSchemaExtensions.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/OpenApiSchemaExtensions.cs @@ -1,82 +1,81 @@ using System.Text; using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public static class OpenApiSchemaExtensions { - public static class OpenApiSchemaExtensions + internal static bool TryParse(this OpenApiSchema schema, string stringValue, out object typedValue) { - internal static bool TryParse(this OpenApiSchema schema, string stringValue, out object typedValue) - { - typedValue = null; + typedValue = null; - if (schema.Type == JsonSchemaTypes.Integer && schema.Format == "int64" && long.TryParse(stringValue, out long longValue)) - { - typedValue = longValue; - } - else if (schema.Type == JsonSchemaTypes.Integer && int.TryParse(stringValue, out int intValue)) - { - typedValue = intValue; - } - else if (schema.Type == JsonSchemaTypes.Number && schema.Format == "double" && double.TryParse(stringValue, out double doubleValue)) - { - typedValue = doubleValue; - } - else if (schema.Type == JsonSchemaTypes.Number && float.TryParse(stringValue, out float floatValue)) - { - typedValue = floatValue; - } - else if (schema.Type == JsonSchemaTypes.String && schema.Format == "byte" && byte.TryParse(stringValue, out byte byteValue)) - { - typedValue = byteValue; - } - else if (schema.Type == JsonSchemaTypes.Boolean && bool.TryParse(stringValue, out bool boolValue)) - { - typedValue = boolValue; - } - else if (schema.Type == JsonSchemaTypes.String && schema.Format == "date" && DateTime.TryParse(stringValue, out DateTime dateValue)) - { - typedValue = dateValue; - } - else if (schema.Type == JsonSchemaTypes.String && schema.Format == "date-time" && DateTime.TryParse(stringValue, out DateTime dateTimeValue)) - { - typedValue = dateTimeValue; - } - else if (schema.Type == JsonSchemaTypes.String && schema.Format == "uuid" && Guid.TryParse(stringValue, out Guid uuidValue)) - { - typedValue = uuidValue; - } - else if (schema.Type == JsonSchemaTypes.String) - { - typedValue = stringValue; - } - else if (schema.Type == JsonSchemaTypes.Array) - { - var arrayValue = (schema.Items == null) - ? stringValue.Split(',') - : stringValue.Split(',').Select(itemStringValue => - { - schema.Items.TryParse(itemStringValue, out object itemTypedValue); - return itemTypedValue; - }); - - typedValue = !arrayValue.Any(itemTypedValue => itemTypedValue == null) ? arrayValue : null; - } + if (schema.Type == JsonSchemaTypes.Integer && schema.Format == "int64" && long.TryParse(stringValue, out long longValue)) + { + typedValue = longValue; + } + else if (schema.Type == JsonSchemaTypes.Integer && int.TryParse(stringValue, out int intValue)) + { + typedValue = intValue; + } + else if (schema.Type == JsonSchemaTypes.Number && schema.Format == "double" && double.TryParse(stringValue, out double doubleValue)) + { + typedValue = doubleValue; + } + else if (schema.Type == JsonSchemaTypes.Number && float.TryParse(stringValue, out float floatValue)) + { + typedValue = floatValue; + } + else if (schema.Type == JsonSchemaTypes.String && schema.Format == "byte" && byte.TryParse(stringValue, out byte byteValue)) + { + typedValue = byteValue; + } + else if (schema.Type == JsonSchemaTypes.Boolean && bool.TryParse(stringValue, out bool boolValue)) + { + typedValue = boolValue; + } + else if (schema.Type == JsonSchemaTypes.String && schema.Format == "date" && DateTime.TryParse(stringValue, out DateTime dateValue)) + { + typedValue = dateValue; + } + else if (schema.Type == JsonSchemaTypes.String && schema.Format == "date-time" && DateTime.TryParse(stringValue, out DateTime dateTimeValue)) + { + typedValue = dateTimeValue; + } + else if (schema.Type == JsonSchemaTypes.String && schema.Format == "uuid" && Guid.TryParse(stringValue, out Guid uuidValue)) + { + typedValue = uuidValue; + } + else if (schema.Type == JsonSchemaTypes.String) + { + typedValue = stringValue; + } + else if (schema.Type == JsonSchemaTypes.Array) + { + var arrayValue = (schema.Items == null) + ? stringValue.Split(',') + : stringValue.Split(',').Select(itemStringValue => + { + schema.Items.TryParse(itemStringValue, out object itemTypedValue); + return itemTypedValue; + }); - return typedValue != null; + typedValue = !arrayValue.Any(itemTypedValue => itemTypedValue == null) ? arrayValue : null; } - internal static string TypeIdentifier(this OpenApiSchema schema) - { - var idBuilder = new StringBuilder(); + return typedValue != null; + } - idBuilder.Append(schema.Type); + internal static string TypeIdentifier(this OpenApiSchema schema) + { + var idBuilder = new StringBuilder(); - if (schema.Type == JsonSchemaTypes.Array && schema.Items != null) - { - idBuilder.Append($"[{schema.Items.Type}]"); - } + idBuilder.Append(schema.Type); - return idBuilder.ToString(); + if (schema.Type == JsonSchemaTypes.Array && schema.Items != null) + { + idBuilder.Append($"[{schema.Items.Type}]"); } + + return idBuilder.ToString(); } } diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/RequestValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/RequestValidator.cs index 90ad710294..793abf6cf4 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/RequestValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/RequestValidator.cs @@ -5,157 +5,156 @@ using Microsoft.AspNetCore.Routing.Template; using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class RequestValidator(IEnumerable contentValidators) { - public class RequestValidator(IEnumerable contentValidators) + private readonly IEnumerable _contentValidators = contentValidators; + + public void Validate( + HttpRequestMessage request, + OpenApiDocument openApiDocument, + string pathTemplate, + OperationType operationType) { - private readonly IEnumerable _contentValidators = contentValidators; + var operationSpec = openApiDocument.GetOperationByPathAndType(pathTemplate, operationType, out OpenApiPathItem pathSpec); + var parameterSpecs = ExpandParameterSpecs(pathSpec, operationSpec, openApiDocument); - public void Validate( - HttpRequestMessage request, - OpenApiDocument openApiDocument, - string pathTemplate, - OperationType operationType) + // Convert to absolute Uri as a workaround to limitation with Uri class - i.e. most of it's methods are not supported for relative Uri's. + var requestUri = new Uri(new Uri("http://tempuri.org"), request.RequestUri); + + if (!TryParsePathNameValues(pathTemplate, requestUri.AbsolutePath, out NameValueCollection pathNameValues)) { - var operationSpec = openApiDocument.GetOperationByPathAndType(pathTemplate, operationType, out OpenApiPathItem pathSpec); - var parameterSpecs = ExpandParameterSpecs(pathSpec, operationSpec, openApiDocument); + throw new RequestDoesNotMatchSpecException($"Request URI '{requestUri.AbsolutePath}' does not match specified template '{pathTemplate}'"); + } - // Convert to absolute Uri as a workaround to limitation with Uri class - i.e. most of it's methods are not supported for relative Uri's. - var requestUri = new Uri(new Uri("http://tempuri.org"), request.RequestUri); + if (request.Method != new HttpMethod(operationType.ToString())) + { + throw new RequestDoesNotMatchSpecException($"Request method '{request.Method}' does not match specified operation type '{operationType}'"); + } - if (!TryParsePathNameValues(pathTemplate, requestUri.AbsolutePath, out NameValueCollection pathNameValues)) - { - throw new RequestDoesNotMatchSpecException($"Request URI '{requestUri.AbsolutePath}' does not match specified template '{pathTemplate}'"); - } + ValidateParameters(parameterSpecs.Where(p => p.In == ParameterLocation.Path), openApiDocument, pathNameValues); + ValidateParameters(parameterSpecs.Where(p => p.In == ParameterLocation.Query), openApiDocument, HttpUtility.ParseQueryString(requestUri.Query)); + ValidateParameters(parameterSpecs.Where(p => p.In == ParameterLocation.Header), openApiDocument, request.Headers.ToNameValueCollection()); - if (request.Method != new HttpMethod(operationType.ToString())) - { - throw new RequestDoesNotMatchSpecException($"Request method '{request.Method}' does not match specified operation type '{operationType}'"); - } + if (operationSpec.RequestBody != null) + { + ValidateContent(operationSpec.RequestBody, openApiDocument, request.Content); + } + } - ValidateParameters(parameterSpecs.Where(p => p.In == ParameterLocation.Path), openApiDocument, pathNameValues); - ValidateParameters(parameterSpecs.Where(p => p.In == ParameterLocation.Query), openApiDocument, HttpUtility.ParseQueryString(requestUri.Query)); - ValidateParameters(parameterSpecs.Where(p => p.In == ParameterLocation.Header), openApiDocument, request.Headers.ToNameValueCollection()); + private static IEnumerable ExpandParameterSpecs( + OpenApiPathItem pathSpec, + OpenApiOperation operationSpec, + OpenApiDocument openApiDocument) + { + var securityParameterSpecs = DeriveSecurityParameterSpecs(operationSpec, openApiDocument); - if (operationSpec.RequestBody != null) + return securityParameterSpecs + .Concat(pathSpec.Parameters) + .Concat(operationSpec.Parameters) + .Select(p => { - ValidateContent(operationSpec.RequestBody, openApiDocument, request.Content); - } - } + return p.Reference != null ? + (OpenApiParameter)openApiDocument.ResolveReference(p.Reference) + : p; + }); + } - private static IEnumerable ExpandParameterSpecs( - OpenApiPathItem pathSpec, - OpenApiOperation operationSpec, - OpenApiDocument openApiDocument) - { - var securityParameterSpecs = DeriveSecurityParameterSpecs(operationSpec, openApiDocument); + private static IEnumerable DeriveSecurityParameterSpecs( + OpenApiOperation operationSpec, + OpenApiDocument openApiDocument) + { + // TODO + return []; + } - return securityParameterSpecs - .Concat(pathSpec.Parameters) - .Concat(operationSpec.Parameters) - .Select(p => - { - return p.Reference != null ? - (OpenApiParameter)openApiDocument.ResolveReference(p.Reference) - : p; - }); + private static bool TryParsePathNameValues(string pathTemplate, string requestUri, out NameValueCollection pathNameValues) + { + pathNameValues = []; + + var templateMatcher = new TemplateMatcher(TemplateParser.Parse(pathTemplate), null); + var routeValues = new RouteValueDictionary(); + if (!templateMatcher.TryMatch(new PathString(requestUri), routeValues)) + { + return false; } - private static IEnumerable DeriveSecurityParameterSpecs( - OpenApiOperation operationSpec, - OpenApiDocument openApiDocument) + foreach (var entry in routeValues) { - // TODO - return []; + pathNameValues.Add(entry.Key, entry.Value.ToString()); } - private static bool TryParsePathNameValues(string pathTemplate, string requestUri, out NameValueCollection pathNameValues) + return true; + } + + + private static void ValidateParameters( + IEnumerable parameterSpecs, + OpenApiDocument openApiDocument, + NameValueCollection parameterNameValues) + { + foreach (var parameterSpec in parameterSpecs) { - pathNameValues = []; + var value = parameterNameValues[parameterSpec.Name]; - var templateMatcher = new TemplateMatcher(TemplateParser.Parse(pathTemplate), null); - var routeValues = new RouteValueDictionary(); - if (!templateMatcher.TryMatch(new PathString(requestUri), routeValues)) + if ((parameterSpec.In == ParameterLocation.Path || parameterSpec.Required) && value == null) { - return false; + throw new RequestDoesNotMatchSpecException($"Required parameter '{parameterSpec.Name}' is not present"); } - foreach (var entry in routeValues) + if (value == null || parameterSpec.Schema == null) { - pathNameValues.Add(entry.Key, entry.Value.ToString()); + continue; } - return true; - } - + var schema = (parameterSpec.Schema.Reference != null) ? + (OpenApiSchema)openApiDocument.ResolveReference(parameterSpec.Schema.Reference) + : parameterSpec.Schema; - private static void ValidateParameters( - IEnumerable parameterSpecs, - OpenApiDocument openApiDocument, - NameValueCollection parameterNameValues) - { - foreach (var parameterSpec in parameterSpecs) + if (!schema.TryParse(value, out object typedValue)) { - var value = parameterNameValues[parameterSpec.Name]; - - if ((parameterSpec.In == ParameterLocation.Path || parameterSpec.Required) && value == null) - { - throw new RequestDoesNotMatchSpecException($"Required parameter '{parameterSpec.Name}' is not present"); - } - - if (value == null || parameterSpec.Schema == null) - { - continue; - } - - var schema = (parameterSpec.Schema.Reference != null) ? - (OpenApiSchema)openApiDocument.ResolveReference(parameterSpec.Schema.Reference) - : parameterSpec.Schema; - - if (!schema.TryParse(value, out object typedValue)) - { - throw new RequestDoesNotMatchSpecException($"Parameter '{parameterSpec.Name}' is not of type '{parameterSpec.Schema.TypeIdentifier()}'"); - } + throw new RequestDoesNotMatchSpecException($"Parameter '{parameterSpec.Name}' is not of type '{parameterSpec.Schema.TypeIdentifier()}'"); } } + } - private void ValidateContent(OpenApiRequestBody requestBodySpec, OpenApiDocument openApiDocument, HttpContent content) - { - requestBodySpec = requestBodySpec.Reference != null ? - (OpenApiRequestBody)openApiDocument.ResolveReference(requestBodySpec.Reference) - : requestBodySpec; + private void ValidateContent(OpenApiRequestBody requestBodySpec, OpenApiDocument openApiDocument, HttpContent content) + { + requestBodySpec = requestBodySpec.Reference != null ? + (OpenApiRequestBody)openApiDocument.ResolveReference(requestBodySpec.Reference) + : requestBodySpec; - if (requestBodySpec.Required && content == null) - { - throw new RequestDoesNotMatchSpecException("Required content is not present"); - } + if (requestBodySpec.Required && content == null) + { + throw new RequestDoesNotMatchSpecException("Required content is not present"); + } - if (content == null) - { - return; - } + if (content == null) + { + return; + } - if (!requestBodySpec.Content.TryGetValue(content.Headers.ContentType.MediaType, out OpenApiMediaType mediaTypeSpec)) - { - throw new RequestDoesNotMatchSpecException($"Content media type '{content.Headers.ContentType.MediaType}' is not specified"); - } + if (!requestBodySpec.Content.TryGetValue(content.Headers.ContentType.MediaType, out OpenApiMediaType mediaTypeSpec)) + { + throw new RequestDoesNotMatchSpecException($"Content media type '{content.Headers.ContentType.MediaType}' is not specified"); + } - try + try + { + foreach (var contentValidator in _contentValidators) { - foreach (var contentValidator in _contentValidators) + if (contentValidator.CanValidate(content.Headers.ContentType.MediaType)) { - if (contentValidator.CanValidate(content.Headers.ContentType.MediaType)) - { - contentValidator.Validate(mediaTypeSpec, openApiDocument, content); - } + contentValidator.Validate(mediaTypeSpec, openApiDocument, content); } } - catch (ContentDoesNotMatchSpecException contentException) - { - throw new RequestDoesNotMatchSpecException($"Content does not match spec. {contentException.Message}"); - } + } + catch (ContentDoesNotMatchSpecException contentException) + { + throw new RequestDoesNotMatchSpecException($"Content does not match spec. {contentException.Message}"); } } - - public class RequestDoesNotMatchSpecException(string message) : Exception(message); } + +public class RequestDoesNotMatchSpecException(string message) : Exception(message); diff --git a/src/Swashbuckle.AspNetCore.ApiTesting/ResponseValidator.cs b/src/Swashbuckle.AspNetCore.ApiTesting/ResponseValidator.cs index 1d04dbb12d..ec7f58c793 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting/ResponseValidator.cs +++ b/src/Swashbuckle.AspNetCore.ApiTesting/ResponseValidator.cs @@ -1,106 +1,105 @@ using System.Collections.Specialized; using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.ApiTesting +namespace Swashbuckle.AspNetCore.ApiTesting; + +public class ResponseValidator(IEnumerable contentValidators) { - public class ResponseValidator(IEnumerable contentValidators) + private readonly IEnumerable _contentValidators = contentValidators; + + public void Validate( + HttpResponseMessage response, + OpenApiDocument openApiDocument, + string pathTemplate, + OperationType operationType, + string expectedStatusCode) { - private readonly IEnumerable _contentValidators = contentValidators; - - public void Validate( - HttpResponseMessage response, - OpenApiDocument openApiDocument, - string pathTemplate, - OperationType operationType, - string expectedStatusCode) + var operationSpec = openApiDocument.GetOperationByPathAndType(pathTemplate, operationType, out _); + if (!operationSpec.Responses.TryGetValue(expectedStatusCode, out OpenApiResponse responseSpec)) { - var operationSpec = openApiDocument.GetOperationByPathAndType(pathTemplate, operationType, out _); - if (!operationSpec.Responses.TryGetValue(expectedStatusCode, out OpenApiResponse responseSpec)) - { - throw new InvalidOperationException($"Response for status '{expectedStatusCode}' not found for operation '{operationSpec.OperationId}'"); - } + throw new InvalidOperationException($"Response for status '{expectedStatusCode}' not found for operation '{operationSpec.OperationId}'"); + } - var statusCode = (int)response.StatusCode; - if (statusCode.ToString() != expectedStatusCode) - { - throw new ResponseDoesNotMatchSpecException($"Status code '{statusCode}' does not match expected value '{expectedStatusCode}'"); - } + var statusCode = (int)response.StatusCode; + if (statusCode.ToString() != expectedStatusCode) + { + throw new ResponseDoesNotMatchSpecException($"Status code '{statusCode}' does not match expected value '{expectedStatusCode}'"); + } - ValidateHeaders(responseSpec.Headers, openApiDocument, response.Headers.ToNameValueCollection()); + ValidateHeaders(responseSpec.Headers, openApiDocument, response.Headers.ToNameValueCollection()); - if (responseSpec.Content != null && responseSpec.Content.Keys.Count != 0) - { - ValidateContent(responseSpec.Content, openApiDocument, response.Content); - } + if (responseSpec.Content != null && responseSpec.Content.Keys.Count != 0) + { + ValidateContent(responseSpec.Content, openApiDocument, response.Content); } + } - private static void ValidateHeaders( - IDictionary headerSpecs, - OpenApiDocument openApiDocument, - NameValueCollection headerValues) + private static void ValidateHeaders( + IDictionary headerSpecs, + OpenApiDocument openApiDocument, + NameValueCollection headerValues) + { + foreach (var entry in headerSpecs) { - foreach (var entry in headerSpecs) - { - var value = headerValues[entry.Key]; - var headerSpec = entry.Value; + var value = headerValues[entry.Key]; + var headerSpec = entry.Value; - if (headerSpec.Required && value == null) - { - throw new ResponseDoesNotMatchSpecException($"Required header '{entry.Key}' is not present"); - } + if (headerSpec.Required && value == null) + { + throw new ResponseDoesNotMatchSpecException($"Required header '{entry.Key}' is not present"); + } - if (value == null || headerSpec.Schema == null) - { - continue; - } + if (value == null || headerSpec.Schema == null) + { + continue; + } - var schema = (headerSpec.Schema.Reference != null) ? - (OpenApiSchema)openApiDocument.ResolveReference(headerSpec.Schema.Reference) - : headerSpec.Schema; + var schema = (headerSpec.Schema.Reference != null) ? + (OpenApiSchema)openApiDocument.ResolveReference(headerSpec.Schema.Reference) + : headerSpec.Schema; - if (value == null) - { - continue; - } + if (value == null) + { + continue; + } - if (!schema.TryParse(value, out object typedValue)) - { - throw new ResponseDoesNotMatchSpecException($"Header '{entry.Key}' is not of type '{headerSpec.Schema.TypeIdentifier()}'"); - } + if (!schema.TryParse(value, out object typedValue)) + { + throw new ResponseDoesNotMatchSpecException($"Header '{entry.Key}' is not of type '{headerSpec.Schema.TypeIdentifier()}'"); } } + } - private void ValidateContent( - IDictionary contentSpecs, - OpenApiDocument openApiDocument, - HttpContent content) + private void ValidateContent( + IDictionary contentSpecs, + OpenApiDocument openApiDocument, + HttpContent content) + { + if (content == null || content?.Headers?.ContentLength == 0) { - if (content == null || content?.Headers?.ContentLength == 0) - { - throw new RequestDoesNotMatchSpecException("Expected content is not present"); - } + throw new RequestDoesNotMatchSpecException("Expected content is not present"); + } - if (!contentSpecs.TryGetValue(content.Headers.ContentType.MediaType, out OpenApiMediaType mediaTypeSpec)) - { - throw new ResponseDoesNotMatchSpecException($"Content media type '{content.Headers.ContentType.MediaType}' is not specified"); - } + if (!contentSpecs.TryGetValue(content.Headers.ContentType.MediaType, out OpenApiMediaType mediaTypeSpec)) + { + throw new ResponseDoesNotMatchSpecException($"Content media type '{content.Headers.ContentType.MediaType}' is not specified"); + } - try + try + { + foreach (var contentValidator in _contentValidators) { - foreach (var contentValidator in _contentValidators) + if (contentValidator.CanValidate(content.Headers.ContentType.MediaType)) { - if (contentValidator.CanValidate(content.Headers.ContentType.MediaType)) - { - contentValidator.Validate(mediaTypeSpec, openApiDocument, content); - } + contentValidator.Validate(mediaTypeSpec, openApiDocument, content); } } - catch (ContentDoesNotMatchSpecException contentException) - { - throw new ResponseDoesNotMatchSpecException($"Content does not match spec. {contentException.Message}"); - } + } + catch (ContentDoesNotMatchSpecException contentException) + { + throw new ResponseDoesNotMatchSpecException($"Content does not match spec. {contentException.Message}"); } } - - public class ResponseDoesNotMatchSpecException(string message) : Exception(message); } + +public class ResponseDoesNotMatchSpecException(string message) : Exception(message); diff --git a/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs b/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs index 461b8a20f8..c106119644 100644 --- a/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs +++ b/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs @@ -1,146 +1,145 @@ -namespace Swashbuckle.AspNetCore.Cli +namespace Swashbuckle.AspNetCore.Cli; + +internal class CommandRunner { - internal class CommandRunner + private readonly Dictionary _argumentDescriptors; + private readonly Dictionary _optionDescriptors; + private Func, int> _runFunc; + private readonly List _subRunners; + private readonly TextWriter _output; + + public CommandRunner(string commandName, string commandDescription, TextWriter output) { - private readonly Dictionary _argumentDescriptors; - private readonly Dictionary _optionDescriptors; - private Func, int> _runFunc; - private readonly List _subRunners; - private readonly TextWriter _output; + CommandName = commandName; + CommandDescription = commandDescription; + _argumentDescriptors = []; + _optionDescriptors = []; + _runFunc = (_) => 1; // no-op + _subRunners = []; + _output = output; + } - public CommandRunner(string commandName, string commandDescription, TextWriter output) - { - CommandName = commandName; - CommandDescription = commandDescription; - _argumentDescriptors = []; - _optionDescriptors = []; - _runFunc = (_) => 1; // no-op - _subRunners = []; - _output = output; - } + public string CommandName { get; private set; } - public string CommandName { get; private set; } + public string CommandDescription { get; private set; } - public string CommandDescription { get; private set; } + public void Argument(string name, string description) + { + _argumentDescriptors.Add(name, description); + } - public void Argument(string name, string description) - { - _argumentDescriptors.Add(name, description); - } + public void Option(string name, string description, bool isFlag = false) + { + if (!name.StartsWith("--")) throw new ArgumentException("name of option must begin with --"); + _optionDescriptors.Add(name, new OptionDescriptor { Description = description, IsFlag = isFlag }); + } - public void Option(string name, string description, bool isFlag = false) - { - if (!name.StartsWith("--")) throw new ArgumentException("name of option must begin with --"); - _optionDescriptors.Add(name, new OptionDescriptor { Description = description, IsFlag = isFlag }); - } + public void OnRun(Func, int> runFunc) + { + _runFunc = runFunc; + } + + public void SubCommand(string name, string description, Action configAction) + { + var runner = new CommandRunner($"{CommandName} {name}", description, _output); + configAction(runner); + _subRunners.Add(runner); + } - public void OnRun(Func, int> runFunc) + public int Run(IEnumerable args) + { + if (args.Any()) { - _runFunc = runFunc; + var subRunner = _subRunners.FirstOrDefault(r => r.CommandName.Split(' ').Last() == args.First()); + if (subRunner != null) return subRunner.Run(args.Skip(1)); } - public void SubCommand(string name, string description, Action configAction) + if (_subRunners.Any() || !TryParseArgs(args, out IDictionary namedArgs)) { - var runner = new CommandRunner($"{CommandName} {name}", description, _output); - configAction(runner); - _subRunners.Add(runner); + PrintUsage(); + return 1; } - public int Run(IEnumerable args) + return _runFunc(namedArgs); + } + + private bool TryParseArgs(IEnumerable args, out IDictionary namedArgs) + { + namedArgs = new Dictionary(); + var argsQueue = new Queue(args); + + // Process options first + while (argsQueue.Any() && argsQueue.Peek().StartsWith("--")) { - if (args.Any()) - { - var subRunner = _subRunners.FirstOrDefault(r => r.CommandName.Split(' ').Last() == args.First()); - if (subRunner != null) return subRunner.Run(args.Skip(1)); - } + // Ensure it's a known option + var name = argsQueue.Dequeue(); + if (!_optionDescriptors.TryGetValue(name, out OptionDescriptor optionDescriptor)) + return false; - if (_subRunners.Any() || !TryParseArgs(args, out IDictionary namedArgs)) - { - PrintUsage(); - return 1; - } + // If it's not a flag, ensure it's followed by a corresponding value + if (!optionDescriptor.IsFlag && (!argsQueue.Any() || argsQueue.Peek().StartsWith("--"))) + return false; - return _runFunc(namedArgs); + namedArgs.Add(name, (!optionDescriptor.IsFlag ? argsQueue.Dequeue() : null)); } - private bool TryParseArgs(IEnumerable args, out IDictionary namedArgs) + // Process required args - ensure corresponding values are provided + foreach (var name in _argumentDescriptors.Keys) { - namedArgs = new Dictionary(); - var argsQueue = new Queue(args); - - // Process options first - while (argsQueue.Any() && argsQueue.Peek().StartsWith("--")) - { - // Ensure it's a known option - var name = argsQueue.Dequeue(); - if (!_optionDescriptors.TryGetValue(name, out OptionDescriptor optionDescriptor)) - return false; - - // If it's not a flag, ensure it's followed by a corresponding value - if (!optionDescriptor.IsFlag && (!argsQueue.Any() || argsQueue.Peek().StartsWith("--"))) - return false; + if (!argsQueue.Any() || argsQueue.Peek().StartsWith("--")) return false; + namedArgs.Add(name, argsQueue.Dequeue()); + } - namedArgs.Add(name, (!optionDescriptor.IsFlag ? argsQueue.Dequeue() : null)); - } + return argsQueue.Count() == 0; + } - // Process required args - ensure corresponding values are provided - foreach (var name in _argumentDescriptors.Keys) + private void PrintUsage() + { + if (_subRunners.Any()) + { + // List sub commands + _output.WriteLine(CommandDescription); + _output.WriteLine("Commands:"); + foreach (var runner in _subRunners) { - if (!argsQueue.Any() || argsQueue.Peek().StartsWith("--")) return false; - namedArgs.Add(name, argsQueue.Dequeue()); + var shortName = runner.CommandName.Split(' ').Last(); + if (shortName.StartsWith("_")) continue; // convention to hide commands + _output.WriteLine($" {shortName}: {runner.CommandDescription}"); } - - return argsQueue.Count() == 0; + _output.WriteLine(); } - - private void PrintUsage() + else { - if (_subRunners.Any()) + // Usage for this command + var optionsPart = _optionDescriptors.Any() ? "[options] " : ""; + var argParts = _argumentDescriptors.Keys.Select(name => $"[{name}]"); + _output.WriteLine($"Usage: {CommandName} {optionsPart}{string.Join(" ", argParts)}"); + _output.WriteLine(); + + // Arguments + foreach (var entry in _argumentDescriptors) { - // List sub commands - _output.WriteLine(CommandDescription); - _output.WriteLine("Commands:"); - foreach (var runner in _subRunners) - { - var shortName = runner.CommandName.Split(' ').Last(); - if (shortName.StartsWith("_")) continue; // convention to hide commands - _output.WriteLine($" {shortName}: {runner.CommandDescription}"); - } + _output.WriteLine($"{entry.Key}:"); + _output.WriteLine($" {entry.Value}"); _output.WriteLine(); } - else - { - // Usage for this command - var optionsPart = _optionDescriptors.Any() ? "[options] " : ""; - var argParts = _argumentDescriptors.Keys.Select(name => $"[{name}]"); - _output.WriteLine($"Usage: {CommandName} {optionsPart}{string.Join(" ", argParts)}"); - _output.WriteLine(); - - // Arguments - foreach (var entry in _argumentDescriptors) - { - _output.WriteLine($"{entry.Key}:"); - _output.WriteLine($" {entry.Value}"); - _output.WriteLine(); - } - // Options - if (_optionDescriptors.Any()) + // Options + if (_optionDescriptors.Any()) + { + _output.WriteLine("options:"); + foreach (var entry in _optionDescriptors) { - _output.WriteLine("options:"); - foreach (var entry in _optionDescriptors) - { - _output.WriteLine($" {entry.Key}: {entry.Value.Description}"); - } - _output.WriteLine(); + _output.WriteLine($" {entry.Key}: {entry.Value.Description}"); } + _output.WriteLine(); } } + } - private struct OptionDescriptor - { - public string Description; - public bool IsFlag; - } + private struct OptionDescriptor + { + public string Description; + public bool IsFlag; } } diff --git a/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs b/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs index 9ed39781c2..f20841dcb1 100644 --- a/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs +++ b/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs @@ -4,323 +4,322 @@ using System.Diagnostics; using System.Reflection; -namespace Microsoft.Extensions.Hosting +namespace Microsoft.Extensions.Hosting; + +internal sealed class HostFactoryResolver { - internal sealed class HostFactoryResolver - { - private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; - public const string BuildWebHost = nameof(BuildWebHost); - public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder); - public const string CreateHostBuilder = nameof(CreateHostBuilder); + public const string BuildWebHost = nameof(BuildWebHost); + public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder); + public const string CreateHostBuilder = nameof(CreateHostBuilder); - // The amount of time we wait for the diagnostic source events to fire - private static readonly TimeSpan s_defaultWaitTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(30); + // The amount of time we wait for the diagnostic source events to fire + private static readonly TimeSpan s_defaultWaitTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(30); - public static Func ResolveWebHostFactory(Assembly assembly) - { - return ResolveFactory(assembly, BuildWebHost); - } + public static Func ResolveWebHostFactory(Assembly assembly) + { + return ResolveFactory(assembly, BuildWebHost); + } - public static Func ResolveWebHostBuilderFactory(Assembly assembly) - { - return ResolveFactory(assembly, CreateWebHostBuilder); - } + public static Func ResolveWebHostBuilderFactory(Assembly assembly) + { + return ResolveFactory(assembly, CreateWebHostBuilder); + } + + public static Func ResolveHostBuilderFactory(Assembly assembly) + { + return ResolveFactory(assembly, CreateHostBuilder); + } - public static Func ResolveHostBuilderFactory(Assembly assembly) + // This helpers encapsulates all of the complex logic required to: + // 1. Execute the entry point of the specified assembly in a different thread. + // 2. Wait for the diagnostic source events to fire + // 3. Give the caller a chance to execute logic to mutate the IHostBuilder + // 4. Resolve the instance of the applications's IHost + // 5. Allow the caller to determine if the entry point has completed + public static Func ResolveHostFactory(Assembly assembly, + TimeSpan? waitTimeout = null, + bool stopApplication = true, + Action configureHostBuilder = null, + Action entrypointCompleted = null) + { + if (assembly.EntryPoint is null) { - return ResolveFactory(assembly, CreateHostBuilder); + return null; } - // This helpers encapsulates all of the complex logic required to: - // 1. Execute the entry point of the specified assembly in a different thread. - // 2. Wait for the diagnostic source events to fire - // 3. Give the caller a chance to execute logic to mutate the IHostBuilder - // 4. Resolve the instance of the applications's IHost - // 5. Allow the caller to determine if the entry point has completed - public static Func ResolveHostFactory(Assembly assembly, - TimeSpan? waitTimeout = null, - bool stopApplication = true, - Action configureHostBuilder = null, - Action entrypointCompleted = null) + try { - if (assembly.EntryPoint is null) + // Attempt to load hosting and check the version to make sure the events + // even have a chance of firing (they were added in .NET >= 6) + var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting"); + if (hostingAssembly.GetName().Version is Version version && version.Major < 6) { return null; } - try - { - // Attempt to load hosting and check the version to make sure the events - // even have a chance of firing (they were added in .NET >= 6) - var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting"); - if (hostingAssembly.GetName().Version is Version version && version.Major < 6) - { - return null; - } + // We're using a version >= 6 so the events can fire. If they don't fire + // then it's because the application isn't using the hosting APIs + } + catch + { + // There was an error loading the extensions assembly, return null. + return null; + } - // We're using a version >= 6 so the events can fire. If they don't fire - // then it's because the application isn't using the hosting APIs - } - catch - { - // There was an error loading the extensions assembly, return null. - return null; - } + return args => new HostingListener(args, assembly.EntryPoint, waitTimeout ?? s_defaultWaitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost(); + } - return args => new HostingListener(args, assembly.EntryPoint, waitTimeout ?? s_defaultWaitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost(); + private static Func ResolveFactory(Assembly assembly, string name) + { + var programType = assembly?.EntryPoint?.DeclaringType; + if (programType == null) + { + return null; } - private static Func ResolveFactory(Assembly assembly, string name) + var factory = programType.GetMethod(name, DeclaredOnlyLookup); + if (!IsFactory(factory)) { - var programType = assembly?.EntryPoint?.DeclaringType; - if (programType == null) - { - return null; - } + return null; + } - var factory = programType.GetMethod(name, DeclaredOnlyLookup); - if (!IsFactory(factory)) - { - return null; - } + return args => (T)factory.Invoke(null, [args]); + } - return args => (T)factory.Invoke(null, [args]); - } + // TReturn Factory(string[] args); + private static bool IsFactory(MethodInfo factory) + { + return factory != null + && typeof(TReturn).IsAssignableFrom(factory.ReturnType) + && factory.GetParameters().Length == 1 + && typeof(string[]).Equals(factory.GetParameters()[0].ParameterType); + } - // TReturn Factory(string[] args); - private static bool IsFactory(MethodInfo factory) + // Used by EF tooling without any Hosting references. Looses some return type safety checks. + public static Func ResolveServiceProviderFactory(Assembly assembly, TimeSpan? waitTimeout = null) + { + // Prefer the older patterns by default for back compat. + var webHostFactory = ResolveWebHostFactory(assembly); + if (webHostFactory != null) { - return factory != null - && typeof(TReturn).IsAssignableFrom(factory.ReturnType) - && factory.GetParameters().Length == 1 - && typeof(string[]).Equals(factory.GetParameters()[0].ParameterType); + return args => + { + var webHost = webHostFactory(args); + return GetServiceProvider(webHost); + }; } - // Used by EF tooling without any Hosting references. Looses some return type safety checks. - public static Func ResolveServiceProviderFactory(Assembly assembly, TimeSpan? waitTimeout = null) + var webHostBuilderFactory = ResolveWebHostBuilderFactory(assembly); + if (webHostBuilderFactory != null) { - // Prefer the older patterns by default for back compat. - var webHostFactory = ResolveWebHostFactory(assembly); - if (webHostFactory != null) + return args => { - return args => - { - var webHost = webHostFactory(args); - return GetServiceProvider(webHost); - }; - } + var webHostBuilder = webHostBuilderFactory(args); + var webHost = Build(webHostBuilder); + return GetServiceProvider(webHost); + }; + } - var webHostBuilderFactory = ResolveWebHostBuilderFactory(assembly); - if (webHostBuilderFactory != null) + var hostBuilderFactory = ResolveHostBuilderFactory(assembly); + if (hostBuilderFactory != null) + { + return args => { - return args => - { - var webHostBuilder = webHostBuilderFactory(args); - var webHost = Build(webHostBuilder); - return GetServiceProvider(webHost); - }; - } + var hostBuilder = hostBuilderFactory(args); + var host = Build(hostBuilder); + return GetServiceProvider(host); + }; + } - var hostBuilderFactory = ResolveHostBuilderFactory(assembly); - if (hostBuilderFactory != null) + var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout); + if (hostFactory != null) + { + return args => { - return args => - { - var hostBuilder = hostBuilderFactory(args); - var host = Build(hostBuilder); - return GetServiceProvider(host); - }; - } + var host = hostFactory(args); + return GetServiceProvider(host); + }; + } - var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout); - if (hostFactory != null) - { - return args => - { - var host = hostFactory(args); - return GetServiceProvider(host); - }; - } + return null; + } - return null; - } + private static object Build(object builder) + { + var buildMethod = builder.GetType().GetMethod("Build"); + return buildMethod?.Invoke(builder, []); + } - private static object Build(object builder) + private static IServiceProvider GetServiceProvider(object host) + { + if (host == null) { - var buildMethod = builder.GetType().GetMethod("Build"); - return buildMethod?.Invoke(builder, []); + return null; } + var hostType = host.GetType(); + var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup); + return (IServiceProvider)servicesProperty?.GetValue(host); + } - private static IServiceProvider GetServiceProvider(object host) + private sealed class HostingListener : IObserver, IObserver> + { + private readonly string[] _args; + private readonly MethodInfo _entryPoint; + private readonly TimeSpan _waitTimeout; + private readonly bool _stopApplication; + + private readonly TaskCompletionSource _hostTcs = new(); + private IDisposable _disposable; + private readonly Action _configure; + private readonly Action _entrypointCompleted; + private static readonly AsyncLocal _currentListener = new(); + + public HostingListener( + string[] args, + MethodInfo entryPoint, + TimeSpan waitTimeout, + bool stopApplication, + Action configure, + Action entrypointCompleted) { - if (host == null) - { - return null; - } - var hostType = host.GetType(); - var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup); - return (IServiceProvider)servicesProperty?.GetValue(host); + _args = args; + _entryPoint = entryPoint; + _waitTimeout = waitTimeout; + _stopApplication = stopApplication; + _configure = configure; + _entrypointCompleted = entrypointCompleted; } - private sealed class HostingListener : IObserver, IObserver> + public object CreateHost() { - private readonly string[] _args; - private readonly MethodInfo _entryPoint; - private readonly TimeSpan _waitTimeout; - private readonly bool _stopApplication; - - private readonly TaskCompletionSource _hostTcs = new(); - private IDisposable _disposable; - private readonly Action _configure; - private readonly Action _entrypointCompleted; - private static readonly AsyncLocal _currentListener = new(); - - public HostingListener( - string[] args, - MethodInfo entryPoint, - TimeSpan waitTimeout, - bool stopApplication, - Action configure, - Action entrypointCompleted) - { - _args = args; - _entryPoint = entryPoint; - _waitTimeout = waitTimeout; - _stopApplication = stopApplication; - _configure = configure; - _entrypointCompleted = entrypointCompleted; - } + using var subscription = DiagnosticListener.AllListeners.Subscribe(this); - public object CreateHost() + // Kick off the entry point on a new thread so we don't block the current one + // in case we need to timeout the execution + var thread = new Thread(() => { - using var subscription = DiagnosticListener.AllListeners.Subscribe(this); + Exception exception = null; - // Kick off the entry point on a new thread so we don't block the current one - // in case we need to timeout the execution - var thread = new Thread(() => + try { - Exception exception = null; + // Set the async local to the instance of the HostingListener so we can filter events that + // aren't scoped to this execution of the entry point. + _currentListener.Value = this; - try - { - // Set the async local to the instance of the HostingListener so we can filter events that - // aren't scoped to this execution of the entry point. - _currentListener.Value = this; - - var parameters = _entryPoint.GetParameters(); - if (parameters.Length == 0) - { - _entryPoint.Invoke(null, []); - } - else - { - _entryPoint.Invoke(null, [_args]); - } - - // Try to set an exception if the entry point returns gracefully, this will force - // build to throw - _hostTcs.TrySetException(new InvalidOperationException("Unable to build IHost")); - } - catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException) + var parameters = _entryPoint.GetParameters(); + if (parameters.Length == 0) { - // The host was stopped by our own logic + _entryPoint.Invoke(null, []); } - catch (TargetInvocationException tie) + else { - exception = tie.InnerException ?? tie; - - // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(exception); + _entryPoint.Invoke(null, [_args]); } - catch (Exception ex) - { - exception = ex; - // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(ex); - } - finally - { - // Signal that the entry point is completed - _entrypointCompleted?.Invoke(exception); - } - }) + // Try to set an exception if the entry point returns gracefully, this will force + // build to throw + _hostTcs.TrySetException(new InvalidOperationException("Unable to build IHost")); + } + catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException) { - // Make sure this doesn't hang the process - IsBackground = true - }; - - // Start the thread - thread.Start(); + // The host was stopped by our own logic + } + catch (TargetInvocationException tie) + { + exception = tie.InnerException ?? tie; - try + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(exception); + } + catch (Exception ex) { - // Wait before throwing an exception - if (!_hostTcs.Task.Wait(_waitTimeout)) - { - throw new InvalidOperationException("Unable to build IHost"); - } + exception = ex; + + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(ex); } - catch (AggregateException) when (_hostTcs.Task.IsCompleted) + finally { - // Lets this propagate out of the call to GetAwaiter().GetResult() + // Signal that the entry point is completed + _entrypointCompleted?.Invoke(exception); } + }) + { + // Make sure this doesn't hang the process + IsBackground = true + }; - Debug.Assert(_hostTcs.Task.IsCompleted); + // Start the thread + thread.Start(); - return _hostTcs.Task.GetAwaiter().GetResult(); + try + { + // Wait before throwing an exception + if (!_hostTcs.Task.Wait(_waitTimeout)) + { + throw new InvalidOperationException("Unable to build IHost"); + } } - - public void OnCompleted() + catch (AggregateException) when (_hostTcs.Task.IsCompleted) { - _disposable?.Dispose(); + // Lets this propagate out of the call to GetAwaiter().GetResult() } - public void OnError(Exception error) + Debug.Assert(_hostTcs.Task.IsCompleted); + + return _hostTcs.Task.GetAwaiter().GetResult(); + } + + public void OnCompleted() + { + _disposable?.Dispose(); + } + + public void OnError(Exception error) + { + } + + public void OnNext(DiagnosticListener value) + { + if (_currentListener.Value != this) { + // Ignore events that aren't for this listener + return; } - public void OnNext(DiagnosticListener value) + if (value.Name == "Microsoft.Extensions.Hosting") { - if (_currentListener.Value != this) - { - // Ignore events that aren't for this listener - return; - } + _disposable = value.Subscribe(this); + } + } - if (value.Name == "Microsoft.Extensions.Hosting") - { - _disposable = value.Subscribe(this); - } + public void OnNext(KeyValuePair value) + { + if (_currentListener.Value != this) + { + // Ignore events that aren't for this listener + return; } - public void OnNext(KeyValuePair value) + if (value.Key == "HostBuilding") { - if (_currentListener.Value != this) - { - // Ignore events that aren't for this listener - return; - } + _configure?.Invoke(value.Value); + } - if (value.Key == "HostBuilding") - { - _configure?.Invoke(value.Value); - } + if (value.Key == "HostBuilt") + { + _hostTcs.TrySetResult(value.Value); - if (value.Key == "HostBuilt") + if (_stopApplication) { - _hostTcs.TrySetResult(value.Value); - - if (_stopApplication) - { - // Stop the host from running further - throw new StopTheHostException(); - } + // Stop the host from running further + throw new StopTheHostException(); } } - - private sealed class StopTheHostException : Exception; } + + private sealed class StopTheHostException : Exception; } } diff --git a/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs b/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs index a3638c7baa..3d527172cc 100644 --- a/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs +++ b/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs @@ -4,110 +4,109 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Swashbuckle.AspNetCore.Cli +namespace Swashbuckle.AspNetCore.Cli; + +// Represents an application that uses Microsoft.Extensions.Hosting and supports +// the various entry point flavors. The final model *does not* have an explicit CreateHost entry point and thus inverts the typical flow where the +// execute Main and we wait for events to fire in order to access the appropriate state. +// This is what allows top level statements to work, but getting the IServiceProvider is slightly more complex. +internal class HostingApplication { - // Represents an application that uses Microsoft.Extensions.Hosting and supports - // the various entry point flavors. The final model *does not* have an explicit CreateHost entry point and thus inverts the typical flow where the - // execute Main and we wait for events to fire in order to access the appropriate state. - // This is what allows top level statements to work, but getting the IServiceProvider is slightly more complex. - internal class HostingApplication + internal static IServiceProvider GetServiceProvider(Assembly assembly) { - internal static IServiceProvider GetServiceProvider(Assembly assembly) + // We're disabling the default server and the console host lifetime. This will disable: + // 1. Listening on ports + // 2. Logging to the console from the default host. + // This is essentially what the test server does in order to get access to the application's + // IServicerProvider *and* middleware pipeline. + void ConfigureHostBuilder(object hostBuilder) { - // We're disabling the default server and the console host lifetime. This will disable: - // 1. Listening on ports - // 2. Logging to the console from the default host. - // This is essentially what the test server does in order to get access to the application's - // IServicerProvider *and* middleware pipeline. - void ConfigureHostBuilder(object hostBuilder) + ((IHostBuilder)hostBuilder).ConfigureServices((context, services) => { - ((IHostBuilder)hostBuilder).ConfigureServices((context, services) => - { - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - for (var i = services.Count - 1; i >= 0; i--) + for (var i = services.Count - 1; i >= 0; i--) + { + // exclude all implementations of IHostedService + // except Microsoft.AspNetCore.Hosting.GenericWebHostService because that one will build/configure + // the WebApplication/Middleware pipeline in the case of the GenericWebHostBuilder. + var registration = services[i]; + if (registration.ServiceType == typeof(IHostedService) + && registration.ImplementationType is not { FullName: "Microsoft.AspNetCore.Hosting.GenericWebHostService" }) { - // exclude all implementations of IHostedService - // except Microsoft.AspNetCore.Hosting.GenericWebHostService because that one will build/configure - // the WebApplication/Middleware pipeline in the case of the GenericWebHostBuilder. - var registration = services[i]; - if (registration.ServiceType == typeof(IHostedService) - && registration.ImplementationType is not { FullName: "Microsoft.AspNetCore.Hosting.GenericWebHostService" }) - { - services.RemoveAt(i); - } + services.RemoveAt(i); } - }); - } - - var waitForStartTcs = new TaskCompletionSource(); - - void OnEntryPointExit(Exception exception) - { - // If the entry point exited, we'll try to complete the wait - if (exception != null) - { - waitForStartTcs.TrySetException(exception); } - else - { - waitForStartTcs.TrySetResult(null); - } - } + }); + } - // If all of the existing techniques fail, then try to resolve the ResolveHostFactory - var factory = HostFactoryResolver.ResolveHostFactory(assembly, - stopApplication: false, - configureHostBuilder: ConfigureHostBuilder, - entrypointCompleted: OnEntryPointExit); + var waitForStartTcs = new TaskCompletionSource(); - // We're unable to resolve the factory. This could mean the application wasn't referencing the right - // version of hosting. - if (factory == null) + void OnEntryPointExit(Exception exception) + { + // If the entry point exited, we'll try to complete the wait + if (exception != null) { - return null; + waitForStartTcs.TrySetException(exception); } - - try + else { - // Get the IServiceProvider from the host - var assemblyName = assembly.GetName()?.FullName ?? string.Empty; - // We set the application name in the hosting environment to the startup assembly - // to avoid falling back to the entry assembly (dotnet-swagger) when configuring our - // application. - var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services; - - // Wait for the application to start so that we know it's fully configured. This is important because - // we need the middleware pipeline to be configured before we access the ISwaggerProvider in - // in the IServiceProvider - var applicationLifetime = services.GetRequiredService(); - - using var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null)); - waitForStartTcs.Task.Wait(); - - return services; - } - catch (InvalidOperationException) - { - // We're unable to resolve the host, swallow the exception and return null + waitForStartTcs.TrySetResult(null); } + } + + // If all of the existing techniques fail, then try to resolve the ResolveHostFactory + var factory = HostFactoryResolver.ResolveHostFactory(assembly, + stopApplication: false, + configureHostBuilder: ConfigureHostBuilder, + entrypointCompleted: OnEntryPointExit); + // We're unable to resolve the factory. This could mean the application wasn't referencing the right + // version of hosting. + if (factory == null) + { return null; } - private class NoopHostLifetime : IHostLifetime + try { - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } + // Get the IServiceProvider from the host + var assemblyName = assembly.GetName()?.FullName ?? string.Empty; + // We set the application name in the hosting environment to the startup assembly + // to avoid falling back to the entry assembly (dotnet-swagger) when configuring our + // application. + var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services; + + // Wait for the application to start so that we know it's fully configured. This is important because + // we need the middleware pipeline to be configured before we access the ISwaggerProvider in + // in the IServiceProvider + var applicationLifetime = services.GetRequiredService(); + + using var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null)); + waitForStartTcs.Task.Wait(); - private class NoopServer : IServer + return services; + } + catch (InvalidOperationException) { - public IFeatureCollection Features { get; } = new FeatureCollection(); - public void Dispose() { } - public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) => Task.CompletedTask; - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + // We're unable to resolve the host, swallow the exception and return null } + + return null; + } + + private class NoopHostLifetime : IHostLifetime + { + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + private class NoopServer : IServer + { + public IFeatureCollection Features { get; } = new FeatureCollection(); + public void Dispose() { } + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } } diff --git a/src/Swashbuckle.AspNetCore.Cli/Program.cs b/src/Swashbuckle.AspNetCore.Cli/Program.cs index fe516fbc7d..dc2d11a56d 100644 --- a/src/Swashbuckle.AspNetCore.Cli/Program.cs +++ b/src/Swashbuckle.AspNetCore.Cli/Program.cs @@ -12,337 +12,336 @@ using Microsoft.OpenApi.Writers; using Swashbuckle.AspNetCore.Swagger; -namespace Swashbuckle.AspNetCore.Cli +namespace Swashbuckle.AspNetCore.Cli; + +internal class Program { - internal class Program - { - private const string OpenApiVersionOption = "--openapiversion"; - private const string SerializeAsV2Flag = "--serializeasv2"; + private const string OpenApiVersionOption = "--openapiversion"; + private const string SerializeAsV2Flag = "--serializeasv2"; - public static int Main(string[] args) - { - // Helper to simplify command line parsing etc. - var runner = new CommandRunner("dotnet swagger", "Swashbuckle (Swagger) Command Line Tools", Console.Out); + public static int Main(string[] args) + { + // Helper to simplify command line parsing etc. + var runner = new CommandRunner("dotnet swagger", "Swashbuckle (Swagger) Command Line Tools", Console.Out); - // NOTE: The "dotnet swagger tofile" command does not serve the request directly. Instead, it invokes a corresponding - // command (called _tofile) via "dotnet exec" so that the runtime configuration (*.runtimeconfig & *.deps.json) of the - // provided startupassembly can be used instead of the tool's. This is neccessary to successfully load the - // startupassembly and it's transitive dependencies. See https://github.com/dotnet/coreclr/issues/13277 for more. + // NOTE: The "dotnet swagger tofile" command does not serve the request directly. Instead, it invokes a corresponding + // command (called _tofile) via "dotnet exec" so that the runtime configuration (*.runtimeconfig & *.deps.json) of the + // provided startupassembly can be used instead of the tool's. This is neccessary to successfully load the + // startupassembly and it's transitive dependencies. See https://github.com/dotnet/coreclr/issues/13277 for more. - // > dotnet swagger tofile ... - runner.SubCommand("tofile", "retrieves Swagger from a startup assembly, and writes to file", c => - { - c.Argument("startupassembly", "relative path to the application's startup assembly"); - c.Argument("swaggerdoc", "name of the swagger doc you want to retrieve, as configured in your startup class"); + // > dotnet swagger tofile ... + runner.SubCommand("tofile", "retrieves Swagger from a startup assembly, and writes to file", c => + { + c.Argument("startupassembly", "relative path to the application's startup assembly"); + c.Argument("swaggerdoc", "name of the swagger doc you want to retrieve, as configured in your startup class"); - c.Option("--output", "relative path where the Swagger will be output, defaults to stdout"); - c.Option("--host", "a specific host to include in the Swagger output"); - c.Option("--basepath", "a specific basePath to include in the Swagger output"); - c.Option(OpenApiVersionOption, "output Swagger in the specified version, defaults to 3.0"); - c.Option("--yaml", "exports swagger in a yaml format", true); + c.Option("--output", "relative path where the Swagger will be output, defaults to stdout"); + c.Option("--host", "a specific host to include in the Swagger output"); + c.Option("--basepath", "a specific basePath to include in the Swagger output"); + c.Option(OpenApiVersionOption, "output Swagger in the specified version, defaults to 3.0"); + c.Option("--yaml", "exports swagger in a yaml format", true); - // TODO Remove this option in the major version that adds support for OpenAPI 3.1 - c.Option(SerializeAsV2Flag, "output Swagger in the V2 format rather than V3 [deprecated]", true); + // TODO Remove this option in the major version that adds support for OpenAPI 3.1 + c.Option(SerializeAsV2Flag, "output Swagger in the V2 format rather than V3 [deprecated]", true); - c.OnRun((namedArgs) => - { - string subProcessCommandLine = PrepareCommandLine(args, namedArgs); + c.OnRun((namedArgs) => + { + string subProcessCommandLine = PrepareCommandLine(args, namedArgs); - using var child = Process.Start("dotnet", subProcessCommandLine); + using var child = Process.Start("dotnet", subProcessCommandLine); - child.WaitForExit(); - return child.ExitCode; - }); + child.WaitForExit(); + return child.ExitCode; }); + }); - // > dotnet swagger _tofile ... (* should only be invoked via "dotnet exec") - runner.SubCommand("_tofile", "", c => + // > dotnet swagger _tofile ... (* should only be invoked via "dotnet exec") + runner.SubCommand("_tofile", "", c => + { + c.Argument("startupassembly", ""); + c.Argument("swaggerdoc", ""); + c.Option("--output", ""); + c.Option("--host", ""); + c.Option("--basepath", ""); + c.Option(OpenApiVersionOption, ""); + c.Option("--yaml", "", true); + + // TODO Remove this option in the major version that adds support for OpenAPI 3.1 + c.Option(SerializeAsV2Flag, "", true); + + c.OnRun((namedArgs) => { - c.Argument("startupassembly", ""); - c.Argument("swaggerdoc", ""); - c.Option("--output", ""); - c.Option("--host", ""); - c.Option("--basepath", ""); - c.Option(OpenApiVersionOption, ""); - c.Option("--yaml", "", true); - - // TODO Remove this option in the major version that adds support for OpenAPI 3.1 - c.Option(SerializeAsV2Flag, "", true); - - c.OnRun((namedArgs) => + SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); + var swaggerDocumentSerializer = swaggerOptions?.Value?.CustomDocumentSerializer; + var swagger = swaggerProvider.GetSwagger( + namedArgs["swaggerdoc"], + namedArgs.TryGetValue("--host", out var arg) ? arg : null, + namedArgs.TryGetValue("--basepath", out var namedArg) ? namedArg : null); + + // 4) Serialize to specified output location or stdout + var outputPath = namedArgs.TryGetValue("--output", out var arg1) + ? Path.Combine(Directory.GetCurrentDirectory(), arg1) + : null; + + if (!string.IsNullOrEmpty(outputPath)) { - SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); - var swaggerDocumentSerializer = swaggerOptions?.Value?.CustomDocumentSerializer; - var swagger = swaggerProvider.GetSwagger( - namedArgs["swaggerdoc"], - namedArgs.TryGetValue("--host", out var arg) ? arg : null, - namedArgs.TryGetValue("--basepath", out var namedArg) ? namedArg : null); - - // 4) Serialize to specified output location or stdout - var outputPath = namedArgs.TryGetValue("--output", out var arg1) - ? Path.Combine(Directory.GetCurrentDirectory(), arg1) - : null; - - if (!string.IsNullOrEmpty(outputPath)) + string directoryPath = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) { - string directoryPath = Path.GetDirectoryName(outputPath); - if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) - { - Directory.CreateDirectory(directoryPath); - } + Directory.CreateDirectory(directoryPath); } + } - using Stream stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput(); - using var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture); + using Stream stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput(); + using var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture); - IOpenApiWriter writer; - if (namedArgs.ContainsKey("--yaml")) - { - writer = new OpenApiYamlWriter(streamWriter); - } - else - { - writer = new OpenApiJsonWriter(streamWriter); - } + IOpenApiWriter writer; + if (namedArgs.ContainsKey("--yaml")) + { + writer = new OpenApiYamlWriter(streamWriter); + } + else + { + writer = new OpenApiJsonWriter(streamWriter); + } - OpenApiSpecVersion specVersion = OpenApiSpecVersion.OpenApi3_0; + OpenApiSpecVersion specVersion = OpenApiSpecVersion.OpenApi3_0; - if (namedArgs.TryGetValue(OpenApiVersionOption, out var versionArg)) - { - specVersion = versionArg switch - { - "2.0" => OpenApiSpecVersion.OpenApi2_0, - "3.0" => OpenApiSpecVersion.OpenApi3_0, - _ => throw new NotSupportedException($"The specified OpenAPI version \"{versionArg}\" is not supported."), - }; - } - else if (namedArgs.ContainsKey(SerializeAsV2Flag)) + if (namedArgs.TryGetValue(OpenApiVersionOption, out var versionArg)) + { + specVersion = versionArg switch { - specVersion = OpenApiSpecVersion.OpenApi2_0; - WriteSerializeAsV2DeprecationWarning(); - } + "2.0" => OpenApiSpecVersion.OpenApi2_0, + "3.0" => OpenApiSpecVersion.OpenApi3_0, + _ => throw new NotSupportedException($"The specified OpenAPI version \"{versionArg}\" is not supported."), + }; + } + else if (namedArgs.ContainsKey(SerializeAsV2Flag)) + { + specVersion = OpenApiSpecVersion.OpenApi2_0; + WriteSerializeAsV2DeprecationWarning(); + } - if (swaggerDocumentSerializer != null) - { - swaggerDocumentSerializer.SerializeDocument(swagger, writer, specVersion); - } - else + if (swaggerDocumentSerializer != null) + { + swaggerDocumentSerializer.SerializeDocument(swagger, writer, specVersion); + } + else + { + switch (specVersion) { - switch (specVersion) - { - case OpenApiSpecVersion.OpenApi2_0: - swagger.SerializeAsV2(writer); - break; - - case OpenApiSpecVersion.OpenApi3_0: - default: - swagger.SerializeAsV3(writer); - break; - } + case OpenApiSpecVersion.OpenApi2_0: + swagger.SerializeAsV2(writer); + break; + + case OpenApiSpecVersion.OpenApi3_0: + default: + swagger.SerializeAsV3(writer); + break; } + } - if (outputPath != null) - { - Console.WriteLine($"Swagger JSON/YAML successfully written to {outputPath}"); - } + if (outputPath != null) + { + Console.WriteLine($"Swagger JSON/YAML successfully written to {outputPath}"); + } - return 0; - }); + return 0; }); + }); - // > dotnet swagger list - runner.SubCommand("list", "retrieves the list of Swagger document names from a startup assembly", c => + // > dotnet swagger list + runner.SubCommand("list", "retrieves the list of Swagger document names from a startup assembly", c => + { + c.Argument("startupassembly", "relative path to the application's startup assembly"); + c.Option("--output", "relative path where the document names will be output, defaults to stdout"); + c.OnRun((namedArgs) => { - c.Argument("startupassembly", "relative path to the application's startup assembly"); - c.Option("--output", "relative path where the document names will be output, defaults to stdout"); - c.OnRun((namedArgs) => - { - string subProcessCommandLine = PrepareCommandLine(args, namedArgs); + string subProcessCommandLine = PrepareCommandLine(args, namedArgs); - using var child = Process.Start("dotnet", subProcessCommandLine); + using var child = Process.Start("dotnet", subProcessCommandLine); - child.WaitForExit(); - return child.ExitCode; - }); + child.WaitForExit(); + return child.ExitCode; }); + }); - // > dotnet swagger _list ... (* should only be invoked via "dotnet exec") - runner.SubCommand("_list", "", c => + // > dotnet swagger _list ... (* should only be invoked via "dotnet exec") + runner.SubCommand("_list", "", c => + { + c.Argument("startupassembly", ""); + c.Option("--output", ""); + c.OnRun((namedArgs) => { - c.Argument("startupassembly", ""); - c.Option("--output", ""); - c.OnRun((namedArgs) => + SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); + IList docNames = []; + + string outputPath = namedArgs.TryGetValue("--output", out var arg1) + ? Path.Combine(Directory.GetCurrentDirectory(), arg1) + : null; + bool outputViaConsole = outputPath == null; + if (!string.IsNullOrEmpty(outputPath)) { - SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); - IList docNames = []; - - string outputPath = namedArgs.TryGetValue("--output", out var arg1) - ? Path.Combine(Directory.GetCurrentDirectory(), arg1) - : null; - bool outputViaConsole = outputPath == null; - if (!string.IsNullOrEmpty(outputPath)) + string directoryPath = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) { - string directoryPath = Path.GetDirectoryName(outputPath); - if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) - { - Directory.CreateDirectory(directoryPath); - } + Directory.CreateDirectory(directoryPath); } + } - using Stream stream = outputViaConsole ? Console.OpenStandardOutput() : File.Create(outputPath); - using StreamWriter writer = new(stream, outputViaConsole ? Console.OutputEncoding : Encoding.UTF8); + using Stream stream = outputViaConsole ? Console.OpenStandardOutput() : File.Create(outputPath); + using StreamWriter writer = new(stream, outputViaConsole ? Console.OutputEncoding : Encoding.UTF8); - if (swaggerProvider is not ISwaggerDocumentMetadataProvider docMetaProvider) - { - writer.WriteLine($"The registered {nameof(ISwaggerProvider)} instance does not implement {nameof(ISwaggerDocumentMetadataProvider)}; unable to list the Swagger document names."); - return -1; - } + if (swaggerProvider is not ISwaggerDocumentMetadataProvider docMetaProvider) + { + writer.WriteLine($"The registered {nameof(ISwaggerProvider)} instance does not implement {nameof(ISwaggerDocumentMetadataProvider)}; unable to list the Swagger document names."); + return -1; + } - docNames = docMetaProvider.GetDocumentNames(); + docNames = docMetaProvider.GetDocumentNames(); - foreach (var name in docNames) - { - writer.WriteLine($"\"{name}\""); - } + foreach (var name in docNames) + { + writer.WriteLine($"\"{name}\""); + } - return 0; - }); + return 0; }); + }); - return runner.Run(args); - } + return runner.Run(args); + } - private static void SetupAndRetrieveSwaggerProviderAndOptions(IDictionary namedArgs, out ISwaggerProvider swaggerProvider, out IOptions swaggerOptions) - { - // 1) Configure host with provided startupassembly - var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath( - Path.Combine(Directory.GetCurrentDirectory(), namedArgs["startupassembly"])); + private static void SetupAndRetrieveSwaggerProviderAndOptions(IDictionary namedArgs, out ISwaggerProvider swaggerProvider, out IOptions swaggerOptions) + { + // 1) Configure host with provided startupassembly + var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath( + Path.Combine(Directory.GetCurrentDirectory(), namedArgs["startupassembly"])); - // 2) Build a service container that's based on the startup assembly - var serviceProvider = GetServiceProvider(startupAssembly); + // 2) Build a service container that's based on the startup assembly + var serviceProvider = GetServiceProvider(startupAssembly); + + // 3) Retrieve Swagger via configured provider + swaggerProvider = serviceProvider.GetRequiredService(); + swaggerOptions = serviceProvider.GetService>(); + } - // 3) Retrieve Swagger via configured provider - swaggerProvider = serviceProvider.GetRequiredService(); - swaggerOptions = serviceProvider.GetService>(); + private static string PrepareCommandLine(string[] args, IDictionary namedArgs) + { + if (!File.Exists(namedArgs["startupassembly"])) + { + throw new FileNotFoundException(namedArgs["startupassembly"]); } - private static string PrepareCommandLine(string[] args, IDictionary namedArgs) + var depsFile = namedArgs["startupassembly"].Replace(".dll", ".deps.json"); + var runtimeConfig = namedArgs["startupassembly"].Replace(".dll", ".runtimeconfig.json"); + var commandName = args[0]; + + var subProcessArguments = new string[args.Length - 1]; + if (subProcessArguments.Length > 0) { - if (!File.Exists(namedArgs["startupassembly"])) - { - throw new FileNotFoundException(namedArgs["startupassembly"]); - } + Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length); + } - var depsFile = namedArgs["startupassembly"].Replace(".dll", ".deps.json"); - var runtimeConfig = namedArgs["startupassembly"].Replace(".dll", ".runtimeconfig.json"); - var commandName = args[0]; + var subProcessCommandLine = string.Format( + "exec --depsfile {0} --runtimeconfig {1} {2} _{3} {4}", // note the underscore prepended to the command name + EscapePath(depsFile), + EscapePath(runtimeConfig), + EscapePath(typeof(Program).GetTypeInfo().Assembly.Location), + commandName, + string.Join(" ", subProcessArguments.Select(EscapePath)) + ); + return subProcessCommandLine; + } - var subProcessArguments = new string[args.Length - 1]; - if (subProcessArguments.Length > 0) - { - Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length); - } + private static string EscapePath(string path) + { + return path.Contains(' ') + ? "\"" + path + "\"" + : path; + } - var subProcessCommandLine = string.Format( - "exec --depsfile {0} --runtimeconfig {1} {2} _{3} {4}", // note the underscore prepended to the command name - EscapePath(depsFile), - EscapePath(runtimeConfig), - EscapePath(typeof(Program).GetTypeInfo().Assembly.Location), - commandName, - string.Join(" ", subProcessArguments.Select(EscapePath)) - ); - return subProcessCommandLine; + private static IServiceProvider GetServiceProvider(Assembly startupAssembly) + { + if (TryGetCustomHost(startupAssembly, "SwaggerHostFactory", "CreateHost", out IHost host)) + { + return host.Services; } - private static string EscapePath(string path) + if (TryGetCustomHost(startupAssembly, "SwaggerWebHostFactory", "CreateWebHost", out IWebHost webHost)) { - return path.Contains(' ') - ? "\"" + path + "\"" - : path; + return webHost.Services; } - private static IServiceProvider GetServiceProvider(Assembly startupAssembly) + try { - if (TryGetCustomHost(startupAssembly, "SwaggerHostFactory", "CreateHost", out IHost host)) - { - return host.Services; - } + return WebHost.CreateDefaultBuilder() + .UseStartup(startupAssembly.GetName().Name) + .Build() + .Services; + } + catch + { + var serviceProvider = HostingApplication.GetServiceProvider(startupAssembly); - if (TryGetCustomHost(startupAssembly, "SwaggerWebHostFactory", "CreateWebHost", out IWebHost webHost)) + if (serviceProvider != null) { - return webHost.Services; + return serviceProvider; } - try - { - return WebHost.CreateDefaultBuilder() - .UseStartup(startupAssembly.GetName().Name) - .Build() - .Services; - } - catch - { - var serviceProvider = HostingApplication.GetServiceProvider(startupAssembly); + throw; + } + } - if (serviceProvider != null) - { - return serviceProvider; - } + private static bool TryGetCustomHost( + Assembly startupAssembly, + string factoryClassName, + string factoryMethodName, + out THost host) + { + // Scan the assembly for any types that match the provided naming convention + var factoryTypes = startupAssembly.DefinedTypes + .Where(t => t.Name == factoryClassName) + .ToList(); - throw; - } + if (factoryTypes.Count == 0) + { + host = default; + return false; } - - private static bool TryGetCustomHost( - Assembly startupAssembly, - string factoryClassName, - string factoryMethodName, - out THost host) + else if (factoryTypes.Count > 1) { - // Scan the assembly for any types that match the provided naming convention - var factoryTypes = startupAssembly.DefinedTypes - .Where(t => t.Name == factoryClassName) - .ToList(); - - if (factoryTypes.Count == 0) - { - host = default; - return false; - } - else if (factoryTypes.Count > 1) - { - throw new InvalidOperationException($"Multiple {factoryClassName} classes detected"); - } - - var factoryMethod = factoryTypes - .Single() - .GetMethod(factoryMethodName, BindingFlags.Public | BindingFlags.Static); + throw new InvalidOperationException($"Multiple {factoryClassName} classes detected"); + } - if (factoryMethod == null || factoryMethod.ReturnType != typeof(THost)) - { - throw new InvalidOperationException( - $"{factoryClassName} class detected but does not contain a public static method " + - $"called {factoryMethodName} with return type {typeof(THost).Name}"); - } + var factoryMethod = factoryTypes + .Single() + .GetMethod(factoryMethodName, BindingFlags.Public | BindingFlags.Static); - host = (THost)factoryMethod.Invoke(null, null); - return true; + if (factoryMethod == null || factoryMethod.ReturnType != typeof(THost)) + { + throw new InvalidOperationException( + $"{factoryClassName} class detected but does not contain a public static method " + + $"called {factoryMethodName} with return type {typeof(THost).Name}"); } - private static void WriteSerializeAsV2DeprecationWarning() - { - const string AppName = "Swashbuckle.AspNetCore.Cli"; + host = (THost)factoryMethod.Invoke(null, null); + return true; + } - string message = $"The {SerializeAsV2Flag} flag will be removed in a future version of {AppName}. Use the {OpenApiVersionOption} option instead."; + private static void WriteSerializeAsV2DeprecationWarning() + { + const string AppName = "Swashbuckle.AspNetCore.Cli"; - Console.WriteLine(message); + string message = $"The {SerializeAsV2Flag} flag will be removed in a future version of {AppName}. Use the {OpenApiVersionOption} option instead."; - // See https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message - // and https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables - if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is "true") - { - Console.WriteLine($"::warning title={AppName}::{message}"); - } + Console.WriteLine(message); + + // See https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message + // and https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is "true") + { + Console.WriteLine($"::warning title={AppName}::{message}"); } } } diff --git a/src/Swashbuckle.AspNetCore.Newtonsoft/DependencyInjection/NewtonsoftServiceCollectionExtensions.cs b/src/Swashbuckle.AspNetCore.Newtonsoft/DependencyInjection/NewtonsoftServiceCollectionExtensions.cs index 3254e03255..0bcb849bce 100644 --- a/src/Swashbuckle.AspNetCore.Newtonsoft/DependencyInjection/NewtonsoftServiceCollectionExtensions.cs +++ b/src/Swashbuckle.AspNetCore.Newtonsoft/DependencyInjection/NewtonsoftServiceCollectionExtensions.cs @@ -1,28 +1,27 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Newtonsoft.Json; -using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.Newtonsoft; +using Swashbuckle.AspNetCore.SwaggerGen; -#if (NETSTANDARD2_0) +#if !NET using MvcNewtonsoftJsonOptions = Microsoft.AspNetCore.Mvc.MvcJsonOptions; #endif -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class NewtonsoftServiceCollectionExtensions { - public static class NewtonsoftServiceCollectionExtensions + public static IServiceCollection AddSwaggerGenNewtonsoftSupport(this IServiceCollection services) { - public static IServiceCollection AddSwaggerGenNewtonsoftSupport(this IServiceCollection services) - { - return services.Replace( - ServiceDescriptor.Transient((s) => - { - var serializerSettings = s.GetRequiredService>().Value?.SerializerSettings - ?? new JsonSerializerSettings(); + return services.Replace( + ServiceDescriptor.Transient((s) => + { + var serializerSettings = s.GetRequiredService>().Value?.SerializerSettings + ?? new JsonSerializerSettings(); - return new NewtonsoftDataContractResolver(serializerSettings); - })); - } + return new NewtonsoftDataContractResolver(serializerSettings); + })); } } diff --git a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/JsonPropertyExtensions.cs b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/JsonPropertyExtensions.cs index 21c864aea1..c849265d9d 100644 --- a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/JsonPropertyExtensions.cs +++ b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/JsonPropertyExtensions.cs @@ -2,21 +2,20 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Swashbuckle.AspNetCore.Newtonsoft +namespace Swashbuckle.AspNetCore.Newtonsoft; + +public static class JsonPropertyExtensions { - public static class JsonPropertyExtensions + public static bool TryGetMemberInfo(this JsonProperty jsonProperty, out MemberInfo memberInfo) { - public static bool TryGetMemberInfo(this JsonProperty jsonProperty, out MemberInfo memberInfo) - { - memberInfo = jsonProperty.DeclaringType?.GetMember(jsonProperty.UnderlyingName) - .FirstOrDefault(); + memberInfo = jsonProperty.DeclaringType?.GetMember(jsonProperty.UnderlyingName) + .FirstOrDefault(); - return (memberInfo != null); - } + return (memberInfo != null); + } - public static bool IsRequiredSpecified(this JsonProperty jsonProperty) - { - return jsonProperty.Required != Required.Default; - } + public static bool IsRequiredSpecified(this JsonProperty jsonProperty) + { + return jsonProperty.Required != Required.Default; } } diff --git a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs index 2e6bed4d25..26617ae6fd 100644 --- a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs @@ -5,57 +5,103 @@ using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.Newtonsoft +namespace Swashbuckle.AspNetCore.Newtonsoft; + +public class NewtonsoftDataContractResolver : ISerializerDataContractResolver { - public class NewtonsoftDataContractResolver : ISerializerDataContractResolver + private readonly JsonSerializerSettings _serializerSettings; + private readonly IContractResolver _contractResolver; + + public NewtonsoftDataContractResolver(JsonSerializerSettings serializerSettings) { - private readonly JsonSerializerSettings _serializerSettings; - private readonly IContractResolver _contractResolver; + _serializerSettings = serializerSettings; + _contractResolver = serializerSettings.ContractResolver ?? new DefaultContractResolver(); + } - public NewtonsoftDataContractResolver(JsonSerializerSettings serializerSettings) + public DataContract GetDataContractForType(Type type) + { + var effectiveType = Nullable.GetUnderlyingType(type) ?? type; + if (effectiveType.IsOneOf(typeof(object), typeof(JToken), typeof(JObject), typeof(JArray))) { - _serializerSettings = serializerSettings; - _contractResolver = serializerSettings.ContractResolver ?? new DefaultContractResolver(); + return DataContract.ForDynamic( + underlyingType: effectiveType, + jsonConverter: JsonConverterFunc); } - public DataContract GetDataContractForType(Type type) + var jsonContract = _contractResolver.ResolveContract(effectiveType); + + if (jsonContract is JsonPrimitiveContract && !jsonContract.UnderlyingType.IsEnum) { - var effectiveType = Nullable.GetUnderlyingType(type) ?? type; - if (effectiveType.IsOneOf(typeof(object), typeof(JToken), typeof(JObject), typeof(JArray))) + if (!PrimitiveTypesAndFormats.TryGetValue(jsonContract.UnderlyingType, out var primitiveTypeAndFormat)) { - return DataContract.ForDynamic( - underlyingType: effectiveType, - jsonConverter: JsonConverterFunc); + primitiveTypeAndFormat = Tuple.Create(DataType.String, (string)null); } - var jsonContract = _contractResolver.ResolveContract(effectiveType); + return DataContract.ForPrimitive( + underlyingType: jsonContract.UnderlyingType, + dataType: primitiveTypeAndFormat.Item1, + dataFormat: primitiveTypeAndFormat.Item2, + jsonConverter: JsonConverterFunc); + } - if (jsonContract is JsonPrimitiveContract && !jsonContract.UnderlyingType.IsEnum) - { - if (!PrimitiveTypesAndFormats.TryGetValue(jsonContract.UnderlyingType, out var primitiveTypeAndFormat)) - { - primitiveTypeAndFormat = Tuple.Create(DataType.String, (string)null); - } + if (jsonContract is JsonPrimitiveContract && jsonContract.UnderlyingType.IsEnum) + { + var enumValues = jsonContract.UnderlyingType.GetEnumValues(); - return DataContract.ForPrimitive( - underlyingType: jsonContract.UnderlyingType, - dataType: primitiveTypeAndFormat.Item1, - dataFormat: primitiveTypeAndFormat.Item2, - jsonConverter: JsonConverterFunc); - } + // Test to determine if the serializer will treat as string + var serializeAsString = (enumValues.Length > 0) + && JsonConverterFunc(enumValues.GetValue(0)).StartsWith("\""); - if (jsonContract is JsonPrimitiveContract && jsonContract.UnderlyingType.IsEnum) - { - var enumValues = jsonContract.UnderlyingType.GetEnumValues(); + var primitiveTypeAndFormat = serializeAsString + ? PrimitiveTypesAndFormats[typeof(string)] + : PrimitiveTypesAndFormats[jsonContract.UnderlyingType.GetEnumUnderlyingType()]; + + return DataContract.ForPrimitive( + underlyingType: jsonContract.UnderlyingType, + dataType: primitiveTypeAndFormat.Item1, + dataFormat: primitiveTypeAndFormat.Item2, + jsonConverter: JsonConverterFunc); + } + + if (jsonContract is JsonArrayContract jsonArrayContract) + { + return DataContract.ForArray( + underlyingType: jsonArrayContract.UnderlyingType, + itemType: jsonArrayContract.CollectionItemType ?? typeof(object), + jsonConverter: JsonConverterFunc); + } - // Test to determine if the serializer will treat as string - var serializeAsString = (enumValues.Length > 0) - && JsonConverterFunc(enumValues.GetValue(0)).StartsWith("\""); + if (jsonContract is JsonDictionaryContract jsonDictionaryContract) + { + var keyType = jsonDictionaryContract.DictionaryKeyType ?? typeof(object); + var valueType = jsonDictionaryContract.DictionaryValueType ?? typeof(object); - var primitiveTypeAndFormat = serializeAsString - ? PrimitiveTypesAndFormats[typeof(string)] - : PrimitiveTypesAndFormats[jsonContract.UnderlyingType.GetEnumUnderlyingType()]; + IEnumerable keys = null; + if (keyType.IsEnum) + { + // This is a special case where we know the possible key values + var enumValuesAsJson = keyType.GetEnumValues() + .Cast() + .Select(JsonConverterFunc); + + keys = enumValuesAsJson.Any(json => json.StartsWith("\"")) + ? enumValuesAsJson.Select(json => json.Replace("\"", string.Empty)) + : keyType.GetEnumNames(); + } + + return DataContract.ForDictionary( + underlyingType: jsonDictionaryContract.UnderlyingType, + valueType: valueType, + keys: keys, + jsonConverter: JsonConverterFunc); + } + + if (jsonContract is JsonObjectContract jsonObjectContract) + { + // This handles DateOnly and TimeOnly + if (PrimitiveTypesAndFormats.TryGetValue(jsonContract.UnderlyingType, out var primitiveTypeAndFormat)) + { return DataContract.ForPrimitive( underlyingType: jsonContract.UnderlyingType, dataType: primitiveTypeAndFormat.Item1, @@ -63,170 +109,121 @@ public DataContract GetDataContractForType(Type type) jsonConverter: JsonConverterFunc); } - if (jsonContract is JsonArrayContract jsonArrayContract) - { - return DataContract.ForArray( - underlyingType: jsonArrayContract.UnderlyingType, - itemType: jsonArrayContract.CollectionItemType ?? typeof(object), - jsonConverter: JsonConverterFunc); - } + string typeNameProperty = null; + string typeNameValue = null; - if (jsonContract is JsonDictionaryContract jsonDictionaryContract) + if (_serializerSettings.TypeNameHandling == TypeNameHandling.Objects + || _serializerSettings.TypeNameHandling == TypeNameHandling.All + || _serializerSettings.TypeNameHandling == TypeNameHandling.Auto) { - var keyType = jsonDictionaryContract.DictionaryKeyType ?? typeof(object); - var valueType = jsonDictionaryContract.DictionaryValueType ?? typeof(object); + typeNameProperty = "$type"; - IEnumerable keys = null; - - if (keyType.IsEnum) - { - // This is a special case where we know the possible key values - var enumValuesAsJson = keyType.GetEnumValues() - .Cast() - .Select(JsonConverterFunc); - - keys = enumValuesAsJson.Any(json => json.StartsWith("\"")) - ? enumValuesAsJson.Select(json => json.Replace("\"", string.Empty)) - : keyType.GetEnumNames(); - } - - return DataContract.ForDictionary( - underlyingType: jsonDictionaryContract.UnderlyingType, - valueType: valueType, - keys: keys, - jsonConverter: JsonConverterFunc); - } - - if (jsonContract is JsonObjectContract jsonObjectContract) - { - // This handles DateOnly and TimeOnly - if (PrimitiveTypesAndFormats.TryGetValue(jsonContract.UnderlyingType, out var primitiveTypeAndFormat)) - { - return DataContract.ForPrimitive( - underlyingType: jsonContract.UnderlyingType, - dataType: primitiveTypeAndFormat.Item1, - dataFormat: primitiveTypeAndFormat.Item2, - jsonConverter: JsonConverterFunc); - } - - string typeNameProperty = null; - string typeNameValue = null; - - if (_serializerSettings.TypeNameHandling == TypeNameHandling.Objects - || _serializerSettings.TypeNameHandling == TypeNameHandling.All - || _serializerSettings.TypeNameHandling == TypeNameHandling.Auto) - { - typeNameProperty = "$type"; - - typeNameValue = (_serializerSettings.TypeNameAssemblyFormatHandling == TypeNameAssemblyFormatHandling.Full) - ? jsonObjectContract.UnderlyingType.AssemblyQualifiedName - : $"{jsonObjectContract.UnderlyingType.FullName}, {jsonObjectContract.UnderlyingType.Assembly.GetName().Name}"; - } - - return DataContract.ForObject( - underlyingType: jsonObjectContract.UnderlyingType, - properties: GetDataPropertiesFor(jsonObjectContract, out Type extensionDataType), - extensionDataType: extensionDataType, - typeNameProperty: typeNameProperty, - typeNameValue: typeNameValue, - jsonConverter: JsonConverterFunc); + typeNameValue = (_serializerSettings.TypeNameAssemblyFormatHandling == TypeNameAssemblyFormatHandling.Full) + ? jsonObjectContract.UnderlyingType.AssemblyQualifiedName + : $"{jsonObjectContract.UnderlyingType.FullName}, {jsonObjectContract.UnderlyingType.Assembly.GetName().Name}"; } - return DataContract.ForDynamic( - underlyingType: effectiveType, + return DataContract.ForObject( + underlyingType: jsonObjectContract.UnderlyingType, + properties: GetDataPropertiesFor(jsonObjectContract, out Type extensionDataType), + extensionDataType: extensionDataType, + typeNameProperty: typeNameProperty, + typeNameValue: typeNameValue, jsonConverter: JsonConverterFunc); } - private string JsonConverterFunc(object value) - { - return JsonConvert.SerializeObject(value, _serializerSettings); - } + return DataContract.ForDynamic( + underlyingType: effectiveType, + jsonConverter: JsonConverterFunc); + } - private List GetDataPropertiesFor(JsonObjectContract jsonObjectContract, out Type extensionDataType) - { - var dataProperties = new List(); + private string JsonConverterFunc(object value) + { + return JsonConvert.SerializeObject(value, _serializerSettings); + } - foreach (var jsonProperty in jsonObjectContract.Properties) + private List GetDataPropertiesFor(JsonObjectContract jsonObjectContract, out Type extensionDataType) + { + var dataProperties = new List(); + + foreach (var jsonProperty in jsonObjectContract.Properties) + { + bool memberInfoIsObtained = jsonProperty.TryGetMemberInfo(out MemberInfo memberInfo); + if (jsonProperty.Ignored || (memberInfoIsObtained && memberInfo.CustomAttributes.Any(t => t.AttributeType == typeof(SwaggerIgnoreAttribute)))) { - bool memberInfoIsObtained = jsonProperty.TryGetMemberInfo(out MemberInfo memberInfo); - if (jsonProperty.Ignored || (memberInfoIsObtained && memberInfo.CustomAttributes.Any(t => t.AttributeType == typeof(SwaggerIgnoreAttribute)))) - { - continue; - } - var required = jsonProperty.IsRequiredSpecified() - ? jsonProperty.Required - : jsonObjectContract.ItemRequired ?? Required.Default; - - var isSetViaConstructor = jsonProperty.DeclaringType != null && jsonProperty.DeclaringType.GetConstructors() - .SelectMany(c => c.GetParameters()) - .Any(p => - { - // Newtonsoft supports setting via constructor if either underlying OR JSON names match - return - string.Equals(p.Name, jsonProperty.UnderlyingName, StringComparison.OrdinalIgnoreCase) || - string.Equals(p.Name, jsonProperty.PropertyName, StringComparison.OrdinalIgnoreCase); - }); - - dataProperties.Add( - new DataProperty( - name: jsonProperty.PropertyName, - isRequired: (required == Required.Always || required == Required.AllowNull), - isNullable: (required == Required.AllowNull || required == Required.Default) && jsonProperty.PropertyType.IsReferenceOrNullableType(), - isReadOnly: jsonProperty.Readable && !jsonProperty.Writable && !isSetViaConstructor, - isWriteOnly: jsonProperty.Writable && !jsonProperty.Readable, - memberType: jsonProperty.PropertyType, - memberInfo: memberInfo)); + continue; } + var required = jsonProperty.IsRequiredSpecified() + ? jsonProperty.Required + : jsonObjectContract.ItemRequired ?? Required.Default; + + var isSetViaConstructor = jsonProperty.DeclaringType != null && jsonProperty.DeclaringType.GetConstructors() + .SelectMany(c => c.GetParameters()) + .Any(p => + { + // Newtonsoft supports setting via constructor if either underlying OR JSON names match + return + string.Equals(p.Name, jsonProperty.UnderlyingName, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.Name, jsonProperty.PropertyName, StringComparison.OrdinalIgnoreCase); + }); + + dataProperties.Add( + new DataProperty( + name: jsonProperty.PropertyName, + isRequired: (required == Required.Always || required == Required.AllowNull), + isNullable: (required == Required.AllowNull || required == Required.Default) && jsonProperty.PropertyType.IsReferenceOrNullableType(), + isReadOnly: jsonProperty.Readable && !jsonProperty.Writable && !isSetViaConstructor, + isWriteOnly: jsonProperty.Writable && !jsonProperty.Readable, + memberType: jsonProperty.PropertyType, + memberInfo: memberInfo)); + } - extensionDataType = jsonObjectContract.ExtensionDataValueType; + extensionDataType = jsonObjectContract.ExtensionDataValueType; -#if (!NETSTANDARD2_0) - // If applicable, honor ProblemDetailsConverter - if (jsonObjectContract.UnderlyingType.IsAssignableTo(typeof(Microsoft.AspNetCore.Mvc.ProblemDetails)) - && _serializerSettings.Converters.OfType().Any()) +#if NET + // If applicable, honor ProblemDetailsConverter + if (jsonObjectContract.UnderlyingType.IsAssignableTo(typeof(Microsoft.AspNetCore.Mvc.ProblemDetails)) + && _serializerSettings.Converters.OfType().Any()) + { + var extensionsProperty = dataProperties.FirstOrDefault(p => p.MemberInfo.Name == "Extensions"); + if (extensionsProperty != null) { - var extensionsProperty = dataProperties.FirstOrDefault(p => p.MemberInfo.Name == "Extensions"); - if (extensionsProperty != null) - { - dataProperties.Remove(extensionsProperty); - extensionDataType = typeof(object); - } + dataProperties.Remove(extensionsProperty); + extensionDataType = typeof(object); } + } #endif - return dataProperties; - } + return dataProperties; + } - private static readonly Dictionary> PrimitiveTypesAndFormats = new() - { - [typeof(bool)] = Tuple.Create(DataType.Boolean, (string)null), - [typeof(byte)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(sbyte)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(short)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(ushort)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(int)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(uint)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(long)] = Tuple.Create(DataType.Integer, "int64"), - [typeof(ulong)] = Tuple.Create(DataType.Integer, "int64"), - [typeof(float)] = Tuple.Create(DataType.Number, "float"), - [typeof(double)] = Tuple.Create(DataType.Number, "double"), - [typeof(decimal)] = Tuple.Create(DataType.Number, "double"), - [typeof(byte[])] = Tuple.Create(DataType.String, "byte"), - [typeof(string)] = Tuple.Create(DataType.String, (string)null), - [typeof(char)] = Tuple.Create(DataType.String, (string)null), - [typeof(DateTime)] = Tuple.Create(DataType.String, "date-time"), - [typeof(DateTimeOffset)] = Tuple.Create(DataType.String, "date-time"), - [typeof(Guid)] = Tuple.Create(DataType.String, "uuid"), - [typeof(Uri)] = Tuple.Create(DataType.String, "uri"), - [typeof(TimeSpan)] = Tuple.Create(DataType.String, "date-span"), -#if NET6_0_OR_GREATER - [ typeof(DateOnly) ] = Tuple.Create(DataType.String, "date"), - [ typeof(TimeOnly) ] = Tuple.Create(DataType.String, "time"), -#endif -#if NET7_0_OR_GREATER - [ typeof(Int128) ] = Tuple.Create(DataType.Integer, "int128"), - [ typeof(UInt128) ] = Tuple.Create(DataType.Integer, "int128"), + private static readonly Dictionary> PrimitiveTypesAndFormats = new() + { + [typeof(bool)] = Tuple.Create(DataType.Boolean, (string)null), + [typeof(byte)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(sbyte)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(short)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(ushort)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(int)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(uint)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(long)] = Tuple.Create(DataType.Integer, "int64"), + [typeof(ulong)] = Tuple.Create(DataType.Integer, "int64"), + [typeof(float)] = Tuple.Create(DataType.Number, "float"), + [typeof(double)] = Tuple.Create(DataType.Number, "double"), + [typeof(decimal)] = Tuple.Create(DataType.Number, "double"), + [typeof(byte[])] = Tuple.Create(DataType.String, "byte"), + [typeof(string)] = Tuple.Create(DataType.String, (string)null), + [typeof(char)] = Tuple.Create(DataType.String, (string)null), + [typeof(DateTime)] = Tuple.Create(DataType.String, "date-time"), + [typeof(DateTimeOffset)] = Tuple.Create(DataType.String, "date-time"), + [typeof(Guid)] = Tuple.Create(DataType.String, "uuid"), + [typeof(Uri)] = Tuple.Create(DataType.String, "uri"), + [typeof(TimeSpan)] = Tuple.Create(DataType.String, "date-span"), +#if NET + [typeof(DateOnly)] = Tuple.Create(DataType.String, "date"), + [typeof(TimeOnly)] = Tuple.Create(DataType.String, "time"), + [typeof(Int128)] = Tuple.Create(DataType.Integer, "int128"), + [typeof(UInt128)] = Tuple.Create(DataType.Integer, "int128"), #endif - }; - } + }; } diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs index 53081e4694..b169c0f2d5 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs @@ -2,39 +2,38 @@ using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.ReDoc; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public static class ReDocBuilderExtensions { - public static class ReDocBuilderExtensions + /// + /// Register the Redoc middleware with provided options + /// + public static IApplicationBuilder UseReDoc(this IApplicationBuilder app, ReDocOptions options) + { + return app.UseMiddleware(options); + } + + /// + /// Register the Redoc middleware with optional setup action for DI-injected options + /// + public static IApplicationBuilder UseReDoc( + this IApplicationBuilder app, + Action setupAction = null) { - /// - /// Register the Redoc middleware with provided options - /// - public static IApplicationBuilder UseReDoc(this IApplicationBuilder app, ReDocOptions options) + ReDocOptions options; + using (var scope = app.ApplicationServices.CreateScope()) { - return app.UseMiddleware(options); + options = scope.ServiceProvider.GetRequiredService>().Value; + setupAction?.Invoke(options); } - /// - /// Register the Redoc middleware with optional setup action for DI-injected options - /// - public static IApplicationBuilder UseReDoc( - this IApplicationBuilder app, - Action setupAction = null) + // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults + if (options.SpecUrl == null) { - ReDocOptions options; - using (var scope = app.ApplicationServices.CreateScope()) - { - options = scope.ServiceProvider.GetRequiredService>().Value; - setupAction?.Invoke(options); - } - - // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults - if (options.SpecUrl == null) - { - options.SpecUrl = "../swagger/v1/swagger.json"; - } - - return app.UseReDoc(options); + options.SpecUrl = "../swagger/v1/swagger.json"; } + + return app.UseReDoc(options); } } diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs index 6c2fb7f728..cca8e7a783 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs @@ -12,173 +12,168 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -#if (NETSTANDARD2_0) +#if !NET using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; #endif -namespace Swashbuckle.AspNetCore.ReDoc +namespace Swashbuckle.AspNetCore.ReDoc; + +internal sealed class ReDocMiddleware { - internal sealed class ReDocMiddleware - { - private const string EmbeddedFileNamespace = "Swashbuckle.AspNetCore.ReDoc.node_modules.redoc.bundles"; + private const string EmbeddedFileNamespace = "Swashbuckle.AspNetCore.ReDoc.node_modules.redoc.bundles"; - private readonly ReDocOptions _options; - private readonly StaticFileMiddleware _staticFileMiddleware; - private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly ReDocOptions _options; + private readonly StaticFileMiddleware _staticFileMiddleware; + private readonly JsonSerializerOptions _jsonSerializerOptions; - public ReDocMiddleware( - RequestDelegate next, - IWebHostEnvironment hostingEnv, - ILoggerFactory loggerFactory, - ReDocOptions options) - { - _options = options ?? new ReDocOptions(); + public ReDocMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + ReDocOptions options) + { + _options = options ?? new ReDocOptions(); - _staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options); + _staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options); - if (options.JsonSerializerOptions != null) - { - _jsonSerializerOptions = options.JsonSerializerOptions; - } -#if !NET6_0_OR_GREATER - else + if (options.JsonSerializerOptions != null) + { + _jsonSerializerOptions = options.JsonSerializerOptions; + } +#if !NET + else + { + _jsonSerializerOptions = new JsonSerializerOptions() { - _jsonSerializerOptions = new JsonSerializerOptions() - { -#if NET5_0_OR_GREATER - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, -#else - IgnoreNullValues = true, -#endif - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false) } - }; - } -#endif + IgnoreNullValues = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false) } + }; } +#endif + } - public async Task Invoke(HttpContext httpContext) + public async Task Invoke(HttpContext httpContext) + { + var httpMethod = httpContext.Request.Method; + + if (HttpMethods.IsGet(httpMethod)) { - var httpMethod = httpContext.Request.Method; + var path = httpContext.Request.Path.Value; - if (HttpMethods.IsGet(httpMethod)) + // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL + if (Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) { - var path = httpContext.Request.Path.Value; - - // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL - if (Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) - { - // Use relative redirect to support proxy environments - var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") - ? "index.html" - : $"{path.Split('/').Last()}/index.html"; - - RespondWithRedirect(httpContext.Response, relativeIndexUrl); - return; - } - - var match = Regex.Match(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?(index.(html|css|js))$", RegexOptions.IgnoreCase); - - if (match.Success) - { - await RespondWithFile(httpContext.Response, match.Groups[1].Value); - return; - } + // Use relative redirect to support proxy environments + var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") + ? "index.html" + : $"{path.Split('/').Last()}/index.html"; + + RespondWithRedirect(httpContext.Response, relativeIndexUrl); + return; } - await _staticFileMiddleware.Invoke(httpContext); - } + var match = Regex.Match(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?(index.(html|css|js))$", RegexOptions.IgnoreCase); - private static StaticFileMiddleware CreateStaticFileMiddleware( - RequestDelegate next, - IWebHostEnvironment hostingEnv, - ILoggerFactory loggerFactory, - ReDocOptions options) - { - var staticFileOptions = new StaticFileOptions + if (match.Success) { - RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}", - FileProvider = new EmbeddedFileProvider(typeof(ReDocMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace), - }; - - return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory); + await RespondWithFile(httpContext.Response, match.Groups[1].Value); + return; + } } - private static void RespondWithRedirect(HttpResponse response, string location) + await _staticFileMiddleware.Invoke(httpContext); + } + + private static StaticFileMiddleware CreateStaticFileMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + ReDocOptions options) + { + var staticFileOptions = new StaticFileOptions { - response.StatusCode = StatusCodes.Status301MovedPermanently; -#if NET6_0_OR_GREATER - response.Headers.Location = location; + RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}", + FileProvider = new EmbeddedFileProvider(typeof(ReDocMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace), + }; + + return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory); + } + + private static void RespondWithRedirect(HttpResponse response, string location) + { + response.StatusCode = StatusCodes.Status301MovedPermanently; +#if NET + response.Headers.Location = location; #else - response.Headers["Location"] = location; + response.Headers["Location"] = location; #endif - } + } - private async Task RespondWithFile(HttpResponse response, string fileName) - { - response.StatusCode = 200; + private async Task RespondWithFile(HttpResponse response, string fileName) + { + response.StatusCode = 200; - Stream stream; + Stream stream; - switch (fileName) - { - case "index.css": - response.ContentType = "text/css"; - stream = ResourceHelper.GetEmbeddedResource(fileName); - break; - case "index.js": - response.ContentType = "application/javascript;charset=utf-8"; - stream = ResourceHelper.GetEmbeddedResource(fileName); - break; - default: - response.ContentType = "text/html;charset=utf-8"; - stream = _options.IndexStream(); - break; - } + switch (fileName) + { + case "index.css": + response.ContentType = "text/css"; + stream = ResourceHelper.GetEmbeddedResource(fileName); + break; + case "index.js": + response.ContentType = "application/javascript;charset=utf-8"; + stream = ResourceHelper.GetEmbeddedResource(fileName); + break; + default: + response.ContentType = "text/html;charset=utf-8"; + stream = _options.IndexStream(); + break; + } - using (stream) + using (stream) + { + // Inject arguments before writing to response + var content = new StringBuilder(new StreamReader(stream).ReadToEnd()); + foreach (var entry in GetIndexArguments()) { - // Inject arguments before writing to response - var content = new StringBuilder(new StreamReader(stream).ReadToEnd()); - foreach (var entry in GetIndexArguments()) - { - content.Replace(entry.Key, entry.Value); - } - - await response.WriteAsync(content.ToString(), Encoding.UTF8); + content.Replace(entry.Key, entry.Value); } + + await response.WriteAsync(content.ToString(), Encoding.UTF8); } + } -#if NET5_0_OR_GREATER - [UnconditionalSuppressMessage( - "AOT", - "IL2026:RequiresUnreferencedCode", - Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] - [UnconditionalSuppressMessage( - "AOT", - "IL3050:RequiresDynamicCode", - Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] +#if NET + [UnconditionalSuppressMessage( + "AOT", + "IL2026:RequiresUnreferencedCode", + Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050:RequiresDynamicCode", + Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] #endif - private Dictionary GetIndexArguments() - { - string configObject = null; + private Dictionary GetIndexArguments() + { + string configObject = null; -#if NET6_0_OR_GREATER - if (_jsonSerializerOptions is null) - { - configObject = JsonSerializer.Serialize(_options.ConfigObject, ReDocOptionsJsonContext.Default.ConfigObject); - } +#if NET + if (_jsonSerializerOptions is null) + { + configObject = JsonSerializer.Serialize(_options.ConfigObject, ReDocOptionsJsonContext.Default.ConfigObject); + } #endif - configObject ??= JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions); + configObject ??= JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions); - return new Dictionary() - { - { "%(DocumentTitle)", _options.DocumentTitle }, - { "%(HeadContent)", _options.HeadContent }, - { "%(SpecUrl)", _options.SpecUrl }, - { "%(ConfigObject)", configObject }, - }; - } + return new Dictionary() + { + { "%(DocumentTitle)", _options.DocumentTitle }, + { "%(HeadContent)", _options.HeadContent }, + { "%(SpecUrl)", _options.SpecUrl }, + { "%(ConfigObject)", configObject }, + }; } } diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptions.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptions.cs index b9e799310e..0dcae7389c 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptions.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptions.cs @@ -1,118 +1,117 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Swashbuckle.AspNetCore.ReDoc +namespace Swashbuckle.AspNetCore.ReDoc; + +public class ReDocOptions +{ + /// + /// Gets or sets a route prefix for accessing the redoc page + /// + public string RoutePrefix { get; set; } = "api-docs"; + + /// + /// Gets or sets a Stream function for retrieving the redoc page + /// + public Func IndexStream { get; set; } = () => ResourceHelper.GetEmbeddedResource("index.html"); + + /// + /// Gets or sets a title for the redoc page + /// + public string DocumentTitle { get; set; } = "API Docs"; + + /// + /// Gets or sets additional content to place in the head of the redoc page + /// + public string HeadContent { get; set; } = ""; + + /// + /// Gets or sets the Swagger JSON endpoint. Can be fully-qualified or relative to the redoc page + /// + public string SpecUrl { get; set; } = null; + + /// + /// Gets or sets the to use. + /// + public ConfigObject ConfigObject { get; set; } = new ConfigObject(); + + /// + /// Gets or sets the optional to use. + /// + public JsonSerializerOptions JsonSerializerOptions { get; set; } +} + +public class ConfigObject { - public class ReDocOptions - { - /// - /// Gets or sets a route prefix for accessing the redoc page - /// - public string RoutePrefix { get; set; } = "api-docs"; - - /// - /// Gets or sets a Stream function for retrieving the redoc page - /// - public Func IndexStream { get; set; } = () => ResourceHelper.GetEmbeddedResource("index.html"); - - /// - /// Gets or sets a title for the redoc page - /// - public string DocumentTitle { get; set; } = "API Docs"; - - /// - /// Gets or sets additional content to place in the head of the redoc page - /// - public string HeadContent { get; set; } = ""; - - /// - /// Gets or sets the Swagger JSON endpoint. Can be fully-qualified or relative to the redoc page - /// - public string SpecUrl { get; set; } = null; - - /// - /// Gets or sets the to use. - /// - public ConfigObject ConfigObject { get; set; } = new ConfigObject(); - - /// - /// Gets or sets the optional to use. - /// - public JsonSerializerOptions JsonSerializerOptions { get; set; } - } - - public class ConfigObject - { - /// - /// If set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. - /// Disabled by default for performance reasons. Enable this option if you work with untrusted user data! - /// - public bool UntrustedSpec { get; set; } = false; - - /// - /// If set, specifies a vertical scroll-offset in pixels. - /// This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc - /// - public int? ScrollYOffset { get; set; } - - /// - /// If set, the protocol and hostname is not shown in the operation definition - /// - public bool HideHostname { get; set; } = false; - - /// - /// Do not show "Download" spec button. THIS DOESN'T MAKE YOUR SPEC PRIVATE, it just hides the button - /// - public bool HideDownloadButton { get; set; } = false; - - /// - /// Specify which responses to expand by default by response codes. - /// Values should be passed as comma-separated list without spaces e.g. "200,201". Special value "all" expands all responses by default. - /// Be careful: this option can slow-down documentation rendering time. - /// - public string ExpandResponses { get; set; } = "all"; - - /// - /// Show required properties first ordered in the same order as in required array - /// - public bool RequiredPropsFirst { get; set; } = false; - - /// - /// Do not inject Authentication section automatically - /// - public bool NoAutoAuth { get; set; } = false; - - /// - /// Show path link and HTTP verb in the middle panel instead of the right one - /// - public bool PathInMiddlePanel { get; set; } = false; - - /// - /// Do not show loading animation. Useful for small docs - /// - public bool HideLoading { get; set; } = false; - - /// - /// Use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs) - /// - public bool NativeScrollbars { get; set; } = false; - - /// - /// Disable search indexing and search box - /// - public bool DisableSearch { get; set; } = false; - - /// - /// Show only required fields in request samples - /// - public bool OnlyRequiredInSamples { get; set; } = false; - - /// - /// Sort properties alphabetically - /// - public bool SortPropsAlphabetically { get; set; } = false; - - [JsonExtensionData] - public Dictionary AdditionalItems { get; set; } = new Dictionary(); - } + /// + /// If set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. + /// Disabled by default for performance reasons. Enable this option if you work with untrusted user data! + /// + public bool UntrustedSpec { get; set; } = false; + + /// + /// If set, specifies a vertical scroll-offset in pixels. + /// This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc + /// + public int? ScrollYOffset { get; set; } + + /// + /// If set, the protocol and hostname is not shown in the operation definition + /// + public bool HideHostname { get; set; } = false; + + /// + /// Do not show "Download" spec button. THIS DOESN'T MAKE YOUR SPEC PRIVATE, it just hides the button + /// + public bool HideDownloadButton { get; set; } = false; + + /// + /// Specify which responses to expand by default by response codes. + /// Values should be passed as comma-separated list without spaces e.g. "200,201". Special value "all" expands all responses by default. + /// Be careful: this option can slow-down documentation rendering time. + /// + public string ExpandResponses { get; set; } = "all"; + + /// + /// Show required properties first ordered in the same order as in required array + /// + public bool RequiredPropsFirst { get; set; } = false; + + /// + /// Do not inject Authentication section automatically + /// + public bool NoAutoAuth { get; set; } = false; + + /// + /// Show path link and HTTP verb in the middle panel instead of the right one + /// + public bool PathInMiddlePanel { get; set; } = false; + + /// + /// Do not show loading animation. Useful for small docs + /// + public bool HideLoading { get; set; } = false; + + /// + /// Use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs) + /// + public bool NativeScrollbars { get; set; } = false; + + /// + /// Disable search indexing and search box + /// + public bool DisableSearch { get; set; } = false; + + /// + /// Show only required fields in request samples + /// + public bool OnlyRequiredInSamples { get; set; } = false; + + /// + /// Sort properties alphabetically + /// + public bool SortPropsAlphabetically { get; set; } = false; + + [JsonExtensionData] + public Dictionary AdditionalItems { get; set; } = new Dictionary(); } diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptionsExtensions.cs index e44b95c77a..c1da2c1133 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptionsExtensions.cs @@ -1,141 +1,140 @@ using System.Text; using Swashbuckle.AspNetCore.ReDoc; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public static class ReDocOptionsExtensions { - public static class ReDocOptionsExtensions + /// + /// Injects additional CSS stylesheets into the index.html page + /// + /// + /// A path to the stylesheet - i.e. the link "href" attribute + /// The target media - i.e. the link "media" attribute + public static void InjectStylesheet(this ReDocOptions options, string path, string media = "screen") + { + var builder = new StringBuilder(options.HeadContent); + builder.AppendLine($""); + options.HeadContent = builder.ToString(); + } + + /// + /// Sets the Swagger JSON endpoint. Can be fully-qualified or relative to the redoc page + /// + public static void SpecUrl(this ReDocOptions options, string url) + { + options.SpecUrl = url; + } + + /// + /// If enabled, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. + /// Disabled by default for performance reasons. Enable this option if you work with untrusted user data! + /// + /// + public static void EnableUntrustedSpec(this ReDocOptions options) + { + options.ConfigObject.UntrustedSpec = true; + } + + /// + /// Specifies a vertical scroll-offset in pixels. + /// This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc + /// + /// + /// + public static void ScrollYOffset(this ReDocOptions options, int offset) + { + options.ConfigObject.ScrollYOffset = offset; + } + + /// + /// Controls if the protocol and hostname is shown in the operation definition + /// + public static void HideHostname(this ReDocOptions options) + { + options.ConfigObject.HideHostname = true; + } + + /// + /// Do not show "Download" spec button. THIS DOESN'T MAKE YOUR SPEC PRIVATE, it just hides the button + /// + public static void HideDownloadButton(this ReDocOptions options) + { + options.ConfigObject.HideDownloadButton = true; + } + + /// + /// Specify which responses to expand by default by response codes. + /// Values should be passed as comma-separated list without spaces e.g. "200,201". Special value "all" expands all responses by default. + /// Be careful: this option can slow-down documentation rendering time. + /// Default is "all" + /// + public static void ExpandResponses(this ReDocOptions options, string responses) + { + options.ConfigObject.ExpandResponses = responses; + } + + /// + /// Show required properties first ordered in the same order as in required array + /// + public static void RequiredPropsFirst(this ReDocOptions options) + { + options.ConfigObject.RequiredPropsFirst = true; + } + + /// + /// Do not inject Authentication section automatically + /// + public static void NoAutoAuth(this ReDocOptions options) + { + options.ConfigObject.NoAutoAuth = true; + } + + /// + /// Show path link and HTTP verb in the middle panel instead of the right one + /// + public static void PathInMiddlePanel(this ReDocOptions options) + { + options.ConfigObject.PathInMiddlePanel = true; + } + + /// + /// Do not show loading animation. Useful for small docs + /// + public static void HideLoading(this ReDocOptions options) + { + options.ConfigObject.HideLoading = true; + } + + /// + /// Use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs) + /// + public static void NativeScrollbars(this ReDocOptions options) + { + options.ConfigObject.NativeScrollbars = true; + } + + /// + /// Disable search indexing and search box + /// + public static void DisableSearch(this ReDocOptions options) + { + options.ConfigObject.DisableSearch = true; + } + + /// + /// Show only required fields in request samples + /// + public static void OnlyRequiredInSamples(this ReDocOptions options) + { + options.ConfigObject.OnlyRequiredInSamples = true; + } + + /// + /// Sort properties alphabetically + /// + public static void SortPropsAlphabetically(this ReDocOptions options) { - /// - /// Injects additional CSS stylesheets into the index.html page - /// - /// - /// A path to the stylesheet - i.e. the link "href" attribute - /// The target media - i.e. the link "media" attribute - public static void InjectStylesheet(this ReDocOptions options, string path, string media = "screen") - { - var builder = new StringBuilder(options.HeadContent); - builder.AppendLine($""); - options.HeadContent = builder.ToString(); - } - - /// - /// Sets the Swagger JSON endpoint. Can be fully-qualified or relative to the redoc page - /// - public static void SpecUrl(this ReDocOptions options, string url) - { - options.SpecUrl = url; - } - - /// - /// If enabled, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. - /// Disabled by default for performance reasons. Enable this option if you work with untrusted user data! - /// - /// - public static void EnableUntrustedSpec(this ReDocOptions options) - { - options.ConfigObject.UntrustedSpec = true; - } - - /// - /// Specifies a vertical scroll-offset in pixels. - /// This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc - /// - /// - /// - public static void ScrollYOffset(this ReDocOptions options, int offset) - { - options.ConfigObject.ScrollYOffset = offset; - } - - /// - /// Controls if the protocol and hostname is shown in the operation definition - /// - public static void HideHostname(this ReDocOptions options) - { - options.ConfigObject.HideHostname = true; - } - - /// - /// Do not show "Download" spec button. THIS DOESN'T MAKE YOUR SPEC PRIVATE, it just hides the button - /// - public static void HideDownloadButton(this ReDocOptions options) - { - options.ConfigObject.HideDownloadButton = true; - } - - /// - /// Specify which responses to expand by default by response codes. - /// Values should be passed as comma-separated list without spaces e.g. "200,201". Special value "all" expands all responses by default. - /// Be careful: this option can slow-down documentation rendering time. - /// Default is "all" - /// - public static void ExpandResponses(this ReDocOptions options, string responses) - { - options.ConfigObject.ExpandResponses = responses; - } - - /// - /// Show required properties first ordered in the same order as in required array - /// - public static void RequiredPropsFirst(this ReDocOptions options) - { - options.ConfigObject.RequiredPropsFirst = true; - } - - /// - /// Do not inject Authentication section automatically - /// - public static void NoAutoAuth(this ReDocOptions options) - { - options.ConfigObject.NoAutoAuth = true; - } - - /// - /// Show path link and HTTP verb in the middle panel instead of the right one - /// - public static void PathInMiddlePanel(this ReDocOptions options) - { - options.ConfigObject.PathInMiddlePanel = true; - } - - /// - /// Do not show loading animation. Useful for small docs - /// - public static void HideLoading(this ReDocOptions options) - { - options.ConfigObject.HideLoading = true; - } - - /// - /// Use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs) - /// - public static void NativeScrollbars(this ReDocOptions options) - { - options.ConfigObject.NativeScrollbars = true; - } - - /// - /// Disable search indexing and search box - /// - public static void DisableSearch(this ReDocOptions options) - { - options.ConfigObject.DisableSearch = true; - } - - /// - /// Show only required fields in request samples - /// - public static void OnlyRequiredInSamples(this ReDocOptions options) - { - options.ConfigObject.OnlyRequiredInSamples = true; - } - - /// - /// Sort properties alphabetically - /// - public static void SortPropsAlphabetically(this ReDocOptions options) - { - options.ConfigObject.SortPropsAlphabetically = true; - } + options.ConfigObject.SortPropsAlphabetically = true; } } diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptionsJsonContext.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptionsJsonContext.cs index 241f7da19f..1ce1726d1e 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptionsJsonContext.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocOptionsJsonContext.cs @@ -1,5 +1,4 @@ -#if NET6_0_OR_GREATER -using System; +#if NET using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -29,15 +28,11 @@ namespace Swashbuckle.AspNetCore.ReDoc; [JsonSerializable(typeof(JsonArray))] [JsonSerializable(typeof(JsonObject))] [JsonSerializable(typeof(JsonDocument))] -#if NET7_0_OR_GREATER [JsonSerializable(typeof(DateOnly))] [JsonSerializable(typeof(TimeOnly))] -#endif -#if NET8_0_OR_GREATER [JsonSerializable(typeof(Half))] [JsonSerializable(typeof(Int128))] [JsonSerializable(typeof(UInt128))] -#endif [JsonSourceGenerationOptions( DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] diff --git a/src/Swashbuckle.AspNetCore.Swagger/DependencyInjection/SwaggerBuilderExtensions.cs b/src/Swashbuckle.AspNetCore.Swagger/DependencyInjection/SwaggerBuilderExtensions.cs index db9c813e47..54953b148c 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/DependencyInjection/SwaggerBuilderExtensions.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/DependencyInjection/SwaggerBuilderExtensions.cs @@ -1,5 +1,4 @@ -#if (!NETSTANDARD2_0) -using System.Linq; +#if NET using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; @@ -18,7 +17,7 @@ public static class SwaggerBuilderExtensions /// public static IApplicationBuilder UseSwagger(this IApplicationBuilder app, SwaggerOptions options) { -#if (!NETSTANDARD2_0) +#if NET return app.UseMiddleware(options, app.ApplicationServices.GetRequiredService()); #else return app.UseMiddleware(options); @@ -42,7 +41,7 @@ public static IApplicationBuilder UseSwagger( return app.UseSwagger(options); } -#if (!NETSTANDARD2_0) +#if NET public static IEndpointConventionBuilder MapSwagger( this IEndpointRouteBuilder endpoints, string pattern = SwaggerOptions.DefaultRouteTemplate, diff --git a/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs b/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs index 3789f9fe11..8224c06638 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs @@ -1,12 +1,11 @@ using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.Swagger +namespace Swashbuckle.AspNetCore.Swagger; + +public interface IAsyncSwaggerProvider { - public interface IAsyncSwaggerProvider - { - Task GetSwaggerAsync( - string documentName, - string host = null, - string basePath = null); - } + Task GetSwaggerAsync( + string documentName, + string host = null, + string basePath = null); } diff --git a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentMetadataProvider.cs b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentMetadataProvider.cs index 6a5c3ad644..879a77b373 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentMetadataProvider.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentMetadataProvider.cs @@ -1,7 +1,6 @@ -namespace Swashbuckle.AspNetCore.Swagger +namespace Swashbuckle.AspNetCore.Swagger; + +public interface ISwaggerDocumentMetadataProvider { - public interface ISwaggerDocumentMetadataProvider - { - IList GetDocumentNames(); - } + IList GetDocumentNames(); } diff --git a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentSerializer.cs b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentSerializer.cs index 1601b8b656..e69c1e3ffc 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentSerializer.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentSerializer.cs @@ -2,19 +2,18 @@ using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; -namespace Swashbuckle.AspNetCore.Swagger +namespace Swashbuckle.AspNetCore.Swagger; + +/// +/// Provide an implementation for this interface if you wish to customize how the OpenAPI document is written. +/// +public interface ISwaggerDocumentSerializer { /// - /// Provide an implementation for this interface if you wish to customize how the OpenAPI document is written. + /// Serializes an OpenAPI document. /// - public interface ISwaggerDocumentSerializer - { - /// - /// Serializes an OpenAPI document. - /// - /// The OpenAPI document that should be serialized. - /// The writer to which the document needs to be written. - /// The OpenAPI specification version to serialize as. - void SerializeDocument(OpenApiDocument document, IOpenApiWriter writer, OpenApiSpecVersion specVersion); - } + /// The OpenAPI document that should be serialized. + /// The writer to which the document needs to be written. + /// The OpenAPI specification version to serialize as. + void SerializeDocument(OpenApiDocument document, IOpenApiWriter writer, OpenApiSpecVersion specVersion); } diff --git a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs index 7101a060b9..066884fac4 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs @@ -1,21 +1,11 @@ using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.Swagger -{ - public interface ISwaggerProvider - { - OpenApiDocument GetSwagger( - string documentName, - string host = null, - string basePath = null); - } +namespace Swashbuckle.AspNetCore.Swagger; - public class UnknownSwaggerDocument : InvalidOperationException - { - public UnknownSwaggerDocument(string documentName, IEnumerable knownDocuments) - : base(string.Format("Unknown Swagger document - \"{0}\". Known Swagger documents: {1}", - documentName, - string.Join(",", knownDocuments?.Select(x => $"\"{x}\"")))) - {} - } +public interface ISwaggerProvider +{ + OpenApiDocument GetSwagger( + string documentName, + string host = null, + string basePath = null); } diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs index 258a4214aa..4c860de387 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs @@ -2,38 +2,37 @@ using Microsoft.OpenApi; using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.Swagger +namespace Swashbuckle.AspNetCore.Swagger; + +public class SwaggerEndpointOptions { - public class SwaggerEndpointOptions + public SwaggerEndpointOptions() { - public SwaggerEndpointOptions() - { - PreSerializeFilters = []; - OpenApiVersion = OpenApiSpecVersion.OpenApi3_0; - } + PreSerializeFilters = []; + OpenApiVersion = OpenApiSpecVersion.OpenApi3_0; + } - /// - /// Return Swagger JSON/YAML in the V2.0 format rather than V3.0. - /// - [Obsolete($"This property will be removed in a future version of Swashbuckle.AspNetCore. Use the {nameof(OpenApiVersion)} property instead.")] - public bool SerializeAsV2 - { - get => OpenApiVersion == OpenApiSpecVersion.OpenApi2_0; - set => OpenApiVersion = value ? OpenApiSpecVersion.OpenApi2_0 : OpenApiSpecVersion.OpenApi3_0; - } + /// + /// Return Swagger JSON/YAML in the V2.0 format rather than V3.0. + /// + [Obsolete($"This property will be removed in a future version of Swashbuckle.AspNetCore. Use the {nameof(OpenApiVersion)} property instead.")] + public bool SerializeAsV2 + { + get => OpenApiVersion == OpenApiSpecVersion.OpenApi2_0; + set => OpenApiVersion = value ? OpenApiSpecVersion.OpenApi2_0 : OpenApiSpecVersion.OpenApi3_0; + } - /// - /// Gets or sets the OpenAPI (Swagger) document version to use. - /// - /// - /// The default value is . - /// - public OpenApiSpecVersion OpenApiVersion { get; set; } + /// + /// Gets or sets the OpenAPI (Swagger) document version to use. + /// + /// + /// The default value is . + /// + public OpenApiSpecVersion OpenApiVersion { get; set; } - /// - /// Actions that can be applied SwaggerDocument's before they're serialized to JSON. - /// Useful for setting metadata that's derived from the current request - /// - public List> PreSerializeFilters { get; private set; } - } + /// + /// Actions that can be applied SwaggerDocument's before they're serialized to JSON. + /// Useful for setting metadata that's derived from the current request + /// + public List> PreSerializeFilters { get; private set; } } diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs index 1e63306458..88c3413409 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs @@ -2,7 +2,7 @@ using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -#if !NETSTANDARD +#if NET using Microsoft.AspNetCore.Routing.Patterns; #endif using Microsoft.AspNetCore.Routing.Template; @@ -10,188 +10,187 @@ using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; -namespace Swashbuckle.AspNetCore.Swagger +namespace Swashbuckle.AspNetCore.Swagger; + +internal sealed class SwaggerMiddleware { - internal sealed class SwaggerMiddleware + private static readonly Encoding UTF8WithoutBom = new UTF8Encoding(false); + + private readonly RequestDelegate _next; + private readonly SwaggerOptions _options; + private readonly TemplateMatcher _requestMatcher; +#if NET + private readonly TemplateBinder _templateBinder; +#endif + + public SwaggerMiddleware( + RequestDelegate next, + SwaggerOptions options) { - private static readonly Encoding UTF8WithoutBom = new UTF8Encoding(false); + _next = next; + _options = options ?? new SwaggerOptions(); + _requestMatcher = new TemplateMatcher(TemplateParser.Parse(_options.RouteTemplate), []); + } - private readonly RequestDelegate _next; - private readonly SwaggerOptions _options; - private readonly TemplateMatcher _requestMatcher; -#if !NETSTANDARD - private readonly TemplateBinder _templateBinder; +#if NET + [ActivatorUtilitiesConstructor] + public SwaggerMiddleware( + RequestDelegate next, + SwaggerOptions options, + TemplateBinderFactory templateBinderFactory) : this(next, options) + { + _templateBinder = templateBinderFactory.Create(RoutePatternFactory.Parse(_options.RouteTemplate)); + } #endif - public SwaggerMiddleware( - RequestDelegate next, - SwaggerOptions options) + public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) + { + if (!RequestingSwaggerDocument(httpContext.Request, out string documentName, out string extension)) { - _next = next; - _options = options ?? new SwaggerOptions(); - _requestMatcher = new TemplateMatcher(TemplateParser.Parse(_options.RouteTemplate), []); + await _next(httpContext); + return; } -#if !NETSTANDARD - [ActivatorUtilitiesConstructor] - public SwaggerMiddleware( - RequestDelegate next, - SwaggerOptions options, - TemplateBinderFactory templateBinderFactory) : this(next, options) + try { - _templateBinder = templateBinderFactory.Create(RoutePatternFactory.Parse(_options.RouteTemplate)); - } -#endif + var basePath = httpContext.Request.PathBase.HasValue + ? httpContext.Request.PathBase.Value + : null; - public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) - { - if (!RequestingSwaggerDocument(httpContext.Request, out string documentName, out string extension)) + OpenApiDocument swagger; + var asyncSwaggerProvider = httpContext.RequestServices.GetService(); + + if (asyncSwaggerProvider is not null) { - await _next(httpContext); - return; + swagger = await asyncSwaggerProvider.GetSwaggerAsync( + documentName: documentName, + host: null, + basePath: basePath); } - - try + else { - var basePath = httpContext.Request.PathBase.HasValue - ? httpContext.Request.PathBase.Value - : null; - - OpenApiDocument swagger; - var asyncSwaggerProvider = httpContext.RequestServices.GetService(); - - if (asyncSwaggerProvider is not null) - { - swagger = await asyncSwaggerProvider.GetSwaggerAsync( - documentName: documentName, - host: null, - basePath: basePath); - } - else - { - swagger = swaggerProvider.GetSwagger( - documentName: documentName, - host: null, - basePath: basePath); - } + swagger = swaggerProvider.GetSwagger( + documentName: documentName, + host: null, + basePath: basePath); + } - // One last opportunity to modify the Swagger Document - this time with request context - foreach (var filter in _options.PreSerializeFilters) - { - filter(swagger, httpContext.Request); - } + // One last opportunity to modify the Swagger Document - this time with request context + foreach (var filter in _options.PreSerializeFilters) + { + filter(swagger, httpContext.Request); + } - if (extension is ".yaml" or ".yml") - { - await RespondWithSwaggerYaml(httpContext.Response, swagger); - } - else - { - await RespondWithSwaggerJson(httpContext.Response, swagger); - } + if (extension is ".yaml" or ".yml") + { + await RespondWithSwaggerYaml(httpContext.Response, swagger); } - catch (UnknownSwaggerDocument) + else { - httpContext.Response.StatusCode = 404; + await RespondWithSwaggerJson(httpContext.Response, swagger); } } + catch (UnknownSwaggerDocument) + { + httpContext.Response.StatusCode = 404; + } + } + + private bool RequestingSwaggerDocument(HttpRequest request, out string documentName, out string extension) + { + documentName = null; + extension = null; - private bool RequestingSwaggerDocument(HttpRequest request, out string documentName, out string extension) + if (!HttpMethods.IsGet(request.Method)) { - documentName = null; - extension = null; + return false; + } - if (!HttpMethods.IsGet(request.Method)) + var routeValues = new RouteValueDictionary(); + if (_requestMatcher.TryMatch(request.Path, routeValues)) + { +#if NET + if (_templateBinder != null && !_templateBinder.TryProcessConstraints(request.HttpContext, routeValues, out _, out _)) { return false; } - - var routeValues = new RouteValueDictionary(); - if (_requestMatcher.TryMatch(request.Path, routeValues)) +#endif + if (routeValues.TryGetValue("documentName", out var documentNameObject) && documentNameObject is string documentNameString) { -#if !NETSTANDARD - if (_templateBinder != null && !_templateBinder.TryProcessConstraints(request.HttpContext, routeValues, out _, out _)) + documentName = documentNameString; + if (routeValues.TryGetValue("extension", out var extensionObject)) { - return false; + extension = $".{extensionObject}"; } -#endif - if (routeValues.TryGetValue("documentName", out var documentNameObject) && documentNameObject is string documentNameString) + else { - documentName = documentNameString; - if (routeValues.TryGetValue("extension", out var extensionObject)) - { - extension = $".{extensionObject}"; - } - else - { - extension = Path.GetExtension(request.Path.Value); - } - return true; + extension = Path.GetExtension(request.Path.Value); } + return true; } - - return false; } - private async Task RespondWithSwaggerJson(HttpResponse response, OpenApiDocument swagger) + return false; + } + + private async Task RespondWithSwaggerJson(HttpResponse response, OpenApiDocument swagger) + { + string json; + + using (var textWriter = new StringWriter(CultureInfo.InvariantCulture)) { - string json; + var openApiWriter = new OpenApiJsonWriter(textWriter); - using (var textWriter = new StringWriter(CultureInfo.InvariantCulture)) - { - var openApiWriter = new OpenApiJsonWriter(textWriter); + SerializeDocument(swagger, openApiWriter); - SerializeDocument(swagger, openApiWriter); + json = textWriter.ToString(); + } - json = textWriter.ToString(); - } + response.StatusCode = 200; + response.ContentType = "application/json;charset=utf-8"; - response.StatusCode = 200; - response.ContentType = "application/json;charset=utf-8"; + await response.WriteAsync(json, UTF8WithoutBom); + } - await response.WriteAsync(json, UTF8WithoutBom); - } + private async Task RespondWithSwaggerYaml(HttpResponse response, OpenApiDocument swagger) + { + string yaml; - private async Task RespondWithSwaggerYaml(HttpResponse response, OpenApiDocument swagger) + using (var textWriter = new StringWriter(CultureInfo.InvariantCulture)) { - string yaml; + var openApiWriter = new OpenApiYamlWriter(textWriter); - using (var textWriter = new StringWriter(CultureInfo.InvariantCulture)) - { - var openApiWriter = new OpenApiYamlWriter(textWriter); + SerializeDocument(swagger, openApiWriter); - SerializeDocument(swagger, openApiWriter); + yaml = textWriter.ToString(); + } - yaml = textWriter.ToString(); - } + response.StatusCode = 200; + response.ContentType = "text/yaml;charset=utf-8"; - response.StatusCode = 200; - response.ContentType = "text/yaml;charset=utf-8"; + await response.WriteAsync(yaml, UTF8WithoutBom); + } - await response.WriteAsync(yaml, UTF8WithoutBom); + private void SerializeDocument( + OpenApiDocument document, + IOpenApiWriter writer) + { + if (_options.CustomDocumentSerializer != null) + { + _options.CustomDocumentSerializer.SerializeDocument(document, writer, _options.OpenApiVersion); } - - private void SerializeDocument( - OpenApiDocument document, - IOpenApiWriter writer) + else { - if (_options.CustomDocumentSerializer != null) - { - _options.CustomDocumentSerializer.SerializeDocument(document, writer, _options.OpenApiVersion); - } - else + switch (_options.OpenApiVersion) { - switch (_options.OpenApiVersion) - { - case Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0: - document.SerializeAsV2(writer); - break; - - case Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0: - default: - document.SerializeAsV3(writer); - break; - } + case Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0: + document.SerializeAsV2(writer); + break; + + case Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0: + default: + document.SerializeAsV3(writer); + break; } } } diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs index 0e0fc264fd..a3bde0b634 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs @@ -2,51 +2,50 @@ using Microsoft.OpenApi; using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.Swagger +namespace Swashbuckle.AspNetCore.Swagger; + +public class SwaggerOptions { - public class SwaggerOptions + internal const string DefaultRouteTemplate = "/swagger/{documentName}/swagger.{extension:regex(^(json|ya?ml)$)}"; + + public SwaggerOptions() { - internal const string DefaultRouteTemplate = "/swagger/{documentName}/swagger.{extension:regex(^(json|ya?ml)$)}"; - - public SwaggerOptions() - { - PreSerializeFilters = []; - OpenApiVersion = OpenApiSpecVersion.OpenApi3_0; - } - - /// - /// Sets a custom route for the Swagger JSON/YAML endpoint(s). Must include the {documentName} parameter - /// - public string RouteTemplate { get; set; } = DefaultRouteTemplate; - - /// - /// Return Swagger JSON/YAML in the V2.0 format rather than V3.0. - /// - [Obsolete($"This property will be removed in a future version of Swashbuckle.AspNetCore. Use the {nameof(OpenApiVersion)} property instead.")] - public bool SerializeAsV2 - { - get => OpenApiVersion == OpenApiSpecVersion.OpenApi2_0; - set => OpenApiVersion = value ? OpenApiSpecVersion.OpenApi2_0 : OpenApiSpecVersion.OpenApi3_0; - } - - /// - /// Gets or sets the OpenAPI (Swagger) document version to use. - /// - /// - /// The default value is . - /// - public OpenApiSpecVersion OpenApiVersion { get; set; } - - /// - /// Gets or sets an optional custom implementation to use to serialize Swagger documents. - /// - /// For the CLI tool to be able to use this, this needs to be configured for use in the service collection of your application. - public ISwaggerDocumentSerializer CustomDocumentSerializer { get; set; } - - /// - /// Actions that can be applied to an OpenApiDocument before it's serialized. - /// Useful for setting metadata that's derived from the current request. - /// - public List> PreSerializeFilters { get; private set; } + PreSerializeFilters = []; + OpenApiVersion = OpenApiSpecVersion.OpenApi3_0; } + + /// + /// Sets a custom route for the Swagger JSON/YAML endpoint(s). Must include the {documentName} parameter + /// + public string RouteTemplate { get; set; } = DefaultRouteTemplate; + + /// + /// Return Swagger JSON/YAML in the V2.0 format rather than V3.0. + /// + [Obsolete($"This property will be removed in a future version of Swashbuckle.AspNetCore. Use the {nameof(OpenApiVersion)} property instead.")] + public bool SerializeAsV2 + { + get => OpenApiVersion == OpenApiSpecVersion.OpenApi2_0; + set => OpenApiVersion = value ? OpenApiSpecVersion.OpenApi2_0 : OpenApiSpecVersion.OpenApi3_0; + } + + /// + /// Gets or sets the OpenAPI (Swagger) document version to use. + /// + /// + /// The default value is . + /// + public OpenApiSpecVersion OpenApiVersion { get; set; } + + /// + /// Gets or sets an optional custom implementation to use to serialize Swagger documents. + /// + /// For the CLI tool to be able to use this, this needs to be configured for use in the service collection of your application. + public ISwaggerDocumentSerializer CustomDocumentSerializer { get; set; } + + /// + /// Actions that can be applied to an OpenApiDocument before it's serialized. + /// Useful for setting metadata that's derived from the current request. + /// + public List> PreSerializeFilters { get; private set; } } diff --git a/src/Swashbuckle.AspNetCore.Swagger/UnknownSwaggerDocument.cs b/src/Swashbuckle.AspNetCore.Swagger/UnknownSwaggerDocument.cs new file mode 100644 index 0000000000..cd5d4aa359 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.Swagger/UnknownSwaggerDocument.cs @@ -0,0 +1,11 @@ +namespace Swashbuckle.AspNetCore.Swagger; + +public class UnknownSwaggerDocument : InvalidOperationException +{ + public UnknownSwaggerDocument(string documentName, IEnumerable knownDocuments) + : base(string.Format("Unknown Swagger document - \"{0}\". Known Swagger documents: {1}", + documentName, + string.Join(",", knownDocuments?.Select(x => $"\"{x}\"")))) + { + } +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/Annotations/SwaggerIgnoreAttribute.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/Annotations/SwaggerIgnoreAttribute.cs index 2ef99e4d29..deec3b58dd 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/Annotations/SwaggerIgnoreAttribute.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/Annotations/SwaggerIgnoreAttribute.cs @@ -1,14 +1,13 @@ // ReSharper disable once CheckNamespace -namespace Swashbuckle.AspNetCore.Annotations -{ - /// - /// Causes the annotated member to be ignored during schema generation. - /// Does not alter serialization behavior. - /// - /// - /// Can be used in combination with - /// to capture and invalidate unsupported properties. - /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property)] - public sealed class SwaggerIgnoreAttribute : Attribute { } -} +namespace Swashbuckle.AspNetCore.Annotations; + +/// +/// Causes the annotated member to be ignored during schema generation. +/// Does not alter serialization behavior. +/// +/// +/// Can be used in combination with +/// to capture and invalidate unsupported properties. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property)] +public sealed class SwaggerIgnoreAttribute : Attribute; diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs index d9298c42e7..a2f38cfca5 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs @@ -2,51 +2,50 @@ using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +internal class ConfigureSchemaGeneratorOptions : IConfigureOptions { - internal class ConfigureSchemaGeneratorOptions : IConfigureOptions - { - private readonly SwaggerGenOptions _swaggerGenOptions; - private readonly IServiceProvider _serviceProvider; + private readonly SwaggerGenOptions _swaggerGenOptions; + private readonly IServiceProvider _serviceProvider; - public ConfigureSchemaGeneratorOptions( - IOptions swaggerGenOptionsAccessor, - IServiceProvider serviceProvider) - { - _swaggerGenOptions = swaggerGenOptionsAccessor.Value; - _serviceProvider = serviceProvider; - } + public ConfigureSchemaGeneratorOptions( + IOptions swaggerGenOptionsAccessor, + IServiceProvider serviceProvider) + { + _swaggerGenOptions = swaggerGenOptionsAccessor.Value; + _serviceProvider = serviceProvider; + } - public void Configure(SchemaGeneratorOptions options) - { - DeepCopy(_swaggerGenOptions.SchemaGeneratorOptions, options); + public void Configure(SchemaGeneratorOptions options) + { + DeepCopy(_swaggerGenOptions.SchemaGeneratorOptions, options); - // Create and add any filters that were specified through the FilterDescriptor lists - _swaggerGenOptions.SchemaFilterDescriptors.ForEach( - filterDescriptor => options.SchemaFilters.Add(GetOrCreateFilter(filterDescriptor))); - } + // Create and add any filters that were specified through the FilterDescriptor lists + _swaggerGenOptions.SchemaFilterDescriptors.ForEach( + filterDescriptor => options.SchemaFilters.Add(GetOrCreateFilter(filterDescriptor))); + } - private void DeepCopy(SchemaGeneratorOptions source, SchemaGeneratorOptions target) - { - target.CustomTypeMappings = new Dictionary>(source.CustomTypeMappings); - target.UseInlineDefinitionsForEnums = source.UseInlineDefinitionsForEnums; - target.SchemaIdSelector = source.SchemaIdSelector; - target.IgnoreObsoleteProperties = source.IgnoreObsoleteProperties; - target.UseAllOfForInheritance = source.UseAllOfForInheritance; - target.UseOneOfForPolymorphism = source.UseOneOfForPolymorphism; - target.SubTypesSelector = source.SubTypesSelector; - target.DiscriminatorNameSelector = source.DiscriminatorNameSelector; - target.DiscriminatorValueSelector = source.DiscriminatorValueSelector; - target.UseAllOfToExtendReferenceSchemas = source.UseAllOfToExtendReferenceSchemas; - target.SupportNonNullableReferenceTypes = source.SupportNonNullableReferenceTypes; - target.NonNullableReferenceTypesAsRequired = source.NonNullableReferenceTypesAsRequired; - target.SchemaFilters = new List(source.SchemaFilters); - } + private void DeepCopy(SchemaGeneratorOptions source, SchemaGeneratorOptions target) + { + target.CustomTypeMappings = new Dictionary>(source.CustomTypeMappings); + target.UseInlineDefinitionsForEnums = source.UseInlineDefinitionsForEnums; + target.SchemaIdSelector = source.SchemaIdSelector; + target.IgnoreObsoleteProperties = source.IgnoreObsoleteProperties; + target.UseAllOfForInheritance = source.UseAllOfForInheritance; + target.UseOneOfForPolymorphism = source.UseOneOfForPolymorphism; + target.SubTypesSelector = source.SubTypesSelector; + target.DiscriminatorNameSelector = source.DiscriminatorNameSelector; + target.DiscriminatorValueSelector = source.DiscriminatorValueSelector; + target.UseAllOfToExtendReferenceSchemas = source.UseAllOfToExtendReferenceSchemas; + target.SupportNonNullableReferenceTypes = source.SupportNonNullableReferenceTypes; + target.NonNullableReferenceTypesAsRequired = source.NonNullableReferenceTypesAsRequired; + target.SchemaFilters = new List(source.SchemaFilters); + } - private TFilter GetOrCreateFilter(FilterDescriptor filterDescriptor) - { - return (TFilter)(filterDescriptor.FilterInstance - ?? ActivatorUtilities.CreateInstance(_serviceProvider, filterDescriptor.Type, filterDescriptor.Arguments)); - } + private TFilter GetOrCreateFilter(FilterDescriptor filterDescriptor) + { + return (TFilter)(filterDescriptor.FilterInstance + ?? ActivatorUtilities.CreateInstance(_serviceProvider, filterDescriptor.Type, filterDescriptor.Arguments)); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs index ace141e64d..d64632b814 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs @@ -3,124 +3,123 @@ using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; -#if NETSTANDARD +#if !NET using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; #endif -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +internal class ConfigureSwaggerGeneratorOptions : IConfigureOptions { - internal class ConfigureSwaggerGeneratorOptions : IConfigureOptions + private readonly SwaggerGenOptions _swaggerGenOptions; + private readonly IServiceProvider _serviceProvider; + private readonly IWebHostEnvironment _hostingEnv; + + public ConfigureSwaggerGeneratorOptions( + IOptions swaggerGenOptionsAccessor, + IServiceProvider serviceProvider, + IWebHostEnvironment hostingEnv) { - private readonly SwaggerGenOptions _swaggerGenOptions; - private readonly IServiceProvider _serviceProvider; - private readonly IWebHostEnvironment _hostingEnv; - - public ConfigureSwaggerGeneratorOptions( - IOptions swaggerGenOptionsAccessor, - IServiceProvider serviceProvider, - IWebHostEnvironment hostingEnv) - { - _swaggerGenOptions = swaggerGenOptionsAccessor.Value; - _serviceProvider = serviceProvider; - _hostingEnv = hostingEnv; - } + _swaggerGenOptions = swaggerGenOptionsAccessor.Value; + _serviceProvider = serviceProvider; + _hostingEnv = hostingEnv; + } - public void Configure(SwaggerGeneratorOptions options) - { - DeepCopy(_swaggerGenOptions.SwaggerGeneratorOptions, options); + public void Configure(SwaggerGeneratorOptions options) + { + DeepCopy(_swaggerGenOptions.SwaggerGeneratorOptions, options); - // Create and add any filters that were specified through the FilterDescriptor lists ... + // Create and add any filters that were specified through the FilterDescriptor lists ... - foreach (var filterDescriptor in _swaggerGenOptions.ParameterFilterDescriptors) + foreach (var filterDescriptor in _swaggerGenOptions.ParameterFilterDescriptors) + { + if (filterDescriptor.IsAssignableTo(typeof(IParameterFilter))) { - if (filterDescriptor.IsAssignableTo(typeof(IParameterFilter))) - { - options.ParameterFilters.Add(GetOrCreateFilter(filterDescriptor)); - } - - if (filterDescriptor.IsAssignableTo(typeof(IParameterAsyncFilter))) - { - options.ParameterAsyncFilters.Add(GetOrCreateFilter(filterDescriptor)); - } + options.ParameterFilters.Add(GetOrCreateFilter(filterDescriptor)); } - foreach (var filterDescriptor in _swaggerGenOptions.RequestBodyFilterDescriptors) + if (filterDescriptor.IsAssignableTo(typeof(IParameterAsyncFilter))) { - if (filterDescriptor.IsAssignableTo(typeof(IRequestBodyFilter))) - { - options.RequestBodyFilters.Add(GetOrCreateFilter(filterDescriptor)); - } - - if (filterDescriptor.IsAssignableTo(typeof(IRequestBodyAsyncFilter))) - { - options.RequestBodyAsyncFilters.Add(GetOrCreateFilter(filterDescriptor)); - } + options.ParameterAsyncFilters.Add(GetOrCreateFilter(filterDescriptor)); } + } - foreach (var filterDescriptor in _swaggerGenOptions.OperationFilterDescriptors) + foreach (var filterDescriptor in _swaggerGenOptions.RequestBodyFilterDescriptors) + { + if (filterDescriptor.IsAssignableTo(typeof(IRequestBodyFilter))) { - if (filterDescriptor.IsAssignableTo(typeof(IOperationFilter))) - { - options.OperationFilters.Add(GetOrCreateFilter(filterDescriptor)); - } - - if (filterDescriptor.IsAssignableTo(typeof(IOperationAsyncFilter))) - { - options.OperationAsyncFilters.Add(GetOrCreateFilter(filterDescriptor)); - } + options.RequestBodyFilters.Add(GetOrCreateFilter(filterDescriptor)); } - foreach (var filterDescriptor in _swaggerGenOptions.DocumentFilterDescriptors) + if (filterDescriptor.IsAssignableTo(typeof(IRequestBodyAsyncFilter))) { - if (filterDescriptor.IsAssignableTo(typeof(IDocumentFilter))) - { - options.DocumentFilters.Add(GetOrCreateFilter(filterDescriptor)); - } - - if (filterDescriptor.IsAssignableTo(typeof(IDocumentAsyncFilter))) - { - options.DocumentAsyncFilters.Add(GetOrCreateFilter(filterDescriptor)); - } + options.RequestBodyAsyncFilters.Add(GetOrCreateFilter(filterDescriptor)); } + } - if (!options.SwaggerDocs.Any()) + foreach (var filterDescriptor in _swaggerGenOptions.OperationFilterDescriptors) + { + if (filterDescriptor.IsAssignableTo(typeof(IOperationFilter))) { - options.SwaggerDocs.Add("v1", new OpenApiInfo { Title = _hostingEnv.ApplicationName, Version = "1.0" }); + options.OperationFilters.Add(GetOrCreateFilter(filterDescriptor)); + } + + if (filterDescriptor.IsAssignableTo(typeof(IOperationAsyncFilter))) + { + options.OperationAsyncFilters.Add(GetOrCreateFilter(filterDescriptor)); } } - public void DeepCopy(SwaggerGeneratorOptions source, SwaggerGeneratorOptions target) + foreach (var filterDescriptor in _swaggerGenOptions.DocumentFilterDescriptors) { - target.SwaggerDocs = new Dictionary(source.SwaggerDocs); - target.DocInclusionPredicate = source.DocInclusionPredicate; - target.IgnoreObsoleteActions = source.IgnoreObsoleteActions; - target.ConflictingActionsResolver = source.ConflictingActionsResolver; - target.OperationIdSelector = source.OperationIdSelector; - target.TagsSelector = source.TagsSelector; - target.SortKeySelector = source.SortKeySelector; - target.InferSecuritySchemes = source.InferSecuritySchemes; - target.DescribeAllParametersInCamelCase = source.DescribeAllParametersInCamelCase; - target.SchemaComparer = source.SchemaComparer; - target.Servers = new List(source.Servers); - target.SecuritySchemes = new Dictionary(source.SecuritySchemes); - target.SecurityRequirements = new List(source.SecurityRequirements); - target.ParameterFilters = new List(source.ParameterFilters); - target.ParameterAsyncFilters = new List(source.ParameterAsyncFilters); - target.OperationFilters = new List(source.OperationFilters); - target.OperationAsyncFilters = new List(source.OperationAsyncFilters); - target.DocumentFilters = new List(source.DocumentFilters); - target.DocumentAsyncFilters = new List(source.DocumentAsyncFilters); - target.RequestBodyFilters = new List(source.RequestBodyFilters); - target.RequestBodyAsyncFilters = new List(source.RequestBodyAsyncFilters); - target.SecuritySchemesSelector = source.SecuritySchemesSelector; - target.PathGroupSelector = source.PathGroupSelector; - target.XmlCommentEndOfLine = source.XmlCommentEndOfLine; + if (filterDescriptor.IsAssignableTo(typeof(IDocumentFilter))) + { + options.DocumentFilters.Add(GetOrCreateFilter(filterDescriptor)); + } + + if (filterDescriptor.IsAssignableTo(typeof(IDocumentAsyncFilter))) + { + options.DocumentAsyncFilters.Add(GetOrCreateFilter(filterDescriptor)); + } } - private TFilter GetOrCreateFilter(FilterDescriptor filterDescriptor) + if (!options.SwaggerDocs.Any()) { - return (TFilter)(filterDescriptor.FilterInstance - ?? ActivatorUtilities.CreateInstance(_serviceProvider, filterDescriptor.Type, filterDescriptor.Arguments)); + options.SwaggerDocs.Add("v1", new OpenApiInfo { Title = _hostingEnv.ApplicationName, Version = "1.0" }); } } + + public void DeepCopy(SwaggerGeneratorOptions source, SwaggerGeneratorOptions target) + { + target.SwaggerDocs = new Dictionary(source.SwaggerDocs); + target.DocInclusionPredicate = source.DocInclusionPredicate; + target.IgnoreObsoleteActions = source.IgnoreObsoleteActions; + target.ConflictingActionsResolver = source.ConflictingActionsResolver; + target.OperationIdSelector = source.OperationIdSelector; + target.TagsSelector = source.TagsSelector; + target.SortKeySelector = source.SortKeySelector; + target.InferSecuritySchemes = source.InferSecuritySchemes; + target.DescribeAllParametersInCamelCase = source.DescribeAllParametersInCamelCase; + target.SchemaComparer = source.SchemaComparer; + target.Servers = new List(source.Servers); + target.SecuritySchemes = new Dictionary(source.SecuritySchemes); + target.SecurityRequirements = new List(source.SecurityRequirements); + target.ParameterFilters = new List(source.ParameterFilters); + target.ParameterAsyncFilters = new List(source.ParameterAsyncFilters); + target.OperationFilters = new List(source.OperationFilters); + target.OperationAsyncFilters = new List(source.OperationAsyncFilters); + target.DocumentFilters = new List(source.DocumentFilters); + target.DocumentAsyncFilters = new List(source.DocumentAsyncFilters); + target.RequestBodyFilters = new List(source.RequestBodyFilters); + target.RequestBodyAsyncFilters = new List(source.RequestBodyAsyncFilters); + target.SecuritySchemesSelector = source.SecuritySchemesSelector; + target.PathGroupSelector = source.PathGroupSelector; + target.XmlCommentEndOfLine = source.XmlCommentEndOfLine; + } + + private TFilter GetOrCreateFilter(FilterDescriptor filterDescriptor) + { + return (TFilter)(filterDescriptor.FilterInstance + ?? ActivatorUtilities.CreateInstance(_serviceProvider, filterDescriptor.Type, filterDescriptor.Arguments)); + } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs index c0bf9d89ae..ee1f459a26 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs @@ -3,63 +3,51 @@ using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Microsoft.Extensions.ApiDescriptions +namespace Microsoft.Extensions.ApiDescriptions; + +internal class DocumentProvider : IDocumentProvider { - /// - /// This service will be looked up by name from the service collection when using - /// the dotnet-getdocument tool from the Microsoft.Extensions.ApiDescription.Server package. - /// - internal interface IDocumentProvider + private readonly SwaggerGeneratorOptions _generatorOptions; + private readonly SwaggerOptions _options; + private readonly IAsyncSwaggerProvider _swaggerProvider; + + public DocumentProvider( + IOptions generatorOptions, + IOptions options, + IAsyncSwaggerProvider swaggerProvider) { - IEnumerable GetDocumentNames(); + _generatorOptions = generatorOptions.Value; + _options = options.Value; + _swaggerProvider = swaggerProvider; + } - Task GenerateAsync(string documentName, TextWriter writer); + public IEnumerable GetDocumentNames() + { + return _generatorOptions.SwaggerDocs.Keys; } - internal class DocumentProvider : IDocumentProvider + public async Task GenerateAsync(string documentName, TextWriter writer) { - private readonly SwaggerGeneratorOptions _generatorOptions; - private readonly SwaggerOptions _options; - private readonly IAsyncSwaggerProvider _swaggerProvider; + // Let UnknownSwaggerDocument or other exception bubble up to caller. + var swagger = await _swaggerProvider.GetSwaggerAsync(documentName, host: null, basePath: null); + var jsonWriter = new OpenApiJsonWriter(writer); - public DocumentProvider( - IOptions generatorOptions, - IOptions options, - IAsyncSwaggerProvider swaggerProvider) + if (_options.CustomDocumentSerializer != null) { - _generatorOptions = generatorOptions.Value; - _options = options.Value; - _swaggerProvider = swaggerProvider; + _options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, _options.OpenApiVersion); } - - public IEnumerable GetDocumentNames() + else { - return _generatorOptions.SwaggerDocs.Keys; - } - - public async Task GenerateAsync(string documentName, TextWriter writer) - { - // Let UnknownSwaggerDocument or other exception bubble up to caller. - var swagger = await _swaggerProvider.GetSwaggerAsync(documentName, host: null, basePath: null); - var jsonWriter = new OpenApiJsonWriter(writer); - - if (_options.CustomDocumentSerializer != null) + switch (_options.OpenApiVersion) { - _options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, _options.OpenApiVersion); - } - else - { - switch (_options.OpenApiVersion) - { - case OpenApi.OpenApiSpecVersion.OpenApi2_0: - swagger.SerializeAsV2(jsonWriter); - break; - - default: - case OpenApi.OpenApiSpecVersion.OpenApi3_0: - swagger.SerializeAsV3(jsonWriter); - break; - } + case OpenApi.OpenApiSpecVersion.OpenApi2_0: + swagger.SerializeAsV2(jsonWriter); + break; + + default: + case OpenApi.OpenApiSpecVersion.OpenApi3_0: + swagger.SerializeAsV3(jsonWriter); + break; } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/FilterDescriptor.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/FilterDescriptor.cs new file mode 100644 index 0000000000..56f04094d1 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/FilterDescriptor.cs @@ -0,0 +1,16 @@ +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class FilterDescriptor +{ + public Type Type { get; set; } + + public object[] Arguments { get; set; } + + public object FilterInstance { get; set; } + + internal bool IsAssignableTo(Type type) + { + return (FilterInstance != null && type.IsInstanceOfType(FilterInstance)) || + (Type != null && Type.IsAssignableTo(type)); + } +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/IDocumentProvider.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/IDocumentProvider.cs new file mode 100644 index 0000000000..2a1b0c7487 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/IDocumentProvider.cs @@ -0,0 +1,12 @@ +namespace Microsoft.Extensions.ApiDescriptions; + +/// +/// This service will be looked up by name from the service collection when using +/// the dotnet-getdocument tool from the Microsoft.Extensions.ApiDescription.Server package. +/// +internal interface IDocumentProvider +{ + IEnumerable GetDocumentNames(); + + Task GenerateAsync(string documentName, TextWriter writer); +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerApplicationConvention.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerApplicationConvention.cs index c075725e24..b91e611f2c 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerApplicationConvention.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerApplicationConvention.cs @@ -1,12 +1,11 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class SwaggerApplicationConvention : IApplicationModelConvention { - public class SwaggerApplicationConvention : IApplicationModelConvention + public void Apply(ApplicationModel application) { - public void Apply(ApplicationModel application) - { - application.ApiExplorer.IsVisible = true; - } + application.ApiExplorer.IsVisible = true; } -} \ No newline at end of file +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptions.cs index d0911d914c..3975935406 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptions.cs @@ -1,39 +1,23 @@ -namespace Swashbuckle.AspNetCore.SwaggerGen -{ - public class SwaggerGenOptions - { - public SwaggerGeneratorOptions SwaggerGeneratorOptions { get; set; } = new SwaggerGeneratorOptions(); - - public SchemaGeneratorOptions SchemaGeneratorOptions { get; set; } = new SchemaGeneratorOptions(); - - // NOTE: Filter instances can be added directly to the options exposed above OR they can be specified in - // the following lists. In the latter case, they will be instantiated and added when options are injected - // into their target services. This "deferred instantiation" allows the filters to be created from the - // DI container, thus supporting contructor injection of services within filters. +namespace Swashbuckle.AspNetCore.SwaggerGen; - public List ParameterFilterDescriptors { get; set; } = new List(); - - public List RequestBodyFilterDescriptors { get; set; } = new List(); +public class SwaggerGenOptions +{ + public SwaggerGeneratorOptions SwaggerGeneratorOptions { get; set; } = new SwaggerGeneratorOptions(); - public List OperationFilterDescriptors { get; set; } = new List(); + public SchemaGeneratorOptions SchemaGeneratorOptions { get; set; } = new SchemaGeneratorOptions(); - public List DocumentFilterDescriptors { get; set; } = new List(); + // NOTE: Filter instances can be added directly to the options exposed above OR they can be specified in + // the following lists. In the latter case, they will be instantiated and added when options are injected + // into their target services. This "deferred instantiation" allows the filters to be created from the + // DI container, thus supporting contructor injection of services within filters. - public List SchemaFilterDescriptors { get; set; } = new List(); - } + public List ParameterFilterDescriptors { get; set; } = new List(); - public class FilterDescriptor - { - public Type Type { get; set; } + public List RequestBodyFilterDescriptors { get; set; } = new List(); - public object[] Arguments { get; set; } + public List OperationFilterDescriptors { get; set; } = new List(); - public object FilterInstance { get; set; } + public List DocumentFilterDescriptors { get; set; } = new List(); - internal bool IsAssignableTo(Type type) - { - return (FilterInstance != null && type.IsInstanceOfType(FilterInstance)) || - (Type != null && Type.IsAssignableTo(type)); - } - } + public List SchemaFilterDescriptors { get; set; } = new List(); } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs index 9e3145915f..53001fec25 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs @@ -5,775 +5,774 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class SwaggerGenOptionsExtensions { - public static class SwaggerGenOptionsExtensions - { - /// - /// Define one or more documents to be created by the Swagger generator - /// - /// - /// A URI-friendly name that uniquely identifies the document - /// Global metadata to be included in the Swagger output - public static void SwaggerDoc( - this SwaggerGenOptions swaggerGenOptions, - string name, - OpenApiInfo info) - { - swaggerGenOptions.SwaggerGeneratorOptions.SwaggerDocs.Add(name, info); - } + /// + /// Define one or more documents to be created by the Swagger generator + /// + /// + /// A URI-friendly name that uniquely identifies the document + /// Global metadata to be included in the Swagger output + public static void SwaggerDoc( + this SwaggerGenOptions swaggerGenOptions, + string name, + OpenApiInfo info) + { + swaggerGenOptions.SwaggerGeneratorOptions.SwaggerDocs.Add(name, info); + } - /// - /// Provide a custom strategy for selecting actions. - /// - /// - /// - /// A lambda that returns true/false based on document name and ApiDescription - /// - public static void DocInclusionPredicate( - this SwaggerGenOptions swaggerGenOptions, - Func predicate) - { - swaggerGenOptions.SwaggerGeneratorOptions.DocInclusionPredicate = predicate; - } + /// + /// Provide a custom strategy for selecting actions. + /// + /// + /// + /// A lambda that returns true/false based on document name and ApiDescription + /// + public static void DocInclusionPredicate( + this SwaggerGenOptions swaggerGenOptions, + Func predicate) + { + swaggerGenOptions.SwaggerGeneratorOptions.DocInclusionPredicate = predicate; + } - /// - /// Ignore any actions that are decorated with the ObsoleteAttribute - /// - public static void IgnoreObsoleteActions(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SwaggerGeneratorOptions.IgnoreObsoleteActions = true; - } + /// + /// Ignore any actions that are decorated with the ObsoleteAttribute + /// + public static void IgnoreObsoleteActions(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SwaggerGeneratorOptions.IgnoreObsoleteActions = true; + } - /// - /// Merge actions that have conflicting HTTP methods and paths (must be unique for Swagger 2.0) - /// - /// - /// - public static void ResolveConflictingActions( - this SwaggerGenOptions swaggerGenOptions, - Func, ApiDescription> resolver) - { - swaggerGenOptions.SwaggerGeneratorOptions.ConflictingActionsResolver = resolver; - } + /// + /// Merge actions that have conflicting HTTP methods and paths (must be unique for Swagger 2.0) + /// + /// + /// + public static void ResolveConflictingActions( + this SwaggerGenOptions swaggerGenOptions, + Func, ApiDescription> resolver) + { + swaggerGenOptions.SwaggerGeneratorOptions.ConflictingActionsResolver = resolver; + } - /// - /// Provide a custom strategy for assigning "operationId" to operations - /// - public static void CustomOperationIds( - this SwaggerGenOptions swaggerGenOptions, - Func operationIdSelector) - { - swaggerGenOptions.SwaggerGeneratorOptions.OperationIdSelector = operationIdSelector; - } + /// + /// Provide a custom strategy for assigning "operationId" to operations + /// + public static void CustomOperationIds( + this SwaggerGenOptions swaggerGenOptions, + Func operationIdSelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.OperationIdSelector = operationIdSelector; + } - /// - /// Provide a custom strategy for assigning a default "tag" to operations - /// - /// - /// - [Obsolete("Deprecated: Use the overload that accepts a Func that returns a list of tags")] - public static void TagActionsBy( - this SwaggerGenOptions swaggerGenOptions, - Func tagSelector) - { - swaggerGenOptions.SwaggerGeneratorOptions.TagsSelector = (apiDesc) => new[] { tagSelector(apiDesc) }; - } + /// + /// Provide a custom strategy for assigning a default "tag" to operations + /// + /// + /// + [Obsolete("Deprecated: Use the overload that accepts a Func that returns a list of tags")] + public static void TagActionsBy( + this SwaggerGenOptions swaggerGenOptions, + Func tagSelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.TagsSelector = (apiDesc) => new[] { tagSelector(apiDesc) }; + } - /// - /// Provide a custom strategy for assigning "tags" to actions - /// - /// - /// - public static void TagActionsBy( - this SwaggerGenOptions swaggerGenOptions, - Func> tagsSelector) - { - swaggerGenOptions.SwaggerGeneratorOptions.TagsSelector = tagsSelector; - } + /// + /// Provide a custom strategy for assigning "tags" to actions + /// + /// + /// + public static void TagActionsBy( + this SwaggerGenOptions swaggerGenOptions, + Func> tagsSelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.TagsSelector = tagsSelector; + } - /// - /// Provide a custom strategy for sorting actions before they're transformed into the Swagger format - /// - /// - /// - public static void OrderActionsBy( - this SwaggerGenOptions swaggerGenOptions, - Func sortKeySelector) - { - swaggerGenOptions.SwaggerGeneratorOptions.SortKeySelector = sortKeySelector; - } + /// + /// Provide a custom strategy for sorting actions before they're transformed into the Swagger format + /// + /// + /// + public static void OrderActionsBy( + this SwaggerGenOptions swaggerGenOptions, + Func sortKeySelector) + { + swaggerGenOptions.SwaggerGeneratorOptions.SortKeySelector = sortKeySelector; + } - /// - /// Provide a custom comprarer to sort schemas with - /// - /// - /// - public static void SortSchemasWith( - this SwaggerGenOptions swaggerGenOptions, - IComparer schemaComparer) - { - swaggerGenOptions.SwaggerGeneratorOptions.SchemaComparer = schemaComparer; - } + /// + /// Provide a custom comprarer to sort schemas with + /// + /// + /// + public static void SortSchemasWith( + this SwaggerGenOptions swaggerGenOptions, + IComparer schemaComparer) + { + swaggerGenOptions.SwaggerGeneratorOptions.SchemaComparer = schemaComparer; + } - /// - /// Describe all parameters, regardless of how they appear in code, in camelCase - /// - public static void DescribeAllParametersInCamelCase(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SwaggerGeneratorOptions.DescribeAllParametersInCamelCase = true; - } + /// + /// Describe all parameters, regardless of how they appear in code, in camelCase + /// + public static void DescribeAllParametersInCamelCase(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SwaggerGeneratorOptions.DescribeAllParametersInCamelCase = true; + } - /// - /// Provide specific server information to include in the generated Swagger document - /// - /// - /// A description of the server - public static void AddServer(this SwaggerGenOptions swaggerGenOptions, OpenApiServer server) - { - swaggerGenOptions.SwaggerGeneratorOptions.Servers.Add(server); - } + /// + /// Provide specific server information to include in the generated Swagger document + /// + /// + /// A description of the server + public static void AddServer(this SwaggerGenOptions swaggerGenOptions, OpenApiServer server) + { + swaggerGenOptions.SwaggerGeneratorOptions.Servers.Add(server); + } - /// - /// Add one or more "securityDefinitions", describing how your API is protected, to the generated Swagger - /// - /// - /// A unique name for the scheme, as per the Swagger spec. - /// - /// A description of the scheme - can be an instance of BasicAuthScheme, ApiKeyScheme or OAuth2Scheme - /// - public static void AddSecurityDefinition( - this SwaggerGenOptions swaggerGenOptions, - string name, - OpenApiSecurityScheme securityScheme) - { - swaggerGenOptions.SwaggerGeneratorOptions.SecuritySchemes.Add(name, securityScheme); - } + /// + /// Add one or more "securityDefinitions", describing how your API is protected, to the generated Swagger + /// + /// + /// A unique name for the scheme, as per the Swagger spec. + /// + /// A description of the scheme - can be an instance of BasicAuthScheme, ApiKeyScheme or OAuth2Scheme + /// + public static void AddSecurityDefinition( + this SwaggerGenOptions swaggerGenOptions, + string name, + OpenApiSecurityScheme securityScheme) + { + swaggerGenOptions.SwaggerGeneratorOptions.SecuritySchemes.Add(name, securityScheme); + } - /// - /// Adds a global security requirement - /// - /// - /// - /// A dictionary of required schemes (logical AND). Keys must correspond to schemes defined through AddSecurityDefinition - /// If the scheme is of type "oauth2", then the value is a list of scopes, otherwise it MUST be an empty array - /// - public static void AddSecurityRequirement( - this SwaggerGenOptions swaggerGenOptions, - OpenApiSecurityRequirement securityRequirement) - { - swaggerGenOptions.SwaggerGeneratorOptions.SecurityRequirements.Add(securityRequirement); - } + /// + /// Adds a global security requirement + /// + /// + /// + /// A dictionary of required schemes (logical AND). Keys must correspond to schemes defined through AddSecurityDefinition + /// If the scheme is of type "oauth2", then the value is a list of scopes, otherwise it MUST be an empty array + /// + public static void AddSecurityRequirement( + this SwaggerGenOptions swaggerGenOptions, + OpenApiSecurityRequirement securityRequirement) + { + swaggerGenOptions.SwaggerGeneratorOptions.SecurityRequirements.Add(securityRequirement); + } - /// - /// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema - /// - /// - /// System type - /// A factory method that generates Schema's for the provided type - public static void MapType( - this SwaggerGenOptions swaggerGenOptions, - Type type, - Func schemaFactory) - { - swaggerGenOptions.SchemaGeneratorOptions.CustomTypeMappings.Add(type, schemaFactory); - } + /// + /// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema + /// + /// + /// System type + /// A factory method that generates Schema's for the provided type + public static void MapType( + this SwaggerGenOptions swaggerGenOptions, + Type type, + Func schemaFactory) + { + swaggerGenOptions.SchemaGeneratorOptions.CustomTypeMappings.Add(type, schemaFactory); + } - /// - /// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema - /// - /// System type - /// - /// A factory method that generates Schema's for the provided type - public static void MapType( - this SwaggerGenOptions swaggerGenOptions, - Func schemaFactory) - { - swaggerGenOptions.MapType(typeof(T), schemaFactory); - } + /// + /// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema + /// + /// System type + /// + /// A factory method that generates Schema's for the provided type + public static void MapType( + this SwaggerGenOptions swaggerGenOptions, + Func schemaFactory) + { + swaggerGenOptions.MapType(typeof(T), schemaFactory); + } - /// - /// Generate inline schema definitions (as opposed to referencing a shared definition) for enum parameters and properties - /// - /// - public static void UseInlineDefinitionsForEnums(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SchemaGeneratorOptions.UseInlineDefinitionsForEnums = true; - } + /// + /// Generate inline schema definitions (as opposed to referencing a shared definition) for enum parameters and properties + /// + /// + public static void UseInlineDefinitionsForEnums(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.UseInlineDefinitionsForEnums = true; + } - /// - /// Provide a custom strategy for generating the unique Id's that are used to reference object Schema's - /// - /// - /// - /// A lambda that returns a unique identifier for the provided system type - /// - public static void CustomSchemaIds( - this SwaggerGenOptions swaggerGenOptions, - Func schemaIdSelector) - { - swaggerGenOptions.SchemaGeneratorOptions.SchemaIdSelector = schemaIdSelector; - } + /// + /// Provide a custom strategy for generating the unique Id's that are used to reference object Schema's + /// + /// + /// + /// A lambda that returns a unique identifier for the provided system type + /// + public static void CustomSchemaIds( + this SwaggerGenOptions swaggerGenOptions, + Func schemaIdSelector) + { + swaggerGenOptions.SchemaGeneratorOptions.SchemaIdSelector = schemaIdSelector; + } - /// - /// Ignore any properties that are decorated with the ObsoleteAttribute - /// - public static void IgnoreObsoleteProperties(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SchemaGeneratorOptions.IgnoreObsoleteProperties = true; - } + /// + /// Ignore any properties that are decorated with the ObsoleteAttribute + /// + public static void IgnoreObsoleteProperties(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.IgnoreObsoleteProperties = true; + } - /// - /// Enables composite schema generation. If enabled, subtype schemas will contain the allOf construct to - /// incorporate properties from the base class instead of defining those properties inline. - /// - /// - public static void UseAllOfForInheritance(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SchemaGeneratorOptions.UseAllOfForInheritance = true; - } + /// + /// Enables composite schema generation. If enabled, subtype schemas will contain the allOf construct to + /// incorporate properties from the base class instead of defining those properties inline. + /// + /// + public static void UseAllOfForInheritance(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.UseAllOfForInheritance = true; + } - /// - /// Enables polymorphic schema generation. If enabled, request and response schemas will contain the oneOf - /// construct to describe sub types as a set of alternative schemas. - /// - /// - public static void UseOneOfForPolymorphism(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SchemaGeneratorOptions.UseOneOfForPolymorphism = true; - } + /// + /// Enables polymorphic schema generation. If enabled, request and response schemas will contain the oneOf + /// construct to describe sub types as a set of alternative schemas. + /// + /// + public static void UseOneOfForPolymorphism(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.UseOneOfForPolymorphism = true; + } - /// - /// To support polymorphism and inheritance behavior, Swashbuckle needs to detect the "known" subtypes for a given base type. - /// That is, the subtypes exposed by your API. By default, this will be any subtypes in the same assembly as the base type. - /// To override this, you can provide a custom selector function. This setting is only applicable when used in conjunction with - /// UseOneOfForPolymorphism and/or UseAllOfForInheritance. - /// - /// - /// - public static void SelectSubTypesUsing( - this SwaggerGenOptions swaggerGenOptions, - Func> customSelector) - { - swaggerGenOptions.SchemaGeneratorOptions.SubTypesSelector = customSelector; - } + /// + /// To support polymorphism and inheritance behavior, Swashbuckle needs to detect the "known" subtypes for a given base type. + /// That is, the subtypes exposed by your API. By default, this will be any subtypes in the same assembly as the base type. + /// To override this, you can provide a custom selector function. This setting is only applicable when used in conjunction with + /// UseOneOfForPolymorphism and/or UseAllOfForInheritance. + /// + /// + /// + public static void SelectSubTypesUsing( + this SwaggerGenOptions swaggerGenOptions, + Func> customSelector) + { + swaggerGenOptions.SchemaGeneratorOptions.SubTypesSelector = customSelector; + } - /// - /// If the configured serializer supports polymorphic serialization/deserialization by emitting/accepting a special "discriminator" property, - /// and UseOneOfForPolymorphism is enabled, then Swashbuckle will include a description for that property based on the serializer's behavior. - /// However, if you've customized your serializer to support polymorphism, you can provide a custom strategy to tell Swashbuckle which property, - /// for a given type, will be used as a discriminator. This setting is only applicable when used in conjunction with UseOneOfForPolymorphism. - /// - /// - /// - public static void SelectDiscriminatorNameUsing( - this SwaggerGenOptions swaggerGenOptions, - Func customSelector) - { - swaggerGenOptions.SchemaGeneratorOptions.DiscriminatorNameSelector = customSelector; - } + /// + /// If the configured serializer supports polymorphic serialization/deserialization by emitting/accepting a special "discriminator" property, + /// and UseOneOfForPolymorphism is enabled, then Swashbuckle will include a description for that property based on the serializer's behavior. + /// However, if you've customized your serializer to support polymorphism, you can provide a custom strategy to tell Swashbuckle which property, + /// for a given type, will be used as a discriminator. This setting is only applicable when used in conjunction with UseOneOfForPolymorphism. + /// + /// + /// + public static void SelectDiscriminatorNameUsing( + this SwaggerGenOptions swaggerGenOptions, + Func customSelector) + { + swaggerGenOptions.SchemaGeneratorOptions.DiscriminatorNameSelector = customSelector; + } - /// - /// If the configured serializer supports polymorphic serialization/deserialization by emitting/accepting a special "discriminator" property, - /// and UseOneOfForPolymorphism is enabled, then Swashbuckle will include a mapping of possible discriminator values to schema definitions. - /// However, if you've customized your serializer to support polymorphism, you can provide a custom mapping strategy to tell Swashbuckle what - /// the discriminator value should be for a given sub type. This setting is only applicable when used in conjunction with UseOneOfForPolymorphism. - /// - /// - /// - public static void SelectDiscriminatorValueUsing( - this SwaggerGenOptions swaggerGenOptions, - Func customSelector) - { - swaggerGenOptions.SchemaGeneratorOptions.DiscriminatorValueSelector = customSelector; - } + /// + /// If the configured serializer supports polymorphic serialization/deserialization by emitting/accepting a special "discriminator" property, + /// and UseOneOfForPolymorphism is enabled, then Swashbuckle will include a mapping of possible discriminator values to schema definitions. + /// However, if you've customized your serializer to support polymorphism, you can provide a custom mapping strategy to tell Swashbuckle what + /// the discriminator value should be for a given sub type. This setting is only applicable when used in conjunction with UseOneOfForPolymorphism. + /// + /// + /// + public static void SelectDiscriminatorValueUsing( + this SwaggerGenOptions swaggerGenOptions, + Func customSelector) + { + swaggerGenOptions.SchemaGeneratorOptions.DiscriminatorValueSelector = customSelector; + } - /// - /// Extend reference schemas (using the allOf construct) so that contextual metadata can be applied to all parameter and property schemas - /// - /// - public static void UseAllOfToExtendReferenceSchemas(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SchemaGeneratorOptions.UseAllOfToExtendReferenceSchemas = true; - } + /// + /// Extend reference schemas (using the allOf construct) so that contextual metadata can be applied to all parameter and property schemas + /// + /// + public static void UseAllOfToExtendReferenceSchemas(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.UseAllOfToExtendReferenceSchemas = true; + } - /// - /// Enable detection of non nullable reference types to set Nullable flag accordingly on schema properties - /// - /// - public static void SupportNonNullableReferenceTypes(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SchemaGeneratorOptions.SupportNonNullableReferenceTypes = true; - } + /// + /// Enable detection of non nullable reference types to set Nullable flag accordingly on schema properties + /// + /// + public static void SupportNonNullableReferenceTypes(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.SupportNonNullableReferenceTypes = true; + } - /// - /// Enable detection of non nullable reference types to mark the member as required in schema properties - /// - /// - public static void NonNullableReferenceTypesAsRequired(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SchemaGeneratorOptions.NonNullableReferenceTypesAsRequired = true; - } + /// + /// Enable detection of non nullable reference types to mark the member as required in schema properties + /// + /// + public static void NonNullableReferenceTypesAsRequired(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.NonNullableReferenceTypesAsRequired = true; + } - /// - /// Automatically infer security schemes from authentication/authorization state in ASP.NET Core. - /// - /// - /// - /// Provide alternative implementation that maps ASP.NET Core Authentication schemes to Open API security schemes - /// - /// Currently only supports JWT Bearer authentication - public static void InferSecuritySchemes( - this SwaggerGenOptions swaggerGenOptions, - Func, IDictionary> securitySchemesSelector = null) - { - swaggerGenOptions.SwaggerGeneratorOptions.InferSecuritySchemes = true; - swaggerGenOptions.SwaggerGeneratorOptions.SecuritySchemesSelector = securitySchemesSelector; - } + /// + /// Automatically infer security schemes from authentication/authorization state in ASP.NET Core. + /// + /// + /// + /// Provide alternative implementation that maps ASP.NET Core Authentication schemes to Open API security schemes + /// + /// Currently only supports JWT Bearer authentication + public static void InferSecuritySchemes( + this SwaggerGenOptions swaggerGenOptions, + Func, IDictionary> securitySchemesSelector = null) + { + swaggerGenOptions.SwaggerGeneratorOptions.InferSecuritySchemes = true; + swaggerGenOptions.SwaggerGeneratorOptions.SecuritySchemesSelector = securitySchemesSelector; + } - /// - /// Extend the Swagger Generator with "filters" that can modify Schemas after they're initially generated - /// - /// A type that derives from - /// - /// Optionally inject parameters through filter constructors - public static void SchemaFilter( - this SwaggerGenOptions swaggerGenOptions, - params object[] arguments) - where TFilter : ISchemaFilter + /// + /// Extend the Swagger Generator with "filters" that can modify Schemas after they're initially generated + /// + /// A type that derives from + /// + /// Optionally inject parameters through filter constructors + public static void SchemaFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : ISchemaFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + swaggerGenOptions.SchemaFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - swaggerGenOptions.SchemaFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - Arguments = arguments - }); - } + Type = typeof(TFilter), + Arguments = arguments + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify Schemas after they're initially generated - /// - /// A type that derives from - /// - /// The filter instance to use. - public static void AddSchemaFilterInstance( - this SwaggerGenOptions swaggerGenOptions, - TFilter filterInstance) - where TFilter : ISchemaFilter + /// + /// Extend the Swagger Generator with "filters" that can modify Schemas after they're initially generated + /// + /// A type that derives from + /// + /// The filter instance to use. + public static void AddSchemaFilterInstance( + this SwaggerGenOptions swaggerGenOptions, + TFilter filterInstance) + where TFilter : ISchemaFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); + swaggerGenOptions.SchemaFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); - swaggerGenOptions.SchemaFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - FilterInstance = filterInstance - }); - } + Type = typeof(TFilter), + FilterInstance = filterInstance + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify Parameters after they're initially generated - /// - /// A type that derives from - /// - /// Optionally inject parameters through filter constructors - public static void ParameterFilter( - this SwaggerGenOptions swaggerGenOptions, - params object[] arguments) - where TFilter : IParameterFilter + /// + /// Extend the Swagger Generator with "filters" that can modify Parameters after they're initially generated + /// + /// A type that derives from + /// + /// Optionally inject parameters through filter constructors + public static void ParameterFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IParameterFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + swaggerGenOptions.ParameterFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - swaggerGenOptions.ParameterFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - Arguments = arguments - }); - } + Type = typeof(TFilter), + Arguments = arguments + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify Parameters asynchronously after they're initially generated - /// - /// A type that derives from - /// - /// Optionally inject parameters through filter constructors - public static void ParameterAsyncFilter( - this SwaggerGenOptions swaggerGenOptions, - params object[] arguments) - where TFilter : IParameterAsyncFilter + /// + /// Extend the Swagger Generator with "filters" that can modify Parameters asynchronously after they're initially generated + /// + /// A type that derives from + /// + /// Optionally inject parameters through filter constructors + public static void ParameterAsyncFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IParameterAsyncFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + swaggerGenOptions.ParameterFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - swaggerGenOptions.ParameterFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - Arguments = arguments - }); - } + Type = typeof(TFilter), + Arguments = arguments + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify Parameters after they're initially generated - /// - /// A type that derives from - /// - /// The filter instance to use. - public static void AddParameterFilterInstance( - this SwaggerGenOptions swaggerGenOptions, - TFilter filterInstance) - where TFilter : IParameterFilter + /// + /// Extend the Swagger Generator with "filters" that can modify Parameters after they're initially generated + /// + /// A type that derives from + /// + /// The filter instance to use. + public static void AddParameterFilterInstance( + this SwaggerGenOptions swaggerGenOptions, + TFilter filterInstance) + where TFilter : IParameterFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); + swaggerGenOptions.ParameterFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); - swaggerGenOptions.ParameterFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - FilterInstance = filterInstance - }); - } + Type = typeof(TFilter), + FilterInstance = filterInstance + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify Parameters asynchronously after they're initially generated - /// - /// A type that derives from - /// - /// The filter instance to use. - public static void AddParameterAsyncFilterInstance( - this SwaggerGenOptions swaggerGenOptions, - TFilter filterInstance) - where TFilter : IParameterAsyncFilter + /// + /// Extend the Swagger Generator with "filters" that can modify Parameters asynchronously after they're initially generated + /// + /// A type that derives from + /// + /// The filter instance to use. + public static void AddParameterAsyncFilterInstance( + this SwaggerGenOptions swaggerGenOptions, + TFilter filterInstance) + where TFilter : IParameterAsyncFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); + swaggerGenOptions.ParameterFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); - swaggerGenOptions.ParameterFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - FilterInstance = filterInstance - }); - } + Type = typeof(TFilter), + FilterInstance = filterInstance + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify RequestBodys after they're initially generated - /// - /// A type that derives from - /// - /// Optionally inject parameters through filter constructors - public static void RequestBodyFilter( - this SwaggerGenOptions swaggerGenOptions, - params object[] arguments) - where TFilter : IRequestBodyFilter + /// + /// Extend the Swagger Generator with "filters" that can modify RequestBodys after they're initially generated + /// + /// A type that derives from + /// + /// Optionally inject parameters through filter constructors + public static void RequestBodyFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IRequestBodyFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + swaggerGenOptions.RequestBodyFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - swaggerGenOptions.RequestBodyFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - Arguments = arguments - }); - } + Type = typeof(TFilter), + Arguments = arguments + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify RequestBodys asynchronously after they're initially generated - /// - /// A type that derives from - /// - /// Optionally inject parameters through filter constructors - public static void RequestBodyAsyncFilter( - this SwaggerGenOptions swaggerGenOptions, - params object[] arguments) - where TFilter : IRequestBodyAsyncFilter + /// + /// Extend the Swagger Generator with "filters" that can modify RequestBodys asynchronously after they're initially generated + /// + /// A type that derives from + /// + /// Optionally inject parameters through filter constructors + public static void RequestBodyAsyncFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IRequestBodyAsyncFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + swaggerGenOptions.RequestBodyFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - swaggerGenOptions.RequestBodyFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - Arguments = arguments - }); - } + Type = typeof(TFilter), + Arguments = arguments + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify RequestBodys after they're initially generated - /// - /// A type that derives from - /// - /// The filter instance to use. - public static void AddRequestBodyFilterInstance( - this SwaggerGenOptions swaggerGenOptions, - TFilter filterInstance) - where TFilter : IRequestBodyFilter + /// + /// Extend the Swagger Generator with "filters" that can modify RequestBodys after they're initially generated + /// + /// A type that derives from + /// + /// The filter instance to use. + public static void AddRequestBodyFilterInstance( + this SwaggerGenOptions swaggerGenOptions, + TFilter filterInstance) + where TFilter : IRequestBodyFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); + swaggerGenOptions.RequestBodyFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); - swaggerGenOptions.RequestBodyFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - FilterInstance = filterInstance - }); - } + Type = typeof(TFilter), + FilterInstance = filterInstance + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify RequestBodys asynchronously after they're initially generated - /// - /// A type that derives from - /// - /// The filter instance to use. - public static void AddRequestBodyAsyncFilterInstance( - this SwaggerGenOptions swaggerGenOptions, - TFilter filterInstance) - where TFilter : IRequestBodyAsyncFilter + /// + /// Extend the Swagger Generator with "filters" that can modify RequestBodys asynchronously after they're initially generated + /// + /// A type that derives from + /// + /// The filter instance to use. + public static void AddRequestBodyAsyncFilterInstance( + this SwaggerGenOptions swaggerGenOptions, + TFilter filterInstance) + where TFilter : IRequestBodyAsyncFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); + swaggerGenOptions.RequestBodyFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); - swaggerGenOptions.RequestBodyFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - FilterInstance = filterInstance - }); - } + Type = typeof(TFilter), + FilterInstance = filterInstance + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify Operations after they're initially generated - /// - /// A type that derives from - /// - /// Optionally inject parameters through filter constructors - public static void OperationFilter( - this SwaggerGenOptions swaggerGenOptions, - params object[] arguments) - where TFilter : IOperationFilter + /// + /// Extend the Swagger Generator with "filters" that can modify Operations after they're initially generated + /// + /// A type that derives from + /// + /// Optionally inject parameters through filter constructors + public static void OperationFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IOperationFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + swaggerGenOptions.OperationFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - swaggerGenOptions.OperationFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - Arguments = arguments - }); - } + Type = typeof(TFilter), + Arguments = arguments + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify Operations asynchronously after they're initially generated - /// - /// A type that derives from - /// - /// Optionally inject parameters through filter constructors - public static void OperationAsyncFilter( - this SwaggerGenOptions swaggerGenOptions, - params object[] arguments) - where TFilter : IOperationAsyncFilter + /// + /// Extend the Swagger Generator with "filters" that can modify Operations asynchronously after they're initially generated + /// + /// A type that derives from + /// + /// Optionally inject parameters through filter constructors + public static void OperationAsyncFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IOperationAsyncFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + swaggerGenOptions.OperationFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - swaggerGenOptions.OperationFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - Arguments = arguments - }); - } + Type = typeof(TFilter), + Arguments = arguments + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify Operations after they're initially generated - /// - /// A type that derives from - /// - /// The filter instance to use. - public static void AddOperationFilterInstance( - this SwaggerGenOptions swaggerGenOptions, - TFilter filterInstance) - where TFilter : IOperationFilter + /// + /// Extend the Swagger Generator with "filters" that can modify Operations after they're initially generated + /// + /// A type that derives from + /// + /// The filter instance to use. + public static void AddOperationFilterInstance( + this SwaggerGenOptions swaggerGenOptions, + TFilter filterInstance) + where TFilter : IOperationFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); + swaggerGenOptions.OperationFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); - swaggerGenOptions.OperationFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - FilterInstance = filterInstance - }); - } + Type = typeof(TFilter), + FilterInstance = filterInstance + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify Operations asynchronously after they're initially generated - /// - /// A type that derives from - /// - /// The filter instance to use - public static void AddOperationAsyncFilterInstance( - this SwaggerGenOptions swaggerGenOptions, - TFilter filterInstance) - where TFilter : IOperationAsyncFilter + /// + /// Extend the Swagger Generator with "filters" that can modify Operations asynchronously after they're initially generated + /// + /// A type that derives from + /// + /// The filter instance to use + public static void AddOperationAsyncFilterInstance( + this SwaggerGenOptions swaggerGenOptions, + TFilter filterInstance) + where TFilter : IOperationAsyncFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); + swaggerGenOptions.OperationFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); - swaggerGenOptions.OperationFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - FilterInstance = filterInstance - }); - } + Type = typeof(TFilter), + FilterInstance = filterInstance + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify SwaggerDocuments after they're initially generated - /// - /// A type that derives from - /// - /// Optionally inject parameters through filter constructors - public static void DocumentFilter( - this SwaggerGenOptions swaggerGenOptions, - params object[] arguments) - where TFilter : IDocumentFilter + /// + /// Extend the Swagger Generator with "filters" that can modify SwaggerDocuments after they're initially generated + /// + /// A type that derives from + /// + /// Optionally inject parameters through filter constructors + public static void DocumentFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IDocumentFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + swaggerGenOptions.DocumentFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - swaggerGenOptions.DocumentFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - Arguments = arguments - }); - } + Type = typeof(TFilter), + Arguments = arguments + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify SwaggerDocuments asynchronously after they're initially generated - /// - /// A type that derives from - /// - /// Optionally inject parameters through filter constructors - /// - public static void DocumentAsyncFilter( - this SwaggerGenOptions swaggerGenOptions, - params object[] arguments) - where TFilter : IDocumentAsyncFilter + /// + /// Extend the Swagger Generator with "filters" that can modify SwaggerDocuments asynchronously after they're initially generated + /// + /// A type that derives from + /// + /// Optionally inject parameters through filter constructors + /// + public static void DocumentAsyncFilter( + this SwaggerGenOptions swaggerGenOptions, + params object[] arguments) + where TFilter : IDocumentAsyncFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + swaggerGenOptions.DocumentFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - swaggerGenOptions.DocumentFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - Arguments = arguments - }); - } + Type = typeof(TFilter), + Arguments = arguments + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify SwaggerDocuments after they're initially generated - /// - /// A type that derives from - /// - /// The filter instance to use. - public static void AddDocumentFilterInstance( - this SwaggerGenOptions swaggerGenOptions, - TFilter filterInstance) - where TFilter : IDocumentFilter + /// + /// Extend the Swagger Generator with "filters" that can modify SwaggerDocuments after they're initially generated + /// + /// A type that derives from + /// + /// The filter instance to use. + public static void AddDocumentFilterInstance( + this SwaggerGenOptions swaggerGenOptions, + TFilter filterInstance) + where TFilter : IDocumentFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); + swaggerGenOptions.DocumentFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); - swaggerGenOptions.DocumentFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - FilterInstance = filterInstance - }); - } + Type = typeof(TFilter), + FilterInstance = filterInstance + }); + } - /// - /// Extend the Swagger Generator with "filters" that can modify SwaggerDocuments asynchronously after they're initially generated - /// - /// A type that derives from - /// - /// The filter instance to use. - /// - public static void AddDocumentAsyncFilterInstance( - this SwaggerGenOptions swaggerGenOptions, - TFilter filterInstance) - where TFilter : IDocumentAsyncFilter + /// + /// Extend the Swagger Generator with "filters" that can modify SwaggerDocuments asynchronously after they're initially generated + /// + /// A type that derives from + /// + /// The filter instance to use. + /// + public static void AddDocumentAsyncFilterInstance( + this SwaggerGenOptions swaggerGenOptions, + TFilter filterInstance) + where TFilter : IDocumentAsyncFilter + { + if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); + if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); + swaggerGenOptions.DocumentFilterDescriptors.Add(new FilterDescriptor { - if (swaggerGenOptions == null) throw new ArgumentNullException(nameof(swaggerGenOptions)); - if (filterInstance == null) throw new ArgumentNullException(nameof(filterInstance)); - swaggerGenOptions.DocumentFilterDescriptors.Add(new FilterDescriptor - { - Type = typeof(TFilter), - FilterInstance = filterInstance - }); - } + Type = typeof(TFilter), + FilterInstance = filterInstance + }); + } - /// - /// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files - /// - /// - /// A factory method that returns XML Comments as an XPathDocument - /// - /// Flag to indicate if controller XML comments (i.e. summary) should be used to assign Tag descriptions. - /// Don't set this flag if you're customizing the default tag for operations via TagActionsBy. - /// - public static void IncludeXmlComments( - this SwaggerGenOptions swaggerGenOptions, - Func xmlDocFactory, - bool includeControllerXmlComments = false) - { - var xmlDoc = xmlDocFactory(); - var xmlDocMembers = XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc); + /// + /// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files + /// + /// + /// A factory method that returns XML Comments as an XPathDocument + /// + /// Flag to indicate if controller XML comments (i.e. summary) should be used to assign Tag descriptions. + /// Don't set this flag if you're customizing the default tag for operations via TagActionsBy. + /// + public static void IncludeXmlComments( + this SwaggerGenOptions swaggerGenOptions, + Func xmlDocFactory, + bool includeControllerXmlComments = false) + { + var xmlDoc = xmlDocFactory(); + var xmlDocMembers = XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc); - swaggerGenOptions.AddParameterFilterInstance(new XmlCommentsParameterFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); - swaggerGenOptions.AddRequestBodyFilterInstance(new XmlCommentsRequestBodyFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); - swaggerGenOptions.AddOperationFilterInstance(new XmlCommentsOperationFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); - swaggerGenOptions.AddSchemaFilterInstance(new XmlCommentsSchemaFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); + swaggerGenOptions.AddParameterFilterInstance(new XmlCommentsParameterFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); + swaggerGenOptions.AddRequestBodyFilterInstance(new XmlCommentsRequestBodyFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); + swaggerGenOptions.AddOperationFilterInstance(new XmlCommentsOperationFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); + swaggerGenOptions.AddSchemaFilterInstance(new XmlCommentsSchemaFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); - if (includeControllerXmlComments) - swaggerGenOptions.AddDocumentFilterInstance(new XmlCommentsDocumentFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); - } + if (includeControllerXmlComments) + swaggerGenOptions.AddDocumentFilterInstance(new XmlCommentsDocumentFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions)); + } - /// - /// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files - /// - /// - /// An absolute path to the file that contains XML Comments - /// - /// Flag to indicate if controller XML comments (i.e. summary) should be used to assign Tag descriptions. - /// Don't set this flag if you're customizing the default tag for operations via TagActionsBy. - /// - public static void IncludeXmlComments( - this SwaggerGenOptions swaggerGenOptions, - string filePath, - bool includeControllerXmlComments = false) - { - swaggerGenOptions.IncludeXmlComments(() => new XPathDocument(filePath), includeControllerXmlComments); - } + /// + /// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML Comment files + /// + /// + /// An absolute path to the file that contains XML Comments + /// + /// Flag to indicate if controller XML comments (i.e. summary) should be used to assign Tag descriptions. + /// Don't set this flag if you're customizing the default tag for operations via TagActionsBy. + /// + public static void IncludeXmlComments( + this SwaggerGenOptions swaggerGenOptions, + string filePath, + bool includeControllerXmlComments = false) + { + swaggerGenOptions.IncludeXmlComments(() => new XPathDocument(filePath), includeControllerXmlComments); + } + + /// + /// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML comments + /// from specific Assembly + /// + /// + /// Assembly that contains XML Comments + /// + /// Flag to indicate if controller XML comments (i.e. summary) should be used to assign Tag descriptions. + /// Don't set this flag if you're customizing the default tag for operations via TagActionsBy. + /// + public static void IncludeXmlComments( + this SwaggerGenOptions swaggerGenOptions, + Assembly assembly, + bool includeControllerXmlComments = false) + { + swaggerGenOptions.IncludeXmlComments( + Path.Combine(AppContext.BaseDirectory, $"{assembly.GetName().Name}.xml"), + includeControllerXmlComments); + } + + /// + /// Generate polymorphic schemas (i.e. "oneOf") based on discovered subtypes. + /// Deprecated: Use the \"UseOneOfForPolymorphism\" and \"UseAllOfForInheritance\" settings instead + /// + /// + /// + /// + [Obsolete("You can use \"UseOneOfForPolymorphism\", \"UseAllOfForInheritance\" and \"SelectSubTypesUsing\" to configure equivalent behavior")] + public static void GeneratePolymorphicSchemas( + this SwaggerGenOptions swaggerGenOptions, + Func> subTypesResolver = null, + Func discriminatorSelector = null) + { + swaggerGenOptions.UseOneOfForPolymorphism(); + swaggerGenOptions.UseAllOfForInheritance(); - /// - /// Inject human-friendly descriptions for Operations, Parameters and Schemas based on XML comments - /// from specific Assembly - /// - /// - /// Assembly that contains XML Comments - /// - /// Flag to indicate if controller XML comments (i.e. summary) should be used to assign Tag descriptions. - /// Don't set this flag if you're customizing the default tag for operations via TagActionsBy. - /// - public static void IncludeXmlComments( - this SwaggerGenOptions swaggerGenOptions, - Assembly assembly, - bool includeControllerXmlComments = false) + if (subTypesResolver != null) { - swaggerGenOptions.IncludeXmlComments( - Path.Combine(AppContext.BaseDirectory, $"{assembly.GetName().Name}.xml"), - includeControllerXmlComments); + swaggerGenOptions.SelectSubTypesUsing(subTypesResolver); } - /// - /// Generate polymorphic schemas (i.e. "oneOf") based on discovered subtypes. - /// Deprecated: Use the \"UseOneOfForPolymorphism\" and \"UseAllOfForInheritance\" settings instead - /// - /// - /// - /// - [Obsolete("You can use \"UseOneOfForPolymorphism\", \"UseAllOfForInheritance\" and \"SelectSubTypesUsing\" to configure equivalent behavior")] - public static void GeneratePolymorphicSchemas( - this SwaggerGenOptions swaggerGenOptions, - Func> subTypesResolver = null, - Func discriminatorSelector = null) + if (discriminatorSelector != null) { - swaggerGenOptions.UseOneOfForPolymorphism(); - swaggerGenOptions.UseAllOfForInheritance(); - - if (subTypesResolver != null) - { - swaggerGenOptions.SelectSubTypesUsing(subTypesResolver); - } - - if (discriminatorSelector != null) - { - swaggerGenOptions.SelectDiscriminatorNameUsing(discriminatorSelector); - } + swaggerGenOptions.SelectDiscriminatorNameUsing(discriminatorSelector); } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs index 1c7b34590c..9b42e60713 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs @@ -5,93 +5,86 @@ using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class SwaggerGenServiceCollectionExtensions { - public static class SwaggerGenServiceCollectionExtensions + public static IServiceCollection AddSwaggerGen( + this IServiceCollection services, + Action setupAction = null) { - public static IServiceCollection AddSwaggerGen( - this IServiceCollection services, - Action setupAction = null) + // Add Mvc convention to ensure ApiExplorer is enabled for all actions + services.Configure(c => + c.Conventions.Add(new SwaggerApplicationConvention())); + + // Register custom configurators that takes values from SwaggerGenOptions (i.e. high level config) + // and applies them to SwaggerGeneratorOptions and SchemaGeneratorOptions (i.e. lower-level config) + services.AddTransient, ConfigureSwaggerGeneratorOptions>(); + services.AddTransient, ConfigureSchemaGeneratorOptions>(); + + // Register generator and its dependencies + services.TryAddTransient(); + services.TryAddTransient(s => s.GetRequiredService()); + services.TryAddTransient(s => s.GetRequiredService()); + services.TryAddTransient(s => s.GetRequiredService>().Value); + services.TryAddTransient(); + services.TryAddTransient(s => s.GetRequiredService>().Value); + services.AddSingleton(); + services.TryAddSingleton(s => { - // Add Mvc convention to ensure ApiExplorer is enabled for all actions - services.Configure(c => - c.Conventions.Add(new SwaggerApplicationConvention())); + var serializerOptions = s.GetRequiredService().Options; + return new JsonSerializerDataContractResolver(serializerOptions); + }); - // Register custom configurators that takes values from SwaggerGenOptions (i.e. high level config) - // and applies them to SwaggerGeneratorOptions and SchemaGeneratorOptions (i.e. lower-level config) - services.AddTransient, ConfigureSwaggerGeneratorOptions>(); - services.AddTransient, ConfigureSchemaGeneratorOptions>(); + // Used by the dotnet-getdocument tool from the Microsoft.Extensions.ApiDescription.Server package. + services.TryAddSingleton(); - // Register generator and its dependencies - services.TryAddTransient(); - services.TryAddTransient(s => s.GetRequiredService()); - services.TryAddTransient(s => s.GetRequiredService()); - services.TryAddTransient(s => s.GetRequiredService>().Value); - services.TryAddTransient(); - services.TryAddTransient(s => s.GetRequiredService>().Value); - services.AddSingleton(); - services.TryAddSingleton(s => - { - var serializerOptions = s.GetRequiredService().Options; - return new JsonSerializerDataContractResolver(serializerOptions); - }); + if (setupAction != null) services.ConfigureSwaggerGen(setupAction); - // Used by the dotnet-getdocument tool from the Microsoft.Extensions.ApiDescription.Server package. - services.TryAddSingleton(); + return services; + } - if (setupAction != null) services.ConfigureSwaggerGen(setupAction); + public static void ConfigureSwaggerGen( + this IServiceCollection services, + Action setupAction) + { + services.Configure(setupAction); + } - return services; - } + private sealed class JsonSerializerOptionsProvider + { + private JsonSerializerOptions _options; +#if NET + private readonly IServiceProvider _serviceProvider; - public static void ConfigureSwaggerGen( - this IServiceCollection services, - Action setupAction) + public JsonSerializerOptionsProvider(IServiceProvider serviceProvider) { - services.Configure(setupAction); + _serviceProvider = serviceProvider; } - - private sealed class JsonSerializerOptionsProvider - { - private JsonSerializerOptions _options; -#if !NETSTANDARD2_0 - private readonly IServiceProvider _serviceProvider; - - public JsonSerializerOptionsProvider(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } #endif - public JsonSerializerOptions Options => _options ??= ResolveOptions(); + public JsonSerializerOptions Options => _options ??= ResolveOptions(); - private JsonSerializerOptions ResolveOptions() - { - JsonSerializerOptions serializerOptions; + private JsonSerializerOptions ResolveOptions() + { + JsonSerializerOptions serializerOptions; - /* - * First try to get the options configured for MVC, - * then try to get the options configured for Minimal APIs if available, - * then try the default JsonSerializerOptions if available, - * otherwise create a new instance as a last resort as this is an expensive operation. - */ -#if !NETSTANDARD2_0 - serializerOptions = - _serviceProvider.GetService>()?.Value?.JsonSerializerOptions -#if NET8_0_OR_GREATER - ?? _serviceProvider.GetService>()?.Value?.SerializerOptions -#endif -#if NET7_0_OR_GREATER - ?? JsonSerializerOptions.Default; -#else - ?? new JsonSerializerOptions(); -#endif + /* + * First try to get the options configured for MVC, + * then try to get the options configured for Minimal APIs if available, + * then try the default JsonSerializerOptions if available, + * otherwise create a new instance as a last resort as this is an expensive operation. + */ +#if NET + serializerOptions = + _serviceProvider.GetService>()?.Value?.JsonSerializerOptions + ?? _serviceProvider.GetService>()?.Value?.SerializerOptions + ?? JsonSerializerOptions.Default; #else - serializerOptions = new JsonSerializerOptions(); + serializerOptions = new JsonSerializerOptions(); #endif - return serializerOptions; - } + return serializerOptions; } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/DataContract.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/DataContract.cs new file mode 100644 index 0000000000..48b487a679 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/DataContract.cs @@ -0,0 +1,130 @@ +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class DataContract +{ + public static DataContract ForPrimitive( + Type underlyingType, + DataType dataType, + string dataFormat, + Func jsonConverter = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: dataType, + dataFormat: dataFormat, + jsonConverter: jsonConverter); + } + + public static DataContract ForArray( + Type underlyingType, + Type itemType, + Func jsonConverter = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Array, + arrayItemType: itemType, + jsonConverter: jsonConverter); + } + + public static DataContract ForDictionary( + Type underlyingType, + Type valueType, + IEnumerable keys = null, + Func jsonConverter = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Dictionary, + dictionaryValueType: valueType, + dictionaryKeys: keys, + jsonConverter: jsonConverter); + } + + public static DataContract ForObject( + Type underlyingType, + IEnumerable properties, + Type extensionDataType = null, + string typeNameProperty = null, + string typeNameValue = null, + Func jsonConverter = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Object, + objectProperties: properties, + objectExtensionDataType: extensionDataType, + objectTypeNameProperty: typeNameProperty, + objectTypeNameValue: typeNameValue, + jsonConverter: jsonConverter); + } + + public static DataContract ForDynamic( + Type underlyingType, + Func jsonConverter = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Unknown, + jsonConverter: jsonConverter); + } + + [Obsolete("Provide jsonConverter function instead of enumValues")] + public static DataContract ForPrimitive( + Type underlyingType, + DataType dataType, + string dataFormat, + IEnumerable enumValues) + { + return new DataContract( + underlyingType: underlyingType, + dataType: dataType, + dataFormat: dataFormat, + enumValues: enumValues); + } + + private DataContract( + Type underlyingType, + DataType dataType, + string dataFormat = null, + IEnumerable enumValues = null, + Type arrayItemType = null, + Type dictionaryValueType = null, + IEnumerable dictionaryKeys = null, + IEnumerable objectProperties = null, + Type objectExtensionDataType = null, + string objectTypeNameProperty = null, + string objectTypeNameValue = null, + Func jsonConverter = null) + { + UnderlyingType = underlyingType; + DataType = dataType; + DataFormat = dataFormat; +#pragma warning disable CS0618 // Type or member is obsolete + EnumValues = enumValues; +#pragma warning restore CS0618 // Type or member is obsolete + ArrayItemType = arrayItemType; + DictionaryValueType = dictionaryValueType; + DictionaryKeys = dictionaryKeys; + ObjectProperties = objectProperties; + ObjectExtensionDataType = objectExtensionDataType; + ObjectTypeNameProperty = objectTypeNameProperty; + ObjectTypeNameValue = objectTypeNameValue; + JsonConverter = jsonConverter ?? new Func(obj => null); + } + + public Type UnderlyingType { get; } + public DataType DataType { get; } + public string DataFormat { get; } + public Type ArrayItemType { get; } + public Type DictionaryValueType { get; } + public IEnumerable DictionaryKeys { get; } + public IEnumerable ObjectProperties { get; } + public Type ObjectExtensionDataType { get; } + public string ObjectTypeNameProperty { get; } + public string ObjectTypeNameValue { get; } + public Func JsonConverter { get; } + + [Obsolete("Use JsonConverter")] + public IEnumerable EnumValues { get; } +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/DataProperty.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/DataProperty.cs new file mode 100644 index 0000000000..4d7cda277b --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/DataProperty.cs @@ -0,0 +1,32 @@ +using System.Reflection; + +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class DataProperty +{ + public DataProperty( + string name, + Type memberType, + bool isRequired = false, + bool isNullable = false, + bool isReadOnly = false, + bool isWriteOnly = false, + MemberInfo memberInfo = null) + { + Name = name; + IsRequired = isRequired; + IsNullable = isNullable; + IsReadOnly = isReadOnly; + IsWriteOnly = isWriteOnly; + MemberType = memberType; + MemberInfo = memberInfo; + } + + public string Name { get; } + public bool IsRequired { get; } + public bool IsNullable { get; } + public bool IsReadOnly { get; } + public bool IsWriteOnly { get; } + public Type MemberType { get; } + public MemberInfo MemberInfo { get; } +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/DataType.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/DataType.cs new file mode 100644 index 0000000000..9c60153b80 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/DataType.cs @@ -0,0 +1,13 @@ +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public enum DataType +{ + Boolean, + Integer, + Number, + String, + Array, + Dictionary, + Object, + Unknown +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISchemaFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISchemaFilter.cs index 65137a8b25..6dbebbae80 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISchemaFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISchemaFilter.cs @@ -1,39 +1,8 @@ -using System.Reflection; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen -{ - public interface ISchemaFilter - { - void Apply(OpenApiSchema schema, SchemaFilterContext context); - } - - public class SchemaFilterContext - { - public SchemaFilterContext( - Type type, - ISchemaGenerator schemaGenerator, - SchemaRepository schemaRepository, - MemberInfo memberInfo = null, - ParameterInfo parameterInfo = null) - { - Type = type; - SchemaGenerator = schemaGenerator; - SchemaRepository = schemaRepository; - MemberInfo = memberInfo; - ParameterInfo = parameterInfo; - } - - public Type Type { get; } - - public ISchemaGenerator SchemaGenerator { get; } +namespace Swashbuckle.AspNetCore.SwaggerGen; - public SchemaRepository SchemaRepository { get; } - - public MemberInfo MemberInfo { get; } - - public ParameterInfo ParameterInfo { get; } - - public string DocumentName => SchemaRepository.DocumentName; - } -} \ No newline at end of file +public interface ISchemaFilter +{ + void Apply(OpenApiSchema schema, SchemaFilterContext context); +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs index ac89dbabb6..61281411b1 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/ISerializerDataContractResolver.cs @@ -1,179 +1,6 @@ -using System.Reflection; +namespace Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.SwaggerGen +public interface ISerializerDataContractResolver { - public interface ISerializerDataContractResolver - { - DataContract GetDataContractForType(Type type); - } - - public class DataContract - { - public static DataContract ForPrimitive( - Type underlyingType, - DataType dataType, - string dataFormat, - Func jsonConverter = null) - { - return new DataContract( - underlyingType: underlyingType, - dataType: dataType, - dataFormat: dataFormat, - jsonConverter: jsonConverter); - } - - public static DataContract ForArray( - Type underlyingType, - Type itemType, - Func jsonConverter = null) - { - return new DataContract( - underlyingType: underlyingType, - dataType: DataType.Array, - arrayItemType: itemType, - jsonConverter: jsonConverter); - } - - public static DataContract ForDictionary( - Type underlyingType, - Type valueType, - IEnumerable keys = null, - Func jsonConverter = null) - { - return new DataContract( - underlyingType: underlyingType, - dataType: DataType.Dictionary, - dictionaryValueType: valueType, - dictionaryKeys: keys, - jsonConverter: jsonConverter); - } - - public static DataContract ForObject( - Type underlyingType, - IEnumerable properties, - Type extensionDataType = null, - string typeNameProperty = null, - string typeNameValue = null, - Func jsonConverter = null) - { - return new DataContract( - underlyingType: underlyingType, - dataType: DataType.Object, - objectProperties: properties, - objectExtensionDataType: extensionDataType, - objectTypeNameProperty: typeNameProperty, - objectTypeNameValue: typeNameValue, - jsonConverter: jsonConverter); - } - - public static DataContract ForDynamic( - Type underlyingType, - Func jsonConverter = null) - { - return new DataContract( - underlyingType: underlyingType, - dataType: DataType.Unknown, - jsonConverter: jsonConverter); - } - - [Obsolete("Provide jsonConverter function instead of enumValues")] - public static DataContract ForPrimitive( - Type underlyingType, - DataType dataType, - string dataFormat, - IEnumerable enumValues) - { - return new DataContract( - underlyingType: underlyingType, - dataType: dataType, - dataFormat: dataFormat, - enumValues: enumValues); - } - - private DataContract( - Type underlyingType, - DataType dataType, - string dataFormat = null, - IEnumerable enumValues = null, - Type arrayItemType = null, - Type dictionaryValueType = null, - IEnumerable dictionaryKeys = null, - IEnumerable objectProperties = null, - Type objectExtensionDataType = null, - string objectTypeNameProperty = null, - string objectTypeNameValue = null, - Func jsonConverter = null) - { - UnderlyingType = underlyingType; - DataType = dataType; - DataFormat = dataFormat; -#pragma warning disable CS0618 // Type or member is obsolete - EnumValues = enumValues; -#pragma warning restore CS0618 // Type or member is obsolete - ArrayItemType = arrayItemType; - DictionaryValueType = dictionaryValueType; - DictionaryKeys = dictionaryKeys; - ObjectProperties = objectProperties; - ObjectExtensionDataType = objectExtensionDataType; - ObjectTypeNameProperty = objectTypeNameProperty; - ObjectTypeNameValue = objectTypeNameValue; - JsonConverter = jsonConverter ?? new Func(obj => null); - } - - public Type UnderlyingType { get; } - public DataType DataType { get; } - public string DataFormat { get; } - public Type ArrayItemType { get; } - public Type DictionaryValueType { get; } - public IEnumerable DictionaryKeys { get; } - public IEnumerable ObjectProperties { get; } - public Type ObjectExtensionDataType { get; } - public string ObjectTypeNameProperty { get; } - public string ObjectTypeNameValue { get; } - public Func JsonConverter { get; } - - [Obsolete("Use JsonConverter")] - public IEnumerable EnumValues { get; } - } - - public enum DataType - { - Boolean, - Integer, - Number, - String, - Array, - Dictionary, - Object, - Unknown - } - - public class DataProperty - { - public DataProperty( - string name, - Type memberType, - bool isRequired = false, - bool isNullable = false, - bool isReadOnly = false, - bool isWriteOnly = false, - MemberInfo memberInfo = null) - { - Name = name; - IsRequired = isRequired; - IsNullable = isNullable; - IsReadOnly = isReadOnly; - IsWriteOnly = isWriteOnly; - MemberType = memberType; - MemberInfo = memberInfo; - } - - public string Name { get; } - public bool IsRequired { get; } - public bool IsNullable { get; } - public bool IsReadOnly { get; } - public bool IsWriteOnly { get; } - public Type MemberType { get; } - public MemberInfo MemberInfo { get; } - } + DataContract GetDataContractForType(Type type); } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs index 0aa40f94fc..2eac583cc7 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs @@ -4,284 +4,280 @@ using System.Text.Json.Serialization; using Swashbuckle.AspNetCore.Annotations; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class JsonSerializerDataContractResolver : ISerializerDataContractResolver { - public class JsonSerializerDataContractResolver : ISerializerDataContractResolver + private readonly JsonSerializerOptions _serializerOptions; + + public JsonSerializerDataContractResolver(JsonSerializerOptions serializerOptions) { - private readonly JsonSerializerOptions _serializerOptions; + _serializerOptions = serializerOptions; + } - public JsonSerializerDataContractResolver(JsonSerializerOptions serializerOptions) + public DataContract GetDataContractForType(Type type) + { + var effectiveType = Nullable.GetUnderlyingType(type) ?? type; + if (effectiveType.IsOneOf(typeof(object), typeof(JsonDocument), typeof(JsonElement))) { - _serializerOptions = serializerOptions; + return DataContract.ForDynamic( + underlyingType: effectiveType, + jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); } - public DataContract GetDataContractForType(Type type) + if (PrimitiveTypesAndFormats.TryGetValue(effectiveType, out var primitiveTypeAndFormat)) { - var effectiveType = Nullable.GetUnderlyingType(type) ?? type; - if (effectiveType.IsOneOf(typeof(object), typeof(JsonDocument), typeof(JsonElement))) - { - return DataContract.ForDynamic( - underlyingType: effectiveType, - jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); - } - - if (PrimitiveTypesAndFormats.TryGetValue(effectiveType, out var primitiveTypeAndFormat)) - { - return DataContract.ForPrimitive( - underlyingType: effectiveType, - dataType: primitiveTypeAndFormat.Item1, - dataFormat: primitiveTypeAndFormat.Item2, - jsonConverter: (value) => JsonConverterFunc(value, type)); - } + return DataContract.ForPrimitive( + underlyingType: effectiveType, + dataType: primitiveTypeAndFormat.Item1, + dataFormat: primitiveTypeAndFormat.Item2, + jsonConverter: (value) => JsonConverterFunc(value, type)); + } - if (effectiveType.IsEnum) - { - var enumValues = effectiveType.GetEnumValues(); + if (effectiveType.IsEnum) + { + var enumValues = effectiveType.GetEnumValues(); - // Test to determine if the serializer will treat as string - var serializeAsString = - enumValues.Length > 0 && -#if NET5_0_OR_GREATER - JsonConverterFunc(enumValues.GetValue(0), type).StartsWith('\"'); + // Test to determine if the serializer will treat as string + var serializeAsString = + enumValues.Length > 0 && +#if NET + JsonConverterFunc(enumValues.GetValue(0), type).StartsWith('\"'); #else - JsonConverterFunc(enumValues.GetValue(0), type).StartsWith("\""); + JsonConverterFunc(enumValues.GetValue(0), type).StartsWith("\""); #endif - var exampleType = serializeAsString ? - typeof(string) : - effectiveType.GetEnumUnderlyingType(); - - primitiveTypeAndFormat = PrimitiveTypesAndFormats[exampleType]; + var exampleType = serializeAsString ? + typeof(string) : + effectiveType.GetEnumUnderlyingType(); - return DataContract.ForPrimitive( - underlyingType: effectiveType, - dataType: primitiveTypeAndFormat.Item1, - dataFormat: primitiveTypeAndFormat.Item2, - jsonConverter: (value) => JsonConverterFunc(value, type)); - } + primitiveTypeAndFormat = PrimitiveTypesAndFormats[exampleType]; - if (IsSupportedDictionary(effectiveType, out Type keyType, out Type valueType)) - { - IEnumerable keys = null; - - if (keyType.IsEnum) - { - // This is a special case where we know the possible key values - var enumValuesAsJson = keyType.GetEnumValues() - .Cast() - .Select(value => JsonConverterFunc(value, keyType)); - - keys = enumValuesAsJson.Any(json => json.StartsWith("\"")) - ? enumValuesAsJson.Select(json => json.Replace("\"", string.Empty)) - : keyType.GetEnumNames(); - } + return DataContract.ForPrimitive( + underlyingType: effectiveType, + dataType: primitiveTypeAndFormat.Item1, + dataFormat: primitiveTypeAndFormat.Item2, + jsonConverter: (value) => JsonConverterFunc(value, type)); + } - return DataContract.ForDictionary( - underlyingType: effectiveType, - valueType: valueType, - keys: keys, - jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); - } + if (IsSupportedDictionary(effectiveType, out Type keyType, out Type valueType)) + { + IEnumerable keys = null; - if (IsSupportedCollection(effectiveType, out Type itemType)) + if (keyType.IsEnum) { - return DataContract.ForArray( - underlyingType: effectiveType, - itemType: itemType, - jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); + // This is a special case where we know the possible key values + var enumValuesAsJson = keyType.GetEnumValues() + .Cast() + .Select(value => JsonConverterFunc(value, keyType)); + + keys = enumValuesAsJson.Any(json => json.StartsWith("\"")) + ? enumValuesAsJson.Select(json => json.Replace("\"", string.Empty)) + : keyType.GetEnumNames(); } - return DataContract.ForObject( + return DataContract.ForDictionary( underlyingType: effectiveType, - properties: GetDataPropertiesFor(effectiveType, out Type extensionDataType), - extensionDataType: extensionDataType, + valueType: valueType, + keys: keys, jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); } - private string JsonConverterFunc(object value, Type type) + if (IsSupportedCollection(effectiveType, out Type itemType)) { - return JsonSerializer.Serialize(value, type, _serializerOptions); + return DataContract.ForArray( + underlyingType: effectiveType, + itemType: itemType, + jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); } - public bool IsSupportedDictionary(Type type, out Type keyType, out Type valueType) - { - if (type.IsConstructedFrom(typeof(IDictionary<,>), out Type constructedType) - || type.IsConstructedFrom(typeof(IReadOnlyDictionary<,>), out constructedType)) - { - keyType = constructedType.GenericTypeArguments[0]; - valueType = constructedType.GenericTypeArguments[1]; - return true; - } + return DataContract.ForObject( + underlyingType: effectiveType, + properties: GetDataPropertiesFor(effectiveType, out Type extensionDataType), + extensionDataType: extensionDataType, + jsonConverter: (value) => JsonConverterFunc(value, effectiveType)); + } - if (typeof(IDictionary).IsAssignableFrom(type)) - { - keyType = valueType = typeof(object); - return true; - } + private string JsonConverterFunc(object value, Type type) + { + return JsonSerializer.Serialize(value, type, _serializerOptions); + } - keyType = valueType = null; - return false; + public bool IsSupportedDictionary(Type type, out Type keyType, out Type valueType) + { + if (type.IsConstructedFrom(typeof(IDictionary<,>), out Type constructedType) + || type.IsConstructedFrom(typeof(IReadOnlyDictionary<,>), out constructedType)) + { + keyType = constructedType.GenericTypeArguments[0]; + valueType = constructedType.GenericTypeArguments[1]; + return true; } - public bool IsSupportedCollection(Type type, out Type itemType) + if (typeof(IDictionary).IsAssignableFrom(type)) { - if (type.IsConstructedFrom(typeof(IEnumerable<>), out Type constructedType)) - { - itemType = constructedType.GenericTypeArguments[0]; - return true; - } + keyType = valueType = typeof(object); + return true; + } -#if (!NETSTANDARD2_0) - if (type.IsConstructedFrom(typeof(IAsyncEnumerable<>), out constructedType)) - { - itemType = constructedType.GenericTypeArguments[0]; - return true; - } -#endif + keyType = valueType = null; + return false; + } - if (type.IsArray) - { - itemType = type.GetElementType(); - return true; - } + public bool IsSupportedCollection(Type type, out Type itemType) + { + if (type.IsConstructedFrom(typeof(IEnumerable<>), out Type constructedType)) + { + itemType = constructedType.GenericTypeArguments[0]; + return true; + } - if (typeof(IEnumerable).IsAssignableFrom(type)) - { - itemType = typeof(object); - return true; - } +#if NET + if (type.IsConstructedFrom(typeof(IAsyncEnumerable<>), out constructedType)) + { + itemType = constructedType.GenericTypeArguments[0]; + return true; + } +#endif - itemType = null; - return false; + if (type.IsArray) + { + itemType = type.GetElementType(); + return true; } - private List GetDataPropertiesFor(Type objectType, out Type extensionDataType) + if (typeof(IEnumerable).IsAssignableFrom(type)) { - extensionDataType = null; + itemType = typeof(object); + return true; + } + + itemType = null; + return false; + } - const BindingFlags PublicBindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + private List GetDataPropertiesFor(Type objectType, out Type extensionDataType) + { + extensionDataType = null; - var publicProperties = objectType.IsInterface - ? new[] { objectType }.Concat(objectType.GetInterfaces()).SelectMany(i => i.GetProperties(PublicBindingAttr)) - : objectType.GetProperties(PublicBindingAttr); + const BindingFlags PublicBindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - var applicableProperties = publicProperties - .Where(property => - { - // .NET 5 introduces JsonIgnoreAttribute.Condition which should be honored - bool isIgnoredViaNet5Attribute = true; + var publicProperties = objectType.IsInterface + ? new[] { objectType }.Concat(objectType.GetInterfaces()).SelectMany(i => i.GetProperties(PublicBindingAttr)) + : objectType.GetProperties(PublicBindingAttr); -#if NET5_0_OR_GREATER - JsonIgnoreAttribute jsonIgnoreAttribute = property.GetCustomAttribute(); - if (jsonIgnoreAttribute != null) + var applicableProperties = publicProperties + .Where(property => + { + // .NET 5 introduces JsonIgnoreAttribute.Condition which should be honored + bool isIgnoredViaNet5Attribute = true; + +#if NET + JsonIgnoreAttribute jsonIgnoreAttribute = property.GetCustomAttribute(); + if (jsonIgnoreAttribute != null) + { + isIgnoredViaNet5Attribute = jsonIgnoreAttribute.Condition switch { - isIgnoredViaNet5Attribute = jsonIgnoreAttribute.Condition switch - { - JsonIgnoreCondition.Never => false, - JsonIgnoreCondition.Always => true, - JsonIgnoreCondition.WhenWritingDefault => false, - JsonIgnoreCondition.WhenWritingNull => false, - _ => true - }; - } + JsonIgnoreCondition.Never => false, + JsonIgnoreCondition.Always => true, + JsonIgnoreCondition.WhenWritingDefault => false, + JsonIgnoreCondition.WhenWritingNull => false, + _ => true + }; + } #endif - return - (property.IsPubliclyReadable() || property.IsPubliclyWritable()) && - !(property.GetIndexParameters().Length > 0) && - !(property.HasAttribute() && isIgnoredViaNet5Attribute) && - !(property.HasAttribute()) && - !(_serializerOptions.IgnoreReadOnlyProperties && !property.IsPubliclyWritable()); - }) - .OrderBy(property => property.DeclaringType.GetInheritanceChain().Length); + return + (property.IsPubliclyReadable() || property.IsPubliclyWritable()) && + !(property.GetIndexParameters().Length > 0) && + !(property.HasAttribute() && isIgnoredViaNet5Attribute) && + !(property.HasAttribute()) && + !(_serializerOptions.IgnoreReadOnlyProperties && !property.IsPubliclyWritable()); + }) + .OrderBy(property => property.DeclaringType.GetInheritanceChain().Length); - var dataProperties = new List(); + var dataProperties = new List(); - foreach (var propertyInfo in applicableProperties) + foreach (var propertyInfo in applicableProperties) + { + if (propertyInfo.HasAttribute() + && propertyInfo.PropertyType.IsConstructedFrom(typeof(IDictionary<,>), out Type constructedDictionary)) { - if (propertyInfo.HasAttribute() - && propertyInfo.PropertyType.IsConstructedFrom(typeof(IDictionary<,>), out Type constructedDictionary)) - { - extensionDataType = constructedDictionary.GenericTypeArguments[1]; - continue; - } + extensionDataType = constructedDictionary.GenericTypeArguments[1]; + continue; + } - var name = propertyInfo.GetCustomAttribute()?.Name - ?? _serializerOptions.PropertyNamingPolicy?.ConvertName(propertyInfo.Name) ?? propertyInfo.Name; + var name = propertyInfo.GetCustomAttribute()?.Name + ?? _serializerOptions.PropertyNamingPolicy?.ConvertName(propertyInfo.Name) ?? propertyInfo.Name; - // .NET 5 introduces support for serializing immutable types via parameterized constructors - // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability?pivots=dotnet-6-0 - var isDeserializedViaConstructor = false; + // .NET 5 introduces support for serializing immutable types via parameterized constructors + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability?pivots=dotnet-6-0 + var isDeserializedViaConstructor = false; - var isRequired = false; + var isRequired = false; -#if NET5_0_OR_GREATER - var deserializationConstructor = propertyInfo.DeclaringType?.GetConstructors() - .OrderBy(c => - { - if (c.GetCustomAttribute() != null) return 1; - if (c.GetParameters().Length == 0) return 2; - return 3; - }) - .FirstOrDefault(); - - isDeserializedViaConstructor = deserializationConstructor != null && deserializationConstructor.GetParameters() - .Any(p => - { - return - string.Equals(p.Name, propertyInfo.Name, StringComparison.OrdinalIgnoreCase) || - string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase); - }); -#endif -#if NET7_0_OR_GREATER - isRequired = propertyInfo.GetCustomAttribute() != null; -#endif +#if NET + var deserializationConstructor = propertyInfo.DeclaringType?.GetConstructors() + .OrderBy(c => + { + if (c.GetCustomAttribute() != null) return 1; + if (c.GetParameters().Length == 0) return 2; + return 3; + }) + .FirstOrDefault(); - dataProperties.Add( - new DataProperty( - name: name, - isRequired: isRequired, - isNullable: propertyInfo.PropertyType.IsReferenceOrNullableType(), - isReadOnly: propertyInfo.IsPubliclyReadable() && !propertyInfo.IsPubliclyWritable() && !isDeserializedViaConstructor, - isWriteOnly: propertyInfo.IsPubliclyWritable() && !propertyInfo.IsPubliclyReadable(), - memberType: propertyInfo.PropertyType, - memberInfo: propertyInfo)); - } + isDeserializedViaConstructor = deserializationConstructor != null && deserializationConstructor.GetParameters() + .Any(p => + { + return + string.Equals(p.Name, propertyInfo.Name, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase); + }); - return dataProperties; + isRequired = propertyInfo.GetCustomAttribute() != null; +#endif + + dataProperties.Add( + new DataProperty( + name: name, + isRequired: isRequired, + isNullable: propertyInfo.PropertyType.IsReferenceOrNullableType(), + isReadOnly: propertyInfo.IsPubliclyReadable() && !propertyInfo.IsPubliclyWritable() && !isDeserializedViaConstructor, + isWriteOnly: propertyInfo.IsPubliclyWritable() && !propertyInfo.IsPubliclyReadable(), + memberType: propertyInfo.PropertyType, + memberInfo: propertyInfo)); } - private static readonly Dictionary> PrimitiveTypesAndFormats = new() - { - [typeof(bool)] = Tuple.Create(DataType.Boolean, (string)null), - [typeof(byte)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(sbyte)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(short)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(ushort)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(int)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(uint)] = Tuple.Create(DataType.Integer, "int32"), - [typeof(long)] = Tuple.Create(DataType.Integer, "int64"), - [typeof(ulong)] = Tuple.Create(DataType.Integer, "int64"), - [typeof(float)] = Tuple.Create(DataType.Number, "float"), - [typeof(double)] = Tuple.Create(DataType.Number, "double"), - [typeof(decimal)] = Tuple.Create(DataType.Number, "double"), - [typeof(byte[])] = Tuple.Create(DataType.String, "byte"), - [typeof(string)] = Tuple.Create(DataType.String, (string)null), - [typeof(char)] = Tuple.Create(DataType.String, (string)null), - [typeof(DateTime)] = Tuple.Create(DataType.String, "date-time"), - [typeof(DateTimeOffset)] = Tuple.Create(DataType.String, "date-time"), - [typeof(TimeSpan)] = Tuple.Create(DataType.String, "date-span"), - [typeof(Guid)] = Tuple.Create(DataType.String, "uuid"), - [typeof(Uri)] = Tuple.Create(DataType.String, "uri"), - [typeof(Version)] = Tuple.Create(DataType.String, (string)null), -#if NET6_0_OR_GREATER - [typeof(DateOnly)] = Tuple.Create(DataType.String, "date"), - [typeof(TimeOnly)] = Tuple.Create(DataType.String, "time"), -#endif -#if NET7_0_OR_GREATER - [ typeof(Int128) ] = Tuple.Create(DataType.Integer, "int128"), - [ typeof(UInt128) ] = Tuple.Create(DataType.Integer, "int128"), -#endif - }; + return dataProperties; } + + private static readonly Dictionary> PrimitiveTypesAndFormats = new() + { + [typeof(bool)] = Tuple.Create(DataType.Boolean, (string)null), + [typeof(byte)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(sbyte)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(short)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(ushort)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(int)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(uint)] = Tuple.Create(DataType.Integer, "int32"), + [typeof(long)] = Tuple.Create(DataType.Integer, "int64"), + [typeof(ulong)] = Tuple.Create(DataType.Integer, "int64"), + [typeof(float)] = Tuple.Create(DataType.Number, "float"), + [typeof(double)] = Tuple.Create(DataType.Number, "double"), + [typeof(decimal)] = Tuple.Create(DataType.Number, "double"), + [typeof(byte[])] = Tuple.Create(DataType.String, "byte"), + [typeof(string)] = Tuple.Create(DataType.String, (string)null), + [typeof(char)] = Tuple.Create(DataType.String, (string)null), + [typeof(DateTime)] = Tuple.Create(DataType.String, "date-time"), + [typeof(DateTimeOffset)] = Tuple.Create(DataType.String, "date-time"), + [typeof(TimeSpan)] = Tuple.Create(DataType.String, "date-span"), + [typeof(Guid)] = Tuple.Create(DataType.String, "uuid"), + [typeof(Uri)] = Tuple.Create(DataType.String, "uri"), + [typeof(Version)] = Tuple.Create(DataType.String, (string)null), +#if NET + [typeof(DateOnly)] = Tuple.Create(DataType.String, "date"), + [typeof(TimeOnly)] = Tuple.Create(DataType.String, "time"), + [typeof(Int128)] = Tuple.Create(DataType.Integer, "int128"), + [typeof(UInt128)] = Tuple.Create(DataType.Integer, "int128"), +#endif + }; } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs index a097300998..22972a5877 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs @@ -1,200 +1,199 @@ using System.Reflection; using Microsoft.AspNetCore.Mvc; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public static class MemberInfoExtensions { - public static class MemberInfoExtensions - { -#if !NET6_0_OR_GREATER - private const string NullableAttributeFullTypeName = "System.Runtime.CompilerServices.NullableAttribute"; - private const string NullableFlagsFieldName = "NullableFlags"; - private const string NullableContextAttributeFullTypeName = "System.Runtime.CompilerServices.NullableContextAttribute"; - private const string FlagFieldName = "Flag"; - private const int NotAnnotated = 1; // See https://github.com/dotnet/roslyn/blob/af7b0ebe2b0ed5c335a928626c25620566372dd1/docs/features/nullable-metadata.md?plain=1#L40 +#if !NET + private const string NullableAttributeFullTypeName = "System.Runtime.CompilerServices.NullableAttribute"; + private const string NullableFlagsFieldName = "NullableFlags"; + private const string NullableContextAttributeFullTypeName = "System.Runtime.CompilerServices.NullableContextAttribute"; + private const string FlagFieldName = "Flag"; + private const int NotAnnotated = 1; // See https://github.com/dotnet/roslyn/blob/af7b0ebe2b0ed5c335a928626c25620566372dd1/docs/features/nullable-metadata.md?plain=1#L40 #endif - public static IEnumerable GetInlineAndMetadataAttributes(this MemberInfo memberInfo) - { - var attributes = memberInfo.GetCustomAttributes(true) - .ToList(); + public static IEnumerable GetInlineAndMetadataAttributes(this MemberInfo memberInfo) + { + var attributes = memberInfo.GetCustomAttributes(true) + .ToList(); - var metadataTypeAttribute = memberInfo.DeclaringType.GetCustomAttributes(true) - .OfType() - .FirstOrDefault(); + var metadataTypeAttribute = memberInfo.DeclaringType.GetCustomAttributes(true) + .OfType() + .FirstOrDefault(); - var metadataMemberInfo = metadataTypeAttribute?.MetadataType.GetMember(memberInfo.Name) - .FirstOrDefault(); + var metadataMemberInfo = metadataTypeAttribute?.MetadataType.GetMember(memberInfo.Name) + .FirstOrDefault(); - if (metadataMemberInfo != null) - { - attributes.AddRange(metadataMemberInfo.GetCustomAttributes(true)); - } - - return attributes; + if (metadataMemberInfo != null) + { + attributes.AddRange(metadataMemberInfo.GetCustomAttributes(true)); } -#if NET6_0_OR_GREATER - private static NullabilityInfo GetNullabilityInfo(this MemberInfo memberInfo) - { - var context = new NullabilityInfoContext(); + return attributes; + } - return memberInfo switch - { - FieldInfo fieldInfo => context.Create(fieldInfo), - PropertyInfo propertyInfo => context.Create(propertyInfo), - EventInfo eventInfo => context.Create(eventInfo), - _ => throw new InvalidOperationException($"MemberInfo type {memberInfo.MemberType} is not supported.") - }; - } -#endif +#if NET + private static NullabilityInfo GetNullabilityInfo(this MemberInfo memberInfo) + { + var context = new NullabilityInfoContext(); - public static bool IsNonNullableReferenceType(this MemberInfo memberInfo) + return memberInfo switch { -#if NET6_0_OR_GREATER - var nullableInfo = GetNullabilityInfo(memberInfo); - return nullableInfo.ReadState == NullabilityState.NotNull; + FieldInfo fieldInfo => context.Create(fieldInfo), + PropertyInfo propertyInfo => context.Create(propertyInfo), + EventInfo eventInfo => context.Create(eventInfo), + _ => throw new InvalidOperationException($"MemberInfo type {memberInfo.MemberType} is not supported.") + }; + } +#endif + + public static bool IsNonNullableReferenceType(this MemberInfo memberInfo) + { +#if NET + var nullableInfo = GetNullabilityInfo(memberInfo); + return nullableInfo.ReadState == NullabilityState.NotNull; #else - var memberType = memberInfo.MemberType == MemberTypes.Field - ? ((FieldInfo)memberInfo).FieldType - : ((PropertyInfo)memberInfo).PropertyType; + var memberType = memberInfo.MemberType == MemberTypes.Field + ? ((FieldInfo)memberInfo).FieldType + : ((PropertyInfo)memberInfo).PropertyType; - if (memberType.IsValueType) return false; + if (memberType.IsValueType) return false; - var nullableAttribute = memberInfo.GetNullableAttribute(); + var nullableAttribute = memberInfo.GetNullableAttribute(); - if (nullableAttribute == null) - { - return memberInfo.GetNullableFallbackValue(); - } + if (nullableAttribute == null) + { + return memberInfo.GetNullableFallbackValue(); + } - if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field && - field.GetValue(nullableAttribute) is byte[] flags && - flags.Length >= 1 && flags[0] == NotAnnotated) - { - return true; - } + if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field && + field.GetValue(nullableAttribute) is byte[] flags && + flags.Length >= 1 && flags[0] == NotAnnotated) + { + return true; + } - return false; + return false; #endif - } + } - public static bool IsDictionaryValueNonNullable(this MemberInfo memberInfo) + public static bool IsDictionaryValueNonNullable(this MemberInfo memberInfo) + { +#if NET + var nullableInfo = GetNullabilityInfo(memberInfo); + + // Assume one generic argument means TKey and TValue are the same type. + // Assume two generic arguments match TKey and TValue for a dictionary. + // A better solution would be to inspect the type declaration (base types, + // interfaces, etc.) to determine if the type is a dictionary, but the + // nullability information is not available to be able to do that. + // See https://stackoverflow.com/q/75786306/1064169. + return nullableInfo.GenericTypeArguments.Length switch { -#if NET6_0_OR_GREATER - var nullableInfo = GetNullabilityInfo(memberInfo); - - // Assume one generic argument means TKey and TValue are the same type. - // Assume two generic arguments match TKey and TValue for a dictionary. - // A better solution would be to inspect the type declaration (base types, - // interfaces, etc.) to determine if the type is a dictionary, but the - // nullability information is not available to be able to do that. - // See https://stackoverflow.com/q/75786306/1064169. - return nullableInfo.GenericTypeArguments.Length switch - { - 1 => nullableInfo.GenericTypeArguments[0].ReadState == NullabilityState.NotNull, - 2 => nullableInfo.GenericTypeArguments[1].ReadState == NullabilityState.NotNull, - _ => false, - }; + 1 => nullableInfo.GenericTypeArguments[0].ReadState == NullabilityState.NotNull, + 2 => nullableInfo.GenericTypeArguments[1].ReadState == NullabilityState.NotNull, + _ => false, + }; #else - var memberType = memberInfo.MemberType == MemberTypes.Field - ? ((FieldInfo)memberInfo).FieldType - : ((PropertyInfo)memberInfo).PropertyType; + var memberType = memberInfo.MemberType == MemberTypes.Field + ? ((FieldInfo)memberInfo).FieldType + : ((PropertyInfo)memberInfo).PropertyType; - if (memberType.IsValueType) return false; + if (memberType.IsValueType) return false; - var nullableAttribute = memberInfo.GetNullableAttribute(); - var genericArguments = memberType.GetGenericArguments(); + var nullableAttribute = memberInfo.GetNullableAttribute(); + var genericArguments = memberType.GetGenericArguments(); - if (genericArguments.Length != 2) - { - return false; - } + if (genericArguments.Length != 2) + { + return false; + } - var valueArgument = genericArguments[1]; - var valueArgumentIsNullable = valueArgument.IsGenericType && valueArgument.GetGenericTypeDefinition() == typeof(Nullable<>); + var valueArgument = genericArguments[1]; + var valueArgumentIsNullable = valueArgument.IsGenericType && valueArgument.GetGenericTypeDefinition() == typeof(Nullable<>); - if (nullableAttribute == null) - { - return !valueArgumentIsNullable && memberInfo.GetNullableFallbackValue(); - } + if (nullableAttribute == null) + { + return !valueArgumentIsNullable && memberInfo.GetNullableFallbackValue(); + } - if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field) + if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field) + { + if (field.GetValue(nullableAttribute) is byte[] flags) { - if (field.GetValue(nullableAttribute) is byte[] flags) + // See https://github.com/dotnet/roslyn/blob/af7b0ebe2b0ed5c335a928626c25620566372dd1/docs/features/nullable-metadata.md + // Observations in the debugger show that the arity of the flags array is 3 only if all 3 items are reference types, i.e. + // Dictionary would have arity 3 (one for the Dictionary, one for the string key, one for the object value), + // however Dictionary would have arity 2 (one for the Dictionary, one for the string key), the value is skipped + // due it being a value type. + if (flags.Length == 2) // Value in the dictionary is a value type. + { + return !valueArgumentIsNullable; + } + else if (flags.Length == 3) // Value in the dictionary is a reference type. { - // See https://github.com/dotnet/roslyn/blob/af7b0ebe2b0ed5c335a928626c25620566372dd1/docs/features/nullable-metadata.md - // Observations in the debugger show that the arity of the flags array is 3 only if all 3 items are reference types, i.e. - // Dictionary would have arity 3 (one for the Dictionary, one for the string key, one for the object value), - // however Dictionary would have arity 2 (one for the Dictionary, one for the string key), the value is skipped - // due it being a value type. - if (flags.Length == 2) // Value in the dictionary is a value type. - { - return !valueArgumentIsNullable; - } - else if (flags.Length == 3) // Value in the dictionary is a reference type. - { - return flags[2] == NotAnnotated; - } + return flags[2] == NotAnnotated; } } + } - return false; + return false; #endif - } + } -#if !NET6_0_OR_GREATER - private static object GetNullableAttribute(this MemberInfo memberInfo) - { - var nullableAttribute = memberInfo - .GetCustomAttributes() - .FirstOrDefault(attr => string.Equals(attr.GetType().FullName, NullableAttributeFullTypeName)); +#if !NET + private static object GetNullableAttribute(this MemberInfo memberInfo) + { + var nullableAttribute = memberInfo + .GetCustomAttributes() + .FirstOrDefault(attr => string.Equals(attr.GetType().FullName, NullableAttributeFullTypeName)); - return nullableAttribute; - } + return nullableAttribute; + } - private static bool GetNullableFallbackValue(this MemberInfo memberInfo) - { - var declaringTypes = memberInfo.DeclaringType.IsNested - ? GetDeclaringTypeChain(memberInfo) - : [memberInfo.DeclaringType]; + private static bool GetNullableFallbackValue(this MemberInfo memberInfo) + { + var declaringTypes = memberInfo.DeclaringType.IsNested + ? GetDeclaringTypeChain(memberInfo) + : [memberInfo.DeclaringType]; - foreach (var declaringType in declaringTypes) - { - IEnumerable attributes = declaringType.GetCustomAttributes(false); + foreach (var declaringType in declaringTypes) + { + IEnumerable attributes = declaringType.GetCustomAttributes(false); - var nullableContext = attributes - .FirstOrDefault(attr => string.Equals(attr.GetType().FullName, NullableContextAttributeFullTypeName)); + var nullableContext = attributes + .FirstOrDefault(attr => string.Equals(attr.GetType().FullName, NullableContextAttributeFullTypeName)); - if (nullableContext != null) + if (nullableContext != null) + { + if (nullableContext.GetType().GetField(FlagFieldName) is FieldInfo field && + field.GetValue(nullableContext) is byte flag && flag == NotAnnotated) { - if (nullableContext.GetType().GetField(FlagFieldName) is FieldInfo field && - field.GetValue(nullableContext) is byte flag && flag == NotAnnotated) - { - return true; - } - else - { - return false; - } + return true; + } + else + { + return false; } } - - return false; } -#endif - private static List GetDeclaringTypeChain(MemberInfo memberInfo) - { - var chain = new List(); - var currentType = memberInfo.DeclaringType; + return false; + } +#endif - while (currentType != null) - { - chain.Add(currentType); - currentType = currentType.DeclaringType; - } + private static List GetDeclaringTypeChain(MemberInfo memberInfo) + { + var chain = new List(); + var currentType = memberInfo.DeclaringType; - return chain; + while (currentType != null) + { + chain.Add(currentType); + currentType = currentType.DeclaringType; } + + return chain; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs index b96947e124..3e0e24300b 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs @@ -5,257 +5,256 @@ using Microsoft.OpenApi.Models; using AnnotationsDataType = System.ComponentModel.DataAnnotations.DataType; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public static class OpenApiSchemaExtensions { - public static class OpenApiSchemaExtensions + private static readonly Dictionary DataFormatMappings = new() { - private static readonly Dictionary DataFormatMappings = new() - { - [AnnotationsDataType.DateTime] = "date-time", - [AnnotationsDataType.Date] = "date", - [AnnotationsDataType.Time] = "time", - [AnnotationsDataType.Duration] = "duration", - [AnnotationsDataType.PhoneNumber] = "tel", - [AnnotationsDataType.Currency] = "currency", - [AnnotationsDataType.Text] = "string", - [AnnotationsDataType.Html] = "html", - [AnnotationsDataType.MultilineText] = "multiline", - [AnnotationsDataType.EmailAddress] = "email", - [AnnotationsDataType.Password] = "password", - [AnnotationsDataType.Url] = "uri", - [AnnotationsDataType.ImageUrl] = "uri", - [AnnotationsDataType.CreditCard] = "credit-card", - [AnnotationsDataType.PostalCode] = "postal-code", - [AnnotationsDataType.Upload] = "binary", - }; - - public static void ApplyValidationAttributes(this OpenApiSchema schema, IEnumerable customAttributes) + [AnnotationsDataType.DateTime] = "date-time", + [AnnotationsDataType.Date] = "date", + [AnnotationsDataType.Time] = "time", + [AnnotationsDataType.Duration] = "duration", + [AnnotationsDataType.PhoneNumber] = "tel", + [AnnotationsDataType.Currency] = "currency", + [AnnotationsDataType.Text] = "string", + [AnnotationsDataType.Html] = "html", + [AnnotationsDataType.MultilineText] = "multiline", + [AnnotationsDataType.EmailAddress] = "email", + [AnnotationsDataType.Password] = "password", + [AnnotationsDataType.Url] = "uri", + [AnnotationsDataType.ImageUrl] = "uri", + [AnnotationsDataType.CreditCard] = "credit-card", + [AnnotationsDataType.PostalCode] = "postal-code", + [AnnotationsDataType.Upload] = "binary", + }; + + public static void ApplyValidationAttributes(this OpenApiSchema schema, IEnumerable customAttributes) + { + foreach (var attribute in customAttributes) { - foreach (var attribute in customAttributes) - { - if (attribute is DataTypeAttribute dataTypeAttribute) - ApplyDataTypeAttribute(schema, dataTypeAttribute); + if (attribute is DataTypeAttribute dataTypeAttribute) + ApplyDataTypeAttribute(schema, dataTypeAttribute); - else if (attribute is MinLengthAttribute minLengthAttribute) - ApplyMinLengthAttribute(schema, minLengthAttribute); + else if (attribute is MinLengthAttribute minLengthAttribute) + ApplyMinLengthAttribute(schema, minLengthAttribute); - else if (attribute is MaxLengthAttribute maxLengthAttribute) - ApplyMaxLengthAttribute(schema, maxLengthAttribute); + else if (attribute is MaxLengthAttribute maxLengthAttribute) + ApplyMaxLengthAttribute(schema, maxLengthAttribute); -#if NET8_0_OR_GREATER +#if NET - else if (attribute is LengthAttribute lengthAttribute) - ApplyLengthAttribute(schema, lengthAttribute); + else if (attribute is LengthAttribute lengthAttribute) + ApplyLengthAttribute(schema, lengthAttribute); - else if (attribute is Base64StringAttribute base64Attribute) - ApplyBase64Attribute(schema); + else if (attribute is Base64StringAttribute base64Attribute) + ApplyBase64Attribute(schema); #endif - else if (attribute is RangeAttribute rangeAttribute) - ApplyRangeAttribute(schema, rangeAttribute); + else if (attribute is RangeAttribute rangeAttribute) + ApplyRangeAttribute(schema, rangeAttribute); - else if (attribute is RegularExpressionAttribute regularExpressionAttribute) - ApplyRegularExpressionAttribute(schema, regularExpressionAttribute); + else if (attribute is RegularExpressionAttribute regularExpressionAttribute) + ApplyRegularExpressionAttribute(schema, regularExpressionAttribute); - else if (attribute is StringLengthAttribute stringLengthAttribute) - ApplyStringLengthAttribute(schema, stringLengthAttribute); + else if (attribute is StringLengthAttribute stringLengthAttribute) + ApplyStringLengthAttribute(schema, stringLengthAttribute); - else if (attribute is ReadOnlyAttribute readOnlyAttribute) - ApplyReadOnlyAttribute(schema, readOnlyAttribute); + else if (attribute is ReadOnlyAttribute readOnlyAttribute) + ApplyReadOnlyAttribute(schema, readOnlyAttribute); - else if (attribute is DescriptionAttribute descriptionAttribute) - ApplyDescriptionAttribute(schema, descriptionAttribute); + else if (attribute is DescriptionAttribute descriptionAttribute) + ApplyDescriptionAttribute(schema, descriptionAttribute); - } } + } - public static void ApplyRouteConstraints(this OpenApiSchema schema, ApiParameterRouteInfo routeInfo) + public static void ApplyRouteConstraints(this OpenApiSchema schema, ApiParameterRouteInfo routeInfo) + { + foreach (var constraint in routeInfo.Constraints) { - foreach (var constraint in routeInfo.Constraints) - { - if (constraint is MinRouteConstraint minRouteConstraint) - ApplyMinRouteConstraint(schema, minRouteConstraint); + if (constraint is MinRouteConstraint minRouteConstraint) + ApplyMinRouteConstraint(schema, minRouteConstraint); - else if (constraint is MaxRouteConstraint maxRouteConstraint) - ApplyMaxRouteConstraint(schema, maxRouteConstraint); + else if (constraint is MaxRouteConstraint maxRouteConstraint) + ApplyMaxRouteConstraint(schema, maxRouteConstraint); - else if (constraint is MinLengthRouteConstraint minLengthRouteConstraint) - ApplyMinLengthRouteConstraint(schema, minLengthRouteConstraint); + else if (constraint is MinLengthRouteConstraint minLengthRouteConstraint) + ApplyMinLengthRouteConstraint(schema, minLengthRouteConstraint); - else if (constraint is MaxLengthRouteConstraint maxLengthRouteConstraint) - ApplyMaxLengthRouteConstraint(schema, maxLengthRouteConstraint); + else if (constraint is MaxLengthRouteConstraint maxLengthRouteConstraint) + ApplyMaxLengthRouteConstraint(schema, maxLengthRouteConstraint); - else if (constraint is RangeRouteConstraint rangeRouteConstraint) - ApplyRangeRouteConstraint(schema, rangeRouteConstraint); + else if (constraint is RangeRouteConstraint rangeRouteConstraint) + ApplyRangeRouteConstraint(schema, rangeRouteConstraint); - else if (constraint is RegexRouteConstraint regexRouteConstraint) - ApplyRegexRouteConstraint(schema, regexRouteConstraint); + else if (constraint is RegexRouteConstraint regexRouteConstraint) + ApplyRegexRouteConstraint(schema, regexRouteConstraint); - else if (constraint is LengthRouteConstraint lengthRouteConstraint) - ApplyLengthRouteConstraint(schema, lengthRouteConstraint); + else if (constraint is LengthRouteConstraint lengthRouteConstraint) + ApplyLengthRouteConstraint(schema, lengthRouteConstraint); - else if (constraint is FloatRouteConstraint or DecimalRouteConstraint) - schema.Type = JsonSchemaTypes.Number; + else if (constraint is FloatRouteConstraint or DecimalRouteConstraint) + schema.Type = JsonSchemaTypes.Number; - else if (constraint is LongRouteConstraint or IntRouteConstraint) - schema.Type = JsonSchemaTypes.Integer; + else if (constraint is LongRouteConstraint or IntRouteConstraint) + schema.Type = JsonSchemaTypes.Integer; - else if (constraint is GuidRouteConstraint or StringRouteConstraint) - schema.Type = JsonSchemaTypes.String; + else if (constraint is GuidRouteConstraint or StringRouteConstraint) + schema.Type = JsonSchemaTypes.String; - else if (constraint is BoolRouteConstraint) - schema.Type = JsonSchemaTypes.Boolean; - } + else if (constraint is BoolRouteConstraint) + schema.Type = JsonSchemaTypes.Boolean; } + } - public static string ResolveType(this OpenApiSchema schema, SchemaRepository schemaRepository) + public static string ResolveType(this OpenApiSchema schema, SchemaRepository schemaRepository) + { + if (schema.Reference != null && schemaRepository.Schemas.TryGetValue(schema.Reference.Id, out OpenApiSchema definitionSchema)) { - if (schema.Reference != null && schemaRepository.Schemas.TryGetValue(schema.Reference.Id, out OpenApiSchema definitionSchema)) - { - return definitionSchema.ResolveType(schemaRepository); - } - - foreach (var subSchema in schema.AllOf) - { - var type = subSchema.ResolveType(schemaRepository); - if (type != null) - { - return type; - } - } - - return schema.Type; + return definitionSchema.ResolveType(schemaRepository); } - private static void ApplyDataTypeAttribute(OpenApiSchema schema, DataTypeAttribute dataTypeAttribute) + foreach (var subSchema in schema.AllOf) { - if (DataFormatMappings.TryGetValue(dataTypeAttribute.DataType, out string format)) + var type = subSchema.ResolveType(schemaRepository); + if (type != null) { - schema.Format = format; + return type; } } - private static void ApplyMinLengthAttribute(OpenApiSchema schema, MinLengthAttribute minLengthAttribute) - { - if (schema.Type == JsonSchemaTypes.Array) - schema.MinItems = minLengthAttribute.Length; - else - schema.MinLength = minLengthAttribute.Length; - } + return schema.Type; + } - private static void ApplyMinLengthRouteConstraint(OpenApiSchema schema, MinLengthRouteConstraint minLengthRouteConstraint) + private static void ApplyDataTypeAttribute(OpenApiSchema schema, DataTypeAttribute dataTypeAttribute) + { + if (DataFormatMappings.TryGetValue(dataTypeAttribute.DataType, out string format)) { - if (schema.Type == JsonSchemaTypes.Array) - schema.MinItems = minLengthRouteConstraint.MinLength; - else - schema.MinLength = minLengthRouteConstraint.MinLength; + schema.Format = format; } + } - private static void ApplyMaxLengthAttribute(OpenApiSchema schema, MaxLengthAttribute maxLengthAttribute) - { - if (schema.Type == JsonSchemaTypes.Array) - schema.MaxItems = maxLengthAttribute.Length; - else - schema.MaxLength = maxLengthAttribute.Length; - } + private static void ApplyMinLengthAttribute(OpenApiSchema schema, MinLengthAttribute minLengthAttribute) + { + if (schema.Type == JsonSchemaTypes.Array) + schema.MinItems = minLengthAttribute.Length; + else + schema.MinLength = minLengthAttribute.Length; + } - private static void ApplyMaxLengthRouteConstraint(OpenApiSchema schema, MaxLengthRouteConstraint maxLengthRouteConstraint) - { - if (schema.Type == JsonSchemaTypes.Array) - schema.MaxItems = maxLengthRouteConstraint.MaxLength; - else - schema.MaxLength = maxLengthRouteConstraint.MaxLength; - } + private static void ApplyMinLengthRouteConstraint(OpenApiSchema schema, MinLengthRouteConstraint minLengthRouteConstraint) + { + if (schema.Type == JsonSchemaTypes.Array) + schema.MinItems = minLengthRouteConstraint.MinLength; + else + schema.MinLength = minLengthRouteConstraint.MinLength; + } + + private static void ApplyMaxLengthAttribute(OpenApiSchema schema, MaxLengthAttribute maxLengthAttribute) + { + if (schema.Type == JsonSchemaTypes.Array) + schema.MaxItems = maxLengthAttribute.Length; + else + schema.MaxLength = maxLengthAttribute.Length; + } + + private static void ApplyMaxLengthRouteConstraint(OpenApiSchema schema, MaxLengthRouteConstraint maxLengthRouteConstraint) + { + if (schema.Type == JsonSchemaTypes.Array) + schema.MaxItems = maxLengthRouteConstraint.MaxLength; + else + schema.MaxLength = maxLengthRouteConstraint.MaxLength; + } -#if NET8_0_OR_GREATER +#if NET - private static void ApplyLengthAttribute(OpenApiSchema schema, LengthAttribute lengthAttribute) + private static void ApplyLengthAttribute(OpenApiSchema schema, LengthAttribute lengthAttribute) + { + if (schema.Type == JsonSchemaTypes.Array) { - if (schema.Type == JsonSchemaTypes.Array) - { - schema.MinItems = lengthAttribute.MinimumLength; - schema.MaxItems = lengthAttribute.MaximumLength; - } - else - { - schema.MinLength = lengthAttribute.MinimumLength; - schema.MaxLength = lengthAttribute.MaximumLength; - } + schema.MinItems = lengthAttribute.MinimumLength; + schema.MaxItems = lengthAttribute.MaximumLength; } - - private static void ApplyBase64Attribute(OpenApiSchema schema) + else { - schema.Format = "byte"; + schema.MinLength = lengthAttribute.MinimumLength; + schema.MaxLength = lengthAttribute.MaximumLength; } + } + + private static void ApplyBase64Attribute(OpenApiSchema schema) + { + schema.Format = "byte"; + } #endif - private static void ApplyRangeAttribute(OpenApiSchema schema, RangeAttribute rangeAttribute) - { -#if NET8_0_OR_GREATER + private static void ApplyRangeAttribute(OpenApiSchema schema, RangeAttribute rangeAttribute) + { +#if NET - if (rangeAttribute.MinimumIsExclusive) - { - schema.ExclusiveMinimum = true; - } + if (rangeAttribute.MinimumIsExclusive) + { + schema.ExclusiveMinimum = true; + } - if (rangeAttribute.MaximumIsExclusive) - { - schema.ExclusiveMaximum = true; - } + if (rangeAttribute.MaximumIsExclusive) + { + schema.ExclusiveMaximum = true; + } #endif - schema.Maximum = decimal.TryParse(rangeAttribute.Maximum.ToString(), out decimal maximum) - ? maximum - : schema.Maximum; + schema.Maximum = decimal.TryParse(rangeAttribute.Maximum.ToString(), out decimal maximum) + ? maximum + : schema.Maximum; - schema.Minimum = decimal.TryParse(rangeAttribute.Minimum.ToString(), out decimal minimum) - ? minimum - : schema.Minimum; - } + schema.Minimum = decimal.TryParse(rangeAttribute.Minimum.ToString(), out decimal minimum) + ? minimum + : schema.Minimum; + } - private static void ApplyRangeRouteConstraint(OpenApiSchema schema, RangeRouteConstraint rangeRouteConstraint) - { - schema.Maximum = rangeRouteConstraint.Max; - schema.Minimum = rangeRouteConstraint.Min; - } + private static void ApplyRangeRouteConstraint(OpenApiSchema schema, RangeRouteConstraint rangeRouteConstraint) + { + schema.Maximum = rangeRouteConstraint.Max; + schema.Minimum = rangeRouteConstraint.Min; + } - private static void ApplyMinRouteConstraint(OpenApiSchema schema, MinRouteConstraint minRouteConstraint) - => schema.Minimum = minRouteConstraint.Min; + private static void ApplyMinRouteConstraint(OpenApiSchema schema, MinRouteConstraint minRouteConstraint) + => schema.Minimum = minRouteConstraint.Min; - private static void ApplyMaxRouteConstraint(OpenApiSchema schema, MaxRouteConstraint maxRouteConstraint) - => schema.Maximum = maxRouteConstraint.Max; + private static void ApplyMaxRouteConstraint(OpenApiSchema schema, MaxRouteConstraint maxRouteConstraint) + => schema.Maximum = maxRouteConstraint.Max; - private static void ApplyRegularExpressionAttribute(OpenApiSchema schema, RegularExpressionAttribute regularExpressionAttribute) - { - schema.Pattern = regularExpressionAttribute.Pattern; - } + private static void ApplyRegularExpressionAttribute(OpenApiSchema schema, RegularExpressionAttribute regularExpressionAttribute) + { + schema.Pattern = regularExpressionAttribute.Pattern; + } - private static void ApplyRegexRouteConstraint(OpenApiSchema schema, RegexRouteConstraint regexRouteConstraint) - => schema.Pattern = regexRouteConstraint.Constraint.ToString(); + private static void ApplyRegexRouteConstraint(OpenApiSchema schema, RegexRouteConstraint regexRouteConstraint) + => schema.Pattern = regexRouteConstraint.Constraint.ToString(); - private static void ApplyStringLengthAttribute(OpenApiSchema schema, StringLengthAttribute stringLengthAttribute) - { - schema.MinLength = stringLengthAttribute.MinimumLength; - schema.MaxLength = stringLengthAttribute.MaximumLength; - } + private static void ApplyStringLengthAttribute(OpenApiSchema schema, StringLengthAttribute stringLengthAttribute) + { + schema.MinLength = stringLengthAttribute.MinimumLength; + schema.MaxLength = stringLengthAttribute.MaximumLength; + } - private static void ApplyReadOnlyAttribute(OpenApiSchema schema, ReadOnlyAttribute readOnlyAttribute) - { - schema.ReadOnly = readOnlyAttribute.IsReadOnly; - } + private static void ApplyReadOnlyAttribute(OpenApiSchema schema, ReadOnlyAttribute readOnlyAttribute) + { + schema.ReadOnly = readOnlyAttribute.IsReadOnly; + } - private static void ApplyDescriptionAttribute(OpenApiSchema schema, DescriptionAttribute descriptionAttribute) - { - schema.Description ??= descriptionAttribute.Description; - } + private static void ApplyDescriptionAttribute(OpenApiSchema schema, DescriptionAttribute descriptionAttribute) + { + schema.Description ??= descriptionAttribute.Description; + } - private static void ApplyLengthRouteConstraint(OpenApiSchema schema, LengthRouteConstraint lengthRouteConstraint) - { - schema.MinLength = lengthRouteConstraint.MinLength; - schema.MaxLength = lengthRouteConstraint.MaxLength; - } + private static void ApplyLengthRouteConstraint(OpenApiSchema schema, LengthRouteConstraint lengthRouteConstraint) + { + schema.MinLength = lengthRouteConstraint.MinLength; + schema.MaxLength = lengthRouteConstraint.MaxLength; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/PropertyInfoExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/PropertyInfoExtensions.cs index e757230808..c0a073b98c 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/PropertyInfoExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/PropertyInfoExtensions.cs @@ -1,23 +1,22 @@ using System.Reflection; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public static class PropertyInfoExtensions { - public static class PropertyInfoExtensions + public static bool HasAttribute(this PropertyInfo property) + where TAttribute : Attribute { - public static bool HasAttribute(this PropertyInfo property) - where TAttribute : Attribute - { - return property.GetCustomAttribute() != null; - } + return property.GetCustomAttribute() != null; + } - public static bool IsPubliclyReadable(this PropertyInfo property) - { - return property.GetMethod?.IsPublic == true; - } + public static bool IsPubliclyReadable(this PropertyInfo property) + { + return property.GetMethod?.IsPublic == true; + } - public static bool IsPubliclyWritable(this PropertyInfo property) - { - return property.SetMethod?.IsPublic == true; - } + public static bool IsPubliclyWritable(this PropertyInfo property) + { + return property.SetMethod?.IsPublic == true; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaFilterContext.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaFilterContext.cs new file mode 100644 index 0000000000..b5f223fb5a --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaFilterContext.cs @@ -0,0 +1,32 @@ +using System.Reflection; + +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class SchemaFilterContext +{ + public SchemaFilterContext( + Type type, + ISchemaGenerator schemaGenerator, + SchemaRepository schemaRepository, + MemberInfo memberInfo = null, + ParameterInfo parameterInfo = null) + { + Type = type; + SchemaGenerator = schemaGenerator; + SchemaRepository = schemaRepository; + MemberInfo = memberInfo; + ParameterInfo = parameterInfo; + } + + public Type Type { get; } + + public ISchemaGenerator SchemaGenerator { get; } + + public SchemaRepository SchemaRepository { get; } + + public MemberInfo MemberInfo { get; } + + public ParameterInfo ParameterInfo { get; } + + public string DocumentName => SchemaRepository.DocumentName; +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs index 863495609b..17f4f5e13c 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs @@ -9,567 +9,566 @@ using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class SchemaGenerator : ISchemaGenerator { - public class SchemaGenerator : ISchemaGenerator + private readonly SchemaGeneratorOptions _generatorOptions; + private readonly ISerializerDataContractResolver _serializerDataContractResolver; + + public SchemaGenerator( + SchemaGeneratorOptions generatorOptions, + ISerializerDataContractResolver serializerDataContractResolver) { - private readonly SchemaGeneratorOptions _generatorOptions; - private readonly ISerializerDataContractResolver _serializerDataContractResolver; + _generatorOptions = generatorOptions; + _serializerDataContractResolver = serializerDataContractResolver; + } - public SchemaGenerator( - SchemaGeneratorOptions generatorOptions, - ISerializerDataContractResolver serializerDataContractResolver) - { - _generatorOptions = generatorOptions; - _serializerDataContractResolver = serializerDataContractResolver; - } + [Obsolete($"{nameof(IOptions)} is no longer used. This constructor will be removed in a future major release.")] + public SchemaGenerator( + SchemaGeneratorOptions generatorOptions, + ISerializerDataContractResolver serializerDataContractResolver, + IOptions mvcOptions) + : this(generatorOptions, serializerDataContractResolver) + { + } - [Obsolete($"{nameof(IOptions)} is no longer used. This constructor will be removed in a future major release.")] - public SchemaGenerator( - SchemaGeneratorOptions generatorOptions, - ISerializerDataContractResolver serializerDataContractResolver, - IOptions mvcOptions) - : this(generatorOptions, serializerDataContractResolver) - { - } + public OpenApiSchema GenerateSchema( + Type modelType, + SchemaRepository schemaRepository, + MemberInfo memberInfo = null, + ParameterInfo parameterInfo = null, + ApiParameterRouteInfo routeInfo = null) + { + if (memberInfo != null) + return GenerateSchemaForMember(modelType, schemaRepository, memberInfo); - public OpenApiSchema GenerateSchema( - Type modelType, - SchemaRepository schemaRepository, - MemberInfo memberInfo = null, - ParameterInfo parameterInfo = null, - ApiParameterRouteInfo routeInfo = null) - { - if (memberInfo != null) - return GenerateSchemaForMember(modelType, schemaRepository, memberInfo); + if (parameterInfo != null) + return GenerateSchemaForParameter(modelType, schemaRepository, parameterInfo, routeInfo); + + return GenerateSchemaForType(modelType, schemaRepository); + } - if (parameterInfo != null) - return GenerateSchemaForParameter(modelType, schemaRepository, parameterInfo, routeInfo); + private OpenApiSchema GenerateSchemaForMember( + Type modelType, + SchemaRepository schemaRepository, + MemberInfo memberInfo, + DataProperty dataProperty = null) + { + var dataContract = GetDataContractFor(modelType); - return GenerateSchemaForType(modelType, schemaRepository); - } + var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts) + ? GeneratePolymorphicSchema(schemaRepository, knownTypesDataContracts) + : GenerateConcreteSchema(dataContract, schemaRepository); - private OpenApiSchema GenerateSchemaForMember( - Type modelType, - SchemaRepository schemaRepository, - MemberInfo memberInfo, - DataProperty dataProperty = null) + if (_generatorOptions.UseAllOfToExtendReferenceSchemas && schema.Reference != null) { - var dataContract = GetDataContractFor(modelType); + schema.AllOf = [new OpenApiSchema { Reference = schema.Reference }]; + schema.Reference = null; + } - var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts) - ? GeneratePolymorphicSchema(schemaRepository, knownTypesDataContracts) - : GenerateConcreteSchema(dataContract, schemaRepository); + if (schema.Reference == null) + { + var customAttributes = memberInfo.GetInlineAndMetadataAttributes(); - if (_generatorOptions.UseAllOfToExtendReferenceSchemas && schema.Reference != null) + // Nullable, ReadOnly & WriteOnly are only relevant for Schema "properties" (i.e. where dataProperty is non-null) + if (dataProperty != null) { - schema.AllOf = [new OpenApiSchema { Reference = schema.Reference }]; - schema.Reference = null; + var requiredAttribute = customAttributes.OfType().FirstOrDefault(); + + schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes + ? dataProperty.IsNullable && requiredAttribute == null && !memberInfo.IsNonNullableReferenceType() + : dataProperty.IsNullable && requiredAttribute == null; + + schema.ReadOnly = dataProperty.IsReadOnly; + schema.WriteOnly = dataProperty.IsWriteOnly; + schema.MinLength = modelType == typeof(string) && requiredAttribute is { AllowEmptyStrings: false } ? 1 : null; } - if (schema.Reference == null) + var defaultValueAttribute = customAttributes.OfType().FirstOrDefault(); + if (defaultValueAttribute != null) { - var customAttributes = memberInfo.GetInlineAndMetadataAttributes(); - - // Nullable, ReadOnly & WriteOnly are only relevant for Schema "properties" (i.e. where dataProperty is non-null) - if (dataProperty != null) - { - var requiredAttribute = customAttributes.OfType().FirstOrDefault(); + schema.Default = GenerateDefaultValue(dataContract, modelType, defaultValueAttribute.Value); + } - schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes - ? dataProperty.IsNullable && requiredAttribute == null && !memberInfo.IsNonNullableReferenceType() - : dataProperty.IsNullable && requiredAttribute == null; + var obsoleteAttribute = customAttributes.OfType().FirstOrDefault(); + if (obsoleteAttribute != null) + { + schema.Deprecated = true; + } - schema.ReadOnly = dataProperty.IsReadOnly; - schema.WriteOnly = dataProperty.IsWriteOnly; - schema.MinLength = modelType == typeof(string) && requiredAttribute is { AllowEmptyStrings: false } ? 1 : null; - } + // NullableAttribute behaves differently for Dictionaries + if (schema.AdditionalPropertiesAllowed && modelType.IsGenericType) + { + var genericTypes = modelType + .GetInterfaces() +#if !NET + .Concat([modelType]) +#else + .Append(modelType) +#endif + .Where(t => t.IsGenericType) + .ToArray(); - var defaultValueAttribute = customAttributes.OfType().FirstOrDefault(); - if (defaultValueAttribute != null) - { - schema.Default = GenerateDefaultValue(dataContract, modelType, defaultValueAttribute.Value); - } + var isDictionaryType = + genericTypes.Any(t => t.GetGenericTypeDefinition() == typeof(IDictionary<,>)) || + genericTypes.Any(t => t.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)); - var obsoleteAttribute = customAttributes.OfType().FirstOrDefault(); - if (obsoleteAttribute != null) + if (isDictionaryType) { - schema.Deprecated = true; + schema.AdditionalProperties.Nullable = !memberInfo.IsDictionaryValueNonNullable(); } + } - // NullableAttribute behaves differently for Dictionaries - if (schema.AdditionalPropertiesAllowed && modelType.IsGenericType) - { - var genericTypes = modelType - .GetInterfaces() -#if NETSTANDARD2_0 - .Concat([modelType]) -#else - .Append(modelType) -#endif - .Where(t => t.IsGenericType) - .ToArray(); + schema.ApplyValidationAttributes(customAttributes); - var isDictionaryType = - genericTypes.Any(t => t.GetGenericTypeDefinition() == typeof(IDictionary<,>)) || - genericTypes.Any(t => t.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)); + ApplyFilters(schema, modelType, schemaRepository, memberInfo: memberInfo); + } - if (isDictionaryType) - { - schema.AdditionalProperties.Nullable = !memberInfo.IsDictionaryValueNonNullable(); - } - } + return schema; + } - schema.ApplyValidationAttributes(customAttributes); + private OpenApiSchema GenerateSchemaForParameter( + Type modelType, + SchemaRepository schemaRepository, + ParameterInfo parameterInfo, + ApiParameterRouteInfo routeInfo) + { + var dataContract = GetDataContractFor(modelType); - ApplyFilters(schema, modelType, schemaRepository, memberInfo: memberInfo); - } + var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts) + ? GeneratePolymorphicSchema(schemaRepository, knownTypesDataContracts) + : GenerateConcreteSchema(dataContract, schemaRepository); - return schema; + if (_generatorOptions.UseAllOfToExtendReferenceSchemas && schema.Reference != null) + { + schema.AllOf = [new OpenApiSchema { Reference = schema.Reference }]; + schema.Reference = null; } - private OpenApiSchema GenerateSchemaForParameter( - Type modelType, - SchemaRepository schemaRepository, - ParameterInfo parameterInfo, - ApiParameterRouteInfo routeInfo) + if (schema.Reference == null) { - var dataContract = GetDataContractFor(modelType); + var customAttributes = parameterInfo.GetCustomAttributes(); - var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts) - ? GeneratePolymorphicSchema(schemaRepository, knownTypesDataContracts) - : GenerateConcreteSchema(dataContract, schemaRepository); + var defaultValue = parameterInfo.HasDefaultValue + ? parameterInfo.DefaultValue + : customAttributes.OfType().FirstOrDefault()?.Value; - if (_generatorOptions.UseAllOfToExtendReferenceSchemas && schema.Reference != null) + if (defaultValue != null) { - schema.AllOf = [new OpenApiSchema { Reference = schema.Reference }]; - schema.Reference = null; + schema.Default = GenerateDefaultValue(dataContract, modelType, defaultValue); } - if (schema.Reference == null) + schema.ApplyValidationAttributes(customAttributes); + if (routeInfo != null) { - var customAttributes = parameterInfo.GetCustomAttributes(); + schema.ApplyRouteConstraints(routeInfo); + } - var defaultValue = parameterInfo.HasDefaultValue - ? parameterInfo.DefaultValue - : customAttributes.OfType().FirstOrDefault()?.Value; + ApplyFilters(schema, modelType, schemaRepository, parameterInfo: parameterInfo); + } - if (defaultValue != null) - { - schema.Default = GenerateDefaultValue(dataContract, modelType, defaultValue); - } + return schema; + } - schema.ApplyValidationAttributes(customAttributes); - if (routeInfo != null) - { - schema.ApplyRouteConstraints(routeInfo); - } + private OpenApiSchema GenerateSchemaForType(Type modelType, SchemaRepository schemaRepository) + { + var dataContract = GetDataContractFor(modelType); - ApplyFilters(schema, modelType, schemaRepository, parameterInfo: parameterInfo); - } + var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts) + ? GeneratePolymorphicSchema(schemaRepository, knownTypesDataContracts) + : GenerateConcreteSchema(dataContract, schemaRepository); - return schema; + if (schema.Reference == null) + { + ApplyFilters(schema, modelType, schemaRepository); } - private OpenApiSchema GenerateSchemaForType(Type modelType, SchemaRepository schemaRepository) - { - var dataContract = GetDataContractFor(modelType); + return schema; + } - var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts) - ? GeneratePolymorphicSchema(schemaRepository, knownTypesDataContracts) - : GenerateConcreteSchema(dataContract, schemaRepository); + private DataContract GetDataContractFor(Type modelType) + { + return _serializerDataContractResolver.GetDataContractForType(modelType); + } - if (schema.Reference == null) - { - ApplyFilters(schema, modelType, schemaRepository); - } + private bool IsBaseTypeWithKnownTypesDefined(DataContract dataContract, out IEnumerable knownTypesDataContracts) + { + knownTypesDataContracts = null; - return schema; - } + if (dataContract.DataType != DataType.Object) return false; - private DataContract GetDataContractFor(Type modelType) - { - return _serializerDataContractResolver.GetDataContractForType(modelType); - } + var subTypes = _generatorOptions.SubTypesSelector(dataContract.UnderlyingType); - private bool IsBaseTypeWithKnownTypesDefined(DataContract dataContract, out IEnumerable knownTypesDataContracts) - { - knownTypesDataContracts = null; + if (!subTypes.Any()) return false; - if (dataContract.DataType != DataType.Object) return false; + var knownTypes = !dataContract.UnderlyingType.IsAbstract + ? new[] { dataContract.UnderlyingType }.Union(subTypes) + : subTypes; - var subTypes = _generatorOptions.SubTypesSelector(dataContract.UnderlyingType); + knownTypesDataContracts = knownTypes.Select(knownType => GetDataContractFor(knownType)); + return true; + } - if (!subTypes.Any()) return false; + private OpenApiSchema GeneratePolymorphicSchema( + SchemaRepository schemaRepository, + IEnumerable knownTypesDataContracts) + { + return new OpenApiSchema + { + OneOf = [.. knownTypesDataContracts.Select(allowedTypeDataContract => GenerateConcreteSchema(allowedTypeDataContract, schemaRepository))] + }; + } - var knownTypes = !dataContract.UnderlyingType.IsAbstract - ? new[] { dataContract.UnderlyingType }.Union(subTypes) - : subTypes; + private static readonly Type[] BinaryStringTypes = + [ + typeof(IFormFile), + typeof(FileResult), + typeof(Stream), +#if NET + typeof(System.IO.Pipelines.PipeReader), +#endif + ]; - knownTypesDataContracts = knownTypes.Select(knownType => GetDataContractFor(knownType)); - return true; + private OpenApiSchema GenerateConcreteSchema(DataContract dataContract, SchemaRepository schemaRepository) + { + if (TryGetCustomTypeMapping(dataContract.UnderlyingType, out Func customSchemaFactory)) + { + return customSchemaFactory(); } - private OpenApiSchema GeneratePolymorphicSchema( - SchemaRepository schemaRepository, - IEnumerable knownTypesDataContracts) + if (dataContract.UnderlyingType.IsAssignableToOneOf(BinaryStringTypes)) { - return new OpenApiSchema - { - OneOf = [.. knownTypesDataContracts.Select(allowedTypeDataContract => GenerateConcreteSchema(allowedTypeDataContract, schemaRepository))] - }; + return new OpenApiSchema { Type = JsonSchemaTypes.String, Format = "binary" }; } - private static readonly Type[] BinaryStringTypes = - [ - typeof(IFormFile), - typeof(FileResult), - typeof(System.IO.Stream), -#if !NETSTANDARD - typeof(System.IO.Pipelines.PipeReader), -#endif - ]; + Func schemaFactory; + bool returnAsReference; - private OpenApiSchema GenerateConcreteSchema(DataContract dataContract, SchemaRepository schemaRepository) + switch (dataContract.DataType) { - if (TryGetCustomTypeMapping(dataContract.UnderlyingType, out Func customSchemaFactory)) - { - return customSchemaFactory(); - } + case DataType.Boolean: + case DataType.Integer: + case DataType.Number: + case DataType.String: + { + schemaFactory = () => CreatePrimitiveSchema(dataContract); + returnAsReference = dataContract.UnderlyingType.IsEnum && !_generatorOptions.UseInlineDefinitionsForEnums; + break; + } - if (dataContract.UnderlyingType.IsAssignableToOneOf(BinaryStringTypes)) - { - return new OpenApiSchema { Type = JsonSchemaTypes.String, Format = "binary" }; - } + case DataType.Array: + { + schemaFactory = () => CreateArraySchema(dataContract, schemaRepository); + returnAsReference = dataContract.UnderlyingType == dataContract.ArrayItemType; + break; + } - Func schemaFactory; - bool returnAsReference; + case DataType.Dictionary: + { + schemaFactory = () => CreateDictionarySchema(dataContract, schemaRepository); + returnAsReference = dataContract.UnderlyingType == dataContract.DictionaryValueType; + break; + } - switch (dataContract.DataType) - { - case DataType.Boolean: - case DataType.Integer: - case DataType.Number: - case DataType.String: - { - schemaFactory = () => CreatePrimitiveSchema(dataContract); - returnAsReference = dataContract.UnderlyingType.IsEnum && !_generatorOptions.UseInlineDefinitionsForEnums; - break; - } - - case DataType.Array: - { - schemaFactory = () => CreateArraySchema(dataContract, schemaRepository); - returnAsReference = dataContract.UnderlyingType == dataContract.ArrayItemType; - break; - } - - case DataType.Dictionary: - { - schemaFactory = () => CreateDictionarySchema(dataContract, schemaRepository); - returnAsReference = dataContract.UnderlyingType == dataContract.DictionaryValueType; - break; - } - - case DataType.Object: - { - schemaFactory = () => CreateObjectSchema(dataContract, schemaRepository); - returnAsReference = true; - break; - } - - default: - { - schemaFactory = () => new OpenApiSchema(); - returnAsReference = false; - break; - } - } + case DataType.Object: + { + schemaFactory = () => CreateObjectSchema(dataContract, schemaRepository); + returnAsReference = true; + break; + } - return returnAsReference - ? GenerateReferencedSchema(dataContract, schemaRepository, schemaFactory) - : schemaFactory(); + default: + { + schemaFactory = () => new OpenApiSchema(); + returnAsReference = false; + break; + } } - private bool TryGetCustomTypeMapping(Type modelType, out Func schemaFactory) - { - return _generatorOptions.CustomTypeMappings.TryGetValue(modelType, out schemaFactory) - || (modelType.IsConstructedGenericType && _generatorOptions.CustomTypeMappings.TryGetValue(modelType.GetGenericTypeDefinition(), out schemaFactory)); - } + return returnAsReference + ? GenerateReferencedSchema(dataContract, schemaRepository, schemaFactory) + : schemaFactory(); + } + + private bool TryGetCustomTypeMapping(Type modelType, out Func schemaFactory) + { + return _generatorOptions.CustomTypeMappings.TryGetValue(modelType, out schemaFactory) + || (modelType.IsConstructedGenericType && _generatorOptions.CustomTypeMappings.TryGetValue(modelType.GetGenericTypeDefinition(), out schemaFactory)); + } - private static OpenApiSchema CreatePrimitiveSchema(DataContract dataContract) + private static OpenApiSchema CreatePrimitiveSchema(DataContract dataContract) + { + var schema = new OpenApiSchema { - var schema = new OpenApiSchema - { - Type = FromDataType(dataContract.DataType), - Format = dataContract.DataFormat - }; + Type = FromDataType(dataContract.DataType), + Format = dataContract.DataFormat + }; #pragma warning disable CS0618 // Type or member is obsolete - // For backwards compatibility only - EnumValues is obsolete - if (dataContract.EnumValues != null) - { - schema.Enum = dataContract.EnumValues - .Select(value => JsonSerializer.Serialize(value)) - .Distinct() - .Select(JsonModelFactory.CreateFromJson) - .ToList(); - - return schema; - } -#pragma warning restore CS0618 // Type or member is obsolete - - if (dataContract.UnderlyingType.IsEnum) - { - schema.Enum = dataContract.UnderlyingType.GetEnumValues() - .Cast() - .Select(value => dataContract.JsonConverter(value)) - .Distinct() - .Select(JsonModelFactory.CreateFromJson) - .ToList(); - } + // For backwards compatibility only - EnumValues is obsolete + if (dataContract.EnumValues != null) + { + schema.Enum = dataContract.EnumValues + .Select(value => JsonSerializer.Serialize(value)) + .Distinct() + .Select(JsonModelFactory.CreateFromJson) + .ToList(); return schema; } +#pragma warning restore CS0618 // Type or member is obsolete - private OpenApiSchema CreateArraySchema(DataContract dataContract, SchemaRepository schemaRepository) + if (dataContract.UnderlyingType.IsEnum) { - var hasUniqueItems = dataContract.UnderlyingType.IsConstructedFrom(typeof(ISet<>), out _) - || dataContract.UnderlyingType.IsConstructedFrom(typeof(KeyedCollection<,>), out _); - - return new OpenApiSchema - { - Type = JsonSchemaTypes.Array, - Items = GenerateSchema(dataContract.ArrayItemType, schemaRepository), - UniqueItems = hasUniqueItems ? (bool?)true : null - }; + schema.Enum = dataContract.UnderlyingType.GetEnumValues() + .Cast() + .Select(value => dataContract.JsonConverter(value)) + .Distinct() + .Select(JsonModelFactory.CreateFromJson) + .ToList(); } - private OpenApiSchema CreateDictionarySchema(DataContract dataContract, SchemaRepository schemaRepository) + return schema; + } + + private OpenApiSchema CreateArraySchema(DataContract dataContract, SchemaRepository schemaRepository) + { + var hasUniqueItems = dataContract.UnderlyingType.IsConstructedFrom(typeof(ISet<>), out _) + || dataContract.UnderlyingType.IsConstructedFrom(typeof(KeyedCollection<,>), out _); + + return new OpenApiSchema { - var knownKeysProperties = dataContract.DictionaryKeys?.ToDictionary( - name => name, - _ => GenerateSchema(dataContract.DictionaryValueType, schemaRepository)); + Type = JsonSchemaTypes.Array, + Items = GenerateSchema(dataContract.ArrayItemType, schemaRepository), + UniqueItems = hasUniqueItems ? (bool?)true : null + }; + } - if (knownKeysProperties?.Count > 0) - { - // This is a special case where the set of key values is known (e.g. if the key type is an enum) - return new OpenApiSchema - { - Type = JsonSchemaTypes.Object, - Properties = knownKeysProperties, - AdditionalPropertiesAllowed = false - }; - } + private OpenApiSchema CreateDictionarySchema(DataContract dataContract, SchemaRepository schemaRepository) + { + var knownKeysProperties = dataContract.DictionaryKeys?.ToDictionary( + name => name, + _ => GenerateSchema(dataContract.DictionaryValueType, schemaRepository)); + if (knownKeysProperties?.Count > 0) + { + // This is a special case where the set of key values is known (e.g. if the key type is an enum) return new OpenApiSchema { Type = JsonSchemaTypes.Object, - AdditionalPropertiesAllowed = true, - AdditionalProperties = GenerateSchema(dataContract.DictionaryValueType, schemaRepository) + Properties = knownKeysProperties, + AdditionalPropertiesAllowed = false }; } - private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaRepository schemaRepository) + return new OpenApiSchema { - var schema = new OpenApiSchema - { - Type = JsonSchemaTypes.Object, - Properties = new Dictionary(), - Required = new SortedSet(), - AdditionalPropertiesAllowed = false - }; + Type = JsonSchemaTypes.Object, + AdditionalPropertiesAllowed = true, + AdditionalProperties = GenerateSchema(dataContract.DictionaryValueType, schemaRepository) + }; + } + + private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaRepository schemaRepository) + { + var schema = new OpenApiSchema + { + Type = JsonSchemaTypes.Object, + Properties = new Dictionary(), + Required = new SortedSet(), + AdditionalPropertiesAllowed = false + }; - OpenApiSchema root = schema; - var applicableDataProperties = dataContract.ObjectProperties; + OpenApiSchema root = schema; + var applicableDataProperties = dataContract.ObjectProperties; - if (_generatorOptions.UseAllOfForInheritance || _generatorOptions.UseOneOfForPolymorphism) + if (_generatorOptions.UseAllOfForInheritance || _generatorOptions.UseOneOfForPolymorphism) + { + if (IsKnownSubType(dataContract, out var baseTypeDataContract)) { - if (IsKnownSubType(dataContract, out var baseTypeDataContract)) + var baseTypeSchema = GenerateConcreteSchema(baseTypeDataContract, schemaRepository); + + if (_generatorOptions.UseAllOfForInheritance) { - var baseTypeSchema = GenerateConcreteSchema(baseTypeDataContract, schemaRepository); - - if (_generatorOptions.UseAllOfForInheritance) - { - root = new OpenApiSchema(); - root.AllOf.Add(baseTypeSchema); - } - else - { - schema.AllOf.Add(baseTypeSchema); - } - - applicableDataProperties = applicableDataProperties - .Where(dataProperty => dataProperty.MemberInfo.DeclaringType == dataContract.UnderlyingType); + root = new OpenApiSchema(); + root.AllOf.Add(baseTypeSchema); } - - if (IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts)) + else { - foreach (var knownTypeDataContract in knownTypesDataContracts) - { - // Ensure schema is generated for all known types - GenerateConcreteSchema(knownTypeDataContract, schemaRepository); - } - - if (TryGetDiscriminatorFor(dataContract, schemaRepository, knownTypesDataContracts, out var discriminator)) - { - schema.Properties.Add(discriminator.PropertyName, new OpenApiSchema { Type = JsonSchemaTypes.String }); - schema.Required.Add(discriminator.PropertyName); - schema.Discriminator = discriminator; - } + schema.AllOf.Add(baseTypeSchema); } + + applicableDataProperties = applicableDataProperties + .Where(dataProperty => dataProperty.MemberInfo.DeclaringType == dataContract.UnderlyingType); } - foreach (var dataProperty in applicableDataProperties) + if (IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts)) { - var customAttributes = dataProperty.MemberInfo?.GetInlineAndMetadataAttributes() ?? []; - - if (_generatorOptions.IgnoreObsoleteProperties && customAttributes.OfType().Any()) - continue; - - schema.Properties[dataProperty.Name] = (dataProperty.MemberInfo != null) - ? GenerateSchemaForMember(dataProperty.MemberType, schemaRepository, dataProperty.MemberInfo, dataProperty) - : GenerateSchemaForType(dataProperty.MemberType, schemaRepository); - - var markNonNullableTypeAsRequired = _generatorOptions.NonNullableReferenceTypesAsRequired - && (dataProperty.MemberInfo?.IsNonNullableReferenceType() ?? false); + foreach (var knownTypeDataContract in knownTypesDataContracts) + { + // Ensure schema is generated for all known types + GenerateConcreteSchema(knownTypeDataContract, schemaRepository); + } - if (( - dataProperty.IsRequired - || markNonNullableTypeAsRequired - || customAttributes.OfType().Any() -#if NET7_0_OR_GREATER - || customAttributes.OfType().Any() -#endif - ) - && !schema.Required.Contains(dataProperty.Name)) + if (TryGetDiscriminatorFor(dataContract, schemaRepository, knownTypesDataContracts, out var discriminator)) { - schema.Required.Add(dataProperty.Name); + schema.Properties.Add(discriminator.PropertyName, new OpenApiSchema { Type = JsonSchemaTypes.String }); + schema.Required.Add(discriminator.PropertyName); + schema.Discriminator = discriminator; } } + } - if (dataContract.ObjectExtensionDataType != null) - { - schema.AdditionalPropertiesAllowed = true; - schema.AdditionalProperties = GenerateSchema(dataContract.ObjectExtensionDataType, schemaRepository); - } + foreach (var dataProperty in applicableDataProperties) + { + var customAttributes = dataProperty.MemberInfo?.GetInlineAndMetadataAttributes() ?? []; + + if (_generatorOptions.IgnoreObsoleteProperties && customAttributes.OfType().Any()) + continue; + + schema.Properties[dataProperty.Name] = (dataProperty.MemberInfo != null) + ? GenerateSchemaForMember(dataProperty.MemberType, schemaRepository, dataProperty.MemberInfo, dataProperty) + : GenerateSchemaForType(dataProperty.MemberType, schemaRepository); - if (root != schema) + var markNonNullableTypeAsRequired = _generatorOptions.NonNullableReferenceTypesAsRequired + && (dataProperty.MemberInfo?.IsNonNullableReferenceType() ?? false); + + if (( + dataProperty.IsRequired + || markNonNullableTypeAsRequired + || customAttributes.OfType().Any() +#if NET + || customAttributes.OfType().Any() +#endif + ) + && !schema.Required.Contains(dataProperty.Name)) { - root.AllOf.Add(schema); + schema.Required.Add(dataProperty.Name); } + } - return root; + if (dataContract.ObjectExtensionDataType != null) + { + schema.AdditionalPropertiesAllowed = true; + schema.AdditionalProperties = GenerateSchema(dataContract.ObjectExtensionDataType, schemaRepository); } - private bool IsKnownSubType(DataContract dataContract, out DataContract baseTypeDataContract) + if (root != schema) { - baseTypeDataContract = null; + root.AllOf.Add(schema); + } - var baseType = dataContract.UnderlyingType.BaseType; - while (baseType != null && baseType != typeof(object)) - { - if (_generatorOptions.SubTypesSelector(baseType).Contains(dataContract.UnderlyingType)) - { - baseTypeDataContract = GetDataContractFor(baseType); - return true; - } + return root; + } - baseType = baseType.BaseType; + private bool IsKnownSubType(DataContract dataContract, out DataContract baseTypeDataContract) + { + baseTypeDataContract = null; + + var baseType = dataContract.UnderlyingType.BaseType; + while (baseType != null && baseType != typeof(object)) + { + if (_generatorOptions.SubTypesSelector(baseType).Contains(dataContract.UnderlyingType)) + { + baseTypeDataContract = GetDataContractFor(baseType); + return true; } - return false; + baseType = baseType.BaseType; } - private bool TryGetDiscriminatorFor( - DataContract dataContract, - SchemaRepository schemaRepository, - IEnumerable knownTypesDataContracts, - out OpenApiDiscriminator discriminator) - { - discriminator = null; + return false; + } - var discriminatorName = _generatorOptions.DiscriminatorNameSelector(dataContract.UnderlyingType) - ?? dataContract.ObjectTypeNameProperty; + private bool TryGetDiscriminatorFor( + DataContract dataContract, + SchemaRepository schemaRepository, + IEnumerable knownTypesDataContracts, + out OpenApiDiscriminator discriminator) + { + discriminator = null; - if (discriminatorName == null) return false; + var discriminatorName = _generatorOptions.DiscriminatorNameSelector(dataContract.UnderlyingType) + ?? dataContract.ObjectTypeNameProperty; - discriminator = new OpenApiDiscriminator - { - PropertyName = discriminatorName - }; + if (discriminatorName == null) return false; - foreach (var knownTypeDataContract in knownTypesDataContracts) - { - var discriminatorValue = _generatorOptions.DiscriminatorValueSelector(knownTypeDataContract.UnderlyingType) - ?? knownTypeDataContract.ObjectTypeNameValue; + discriminator = new OpenApiDiscriminator + { + PropertyName = discriminatorName + }; - if (discriminatorValue == null) continue; + foreach (var knownTypeDataContract in knownTypesDataContracts) + { + var discriminatorValue = _generatorOptions.DiscriminatorValueSelector(knownTypeDataContract.UnderlyingType) + ?? knownTypeDataContract.ObjectTypeNameValue; - discriminator.Mapping.Add(discriminatorValue, GenerateConcreteSchema(knownTypeDataContract, schemaRepository).Reference.ReferenceV3); - } + if (discriminatorValue == null) continue; - return true; + discriminator.Mapping.Add(discriminatorValue, GenerateConcreteSchema(knownTypeDataContract, schemaRepository).Reference.ReferenceV3); } - private OpenApiSchema GenerateReferencedSchema( - DataContract dataContract, - SchemaRepository schemaRepository, - Func definitionFactory) - { - if (schemaRepository.TryLookupByType(dataContract.UnderlyingType, out OpenApiSchema referenceSchema)) - return referenceSchema; + return true; + } - var schemaId = _generatorOptions.SchemaIdSelector(dataContract.UnderlyingType); + private OpenApiSchema GenerateReferencedSchema( + DataContract dataContract, + SchemaRepository schemaRepository, + Func definitionFactory) + { + if (schemaRepository.TryLookupByType(dataContract.UnderlyingType, out OpenApiSchema referenceSchema)) + return referenceSchema; - schemaRepository.RegisterType(dataContract.UnderlyingType, schemaId); + var schemaId = _generatorOptions.SchemaIdSelector(dataContract.UnderlyingType); - var schema = definitionFactory(); - ApplyFilters(schema, dataContract.UnderlyingType, schemaRepository); + schemaRepository.RegisterType(dataContract.UnderlyingType, schemaId); - return schemaRepository.AddDefinition(schemaId, schema); - } + var schema = definitionFactory(); + ApplyFilters(schema, dataContract.UnderlyingType, schemaRepository); - private void ApplyFilters( - OpenApiSchema schema, - Type type, - SchemaRepository schemaRepository, - MemberInfo memberInfo = null, - ParameterInfo parameterInfo = null) + return schemaRepository.AddDefinition(schemaId, schema); + } + + private void ApplyFilters( + OpenApiSchema schema, + Type type, + SchemaRepository schemaRepository, + MemberInfo memberInfo = null, + ParameterInfo parameterInfo = null) + { + var filterContext = new SchemaFilterContext( + type: type, + schemaGenerator: this, + schemaRepository: schemaRepository, + memberInfo: memberInfo, + parameterInfo: parameterInfo); + + foreach (var filter in _generatorOptions.SchemaFilters) { - var filterContext = new SchemaFilterContext( - type: type, - schemaGenerator: this, - schemaRepository: schemaRepository, - memberInfo: memberInfo, - parameterInfo: parameterInfo); - - foreach (var filter in _generatorOptions.SchemaFilters) - { - filter.Apply(schema, filterContext); - } + filter.Apply(schema, filterContext); } + } - private Microsoft.OpenApi.Any.IOpenApiAny GenerateDefaultValue( - DataContract dataContract, - Type modelType, - object defaultValue) + private Microsoft.OpenApi.Any.IOpenApiAny GenerateDefaultValue( + DataContract dataContract, + Type modelType, + object defaultValue) + { + // If the types do not match (e.g. a default which is an integer is specified for a double), + // attempt to coerce the default value to the correct type so that it can be serialized correctly. + // See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2885 and + // https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2904. + var defaultValueType = defaultValue?.GetType(); + if (defaultValueType != null && defaultValueType != modelType) { - // If the types do not match (e.g. a default which is an integer is specified for a double), - // attempt to coerce the default value to the correct type so that it can be serialized correctly. - // See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2885 and - // https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2904. - var defaultValueType = defaultValue?.GetType(); - if (defaultValueType != null && defaultValueType != modelType) - { - dataContract = GetDataContractFor(defaultValueType); - } - - var defaultAsJson = dataContract.JsonConverter(defaultValue); - return JsonModelFactory.CreateFromJson(defaultAsJson); + dataContract = GetDataContractFor(defaultValueType); } - private static string FromDataType(DataType dataType) - => dataType.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture); + var defaultAsJson = dataContract.JsonConverter(defaultValue); + return JsonModelFactory.CreateFromJson(defaultAsJson); } + + private static string FromDataType(DataType dataType) + => dataType.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture); } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs index fcd7d126ac..2b68c357f0 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs @@ -1,69 +1,68 @@ using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class SchemaGeneratorOptions { - public class SchemaGeneratorOptions + public SchemaGeneratorOptions() { - public SchemaGeneratorOptions() - { - CustomTypeMappings = new Dictionary>(); - SchemaIdSelector = DefaultSchemaIdSelector; - SubTypesSelector = DefaultSubTypesSelector; - DiscriminatorNameSelector = DefaultDiscriminatorNameSelector; - DiscriminatorValueSelector = DefaultDiscriminatorValueSelector; - SchemaFilters = new List(); - } + CustomTypeMappings = new Dictionary>(); + SchemaIdSelector = DefaultSchemaIdSelector; + SubTypesSelector = DefaultSubTypesSelector; + DiscriminatorNameSelector = DefaultDiscriminatorNameSelector; + DiscriminatorValueSelector = DefaultDiscriminatorValueSelector; + SchemaFilters = new List(); + } - public IDictionary> CustomTypeMappings { get; set; } + public IDictionary> CustomTypeMappings { get; set; } - public bool UseInlineDefinitionsForEnums { get; set; } + public bool UseInlineDefinitionsForEnums { get; set; } - public Func SchemaIdSelector { get; set; } + public Func SchemaIdSelector { get; set; } - public bool IgnoreObsoleteProperties { get; set; } + public bool IgnoreObsoleteProperties { get; set; } - public bool UseAllOfForInheritance { get; set; } + public bool UseAllOfForInheritance { get; set; } - public bool UseOneOfForPolymorphism { get; set; } + public bool UseOneOfForPolymorphism { get; set; } - public Func> SubTypesSelector { get; set; } + public Func> SubTypesSelector { get; set; } - public Func DiscriminatorNameSelector { get; set; } + public Func DiscriminatorNameSelector { get; set; } - public Func DiscriminatorValueSelector { get; set; } + public Func DiscriminatorValueSelector { get; set; } - public bool UseAllOfToExtendReferenceSchemas { get; set; } + public bool UseAllOfToExtendReferenceSchemas { get; set; } - public bool SupportNonNullableReferenceTypes { get; set; } + public bool SupportNonNullableReferenceTypes { get; set; } - public bool NonNullableReferenceTypesAsRequired { get; set; } + public bool NonNullableReferenceTypesAsRequired { get; set; } - public IList SchemaFilters { get; set; } + public IList SchemaFilters { get; set; } - private string DefaultSchemaIdSelector(Type modelType) - { - if (!modelType.IsConstructedGenericType) return modelType.Name.Replace("[]", "Array"); + private string DefaultSchemaIdSelector(Type modelType) + { + if (!modelType.IsConstructedGenericType) return modelType.Name.Replace("[]", "Array"); - var prefix = modelType.GetGenericArguments() - .Select(genericArg => DefaultSchemaIdSelector(genericArg)) - .Aggregate((previous, current) => previous + current); + var prefix = modelType.GetGenericArguments() + .Select(genericArg => DefaultSchemaIdSelector(genericArg)) + .Aggregate((previous, current) => previous + current); - return prefix + modelType.Name.Split('`').First(); - } + return prefix + modelType.Name.Split('`').First(); + } - private IEnumerable DefaultSubTypesSelector(Type baseType) - { - return baseType.Assembly.GetTypes().Where(type => type.IsSubclassOf(baseType)); - } + private IEnumerable DefaultSubTypesSelector(Type baseType) + { + return baseType.Assembly.GetTypes().Where(type => type.IsSubclassOf(baseType)); + } - private string DefaultDiscriminatorNameSelector(Type baseType) - { - return null; - } + private string DefaultDiscriminatorNameSelector(Type baseType) + { + return null; + } - private string DefaultDiscriminatorValueSelector(Type subType) - { - return null; - } + private string DefaultDiscriminatorValueSelector(Type subType) + { + return null; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/TypeExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/TypeExtensions.cs index fb5f5e8707..e2ee7ec667 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/TypeExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/TypeExtensions.cs @@ -1,58 +1,57 @@ -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public static class TypeExtensions { - public static class TypeExtensions + public static bool IsOneOf(this Type type, params Type[] possibleTypes) { - public static bool IsOneOf(this Type type, params Type[] possibleTypes) - { - return possibleTypes.Any(possibleType => possibleType == type); - } - - public static bool IsAssignableTo(this Type type, Type baseType) - { - return baseType.IsAssignableFrom(type); - } + return possibleTypes.Any(possibleType => possibleType == type); + } - public static bool IsAssignableToOneOf(this Type type, params Type[] possibleBaseTypes) - { - return possibleBaseTypes.Any(possibleBaseType => possibleBaseType.IsAssignableFrom(type)); - } + public static bool IsAssignableTo(this Type type, Type baseType) + { + return baseType.IsAssignableFrom(type); + } - public static bool IsConstructedFrom(this Type type, Type genericType, out Type constructedType) - { - constructedType = new[] { type } - .Union(type.GetInheritanceChain()) - .Union(type.GetInterfaces()) - .FirstOrDefault(i => i.IsConstructedGenericType && i.GetGenericTypeDefinition() == genericType); + public static bool IsAssignableToOneOf(this Type type, params Type[] possibleBaseTypes) + { + return possibleBaseTypes.Any(possibleBaseType => possibleBaseType.IsAssignableFrom(type)); + } - return (constructedType != null); - } + public static bool IsConstructedFrom(this Type type, Type genericType, out Type constructedType) + { + constructedType = new[] { type } + .Union(type.GetInheritanceChain()) + .Union(type.GetInterfaces()) + .FirstOrDefault(i => i.IsConstructedGenericType && i.GetGenericTypeDefinition() == genericType); - public static bool IsReferenceOrNullableType(this Type type) - { - return (!type.IsValueType || Nullable.GetUnderlyingType(type) != null); - } + return (constructedType != null); + } - public static object GetDefaultValue(this Type type) - { - return type.IsValueType - ? Activator.CreateInstance(type) - : null; - } + public static bool IsReferenceOrNullableType(this Type type) + { + return (!type.IsValueType || Nullable.GetUnderlyingType(type) != null); + } - public static Type[] GetInheritanceChain(this Type type) - { - if (type.IsInterface) { return type.GetInterfaces(); } + public static object GetDefaultValue(this Type type) + { + return type.IsValueType + ? Activator.CreateInstance(type) + : null; + } - var inheritanceChain = new List(); + public static Type[] GetInheritanceChain(this Type type) + { + if (type.IsInterface) { return type.GetInterfaces(); } - var current = type; - while (current.BaseType != null) - { - inheritanceChain.Add(current.BaseType); - current = current.BaseType; - } + var inheritanceChain = new List(); - return inheritanceChain.ToArray(); + var current = type; + while (current.BaseType != null) + { + inheritanceChain.Add(current.BaseType); + current = current.BaseType; } + + return inheritanceChain.ToArray(); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs index 61e4eac607..da146a5cf1 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs @@ -3,67 +3,66 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing.Template; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public static class ApiDescriptionExtensions { - public static class ApiDescriptionExtensions + public static bool TryGetMethodInfo(this ApiDescription apiDescription, out MethodInfo methodInfo) { - public static bool TryGetMethodInfo(this ApiDescription apiDescription, out MethodInfo methodInfo) + if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) { - if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) - { - methodInfo = controllerActionDescriptor.MethodInfo; - return true; - } + methodInfo = controllerActionDescriptor.MethodInfo; + return true; + } -#if NET6_0_OR_GREATER - if (apiDescription.ActionDescriptor?.EndpointMetadata != null) - { - methodInfo = apiDescription.ActionDescriptor.EndpointMetadata - .OfType() - .FirstOrDefault(); +#if NET + if (apiDescription.ActionDescriptor?.EndpointMetadata != null) + { + methodInfo = apiDescription.ActionDescriptor.EndpointMetadata + .OfType() + .FirstOrDefault(); - return methodInfo != null; - } + return methodInfo != null; + } #endif - methodInfo = null; - return false; - } + methodInfo = null; + return false; + } - public static IEnumerable CustomAttributes(this ApiDescription apiDescription) + public static IEnumerable CustomAttributes(this ApiDescription apiDescription) + { + if (apiDescription.TryGetMethodInfo(out MethodInfo methodInfo)) { - if (apiDescription.TryGetMethodInfo(out MethodInfo methodInfo)) - { - return methodInfo.GetCustomAttributes(true) - .Union(methodInfo.DeclaringType.GetCustomAttributes(true)); - } - - return []; + return methodInfo.GetCustomAttributes(true) + .Union(methodInfo.DeclaringType.GetCustomAttributes(true)); } - [Obsolete("Use TryGetMethodInfo() and CustomAttributes() instead")] - public static void GetAdditionalMetadata(this ApiDescription apiDescription, - out MethodInfo methodInfo, - out IEnumerable customAttributes) - { - if (apiDescription.TryGetMethodInfo(out methodInfo)) - { - customAttributes = methodInfo.GetCustomAttributes(true) - .Union(methodInfo.DeclaringType.GetCustomAttributes(true)); + return []; + } - return; - } + [Obsolete("Use TryGetMethodInfo() and CustomAttributes() instead")] + public static void GetAdditionalMetadata(this ApiDescription apiDescription, + out MethodInfo methodInfo, + out IEnumerable customAttributes) + { + if (apiDescription.TryGetMethodInfo(out methodInfo)) + { + customAttributes = methodInfo.GetCustomAttributes(true) + .Union(methodInfo.DeclaringType.GetCustomAttributes(true)); - customAttributes = []; + return; } - internal static string RelativePathSansParameterConstraints(this ApiDescription apiDescription) - { - var routeTemplate = TemplateParser.Parse(apiDescription.RelativePath); - var sanitizedSegments = routeTemplate - .Segments - .Select(s => string.Concat(s.Parts.Select(p => p.Name != null ? $"{{{p.Name}}}" : p.Text))); - return string.Join("/", sanitizedSegments); - } + customAttributes = []; + } + + internal static string RelativePathSansParameterConstraints(this ApiDescription apiDescription) + { + var routeTemplate = TemplateParser.Parse(apiDescription.RelativePath); + var sanitizedSegments = routeTemplate + .Segments + .Select(s => string.Concat(s.Parts.Select(p => p.Name != null ? $"{{{p.Name}}}" : p.Text))); + return string.Join("/", sanitizedSegments); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs index 869370270d..34825aff0a 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs @@ -6,129 +6,128 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Net.Http.Headers; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public static class ApiParameterDescriptionExtensions { - public static class ApiParameterDescriptionExtensions + private static readonly Type[] RequiredAttributeTypes = new[] { - private static readonly Type[] RequiredAttributeTypes = new[] - { - typeof(BindRequiredAttribute), - typeof(RequiredAttribute), -#if NET7_0_OR_GREATER - typeof(System.Runtime.CompilerServices.RequiredMemberAttribute) + typeof(BindRequiredAttribute), + typeof(RequiredAttribute), +#if NET + typeof(System.Runtime.CompilerServices.RequiredMemberAttribute) #endif - }; + }; + + private static readonly HashSet IllegalHeaderParameters = new HashSet(StringComparer.OrdinalIgnoreCase) + { + HeaderNames.Accept, + HeaderNames.Authorization, + HeaderNames.ContentType + }; - private static readonly HashSet IllegalHeaderParameters = new HashSet(StringComparer.OrdinalIgnoreCase) + public static bool IsRequiredParameter(this ApiParameterDescription apiParameter) + { + // From the OpenAPI spec: + // If the parameter location is "path", this property is REQUIRED and its value MUST be true. + if (apiParameter.IsFromPath()) { - HeaderNames.Accept, - HeaderNames.Authorization, - HeaderNames.ContentType - }; + return true; + } - public static bool IsRequiredParameter(this ApiParameterDescription apiParameter) + // This is the default logic for IsRequired + bool IsRequired() => apiParameter.CustomAttributes().Any(attr => RequiredAttributeTypes.Contains(attr.GetType())); + + // This is to keep compatibility with MVC controller logic that has existed in the past + if (apiParameter.ParameterDescriptor is ControllerParameterDescriptor) { - // From the OpenAPI spec: - // If the parameter location is "path", this property is REQUIRED and its value MUST be true. - if (apiParameter.IsFromPath()) - { - return true; - } - - // This is the default logic for IsRequired - bool IsRequired() => apiParameter.CustomAttributes().Any(attr => RequiredAttributeTypes.Contains(attr.GetType())); - - // This is to keep compatibility with MVC controller logic that has existed in the past - if (apiParameter.ParameterDescriptor is ControllerParameterDescriptor) - { - return IsRequired(); - } - - // For non-controllers, prefer the IsRequired flag if we're not on netstandard 2.0, otherwise fallback to the default logic. - return -#if !NETSTANDARD - apiParameter.IsRequired; + return IsRequired(); + } + + // For non-controllers, prefer the IsRequired flag if we're not on netstandard 2.0, otherwise fallback to the default logic. + return +#if NET + apiParameter.IsRequired; #else - IsRequired(); + IsRequired(); #endif - } + } - public static ParameterInfo ParameterInfo(this ApiParameterDescription apiParameter) - { - var parameterDescriptor = apiParameter.ParameterDescriptor as -#if !NETSTANDARD - Microsoft.AspNetCore.Mvc.Infrastructure.IParameterInfoParameterDescriptor; + public static ParameterInfo ParameterInfo(this ApiParameterDescription apiParameter) + { + var parameterDescriptor = apiParameter.ParameterDescriptor as +#if NET + Microsoft.AspNetCore.Mvc.Infrastructure.IParameterInfoParameterDescriptor; #else - ControllerParameterDescriptor; + ControllerParameterDescriptor; #endif - return parameterDescriptor?.ParameterInfo; - } + return parameterDescriptor?.ParameterInfo; + } - public static PropertyInfo PropertyInfo(this ApiParameterDescription apiParameter) - { - var modelMetadata = apiParameter.ModelMetadata; + public static PropertyInfo PropertyInfo(this ApiParameterDescription apiParameter) + { + var modelMetadata = apiParameter.ModelMetadata; - return modelMetadata?.ContainerType?.GetProperty(modelMetadata.PropertyName); - } + return modelMetadata?.ContainerType?.GetProperty(modelMetadata.PropertyName); + } - public static IEnumerable CustomAttributes(this ApiParameterDescription apiParameter) - { - var propertyInfo = apiParameter.PropertyInfo(); - if (propertyInfo != null) return propertyInfo.GetCustomAttributes(true); + public static IEnumerable CustomAttributes(this ApiParameterDescription apiParameter) + { + var propertyInfo = apiParameter.PropertyInfo(); + if (propertyInfo != null) return propertyInfo.GetCustomAttributes(true); - var parameterInfo = apiParameter.ParameterInfo(); - if (parameterInfo != null) return parameterInfo.GetCustomAttributes(true); + var parameterInfo = apiParameter.ParameterInfo(); + if (parameterInfo != null) return parameterInfo.GetCustomAttributes(true); - return Enumerable.Empty(); - } + return Enumerable.Empty(); + } - [Obsolete("Use ParameterInfo(), PropertyInfo() and CustomAttributes() extension methods instead")] - internal static void GetAdditionalMetadata( - this ApiParameterDescription apiParameter, - ApiDescription apiDescription, - out ParameterInfo parameterInfo, - out PropertyInfo propertyInfo, - out IEnumerable parameterOrPropertyAttributes) - { - parameterInfo = apiParameter.ParameterInfo(); - propertyInfo = apiParameter.PropertyInfo(); - parameterOrPropertyAttributes = apiParameter.CustomAttributes(); - } + [Obsolete("Use ParameterInfo(), PropertyInfo() and CustomAttributes() extension methods instead")] + internal static void GetAdditionalMetadata( + this ApiParameterDescription apiParameter, + ApiDescription apiDescription, + out ParameterInfo parameterInfo, + out PropertyInfo propertyInfo, + out IEnumerable parameterOrPropertyAttributes) + { + parameterInfo = apiParameter.ParameterInfo(); + propertyInfo = apiParameter.PropertyInfo(); + parameterOrPropertyAttributes = apiParameter.CustomAttributes(); + } - internal static bool IsFromPath(this ApiParameterDescription apiParameter) - { - return apiParameter.Source == BindingSource.Path; - } + internal static bool IsFromPath(this ApiParameterDescription apiParameter) + { + return apiParameter.Source == BindingSource.Path; + } - internal static bool IsFromBody(this ApiParameterDescription apiParameter) - { - return apiParameter.Source == BindingSource.Body; - } + internal static bool IsFromBody(this ApiParameterDescription apiParameter) + { + return apiParameter.Source == BindingSource.Body; + } - internal static bool IsFromForm(this ApiParameterDescription apiParameter) - { - bool isEnhancedModelMetadataSupported = true; + internal static bool IsFromForm(this ApiParameterDescription apiParameter) + { + bool isEnhancedModelMetadataSupported = true; #if NET9_0_OR_GREATER - if (AppContext.TryGetSwitch("Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported", out var isEnabled)) - { - isEnhancedModelMetadataSupported = isEnabled; - } + if (AppContext.TryGetSwitch("Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported", out var isEnabled)) + { + isEnhancedModelMetadataSupported = isEnabled; + } #endif - var source = apiParameter.Source; - var elementType = isEnhancedModelMetadataSupported ? apiParameter.ModelMetadata?.ElementType : null; + var source = apiParameter.Source; + var elementType = isEnhancedModelMetadataSupported ? apiParameter.ModelMetadata?.ElementType : null; - return (source == BindingSource.Form || source == BindingSource.FormFile) - || (elementType != null && typeof(IFormFile).IsAssignableFrom(elementType)); - } + return (source == BindingSource.Form || source == BindingSource.FormFile) + || (elementType != null && typeof(IFormFile).IsAssignableFrom(elementType)); + } - internal static bool IsIllegalHeaderParameter(this ApiParameterDescription apiParameter) - { - // Certain header parameters are not allowed and should be described using the corresponding OpenAPI keywords - // https://swagger.io/docs/specification/describing-parameters/#header-parameters - return apiParameter.Source == BindingSource.Header && IllegalHeaderParameters.Contains(apiParameter.Name); - } + internal static bool IsIllegalHeaderParameter(this ApiParameterDescription apiParameter) + { + // Certain header parameters are not allowed and should be described using the corresponding OpenAPI keywords + // https://swagger.io/docs/specification/describing-parameters/#header-parameters + return apiParameter.Source == BindingSource.Header && IllegalHeaderParameters.Contains(apiParameter.Name); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiResponseTypeExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiResponseTypeExtensions.cs index 2ee526fd92..55dc39a4cd 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiResponseTypeExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiResponseTypeExtensions.cs @@ -1,19 +1,18 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public static class ApiResponseTypeExtensions { - public static class ApiResponseTypeExtensions + internal static bool IsDefaultResponse(this ApiResponseType apiResponseType) { - internal static bool IsDefaultResponse(this ApiResponseType apiResponseType) + var propertyInfo = apiResponseType.GetType().GetProperty("IsDefaultResponse"); + if (propertyInfo != null) { - var propertyInfo = apiResponseType.GetType().GetProperty("IsDefaultResponse"); - if (propertyInfo != null) - { - return (bool)propertyInfo.GetValue(apiResponseType); - } - - // ApiExplorer < 2.1.0 does not support default response. - return false; + return (bool)propertyInfo.GetValue(apiResponseType); } + + // ApiExplorer < 2.1.0 does not support default response. + return false; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/DocumentFilterContext.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/DocumentFilterContext.cs new file mode 100644 index 0000000000..32fb1652e8 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/DocumentFilterContext.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class DocumentFilterContext +{ + public DocumentFilterContext( + IEnumerable apiDescriptions, + ISchemaGenerator schemaGenerator, + SchemaRepository schemaRepository) + { + ApiDescriptions = apiDescriptions; + SchemaGenerator = schemaGenerator; + SchemaRepository = schemaRepository; + } + + public IEnumerable ApiDescriptions { get; } + + public ISchemaGenerator SchemaGenerator { get; } + + public SchemaRepository SchemaRepository { get; } + + public string DocumentName => SchemaRepository.DocumentName; +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDictionary.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDictionary.cs index 7f995bc4ce..31c3080829 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDictionary.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDictionary.cs @@ -1,6 +1,3 @@ -namespace Swashbuckle.AspNetCore.SwaggerGen -{ - public interface IDictionary - { - } -} \ No newline at end of file +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public interface IDictionary; diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentAsyncFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentAsyncFilter.cs new file mode 100644 index 0000000000..48c4753450 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentAsyncFilter.cs @@ -0,0 +1,8 @@ +using Microsoft.OpenApi.Models; + +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public interface IDocumentAsyncFilter +{ + Task ApplyAsync(OpenApiDocument swaggerDoc, DocumentFilterContext context, CancellationToken cancellationToken); +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentFilter.cs index b5f259346f..40c087d5d9 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentFilter.cs @@ -1,36 +1,8 @@ -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen -{ - public interface IDocumentFilter - { - void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context); - } - - public interface IDocumentAsyncFilter - { - Task ApplyAsync(OpenApiDocument swaggerDoc, DocumentFilterContext context, CancellationToken cancellationToken); - } - - public class DocumentFilterContext - { - public DocumentFilterContext( - IEnumerable apiDescriptions, - ISchemaGenerator schemaGenerator, - SchemaRepository schemaRepository) - { - ApiDescriptions = apiDescriptions; - SchemaGenerator = schemaGenerator; - SchemaRepository = schemaRepository; - } - - public IEnumerable ApiDescriptions { get; } +namespace Swashbuckle.AspNetCore.SwaggerGen; - public ISchemaGenerator SchemaGenerator { get; } - - public SchemaRepository SchemaRepository { get; } - - public string DocumentName => SchemaRepository.DocumentName; - } +public interface IDocumentFilter +{ + void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context); } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IFileResult.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IFileResult.cs index c3efa571e9..b340931489 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IFileResult.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IFileResult.cs @@ -1,6 +1,5 @@ -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +internal interface IFileResult { - internal interface IFileResult - { - } -} \ No newline at end of file +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationAsyncFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationAsyncFilter.cs new file mode 100644 index 0000000000..59ace9898f --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationAsyncFilter.cs @@ -0,0 +1,8 @@ +using Microsoft.OpenApi.Models; + +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public interface IOperationAsyncFilter +{ + Task ApplyAsync(OpenApiOperation operation, OperationFilterContext context, CancellationToken cancellationToken); +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationFilter.cs index ea7a0da693..44b280e74c 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationFilter.cs @@ -1,41 +1,8 @@ -using System.Reflection; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen -{ - public interface IOperationFilter - { - void Apply(OpenApiOperation operation, OperationFilterContext context); - } - - public interface IOperationAsyncFilter - { - Task ApplyAsync(OpenApiOperation operation, OperationFilterContext context, CancellationToken cancellationToken); - } - - public class OperationFilterContext - { - public OperationFilterContext( - ApiDescription apiDescription, - ISchemaGenerator schemaRegistry, - SchemaRepository schemaRepository, - MethodInfo methodInfo) - { - ApiDescription = apiDescription; - SchemaGenerator = schemaRegistry; - SchemaRepository = schemaRepository; - MethodInfo = methodInfo; - } - - public ApiDescription ApiDescription { get; } +namespace Swashbuckle.AspNetCore.SwaggerGen; - public ISchemaGenerator SchemaGenerator { get; } - - public SchemaRepository SchemaRepository { get; } - - public MethodInfo MethodInfo { get; } - - public string DocumentName => SchemaRepository.DocumentName; - } +public interface IOperationFilter +{ + void Apply(OpenApiOperation operation, OperationFilterContext context); } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterAsyncFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterAsyncFilter.cs new file mode 100644 index 0000000000..8294a08462 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterAsyncFilter.cs @@ -0,0 +1,8 @@ +using Microsoft.OpenApi.Models; + +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public interface IParameterAsyncFilter +{ + Task ApplyAsync(OpenApiParameter parameter, ParameterFilterContext context, CancellationToken cancellationToken); +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterFilter.cs index 27c127715a..636ea14893 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterFilter.cs @@ -1,45 +1,8 @@ -using System.Reflection; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen -{ - public interface IParameterFilter - { - void Apply(OpenApiParameter parameter, ParameterFilterContext context); - } - - public interface IParameterAsyncFilter - { - Task ApplyAsync(OpenApiParameter parameter, ParameterFilterContext context, CancellationToken cancellationToken); - } - - public class ParameterFilterContext - { - public ParameterFilterContext( - ApiParameterDescription apiParameterDescription, - ISchemaGenerator schemaGenerator, - SchemaRepository schemaRepository, - PropertyInfo propertyInfo = null, - ParameterInfo parameterInfo = null) - { - ApiParameterDescription = apiParameterDescription; - SchemaGenerator = schemaGenerator; - SchemaRepository = schemaRepository; - PropertyInfo = propertyInfo; - ParameterInfo = parameterInfo; - } - - public ApiParameterDescription ApiParameterDescription { get; } - - public ISchemaGenerator SchemaGenerator { get; } +namespace Swashbuckle.AspNetCore.SwaggerGen; - public SchemaRepository SchemaRepository { get; } - - public PropertyInfo PropertyInfo { get; } - - public ParameterInfo ParameterInfo { get; } - - public string DocumentName => SchemaRepository.DocumentName; - } +public interface IParameterFilter +{ + void Apply(OpenApiParameter parameter, ParameterFilterContext context); } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyAsyncFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyAsyncFilter.cs new file mode 100644 index 0000000000..a7019626a2 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyAsyncFilter.cs @@ -0,0 +1,8 @@ +using Microsoft.OpenApi.Models; + +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public interface IRequestBodyAsyncFilter +{ + Task ApplyAsync(OpenApiRequestBody requestBody, RequestBodyFilterContext context, CancellationToken cancellationToken); +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs index dfbd2664f9..e26acb75b3 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs @@ -1,40 +1,8 @@ -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen -{ - public interface IRequestBodyFilter - { - void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context); - } - - public interface IRequestBodyAsyncFilter - { - Task ApplyAsync(OpenApiRequestBody requestBody, RequestBodyFilterContext context, CancellationToken cancellationToken); - } - - public class RequestBodyFilterContext - { - public RequestBodyFilterContext( - ApiParameterDescription bodyParameterDescription, - IEnumerable formParameterDescriptions, - ISchemaGenerator schemaGenerator, - SchemaRepository schemaRepository) - { - BodyParameterDescription = bodyParameterDescription; - FormParameterDescriptions = formParameterDescriptions; - SchemaGenerator = schemaGenerator; - SchemaRepository = schemaRepository; - } - - public ApiParameterDescription BodyParameterDescription { get; } +namespace Swashbuckle.AspNetCore.SwaggerGen; - public IEnumerable FormParameterDescriptions { get; } - - public ISchemaGenerator SchemaGenerator { get; } - - public SchemaRepository SchemaRepository { get; } - - public string DocumentName => SchemaRepository.DocumentName; - } +public interface IRequestBodyFilter +{ + void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context); } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ISchemaGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ISchemaGenerator.cs index 848ce0f98e..5c0181f2be 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ISchemaGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ISchemaGenerator.cs @@ -2,15 +2,14 @@ using Microsoft.OpenApi.Models; using Microsoft.AspNetCore.Mvc.ApiExplorer; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public interface ISchemaGenerator { - public interface ISchemaGenerator - { - OpenApiSchema GenerateSchema( - Type modelType, - SchemaRepository schemaRepository, - MemberInfo memberInfo = null, - ParameterInfo parameterInfo = null, - ApiParameterRouteInfo routeInfo = null); - } + OpenApiSchema GenerateSchema( + Type modelType, + SchemaRepository schemaRepository, + MemberInfo memberInfo = null, + ParameterInfo parameterInfo = null, + ApiParameterRouteInfo routeInfo = null); } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/OpenApiAnyFactory.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/OpenApiAnyFactory.cs index 93248fb62f..04ee13b55b 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/OpenApiAnyFactory.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/OpenApiAnyFactory.cs @@ -1,83 +1,82 @@ using System.Text.Json; using Microsoft.OpenApi.Any; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public static class OpenApiAnyFactory { - public static class OpenApiAnyFactory - { - public static IOpenApiAny CreateFromJson(string json) - => CreateFromJson(json, null); + public static IOpenApiAny CreateFromJson(string json) + => CreateFromJson(json, null); - public static IOpenApiAny CreateFromJson(string json, JsonSerializerOptions options) + public static IOpenApiAny CreateFromJson(string json, JsonSerializerOptions options) + { + try { - try - { - var element = JsonSerializer.Deserialize(json, options); - return CreateFromJsonElement(element); - } - catch (Exception) - { - return null; - } + var element = JsonSerializer.Deserialize(json, options); + return CreateFromJsonElement(element); } - - private static OpenApiArray CreateOpenApiArray(JsonElement jsonElement) + catch (Exception) { - var openApiArray = new OpenApiArray(); + return null; + } + } - foreach (var item in jsonElement.EnumerateArray()) - { - openApiArray.Add(CreateFromJsonElement(item)); - } + private static OpenApiArray CreateOpenApiArray(JsonElement jsonElement) + { + var openApiArray = new OpenApiArray(); - return openApiArray; + foreach (var item in jsonElement.EnumerateArray()) + { + openApiArray.Add(CreateFromJsonElement(item)); } - private static OpenApiObject CreateOpenApiObject(JsonElement jsonElement) - { - var openApiObject = new OpenApiObject(); + return openApiArray; + } - foreach (var property in jsonElement.EnumerateObject()) - { - openApiObject.Add(property.Name, CreateFromJsonElement(property.Value)); - } + private static OpenApiObject CreateOpenApiObject(JsonElement jsonElement) + { + var openApiObject = new OpenApiObject(); - return openApiObject; + foreach (var property in jsonElement.EnumerateObject()) + { + openApiObject.Add(property.Name, CreateFromJsonElement(property.Value)); } - private static IOpenApiAny CreateFromJsonElement(JsonElement jsonElement) - { - if (jsonElement.ValueKind == JsonValueKind.Null) - return new OpenApiNull(); + return openApiObject; + } - if (jsonElement.ValueKind == JsonValueKind.True || jsonElement.ValueKind == JsonValueKind.False) - return new OpenApiBoolean(jsonElement.GetBoolean()); + private static IOpenApiAny CreateFromJsonElement(JsonElement jsonElement) + { + if (jsonElement.ValueKind == JsonValueKind.Null) + return new OpenApiNull(); - if (jsonElement.ValueKind == JsonValueKind.Number) - { - if (jsonElement.TryGetInt32(out int intValue)) - return new OpenApiInteger(intValue); + if (jsonElement.ValueKind == JsonValueKind.True || jsonElement.ValueKind == JsonValueKind.False) + return new OpenApiBoolean(jsonElement.GetBoolean()); - if (jsonElement.TryGetInt64(out long longValue)) - return new OpenApiLong(longValue); + if (jsonElement.ValueKind == JsonValueKind.Number) + { + if (jsonElement.TryGetInt32(out int intValue)) + return new OpenApiInteger(intValue); + + if (jsonElement.TryGetInt64(out long longValue)) + return new OpenApiLong(longValue); - if (jsonElement.TryGetSingle(out float floatValue) && !float.IsInfinity(floatValue)) - return new OpenApiFloat(floatValue); + if (jsonElement.TryGetSingle(out float floatValue) && !float.IsInfinity(floatValue)) + return new OpenApiFloat(floatValue); - if (jsonElement.TryGetDouble(out double doubleValue)) - return new OpenApiDouble(doubleValue); - } + if (jsonElement.TryGetDouble(out double doubleValue)) + return new OpenApiDouble(doubleValue); + } - if (jsonElement.ValueKind == JsonValueKind.String) - return new OpenApiString(jsonElement.ToString()); + if (jsonElement.ValueKind == JsonValueKind.String) + return new OpenApiString(jsonElement.ToString()); - if (jsonElement.ValueKind == JsonValueKind.Array) - return CreateOpenApiArray(jsonElement); + if (jsonElement.ValueKind == JsonValueKind.Array) + return CreateOpenApiArray(jsonElement); - if (jsonElement.ValueKind == JsonValueKind.Object) - return CreateOpenApiObject(jsonElement); + if (jsonElement.ValueKind == JsonValueKind.Object) + return CreateOpenApiObject(jsonElement); - throw new ArgumentException($"Unsupported value kind {jsonElement.ValueKind}"); - } + throw new ArgumentException($"Unsupported value kind {jsonElement.ValueKind}"); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/OperationFilterContext.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/OperationFilterContext.cs new file mode 100644 index 0000000000..ff409f3180 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/OperationFilterContext.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class OperationFilterContext +{ + public OperationFilterContext( + ApiDescription apiDescription, + ISchemaGenerator schemaRegistry, + SchemaRepository schemaRepository, + MethodInfo methodInfo) + { + ApiDescription = apiDescription; + SchemaGenerator = schemaRegistry; + SchemaRepository = schemaRepository; + MethodInfo = methodInfo; + } + + public ApiDescription ApiDescription { get; } + + public ISchemaGenerator SchemaGenerator { get; } + + public SchemaRepository SchemaRepository { get; } + + public MethodInfo MethodInfo { get; } + + public string DocumentName => SchemaRepository.DocumentName; +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ParameterFilterContext.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ParameterFilterContext.cs new file mode 100644 index 0000000000..ea95936ccb --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ParameterFilterContext.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class ParameterFilterContext +{ + public ParameterFilterContext( + ApiParameterDescription apiParameterDescription, + ISchemaGenerator schemaGenerator, + SchemaRepository schemaRepository, + PropertyInfo propertyInfo = null, + ParameterInfo parameterInfo = null) + { + ApiParameterDescription = apiParameterDescription; + SchemaGenerator = schemaGenerator; + SchemaRepository = schemaRepository; + PropertyInfo = propertyInfo; + ParameterInfo = parameterInfo; + } + + public ApiParameterDescription ApiParameterDescription { get; } + + public ISchemaGenerator SchemaGenerator { get; } + + public SchemaRepository SchemaRepository { get; } + + public PropertyInfo PropertyInfo { get; } + + public ParameterInfo ParameterInfo { get; } + + public string DocumentName => SchemaRepository.DocumentName; +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/RequestBodyFilterContext.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/RequestBodyFilterContext.cs new file mode 100644 index 0000000000..c6db205846 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/RequestBodyFilterContext.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class RequestBodyFilterContext +{ + public RequestBodyFilterContext( + ApiParameterDescription bodyParameterDescription, + IEnumerable formParameterDescriptions, + ISchemaGenerator schemaGenerator, + SchemaRepository schemaRepository) + { + BodyParameterDescription = bodyParameterDescription; + FormParameterDescriptions = formParameterDescriptions; + SchemaGenerator = schemaGenerator; + SchemaRepository = schemaRepository; + } + + public ApiParameterDescription BodyParameterDescription { get; } + + public IEnumerable FormParameterDescriptions { get; } + + public ISchemaGenerator SchemaGenerator { get; } + + public SchemaRepository SchemaRepository { get; } + + public string DocumentName => SchemaRepository.DocumentName; +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SchemaRepository.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SchemaRepository.cs index 36ac8f868f..98b2438cba 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SchemaRepository.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SchemaRepository.cs @@ -1,57 +1,56 @@ using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class SchemaRepository { - public class SchemaRepository - { - private readonly Dictionary _reservedIds = new Dictionary(); + private readonly Dictionary _reservedIds = new Dictionary(); - public SchemaRepository(string documentName = null) - { - DocumentName = documentName; - } + public SchemaRepository(string documentName = null) + { + DocumentName = documentName; + } - public string DocumentName { get; } + public string DocumentName { get; } - public Dictionary Schemas { get; private set; } = new Dictionary(); + public Dictionary Schemas { get; private set; } = new Dictionary(); - public void RegisterType(Type type, string schemaId) + public void RegisterType(Type type, string schemaId) + { + if (_reservedIds.ContainsValue(schemaId)) { - if (_reservedIds.ContainsValue(schemaId)) - { - var conflictingType = _reservedIds.First(entry => entry.Value == schemaId).Key; - - throw new InvalidOperationException( - $"Can't use schemaId \"${schemaId}\" for type \"${type}\". " + - $"The same schemaId is already used for type \"${conflictingType}\""); - } + var conflictingType = _reservedIds.First(entry => entry.Value == schemaId).Key; - _reservedIds.Add(type, schemaId); + throw new InvalidOperationException( + $"Can't use schemaId \"${schemaId}\" for type \"${type}\". " + + $"The same schemaId is already used for type \"${conflictingType}\""); } - public bool TryLookupByType(Type type, out OpenApiSchema referenceSchema) - { - if (_reservedIds.TryGetValue(type, out string schemaId)) - { - referenceSchema = new OpenApiSchema - { - Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId } - }; - return true; - } - - referenceSchema = null; - return false; - } + _reservedIds.Add(type, schemaId); + } - public OpenApiSchema AddDefinition(string schemaId, OpenApiSchema schema) + public bool TryLookupByType(Type type, out OpenApiSchema referenceSchema) + { + if (_reservedIds.TryGetValue(type, out string schemaId)) { - Schemas.Add(schemaId, schema); - - return new OpenApiSchema + referenceSchema = new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId } }; + return true; } + + referenceSchema = null; + return false; + } + + public OpenApiSchema AddDefinition(string schemaId, OpenApiSchema schema) + { + Schemas.Add(schemaId, schema); + + return new OpenApiSchema + { + Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId } + }; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/StringExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/StringExtensions.cs index 8055a1baae..9ef9ba7976 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/StringExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/StringExtensions.cs @@ -1,15 +1,14 @@ -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +internal static class StringExtensions { - internal static class StringExtensions + internal static string ToCamelCase(this string value) { - internal static string ToCamelCase(this string value) - { - if (string.IsNullOrEmpty(value)) return value; + if (string.IsNullOrEmpty(value)) return value; - var cameCasedParts = value.Split('.') - .Select(part => char.ToLowerInvariant(part[0]) + part.Substring(1)); + var cameCasedParts = value.Split('.') + .Select(part => char.ToLowerInvariant(part[0]) + part.Substring(1)); - return string.Join(".", cameCasedParts); - } + return string.Join(".", cameCasedParts); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index e7eb23f3e1..ebf4893421 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -9,49 +9,69 @@ using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Swagger; -#if NET7_0_OR_GREATER +#if NET using Microsoft.AspNetCore.Http.Metadata; #endif using OpenApiTag = Microsoft.OpenApi.Models.OpenApiTag; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class SwaggerGenerator( + SwaggerGeneratorOptions options, + IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, + ISchemaGenerator schemaGenerator) : ISwaggerProvider, IAsyncSwaggerProvider, ISwaggerDocumentMetadataProvider { - public class SwaggerGenerator( + private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider = apiDescriptionsProvider; + private readonly ISchemaGenerator _schemaGenerator = schemaGenerator; + private readonly SwaggerGeneratorOptions _options = options ?? new(); + private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; + + public SwaggerGenerator( SwaggerGeneratorOptions options, IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, - ISchemaGenerator schemaGenerator) : ISwaggerProvider, IAsyncSwaggerProvider, ISwaggerDocumentMetadataProvider + ISchemaGenerator schemaGenerator, + IAuthenticationSchemeProvider authenticationSchemeProvider) : this(options, apiDescriptionsProvider, schemaGenerator) + { + _authenticationSchemeProvider = authenticationSchemeProvider; + } + + public async Task GetSwaggerAsync( + string documentName, + string host = null, + string basePath = null) { - private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider = apiDescriptionsProvider; - private readonly ISchemaGenerator _schemaGenerator = schemaGenerator; - private readonly SwaggerGeneratorOptions _options = options ?? new(); - private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; - - public SwaggerGenerator( - SwaggerGeneratorOptions options, - IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, - ISchemaGenerator schemaGenerator, - IAuthenticationSchemeProvider authenticationSchemeProvider) : this(options, apiDescriptionsProvider, schemaGenerator) + var (filterContext, swaggerDoc) = GetSwaggerDocumentWithoutPaths(documentName, host, basePath); + + swaggerDoc.Paths = await GeneratePathsAsync(swaggerDoc, filterContext.ApiDescriptions, filterContext.SchemaRepository); + swaggerDoc.Components.SecuritySchemes = await GetSecuritySchemesAsync(); + + // NOTE: Filter processing moved here so they may affect generated security schemes + foreach (var filter in _options.DocumentAsyncFilters) { - _authenticationSchemeProvider = authenticationSchemeProvider; + await filter.ApplyAsync(swaggerDoc, filterContext, CancellationToken.None); } - public async Task GetSwaggerAsync( - string documentName, - string host = null, - string basePath = null) + foreach (var filter in _options.DocumentFilters) + { + filter.Apply(swaggerDoc, filterContext); + } + + SortSchemas(swaggerDoc); + + return swaggerDoc; + } + + public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null) + { + try { var (filterContext, swaggerDoc) = GetSwaggerDocumentWithoutPaths(documentName, host, basePath); - swaggerDoc.Paths = await GeneratePathsAsync(swaggerDoc, filterContext.ApiDescriptions, filterContext.SchemaRepository); - swaggerDoc.Components.SecuritySchemes = await GetSecuritySchemesAsync(); + swaggerDoc.Paths = GeneratePaths(swaggerDoc, filterContext.ApiDescriptions, filterContext.SchemaRepository); + swaggerDoc.Components.SecuritySchemes = GetSecuritySchemesAsync().Result; // NOTE: Filter processing moved here so they may affect generated security schemes - foreach (var filter in _options.DocumentAsyncFilters) - { - await filter.ApplyAsync(swaggerDoc, filterContext, CancellationToken.None); - } - foreach (var filter in _options.DocumentFilters) { filter.Apply(swaggerDoc, filterContext); @@ -61,1047 +81,1026 @@ public async Task GetSwaggerAsync( return swaggerDoc; } - - public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null) + catch (AggregateException ex) { - try - { - var (filterContext, swaggerDoc) = GetSwaggerDocumentWithoutPaths(documentName, host, basePath); - - swaggerDoc.Paths = GeneratePaths(swaggerDoc, filterContext.ApiDescriptions, filterContext.SchemaRepository); - swaggerDoc.Components.SecuritySchemes = GetSecuritySchemesAsync().Result; + // Unwrap any AggregateException from using async methods to run the synchronous filters + var inner = ex.InnerException; - // NOTE: Filter processing moved here so they may affect generated security schemes - foreach (var filter in _options.DocumentFilters) + while (inner is not null) + { + if (inner is AggregateException) { - filter.Apply(swaggerDoc, filterContext); + inner = inner.InnerException; } - - SortSchemas(swaggerDoc); - - return swaggerDoc; - } - catch (AggregateException ex) - { - // Unwrap any AggregateException from using async methods to run the synchronous filters - var inner = ex.InnerException; - - while (inner is not null) + else { - if (inner is AggregateException) - { - inner = inner.InnerException; - } - else - { - throw inner; - } + throw inner; } - - throw; } + + throw; } + } + + public IList GetDocumentNames() => _options.SwaggerDocs.Keys.ToList(); - public IList GetDocumentNames() => _options.SwaggerDocs.Keys.ToList(); + private void SortSchemas(OpenApiDocument document) + { + document.Components.Schemas = new SortedDictionary(document.Components.Schemas, _options.SchemaComparer); + } - private void SortSchemas(OpenApiDocument document) + private (DocumentFilterContext, OpenApiDocument) GetSwaggerDocumentWithoutPaths(string documentName, string host = null, string basePath = null) + { + if (!_options.SwaggerDocs.TryGetValue(documentName, out OpenApiInfo info)) { - document.Components.Schemas = new SortedDictionary(document.Components.Schemas, _options.SchemaComparer); + throw new UnknownSwaggerDocument(documentName, _options.SwaggerDocs.Select(d => d.Key)); } - private (DocumentFilterContext, OpenApiDocument) GetSwaggerDocumentWithoutPaths(string documentName, string host = null, string basePath = null) - { - if (!_options.SwaggerDocs.TryGetValue(documentName, out OpenApiInfo info)) + var applicableApiDescriptions = _apiDescriptionsProvider.ApiDescriptionGroups.Items + .SelectMany(group => group.Items) + .Where(apiDesc => { - throw new UnknownSwaggerDocument(documentName, _options.SwaggerDocs.Select(d => d.Key)); - } + var attributes = apiDesc.CustomAttributes().ToList(); + return !(_options.IgnoreObsoleteActions && attributes.OfType().Any()) && + !attributes.OfType().Any() && + _options.DocInclusionPredicate(documentName, apiDesc); + }); - var applicableApiDescriptions = _apiDescriptionsProvider.ApiDescriptionGroups.Items - .SelectMany(group => group.Items) - .Where(apiDesc => - { - var attributes = apiDesc.CustomAttributes().ToList(); - return !(_options.IgnoreObsoleteActions && attributes.OfType().Any()) && - !attributes.OfType().Any() && - _options.DocInclusionPredicate(documentName, apiDesc); - }); - - var schemaRepository = new SchemaRepository(documentName); + var schemaRepository = new SchemaRepository(documentName); - var swaggerDoc = new OpenApiDocument + var swaggerDoc = new OpenApiDocument + { + Info = info, + Servers = GenerateServers(host, basePath), + Components = new OpenApiComponents { - Info = info, - Servers = GenerateServers(host, basePath), - Components = new OpenApiComponents - { - Schemas = schemaRepository.Schemas, - }, - SecurityRequirements = new List(_options.SecurityRequirements) - }; + Schemas = schemaRepository.Schemas, + }, + SecurityRequirements = new List(_options.SecurityRequirements) + }; - return (new DocumentFilterContext(applicableApiDescriptions, _schemaGenerator, schemaRepository), swaggerDoc); - } + return (new DocumentFilterContext(applicableApiDescriptions, _schemaGenerator, schemaRepository), swaggerDoc); + } - private async Task> GetSecuritySchemesAsync() + private async Task> GetSecuritySchemesAsync() + { + if (!_options.InferSecuritySchemes) { - if (!_options.InferSecuritySchemes) - { - return new Dictionary(_options.SecuritySchemes); - } - - var authenticationSchemes = (_authenticationSchemeProvider is not null) - ? await _authenticationSchemeProvider.GetAllSchemesAsync() - : []; + return new Dictionary(_options.SecuritySchemes); + } - if (_options.SecuritySchemesSelector != null) - { - return _options.SecuritySchemesSelector(authenticationSchemes); - } + var authenticationSchemes = (_authenticationSchemeProvider is not null) + ? await _authenticationSchemeProvider.GetAllSchemesAsync() + : []; - // Default implementation, currently only supports JWT Bearer scheme - return authenticationSchemes - .Where(authScheme => authScheme.Name == "Bearer") - .ToDictionary( - (authScheme) => authScheme.Name, - (authScheme) => new OpenApiSecurityScheme - { - Type = SecuritySchemeType.Http, - Scheme = "bearer", // "bearer" refers to the header name here - In = ParameterLocation.Header, - BearerFormat = "Json Web Token" - }); + if (_options.SecuritySchemesSelector != null) + { + return _options.SecuritySchemesSelector(authenticationSchemes); } - private List GenerateServers(string host, string basePath) - { - if (_options.Servers.Count > 0) - { - return new List(_options.Servers); - } + // Default implementation, currently only supports JWT Bearer scheme + return authenticationSchemes + .Where(authScheme => authScheme.Name == "Bearer") + .ToDictionary( + (authScheme) => authScheme.Name, + (authScheme) => new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http, + Scheme = "bearer", // "bearer" refers to the header name here + In = ParameterLocation.Header, + BearerFormat = "Json Web Token" + }); + } - return (host == null && basePath == null) - ? [] - : [new() { Url = $"{host}{basePath}" }]; + private List GenerateServers(string host, string basePath) + { + if (_options.Servers.Count > 0) + { + return new List(_options.Servers); } - private async Task GeneratePathsAsync( - OpenApiDocument document, - IEnumerable apiDescriptions, - SchemaRepository schemaRepository, - Func, SchemaRepository, Task>> operationsGenerator) + return (host == null && basePath == null) + ? [] + : [new() { Url = $"{host}{basePath}" }]; + } + + private async Task GeneratePathsAsync( + OpenApiDocument document, + IEnumerable apiDescriptions, + SchemaRepository schemaRepository, + Func, SchemaRepository, Task>> operationsGenerator) + { + var apiDescriptionsByPath = apiDescriptions + .OrderBy(_options.SortKeySelector) + .GroupBy(_options.PathGroupSelector); + + var paths = new OpenApiPaths(); + foreach (var group in apiDescriptionsByPath) { - var apiDescriptionsByPath = apiDescriptions - .OrderBy(_options.SortKeySelector) - .GroupBy(_options.PathGroupSelector); + paths.Add($"/{group.Key}", + new OpenApiPathItem + { + Operations = await operationsGenerator(document, group, schemaRepository) + }); + }; - var paths = new OpenApiPaths(); - foreach (var group in apiDescriptionsByPath) - { - paths.Add($"/{group.Key}", - new OpenApiPathItem - { - Operations = await operationsGenerator(document, group, schemaRepository) - }); - }; + return paths; + } - return paths; - } + private OpenApiPaths GeneratePaths( + OpenApiDocument document, + IEnumerable apiDescriptions, + SchemaRepository schemaRepository) + { + return GeneratePathsAsync( + document, + apiDescriptions, + schemaRepository, + (document, group, schemaRepository) => Task.FromResult(GenerateOperations(document, group, schemaRepository))).Result; + } - private OpenApiPaths GeneratePaths( - OpenApiDocument document, - IEnumerable apiDescriptions, - SchemaRepository schemaRepository) - { - return GeneratePathsAsync( - document, - apiDescriptions, - schemaRepository, - (document, group, schemaRepository) => Task.FromResult(GenerateOperations(document, group, schemaRepository))).Result; - } + private async Task GeneratePathsAsync( + OpenApiDocument document, + IEnumerable apiDescriptions, + SchemaRepository schemaRepository) + { + return await GeneratePathsAsync( + document, + apiDescriptions, + schemaRepository, + GenerateOperationsAsync); + } + + private IEnumerable<(OperationType, ApiDescription)> GetOperationsGroupedByMethod( + IEnumerable apiDescriptions) + { + return apiDescriptions + .OrderBy(_options.SortKeySelector) + .GroupBy(apiDesc => apiDesc.HttpMethod) + .Select(PrepareGenerateOperation); + } + + private Dictionary GenerateOperations( + OpenApiDocument document, + IEnumerable apiDescriptions, + SchemaRepository schemaRepository) + { + var apiDescriptionsByMethod = GetOperationsGroupedByMethod(apiDescriptions); + var operations = new Dictionary(); - private async Task GeneratePathsAsync( - OpenApiDocument document, - IEnumerable apiDescriptions, - SchemaRepository schemaRepository) + foreach ((var operationType, var description) in apiDescriptionsByMethod) { - return await GeneratePathsAsync( - document, - apiDescriptions, - schemaRepository, - GenerateOperationsAsync); + operations.Add(operationType, GenerateOperation(document, description, schemaRepository)); } - private IEnumerable<(OperationType, ApiDescription)> GetOperationsGroupedByMethod( - IEnumerable apiDescriptions) + return operations; + } + + private async Task> GenerateOperationsAsync( + OpenApiDocument document, + IEnumerable apiDescriptions, + SchemaRepository schemaRepository) + { + var apiDescriptionsByMethod = GetOperationsGroupedByMethod(apiDescriptions); + var operations = new Dictionary(); + + foreach ((var operationType, var description) in apiDescriptionsByMethod) { - return apiDescriptions - .OrderBy(_options.SortKeySelector) - .GroupBy(apiDesc => apiDesc.HttpMethod) - .Select(PrepareGenerateOperation); + operations.Add(operationType, await GenerateOperationAsync(document, description, schemaRepository)); } - private Dictionary GenerateOperations( - OpenApiDocument document, - IEnumerable apiDescriptions, - SchemaRepository schemaRepository) - { - var apiDescriptionsByMethod = GetOperationsGroupedByMethod(apiDescriptions); - var operations = new Dictionary(); + return operations; + } - foreach ((var operationType, var description) in apiDescriptionsByMethod) - { - operations.Add(operationType, GenerateOperation(document, description, schemaRepository)); - } + private (OperationType OperationType, ApiDescription ApiDescription) PrepareGenerateOperation(IGrouping group) + { + var httpMethod = group.Key ?? throw new SwaggerGeneratorException(string.Format( + "Ambiguous HTTP method for action - {0}. " + + "Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0", + group.First().ActionDescriptor.DisplayName)); - return operations; - } + var count = group.Count(); - private async Task> GenerateOperationsAsync( - OpenApiDocument document, - IEnumerable apiDescriptions, - SchemaRepository schemaRepository) + if (count > 1 && _options.ConflictingActionsResolver == null) { - var apiDescriptionsByMethod = GetOperationsGroupedByMethod(apiDescriptions); - var operations = new Dictionary(); + throw new SwaggerGeneratorException(string.Format( + "Conflicting method/path combination \"{0} {1}\" for actions - {2}. " + + "Actions require a unique method/path combination for Swagger/OpenAPI 2.0 and 3.0. Use ConflictingActionsResolver as a workaround or provide your own implementation of PathGroupSelector.", + httpMethod, + group.First().RelativePath, + string.Join(", ", group.Select(apiDesc => apiDesc.ActionDescriptor.DisplayName)))); + } - foreach ((var operationType, var description) in apiDescriptionsByMethod) - { - operations.Add(operationType, await GenerateOperationAsync(document, description, schemaRepository)); - } + var apiDescription = (count > 1) ? _options.ConflictingActionsResolver(group) : group.Single(); - return operations; + var normalizedMethod = httpMethod.ToUpperInvariant(); + if (!OperationTypeMap.TryGetValue(normalizedMethod, out var operationType)) + { + // See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2600 and + // https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2740. + throw new SwaggerGeneratorException($"The \"{httpMethod}\" HTTP method is not supported."); } - private (OperationType OperationType, ApiDescription ApiDescription) PrepareGenerateOperation(IGrouping group) - { - var httpMethod = group.Key ?? throw new SwaggerGeneratorException(string.Format( - "Ambiguous HTTP method for action - {0}. " + - "Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0", - group.First().ActionDescriptor.DisplayName)); + return (operationType, apiDescription); + } - var count = group.Count(); + private async Task GenerateOperationAsync( + OpenApiDocument document, + ApiDescription apiDescription, + SchemaRepository schemaRepository, + Func>> parametersGenerator, + Func> bodyGenerator, + Func applyFilters) + { + OpenApiOperation operation = +#if NET + await GenerateOpenApiOperationFromMetadataAsync(apiDescription, schemaRepository); +#else + null; +#endif - if (count > 1 && _options.ConflictingActionsResolver == null) + try + { + operation ??= new OpenApiOperation { - throw new SwaggerGeneratorException(string.Format( - "Conflicting method/path combination \"{0} {1}\" for actions - {2}. " + - "Actions require a unique method/path combination for Swagger/OpenAPI 2.0 and 3.0. Use ConflictingActionsResolver as a workaround or provide your own implementation of PathGroupSelector.", - httpMethod, - group.First().RelativePath, - string.Join(", ", group.Select(apiDesc => apiDesc.ActionDescriptor.DisplayName)))); - } + Tags = GenerateOperationTags(document, apiDescription), + OperationId = _options.OperationIdSelector(apiDescription), + Parameters = await parametersGenerator(apiDescription, schemaRepository), + RequestBody = await bodyGenerator(apiDescription, schemaRepository), + Responses = GenerateResponses(apiDescription, schemaRepository), + Deprecated = apiDescription.CustomAttributes().OfType().Any(), +#if NET + Summary = GenerateSummary(apiDescription), + Description = GenerateDescription(apiDescription), +#endif + }; - var apiDescription = (count > 1) ? _options.ConflictingActionsResolver(group) : group.Single(); + apiDescription.TryGetMethodInfo(out MethodInfo methodInfo); + var filterContext = new OperationFilterContext(apiDescription, _schemaGenerator, schemaRepository, methodInfo); - var normalizedMethod = httpMethod.ToUpperInvariant(); - if (!OperationTypeMap.TryGetValue(normalizedMethod, out var operationType)) - { - // See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2600 and - // https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2740. - throw new SwaggerGeneratorException($"The \"{httpMethod}\" HTTP method is not supported."); - } + await applyFilters(operation, filterContext); - return (operationType, apiDescription); + return operation; } - - private async Task GenerateOperationAsync( - OpenApiDocument document, - ApiDescription apiDescription, - SchemaRepository schemaRepository, - Func>> parametersGenerator, - Func> bodyGenerator, - Func applyFilters) + catch (Exception ex) { - OpenApiOperation operation = -#if NET6_0_OR_GREATER - await GenerateOpenApiOperationFromMetadataAsync(apiDescription, schemaRepository); -#else - null; -#endif + throw new SwaggerGeneratorException( + message: $"Failed to generate Operation for action - {apiDescription.ActionDescriptor.DisplayName}. See inner exception", + innerException: ex); + } + } - try + private OpenApiOperation GenerateOperation(OpenApiDocument document, ApiDescription apiDescription, SchemaRepository schemaRepository) + { + return GenerateOperationAsync( + document, + apiDescription, + schemaRepository, + (description, repository) => Task.FromResult(GenerateParameters(description, repository)), + (description, repository) => Task.FromResult(GenerateRequestBody(description, repository)), + (operation, filterContext) => { - operation ??= new OpenApiOperation + foreach (var filter in _options.OperationFilters) { - Tags = GenerateOperationTags(document, apiDescription), - OperationId = _options.OperationIdSelector(apiDescription), - Parameters = await parametersGenerator(apiDescription, schemaRepository), - RequestBody = await bodyGenerator(apiDescription, schemaRepository), - Responses = GenerateResponses(apiDescription, schemaRepository), - Deprecated = apiDescription.CustomAttributes().OfType().Any(), -#if NET7_0_OR_GREATER - Summary = GenerateSummary(apiDescription), - Description = GenerateDescription(apiDescription), -#endif - }; - - apiDescription.TryGetMethodInfo(out MethodInfo methodInfo); - var filterContext = new OperationFilterContext(apiDescription, _schemaGenerator, schemaRepository, methodInfo); + filter.Apply(operation, filterContext); + } - await applyFilters(operation, filterContext); + return Task.CompletedTask; + }).Result; + } - return operation; - } - catch (Exception ex) + private async Task GenerateOperationAsync( + OpenApiDocument document, + ApiDescription apiDescription, + SchemaRepository schemaRepository) + { + return await GenerateOperationAsync( + document, + apiDescription, + schemaRepository, + GenerateParametersAsync, + GenerateRequestBodyAsync, + async (operation, filterContext) => { - throw new SwaggerGeneratorException( - message: $"Failed to generate Operation for action - {apiDescription.ActionDescriptor.DisplayName}. See inner exception", - innerException: ex); - } - } + foreach (var filter in _options.OperationAsyncFilters) + { + await filter.ApplyAsync(operation, filterContext, CancellationToken.None); + } - private OpenApiOperation GenerateOperation(OpenApiDocument document, ApiDescription apiDescription, SchemaRepository schemaRepository) - { - return GenerateOperationAsync( - document, - apiDescription, - schemaRepository, - (description, repository) => Task.FromResult(GenerateParameters(description, repository)), - (description, repository) => Task.FromResult(GenerateRequestBody(description, repository)), - (operation, filterContext) => + foreach (var filter in _options.OperationFilters) { - foreach (var filter in _options.OperationFilters) - { - filter.Apply(operation, filterContext); - } + filter.Apply(operation, filterContext); + } + }); + } - return Task.CompletedTask; - }).Result; - } +#if NET + private async Task GenerateOpenApiOperationFromMetadataAsync(ApiDescription apiDescription, SchemaRepository schemaRepository) + { + var metadata = apiDescription.ActionDescriptor?.EndpointMetadata; + var operation = metadata?.OfType().SingleOrDefault(); - private async Task GenerateOperationAsync( - OpenApiDocument document, - ApiDescription apiDescription, - SchemaRepository schemaRepository) + if (operation is null) { - return await GenerateOperationAsync( - document, - apiDescription, - schemaRepository, - GenerateParametersAsync, - GenerateRequestBodyAsync, - async (operation, filterContext) => - { - foreach (var filter in _options.OperationAsyncFilters) - { - await filter.ApplyAsync(operation, filterContext, CancellationToken.None); - } - - foreach (var filter in _options.OperationFilters) - { - filter.Apply(operation, filterContext); - } - }); + return null; } -#if NET6_0_OR_GREATER - private async Task GenerateOpenApiOperationFromMetadataAsync(ApiDescription apiDescription, SchemaRepository schemaRepository) + // Schemas will be generated via Swashbuckle by default. + foreach (var parameter in operation.Parameters) { - var metadata = apiDescription.ActionDescriptor?.EndpointMetadata; - var operation = metadata?.OfType().SingleOrDefault(); - - if (operation is null) + var apiParameter = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.Name == parameter.Name && !desc.IsFromBody() && !desc.IsFromForm() && !desc.IsIllegalHeaderParameter()); + if (apiParameter is not null) { - return null; - } + var (parameterAndContext, filterContext) = GenerateParameterAndContext(apiParameter, schemaRepository); + parameter.Name = parameterAndContext.Name; + parameter.Schema = parameterAndContext.Schema; + parameter.Description ??= parameterAndContext.Description; - // Schemas will be generated via Swashbuckle by default. - foreach (var parameter in operation.Parameters) - { - var apiParameter = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.Name == parameter.Name && !desc.IsFromBody() && !desc.IsFromForm() && !desc.IsIllegalHeaderParameter()); - if (apiParameter is not null) + foreach (var filter in _options.ParameterAsyncFilters) { - var (parameterAndContext, filterContext) = GenerateParameterAndContext(apiParameter, schemaRepository); - parameter.Name = parameterAndContext.Name; - parameter.Schema = parameterAndContext.Schema; - parameter.Description ??= parameterAndContext.Description; - - foreach (var filter in _options.ParameterAsyncFilters) - { - await filter.ApplyAsync(parameter, filterContext, CancellationToken.None); - } + await filter.ApplyAsync(parameter, filterContext, CancellationToken.None); + } - foreach (var filter in _options.ParameterFilters) - { - filter.Apply(parameter, filterContext); - } + foreach (var filter in _options.ParameterFilters) + { + filter.Apply(parameter, filterContext); } } + } - var requestContentTypes = operation.RequestBody?.Content?.Keys; - if (requestContentTypes is not null) + var requestContentTypes = operation.RequestBody?.Content?.Keys; + if (requestContentTypes is not null) + { + foreach (var contentType in requestContentTypes) { - foreach (var contentType in requestContentTypes) + var contentTypeValue = operation.RequestBody.Content[contentType]; + var fromFormParameters = apiDescription.ParameterDescriptions.Where(desc => desc.IsFromForm()).ToList(); + ApiParameterDescription bodyParameterDescription = null; + if (fromFormParameters.Count > 0) { - var contentTypeValue = operation.RequestBody.Content[contentType]; - var fromFormParameters = apiDescription.ParameterDescriptions.Where(desc => desc.IsFromForm()).ToList(); - ApiParameterDescription bodyParameterDescription = null; - if (fromFormParameters.Count > 0) + var generatedContentTypeValue = GenerateRequestBodyFromFormParameters( + apiDescription, + schemaRepository, + fromFormParameters, + [contentType]).Content[contentType]; + + contentTypeValue.Schema = generatedContentTypeValue.Schema; + contentTypeValue.Encoding = generatedContentTypeValue.Encoding; + } + else + { + bodyParameterDescription = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.IsFromBody()); + if (bodyParameterDescription is not null) { - var generatedContentTypeValue = GenerateRequestBodyFromFormParameters( - apiDescription, + contentTypeValue.Schema = GenerateSchema( + bodyParameterDescription.ModelMetadata.ModelType, schemaRepository, - fromFormParameters, - [contentType]).Content[contentType]; - - contentTypeValue.Schema = generatedContentTypeValue.Schema; - contentTypeValue.Encoding = generatedContentTypeValue.Encoding; + bodyParameterDescription.PropertyInfo(), + bodyParameterDescription.ParameterInfo()); } - else + } + + if (fromFormParameters.Count > 0 || bodyParameterDescription is not null) + { + var filterContext = new RequestBodyFilterContext( + bodyParameterDescription: bodyParameterDescription, + formParameterDescriptions: bodyParameterDescription is null ? fromFormParameters : null, + schemaGenerator: _schemaGenerator, + schemaRepository: schemaRepository); + + foreach (var filter in _options.RequestBodyAsyncFilters) { - bodyParameterDescription = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.IsFromBody()); - if (bodyParameterDescription is not null) - { - contentTypeValue.Schema = GenerateSchema( - bodyParameterDescription.ModelMetadata.ModelType, - schemaRepository, - bodyParameterDescription.PropertyInfo(), - bodyParameterDescription.ParameterInfo()); - } + await filter.ApplyAsync(operation.RequestBody, filterContext, CancellationToken.None); } - if (fromFormParameters.Count > 0 || bodyParameterDescription is not null) + foreach (var filter in _options.RequestBodyFilters) { - var filterContext = new RequestBodyFilterContext( - bodyParameterDescription: bodyParameterDescription, - formParameterDescriptions: bodyParameterDescription is null ? fromFormParameters : null, - schemaGenerator: _schemaGenerator, - schemaRepository: schemaRepository); - - foreach (var filter in _options.RequestBodyAsyncFilters) - { - await filter.ApplyAsync(operation.RequestBody, filterContext, CancellationToken.None); - } - - foreach (var filter in _options.RequestBodyFilters) - { - filter.Apply(operation.RequestBody, filterContext); - } + filter.Apply(operation.RequestBody, filterContext); } } } + } - foreach (var kvp in operation.Responses) + foreach (var kvp in operation.Responses) + { + var response = kvp.Value; + var responseModel = apiDescription.SupportedResponseTypes.SingleOrDefault(desc => desc.StatusCode.ToString() == kvp.Key); + if (responseModel is not null) { - var response = kvp.Value; - var responseModel = apiDescription.SupportedResponseTypes.SingleOrDefault(desc => desc.StatusCode.ToString() == kvp.Key); - if (responseModel is not null) + var responseContentTypes = response?.Content?.Values; + if (responseContentTypes is not null) { - var responseContentTypes = response?.Content?.Values; - if (responseContentTypes is not null) + foreach (var content in responseContentTypes) { - foreach (var content in responseContentTypes) - { - content.Schema = GenerateSchema(responseModel.Type, schemaRepository); - } + content.Schema = GenerateSchema(responseModel.Type, schemaRepository); } } } - - return operation; } + + return operation; + } #endif - private List GenerateOperationTags(OpenApiDocument document, ApiDescription apiDescription) - => [.. _options.TagsSelector(apiDescription).Select(tagName => CreateTag(tagName, document))]; + private List GenerateOperationTags(OpenApiDocument document, ApiDescription apiDescription) + => [.. _options.TagsSelector(apiDescription).Select(tagName => CreateTag(tagName, document))]; - private static async Task> GenerateParametersAsync( - ApiDescription apiDescription, - SchemaRepository schemaRespository, - Func> parameterGenerator) + private static async Task> GenerateParametersAsync( + ApiDescription apiDescription, + SchemaRepository schemaRespository, + Func> parameterGenerator) + { + if (apiDescription.ParameterDescriptions.Any(IsFromFormAttributeUsedWithIFormFile)) { - if (apiDescription.ParameterDescriptions.Any(IsFromFormAttributeUsedWithIFormFile)) - { - throw new SwaggerGeneratorException(string.Format( - "Error reading parameter(s) for action {0} as [FromForm] attribute used with IFormFile. " + - "Please refer to https://github.com/domaindrivendev/Swashbuckle.AspNetCore#handle-forms-and-file-uploads for more information", - apiDescription.ActionDescriptor.DisplayName)); - } - - var applicableApiParameters = apiDescription.ParameterDescriptions - .Where(apiParam => - { - return !apiParam.IsFromBody() && !apiParam.IsFromForm() - && (!apiParam.CustomAttributes().OfType().Any()) - && (!apiParam.CustomAttributes().OfType().Any()) - && (apiParam.ModelMetadata == null || apiParam.ModelMetadata.IsBindingAllowed) - && !apiParam.IsIllegalHeaderParameter(); - }); - - var parameters = new List(); + throw new SwaggerGeneratorException(string.Format( + "Error reading parameter(s) for action {0} as [FromForm] attribute used with IFormFile. " + + "Please refer to https://github.com/domaindrivendev/Swashbuckle.AspNetCore#handle-forms-and-file-uploads for more information", + apiDescription.ActionDescriptor.DisplayName)); + } - foreach (var parameter in applicableApiParameters) + var applicableApiParameters = apiDescription.ParameterDescriptions + .Where(apiParam => { - parameters.Add(await parameterGenerator(parameter, schemaRespository)); - } + return !apiParam.IsFromBody() && !apiParam.IsFromForm() + && (!apiParam.CustomAttributes().OfType().Any()) + && (!apiParam.CustomAttributes().OfType().Any()) + && (apiParam.ModelMetadata == null || apiParam.ModelMetadata.IsBindingAllowed) + && !apiParam.IsIllegalHeaderParameter(); + }); - return parameters; - } + var parameters = new List(); - private List GenerateParameters(ApiDescription apiDescription, SchemaRepository schemaRespository) + foreach (var parameter in applicableApiParameters) { - return GenerateParametersAsync( - apiDescription, - schemaRespository, - (parameter, schemaRespository) => Task.FromResult(GenerateParameter(parameter, schemaRespository))).Result; + parameters.Add(await parameterGenerator(parameter, schemaRespository)); } - private async Task> GenerateParametersAsync( - ApiDescription apiDescription, - SchemaRepository schemaRespository) - { - return await GenerateParametersAsync( - apiDescription, - schemaRespository, - GenerateParameterAsync); - } + return parameters; + } - private OpenApiParameter GenerateParameterWithoutFilter( - ApiParameterDescription apiParameter, - SchemaRepository schemaRepository) - { - var name = _options.DescribeAllParametersInCamelCase - ? apiParameter.Name.ToCamelCase() - : apiParameter.Name; + private List GenerateParameters(ApiDescription apiDescription, SchemaRepository schemaRespository) + { + return GenerateParametersAsync( + apiDescription, + schemaRespository, + (parameter, schemaRespository) => Task.FromResult(GenerateParameter(parameter, schemaRespository))).Result; + } - var location = apiParameter.Source != null && - ParameterLocationMap.TryGetValue(apiParameter.Source, out var value) - ? value - : ParameterLocation.Query; + private async Task> GenerateParametersAsync( + ApiDescription apiDescription, + SchemaRepository schemaRespository) + { + return await GenerateParametersAsync( + apiDescription, + schemaRespository, + GenerateParameterAsync); + } - var isRequired = apiParameter.IsRequiredParameter(); + private OpenApiParameter GenerateParameterWithoutFilter( + ApiParameterDescription apiParameter, + SchemaRepository schemaRepository) + { + var name = _options.DescribeAllParametersInCamelCase + ? apiParameter.Name.ToCamelCase() + : apiParameter.Name; - var type = apiParameter.ModelMetadata?.ModelType; + var location = apiParameter.Source != null && + ParameterLocationMap.TryGetValue(apiParameter.Source, out var value) + ? value + : ParameterLocation.Query; - if (type is not null - && type == typeof(string) - && apiParameter.Type is not null - && (Nullable.GetUnderlyingType(apiParameter.Type) ?? apiParameter.Type).IsEnum) - { - type = apiParameter.Type; - } + var isRequired = apiParameter.IsRequiredParameter(); - var schema = (type != null) - ? GenerateSchema( - type, - schemaRepository, - apiParameter.PropertyInfo(), - apiParameter.ParameterInfo(), - apiParameter.RouteInfo) - : new OpenApiSchema { Type = JsonSchemaTypes.String }; + var type = apiParameter.ModelMetadata?.ModelType; - var description = schema.Description; - if (string.IsNullOrEmpty(description) - && !string.IsNullOrEmpty(schema?.Reference?.Id) - && schemaRepository.Schemas.TryGetValue(schema.Reference.Id, out var openApiSchema)) - { - description = openApiSchema.Description; - } - - return new OpenApiParameter - { - Name = name, - In = location, - Required = isRequired, - Schema = schema, - Description = description, - Style = GetParameterStyle(type, apiParameter.Source) - }; + if (type is not null + && type == typeof(string) + && apiParameter.Type is not null + && (Nullable.GetUnderlyingType(apiParameter.Type) ?? apiParameter.Type).IsEnum) + { + type = apiParameter.Type; } - private static ParameterStyle? GetParameterStyle(Type type, BindingSource source) + var schema = (type != null) + ? GenerateSchema( + type, + schemaRepository, + apiParameter.PropertyInfo(), + apiParameter.ParameterInfo(), + apiParameter.RouteInfo) + : new OpenApiSchema { Type = JsonSchemaTypes.String }; + + var description = schema.Description; + if (string.IsNullOrEmpty(description) + && !string.IsNullOrEmpty(schema?.Reference?.Id) + && schemaRepository.Schemas.TryGetValue(schema.Reference.Id, out var openApiSchema)) { - return source == BindingSource.Query && type?.IsGenericType == true && - typeof(IEnumerable>).IsAssignableFrom(type) - ? ParameterStyle.DeepObject - : null; + description = openApiSchema.Description; } - private (OpenApiParameter, ParameterFilterContext) GenerateParameterAndContext( - ApiParameterDescription apiParameter, - SchemaRepository schemaRepository) + return new OpenApiParameter { - var parameter = GenerateParameterWithoutFilter(apiParameter, schemaRepository); + Name = name, + In = location, + Required = isRequired, + Schema = schema, + Description = description, + Style = GetParameterStyle(type, apiParameter.Source) + }; + } - var context = new ParameterFilterContext( - apiParameter, - _schemaGenerator, - schemaRepository, - apiParameter.PropertyInfo(), - apiParameter.ParameterInfo()); + private static ParameterStyle? GetParameterStyle(Type type, BindingSource source) + { + return source == BindingSource.Query && type?.IsGenericType == true && + typeof(IEnumerable>).IsAssignableFrom(type) + ? ParameterStyle.DeepObject + : null; + } - return (parameter, context); - } + private (OpenApiParameter, ParameterFilterContext) GenerateParameterAndContext( + ApiParameterDescription apiParameter, + SchemaRepository schemaRepository) + { + var parameter = GenerateParameterWithoutFilter(apiParameter, schemaRepository); - private OpenApiParameter GenerateParameter( - ApiParameterDescription apiParameter, - SchemaRepository schemaRepository) - { - var (parameter, filterContext) = GenerateParameterAndContext(apiParameter, schemaRepository); + var context = new ParameterFilterContext( + apiParameter, + _schemaGenerator, + schemaRepository, + apiParameter.PropertyInfo(), + apiParameter.ParameterInfo()); - foreach (var filter in _options.ParameterFilters) - { - filter.Apply(parameter, filterContext); - } + return (parameter, context); + } - return parameter; - } + private OpenApiParameter GenerateParameter( + ApiParameterDescription apiParameter, + SchemaRepository schemaRepository) + { + var (parameter, filterContext) = GenerateParameterAndContext(apiParameter, schemaRepository); - private async Task GenerateParameterAsync( - ApiParameterDescription apiParameter, - SchemaRepository schemaRepository) + foreach (var filter in _options.ParameterFilters) { - var (parameter, filterContext) = GenerateParameterAndContext(apiParameter, schemaRepository); + filter.Apply(parameter, filterContext); + } - foreach (var filter in _options.ParameterAsyncFilters) - { - await filter.ApplyAsync(parameter, filterContext, CancellationToken.None); - } + return parameter; + } - foreach (var filter in _options.ParameterFilters) - { - filter.Apply(parameter, filterContext); - } + private async Task GenerateParameterAsync( + ApiParameterDescription apiParameter, + SchemaRepository schemaRepository) + { + var (parameter, filterContext) = GenerateParameterAndContext(apiParameter, schemaRepository); - return parameter; + foreach (var filter in _options.ParameterAsyncFilters) + { + await filter.ApplyAsync(parameter, filterContext, CancellationToken.None); } - private OpenApiSchema GenerateSchema( - Type type, - SchemaRepository schemaRepository, - PropertyInfo propertyInfo = null, - ParameterInfo parameterInfo = null, - ApiParameterRouteInfo routeInfo = null) + foreach (var filter in _options.ParameterFilters) { - try - { - return _schemaGenerator.GenerateSchema(type, schemaRepository, propertyInfo, parameterInfo, routeInfo); - } - catch (Exception ex) - { - throw new SwaggerGeneratorException( - message: $"Failed to generate schema for type - {type}. See inner exception", - innerException: ex); - } + filter.Apply(parameter, filterContext); } - private (OpenApiRequestBody RequestBody, RequestBodyFilterContext FilterContext) GenerateRequestBodyAndFilterContext( - ApiDescription apiDescription, - SchemaRepository schemaRepository) + return parameter; + } + + private OpenApiSchema GenerateSchema( + Type type, + SchemaRepository schemaRepository, + PropertyInfo propertyInfo = null, + ParameterInfo parameterInfo = null, + ApiParameterRouteInfo routeInfo = null) + { + try + { + return _schemaGenerator.GenerateSchema(type, schemaRepository, propertyInfo, parameterInfo, routeInfo); + } + catch (Exception ex) { - OpenApiRequestBody requestBody = null; - RequestBodyFilterContext filterContext = null; + throw new SwaggerGeneratorException( + message: $"Failed to generate schema for type - {type}. See inner exception", + innerException: ex); + } + } - var bodyParameter = apiDescription.ParameterDescriptions - .FirstOrDefault(paramDesc => paramDesc.IsFromBody()); + private (OpenApiRequestBody RequestBody, RequestBodyFilterContext FilterContext) GenerateRequestBodyAndFilterContext( + ApiDescription apiDescription, + SchemaRepository schemaRepository) + { + OpenApiRequestBody requestBody = null; + RequestBodyFilterContext filterContext = null; - var formParameters = apiDescription.ParameterDescriptions - .Where(paramDesc => paramDesc.IsFromForm()) - .ToList(); + var bodyParameter = apiDescription.ParameterDescriptions + .FirstOrDefault(paramDesc => paramDesc.IsFromBody()); - if (bodyParameter != null) - { - requestBody = GenerateRequestBodyFromBodyParameter(apiDescription, schemaRepository, bodyParameter); + var formParameters = apiDescription.ParameterDescriptions + .Where(paramDesc => paramDesc.IsFromForm()) + .ToList(); - filterContext = new RequestBodyFilterContext( - bodyParameterDescription: bodyParameter, - formParameterDescriptions: null, - schemaGenerator: _schemaGenerator, - schemaRepository: schemaRepository); - } - else if (formParameters.Count > 0) - { - requestBody = GenerateRequestBodyFromFormParameters(apiDescription, schemaRepository, formParameters, null); + if (bodyParameter != null) + { + requestBody = GenerateRequestBodyFromBodyParameter(apiDescription, schemaRepository, bodyParameter); - filterContext = new RequestBodyFilterContext( - bodyParameterDescription: null, - formParameterDescriptions: formParameters, - schemaGenerator: _schemaGenerator, - schemaRepository: schemaRepository); - } + filterContext = new RequestBodyFilterContext( + bodyParameterDescription: bodyParameter, + formParameterDescriptions: null, + schemaGenerator: _schemaGenerator, + schemaRepository: schemaRepository); + } + else if (formParameters.Count > 0) + { + requestBody = GenerateRequestBodyFromFormParameters(apiDescription, schemaRepository, formParameters, null); - return (requestBody, filterContext); + filterContext = new RequestBodyFilterContext( + bodyParameterDescription: null, + formParameterDescriptions: formParameters, + schemaGenerator: _schemaGenerator, + schemaRepository: schemaRepository); } - private OpenApiRequestBody GenerateRequestBody( - ApiDescription apiDescription, - SchemaRepository schemaRepository) - { - var (requestBody, filterContext) = GenerateRequestBodyAndFilterContext(apiDescription, schemaRepository); + return (requestBody, filterContext); + } + + private OpenApiRequestBody GenerateRequestBody( + ApiDescription apiDescription, + SchemaRepository schemaRepository) + { + var (requestBody, filterContext) = GenerateRequestBodyAndFilterContext(apiDescription, schemaRepository); - if (requestBody != null) + if (requestBody != null) + { + foreach (var filter in _options.RequestBodyFilters) { - foreach (var filter in _options.RequestBodyFilters) - { - filter.Apply(requestBody, filterContext); - } + filter.Apply(requestBody, filterContext); } - - return requestBody; } - private async Task GenerateRequestBodyAsync( - ApiDescription apiDescription, - SchemaRepository schemaRepository) - { - var (requestBody, filterContext) = GenerateRequestBodyAndFilterContext(apiDescription, schemaRepository); + return requestBody; + } - if (requestBody != null) - { - foreach (var filter in _options.RequestBodyAsyncFilters) - { - await filter.ApplyAsync(requestBody, filterContext, CancellationToken.None); - } + private async Task GenerateRequestBodyAsync( + ApiDescription apiDescription, + SchemaRepository schemaRepository) + { + var (requestBody, filterContext) = GenerateRequestBodyAndFilterContext(apiDescription, schemaRepository); - foreach (var filter in _options.RequestBodyFilters) - { - filter.Apply(requestBody, filterContext); - } + if (requestBody != null) + { + foreach (var filter in _options.RequestBodyAsyncFilters) + { + await filter.ApplyAsync(requestBody, filterContext, CancellationToken.None); } - return requestBody; + foreach (var filter in _options.RequestBodyFilters) + { + filter.Apply(requestBody, filterContext); + } } - private OpenApiRequestBody GenerateRequestBodyFromBodyParameter( - ApiDescription apiDescription, - SchemaRepository schemaRepository, - ApiParameterDescription bodyParameter) - { - var contentTypes = InferRequestContentTypes(apiDescription); + return requestBody; + } - var isRequired = bodyParameter.IsRequiredParameter(); + private OpenApiRequestBody GenerateRequestBodyFromBodyParameter( + ApiDescription apiDescription, + SchemaRepository schemaRepository, + ApiParameterDescription bodyParameter) + { + var contentTypes = InferRequestContentTypes(apiDescription); - var schema = GenerateSchema( - bodyParameter.ModelMetadata.ModelType, - schemaRepository, - bodyParameter.PropertyInfo(), - bodyParameter.ParameterInfo()); + var isRequired = bodyParameter.IsRequiredParameter(); - return new OpenApiRequestBody - { - Content = contentTypes - .ToDictionary( - contentType => contentType, - contentType => new OpenApiMediaType - { - Schema = schema - } - ), - Required = isRequired - }; - } + var schema = GenerateSchema( + bodyParameter.ModelMetadata.ModelType, + schemaRepository, + bodyParameter.PropertyInfo(), + bodyParameter.ParameterInfo()); - private static IEnumerable InferRequestContentTypes(ApiDescription apiDescription) + return new OpenApiRequestBody { - // If there's content types explicitly specified via ConsumesAttribute, use them - var explicitContentTypes = apiDescription.CustomAttributes().OfType() - .SelectMany(attr => attr.ContentTypes) - .Distinct(); - if (explicitContentTypes.Any()) return explicitContentTypes; - - // If there's content types surfaced by ApiExplorer, use them - return apiDescription.SupportedRequestFormats - .Select(format => format.MediaType) - .Where(x => x != null) - .Distinct(); - } + Content = contentTypes + .ToDictionary( + contentType => contentType, + contentType => new OpenApiMediaType + { + Schema = schema + } + ), + Required = isRequired + }; + } + + private static IEnumerable InferRequestContentTypes(ApiDescription apiDescription) + { + // If there's content types explicitly specified via ConsumesAttribute, use them + var explicitContentTypes = apiDescription.CustomAttributes().OfType() + .SelectMany(attr => attr.ContentTypes) + .Distinct(); + if (explicitContentTypes.Any()) return explicitContentTypes; + + // If there's content types surfaced by ApiExplorer, use them + return apiDescription.SupportedRequestFormats + .Select(format => format.MediaType) + .Where(x => x != null) + .Distinct(); + } - private OpenApiRequestBody GenerateRequestBodyFromFormParameters( - ApiDescription apiDescription, - SchemaRepository schemaRepository, - IEnumerable formParameters, - IEnumerable contentTypes) + private OpenApiRequestBody GenerateRequestBodyFromFormParameters( + ApiDescription apiDescription, + SchemaRepository schemaRepository, + IEnumerable formParameters, + IEnumerable contentTypes) + { + if (contentTypes is null) { - if (contentTypes is null) - { - contentTypes = InferRequestContentTypes(apiDescription); - contentTypes = contentTypes.Any() ? contentTypes : ["multipart/form-data"]; - } + contentTypes = InferRequestContentTypes(apiDescription); + contentTypes = contentTypes.Any() ? contentTypes : ["multipart/form-data"]; + } - var schema = GenerateSchemaFromFormParameters(formParameters, schemaRepository); + var schema = GenerateSchemaFromFormParameters(formParameters, schemaRepository); - var totalProperties = schema.AllOf - ?.FirstOrDefault(s => s.Properties.Count > 0) - ?.Properties ?? schema.Properties; + var totalProperties = schema.AllOf + ?.FirstOrDefault(s => s.Properties.Count > 0) + ?.Properties ?? schema.Properties; - return new OpenApiRequestBody - { - Content = contentTypes - .ToDictionary( - contentType => contentType, - contentType => new OpenApiMediaType - { - Schema = schema, - Encoding = totalProperties.ToDictionary( - entry => entry.Key, - entry => new OpenApiEncoding { Style = ParameterStyle.Form } - ) - } - ) - }; - } - - private OpenApiSchema GenerateSchemaFromFormParameters( - IEnumerable formParameters, - SchemaRepository schemaRepository) + return new OpenApiRequestBody { - var properties = new Dictionary(); - var requiredPropertyNames = new List(); - var ownSchemas = new List(); - foreach (var formParameter in formParameters) - { - var propertyInfo = formParameter.PropertyInfo(); - if (!propertyInfo?.HasAttribute() ?? true) - { - var schema = (formParameter.ModelMetadata != null) - ? GenerateSchema( - formParameter.ModelMetadata.ModelType, - schemaRepository, - propertyInfo, - formParameter.ParameterInfo()) - : new OpenApiSchema { Type = JsonSchemaTypes.String }; - - if (schema.Reference is null - || (formParameter.ModelMetadata?.ModelType is not null && (Nullable.GetUnderlyingType(formParameter.ModelMetadata.ModelType) ?? formParameter.ModelMetadata.ModelType).IsEnum)) - { - var name = _options.DescribeAllParametersInCamelCase - ? formParameter.Name.ToCamelCase() - : formParameter.Name; - properties.Add(name, schema); - if (formParameter.IsRequiredParameter()) - { - requiredPropertyNames.Add(name); - } - } - else + Content = contentTypes + .ToDictionary( + contentType => contentType, + contentType => new OpenApiMediaType { - ownSchemas.Add(schema); + Schema = schema, + Encoding = totalProperties.ToDictionary( + entry => entry.Key, + entry => new OpenApiEncoding { Style = ParameterStyle.Form } + ) } - } - } + ) + }; + } - if (ownSchemas.Count > 0) + private OpenApiSchema GenerateSchemaFromFormParameters( + IEnumerable formParameters, + SchemaRepository schemaRepository) + { + var properties = new Dictionary(); + var requiredPropertyNames = new List(); + var ownSchemas = new List(); + foreach (var formParameter in formParameters) + { + var propertyInfo = formParameter.PropertyInfo(); + if (!propertyInfo?.HasAttribute() ?? true) { - bool isAllOf = ownSchemas.Count > 1 || (ownSchemas.Count > 0 && properties.Count > 0); - if (isAllOf) + var schema = (formParameter.ModelMetadata != null) + ? GenerateSchema( + formParameter.ModelMetadata.ModelType, + schemaRepository, + propertyInfo, + formParameter.ParameterInfo()) + : new OpenApiSchema { Type = JsonSchemaTypes.String }; + + if (schema.Reference is null + || (formParameter.ModelMetadata?.ModelType is not null && (Nullable.GetUnderlyingType(formParameter.ModelMetadata.ModelType) ?? formParameter.ModelMetadata.ModelType).IsEnum)) { - var allOfSchema = new OpenApiSchema() - { - AllOf = ownSchemas - }; - if (properties.Count > 0) + var name = _options.DescribeAllParametersInCamelCase + ? formParameter.Name.ToCamelCase() + : formParameter.Name; + properties.Add(name, schema); + if (formParameter.IsRequiredParameter()) { - allOfSchema.AllOf.Add(GenerateSchemaForProperties(properties, requiredPropertyNames)); + requiredPropertyNames.Add(name); } - return allOfSchema; } - return ownSchemas.First(); + else + { + ownSchemas.Add(schema); + } } - - return GenerateSchemaForProperties(properties, requiredPropertyNames); - - static OpenApiSchema GenerateSchemaForProperties(Dictionary properties, List requiredPropertyNames) => - new() - { - Type = JsonSchemaTypes.Object, - Properties = properties, - Required = new SortedSet(requiredPropertyNames) - }; } - private OpenApiResponses GenerateResponses( - ApiDescription apiDescription, - SchemaRepository schemaRepository) + if (ownSchemas.Count > 0) { - var supportedResponseTypes = apiDescription.SupportedResponseTypes - .DefaultIfEmpty(new ApiResponseType { StatusCode = 200 }); - - var responses = new OpenApiResponses(); - foreach (var responseType in supportedResponseTypes) + bool isAllOf = ownSchemas.Count > 1 || (ownSchemas.Count > 0 && properties.Count > 0); + if (isAllOf) { - var statusCode = responseType.IsDefaultResponse() ? "default" : responseType.StatusCode.ToString(); - responses.Add(statusCode, GenerateResponse(apiDescription, schemaRepository, statusCode, responseType)); + var allOfSchema = new OpenApiSchema() + { + AllOf = ownSchemas + }; + if (properties.Count > 0) + { + allOfSchema.AllOf.Add(GenerateSchemaForProperties(properties, requiredPropertyNames)); + } + return allOfSchema; } - return responses; + return ownSchemas.First(); } - private OpenApiResponse GenerateResponse( - ApiDescription apiDescription, - SchemaRepository schemaRepository, - string statusCode, - ApiResponseType apiResponseType) - { - var description = ResponseDescriptionMap - .FirstOrDefault((entry) => Regex.IsMatch(statusCode, entry.Key)) - .Value; + return GenerateSchemaForProperties(properties, requiredPropertyNames); - var responseContentTypes = InferResponseContentTypes(apiDescription, apiResponseType); + static OpenApiSchema GenerateSchemaForProperties(Dictionary properties, List requiredPropertyNames) => + new() + { + Type = JsonSchemaTypes.Object, + Properties = properties, + Required = new SortedSet(requiredPropertyNames) + }; + } - return new OpenApiResponse - { - Description = description, - Content = responseContentTypes.ToDictionary( - contentType => contentType, - contentType => CreateResponseMediaType(apiResponseType.ModelMetadata?.ModelType ?? apiResponseType.Type, schemaRepository) - ) - }; - } + private OpenApiResponses GenerateResponses( + ApiDescription apiDescription, + SchemaRepository schemaRepository) + { + var supportedResponseTypes = apiDescription.SupportedResponseTypes + .DefaultIfEmpty(new ApiResponseType { StatusCode = 200 }); - private static IEnumerable InferResponseContentTypes(ApiDescription apiDescription, ApiResponseType apiResponseType) + var responses = new OpenApiResponses(); + foreach (var responseType in supportedResponseTypes) { - // If there's no associated model type, return an empty list (i.e. no content) - if (apiResponseType.ModelMetadata == null && (apiResponseType.Type == null || apiResponseType.Type == typeof(void))) - { - return []; - } + var statusCode = responseType.IsDefaultResponse() ? "default" : responseType.StatusCode.ToString(); + responses.Add(statusCode, GenerateResponse(apiDescription, schemaRepository, statusCode, responseType)); + } + return responses; + } - // If there's content types explicitly specified via ProducesAttribute, use them - var explicitContentTypes = apiDescription.CustomAttributes().OfType() - .SelectMany(attr => attr.ContentTypes) - .Distinct(); - if (explicitContentTypes.Any()) return explicitContentTypes; + private OpenApiResponse GenerateResponse( + ApiDescription apiDescription, + SchemaRepository schemaRepository, + string statusCode, + ApiResponseType apiResponseType) + { + var description = ResponseDescriptionMap + .FirstOrDefault((entry) => Regex.IsMatch(statusCode, entry.Key)) + .Value; - // If there's content types surfaced by ApiExplorer, use them - var apiExplorerContentTypes = apiResponseType.ApiResponseFormats - .Select(responseFormat => responseFormat.MediaType) - .Distinct(); - if (apiExplorerContentTypes.Any()) return apiExplorerContentTypes; + var responseContentTypes = InferResponseContentTypes(apiDescription, apiResponseType); - return []; - } + return new OpenApiResponse + { + Description = description, + Content = responseContentTypes.ToDictionary( + contentType => contentType, + contentType => CreateResponseMediaType(apiResponseType.ModelMetadata?.ModelType ?? apiResponseType.Type, schemaRepository) + ) + }; + } - private OpenApiMediaType CreateResponseMediaType(Type modelType, SchemaRepository schemaRespository) + private static IEnumerable InferResponseContentTypes(ApiDescription apiDescription, ApiResponseType apiResponseType) + { + // If there's no associated model type, return an empty list (i.e. no content) + if (apiResponseType.ModelMetadata == null && (apiResponseType.Type == null || apiResponseType.Type == typeof(void))) { - return new OpenApiMediaType - { - Schema = GenerateSchema(modelType, schemaRespository) - }; + return []; } - private static bool IsFromFormAttributeUsedWithIFormFile(ApiParameterDescription apiParameter) - { - var parameterInfo = apiParameter.ParameterInfo(); - var fromFormAttribute = parameterInfo?.GetCustomAttribute(); + // If there's content types explicitly specified via ProducesAttribute, use them + var explicitContentTypes = apiDescription.CustomAttributes().OfType() + .SelectMany(attr => attr.ContentTypes) + .Distinct(); + if (explicitContentTypes.Any()) return explicitContentTypes; - return fromFormAttribute != null && parameterInfo?.ParameterType == typeof(IFormFile); - } + // If there's content types surfaced by ApiExplorer, use them + var apiExplorerContentTypes = apiResponseType.ApiResponseFormats + .Select(responseFormat => responseFormat.MediaType) + .Distinct(); + if (apiExplorerContentTypes.Any()) return apiExplorerContentTypes; - private static readonly Dictionary OperationTypeMap = new() - { - ["GET"] = OperationType.Get, - ["PUT"] = OperationType.Put, - ["POST"] = OperationType.Post, - ["DELETE"] = OperationType.Delete, - ["OPTIONS"] = OperationType.Options, - ["HEAD"] = OperationType.Head, - ["PATCH"] = OperationType.Patch, - ["TRACE"] = OperationType.Trace, - }; + return []; + } - private static readonly Dictionary ParameterLocationMap = new() + private OpenApiMediaType CreateResponseMediaType(Type modelType, SchemaRepository schemaRespository) + { + return new OpenApiMediaType { - [BindingSource.Query] = ParameterLocation.Query, - [BindingSource.Header] = ParameterLocation.Header, - [BindingSource.Path] = ParameterLocation.Path, + Schema = GenerateSchema(modelType, schemaRespository) }; + } - private static readonly IReadOnlyCollection> ResponseDescriptionMap = - [ - // Informational responses - new("100", "Continue"), - new("101", "Switching Protocols"), - new("102", "Processing"), - new("103", "Early Hints"), - new("1\\d{2}", "Information"), - - // Successful responses - new("200", "OK"), - new("201", "Created"), - new("202", "Accepted"), - new("203", "Non-Authoritative Information"), - new("204", "No Content"), - new("205", "Reset Content"), - new("206", "Partial Content"), - new("207", "Multi-Status"), - new("208", "Already Reported"), - new("226", "IM Used"), - new("2\\d{2}", "Success"), - - // Redirection messages - new("300", "Multiple Choices"), - new("301", "Moved Permanently"), - new("302", "Found"), - new("303", "See Other"), - new("304", "Not Modified"), - new("305", "Use Proxy"), - new("307", "Temporary Redirect"), - new("308", "Permanent Redirect"), - new("3\\d{2}", "Redirect"), - - // Client error responses - new("400", "Bad Request"), - new("401", "Unauthorized"), - new("402", "Payment Required"), - new("403", "Forbidden"), - new("404", "Not Found"), - new("405", "Method Not Allowed"), - new("406", "Not Acceptable"), - new("407", "Proxy Authentication Required"), - new("408", "Request Timeout"), - new("409", "Conflict"), - new("410", "Gone"), - new("411", "Length Required"), - new("412", "Precondition Failed"), - new("413", "Content Too Large"), - new("414", "URI Too Long"), - new("415", "Unsupported Media Type"), - new("416", "Range Not Satisfiable"), - new("417", "Expectation Failed"), - new("418", "I'm a teapot"), - new("421", "Misdirected Request"), - new("422", "Unprocessable Content"), - new("423", "Locked"), - new("424", "Failed Dependency"), - new("425", "Too Early"), - new("426", "Upgrade Required"), - new("428", "Precondition Required"), - new("429", "Too Many Requests"), - new("431", "Request Header Fields Too Large"), - new("451", "Unavailable For Legal Reasons"), - new("4\\d{2}", "Client Error"), - - // Server error responses - new("500", "Internal Server Error"), - new("501", "Not Implemented"), - new("502", "Bad Gateway"), - new("503", "Service Unavailable"), - new("504", "Gateway Timeout"), - new("505", "HTTP Version Not Supported"), - new("506", "Variant Also Negotiates"), - new("507", "Insufficient Storage"), - new("508", "Loop Detected"), - new("510", "Not Extended"), - new("511", "Network Authentication Required"), - new("5\\d{2}", "Server Error"), - - new("default", "Error") - ]; - -#if NET7_0_OR_GREATER - private static string GenerateSummary(ApiDescription apiDescription) => - apiDescription.ActionDescriptor?.EndpointMetadata - ?.OfType() - .Select(s => s.Summary) - .LastOrDefault(); - - private static string GenerateDescription(ApiDescription apiDescription) => - apiDescription.ActionDescriptor?.EndpointMetadata - ?.OfType() - .Select(s => s.Description) - .LastOrDefault(); -#endif + private static bool IsFromFormAttributeUsedWithIFormFile(ApiParameterDescription apiParameter) + { + var parameterInfo = apiParameter.ParameterInfo(); + var fromFormAttribute = parameterInfo?.GetCustomAttribute(); - private static OpenApiTag CreateTag(string name, OpenApiDocument _) => - new() { Name = name }; + return fromFormAttribute != null && parameterInfo?.ParameterType == typeof(IFormFile); } + + private static readonly Dictionary OperationTypeMap = new() + { + ["GET"] = OperationType.Get, + ["PUT"] = OperationType.Put, + ["POST"] = OperationType.Post, + ["DELETE"] = OperationType.Delete, + ["OPTIONS"] = OperationType.Options, + ["HEAD"] = OperationType.Head, + ["PATCH"] = OperationType.Patch, + ["TRACE"] = OperationType.Trace, + }; + + private static readonly Dictionary ParameterLocationMap = new() + { + [BindingSource.Query] = ParameterLocation.Query, + [BindingSource.Header] = ParameterLocation.Header, + [BindingSource.Path] = ParameterLocation.Path, + }; + + private static readonly IReadOnlyCollection> ResponseDescriptionMap = + [ + // Informational responses + new("100", "Continue"), + new("101", "Switching Protocols"), + new("102", "Processing"), + new("103", "Early Hints"), + new("1\\d{2}", "Information"), + + // Successful responses + new("200", "OK"), + new("201", "Created"), + new("202", "Accepted"), + new("203", "Non-Authoritative Information"), + new("204", "No Content"), + new("205", "Reset Content"), + new("206", "Partial Content"), + new("207", "Multi-Status"), + new("208", "Already Reported"), + new("226", "IM Used"), + new("2\\d{2}", "Success"), + + // Redirection messages + new("300", "Multiple Choices"), + new("301", "Moved Permanently"), + new("302", "Found"), + new("303", "See Other"), + new("304", "Not Modified"), + new("305", "Use Proxy"), + new("307", "Temporary Redirect"), + new("308", "Permanent Redirect"), + new("3\\d{2}", "Redirect"), + + // Client error responses + new("400", "Bad Request"), + new("401", "Unauthorized"), + new("402", "Payment Required"), + new("403", "Forbidden"), + new("404", "Not Found"), + new("405", "Method Not Allowed"), + new("406", "Not Acceptable"), + new("407", "Proxy Authentication Required"), + new("408", "Request Timeout"), + new("409", "Conflict"), + new("410", "Gone"), + new("411", "Length Required"), + new("412", "Precondition Failed"), + new("413", "Content Too Large"), + new("414", "URI Too Long"), + new("415", "Unsupported Media Type"), + new("416", "Range Not Satisfiable"), + new("417", "Expectation Failed"), + new("418", "I'm a teapot"), + new("421", "Misdirected Request"), + new("422", "Unprocessable Content"), + new("423", "Locked"), + new("424", "Failed Dependency"), + new("425", "Too Early"), + new("426", "Upgrade Required"), + new("428", "Precondition Required"), + new("429", "Too Many Requests"), + new("431", "Request Header Fields Too Large"), + new("451", "Unavailable For Legal Reasons"), + new("4\\d{2}", "Client Error"), + + // Server error responses + new("500", "Internal Server Error"), + new("501", "Not Implemented"), + new("502", "Bad Gateway"), + new("503", "Service Unavailable"), + new("504", "Gateway Timeout"), + new("505", "HTTP Version Not Supported"), + new("506", "Variant Also Negotiates"), + new("507", "Insufficient Storage"), + new("508", "Loop Detected"), + new("510", "Not Extended"), + new("511", "Network Authentication Required"), + new("5\\d{2}", "Server Error"), + + new("default", "Error") + ]; + +#if NET + private static string GenerateSummary(ApiDescription apiDescription) => + apiDescription.ActionDescriptor?.EndpointMetadata + ?.OfType() + .Select(s => s.Summary) + .LastOrDefault(); + + private static string GenerateDescription(ApiDescription apiDescription) => + apiDescription.ActionDescriptor?.EndpointMetadata + ?.OfType() + .Select(s => s.Description) + .LastOrDefault(); +#endif + + private static OpenApiTag CreateTag(string name, OpenApiDocument _) => + new() { Name = name }; } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorException.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorException.cs index 38b39d72ab..ff7ec4b4ae 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorException.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorException.cs @@ -1,11 +1,12 @@ -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class SwaggerGeneratorException : Exception { - public class SwaggerGeneratorException : Exception + public SwaggerGeneratorException(string message) : base(message) { - public SwaggerGeneratorException(string message) : base(message) - { } + } - public SwaggerGeneratorException(string message, Exception innerException) : base(message, innerException) - { } + public SwaggerGeneratorException(string message, Exception innerException) : base(message, innerException) + { } -} \ No newline at end of file +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs index 85d730fb78..b6781edf59 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs @@ -1,131 +1,130 @@ -#if NET6_0_OR_GREATER +#if NET using Microsoft.AspNetCore.Http.Metadata; #endif +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi.Models; using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Authentication; +using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class SwaggerGeneratorOptions { - public class SwaggerGeneratorOptions + public SwaggerGeneratorOptions() { - public SwaggerGeneratorOptions() - { - SwaggerDocs = new Dictionary(); - DocInclusionPredicate = DefaultDocInclusionPredicate; - OperationIdSelector = DefaultOperationIdSelector; - TagsSelector = DefaultTagsSelector; - SortKeySelector = DefaultSortKeySelector; - PathGroupSelector = DefaultPathGroupSelector; - SecuritySchemesSelector = null; - SchemaComparer = StringComparer.Ordinal; - Servers = new List(); - SecuritySchemes = new Dictionary(); - SecurityRequirements = new List(); - ParameterFilters = new List(); - ParameterAsyncFilters = new List(); - RequestBodyFilters = new List(); - RequestBodyAsyncFilters = new List(); - OperationFilters = new List(); - OperationAsyncFilters = new List(); - DocumentFilters = new List(); - DocumentAsyncFilters = new List(); - } + SwaggerDocs = new Dictionary(); + DocInclusionPredicate = DefaultDocInclusionPredicate; + OperationIdSelector = DefaultOperationIdSelector; + TagsSelector = DefaultTagsSelector; + SortKeySelector = DefaultSortKeySelector; + PathGroupSelector = DefaultPathGroupSelector; + SecuritySchemesSelector = null; + SchemaComparer = StringComparer.Ordinal; + Servers = new List(); + SecuritySchemes = new Dictionary(); + SecurityRequirements = new List(); + ParameterFilters = new List(); + ParameterAsyncFilters = new List(); + RequestBodyFilters = new List(); + RequestBodyAsyncFilters = new List(); + OperationFilters = new List(); + OperationAsyncFilters = new List(); + DocumentFilters = new List(); + DocumentAsyncFilters = new List(); + } - public IDictionary SwaggerDocs { get; set; } + public IDictionary SwaggerDocs { get; set; } - public Func DocInclusionPredicate { get; set; } + public Func DocInclusionPredicate { get; set; } - public bool IgnoreObsoleteActions { get; set; } + public bool IgnoreObsoleteActions { get; set; } - public Func, ApiDescription> ConflictingActionsResolver { get; set; } + public Func, ApiDescription> ConflictingActionsResolver { get; set; } - public Func OperationIdSelector { get; set; } + public Func OperationIdSelector { get; set; } - public Func> TagsSelector { get; set; } + public Func> TagsSelector { get; set; } - public Func SortKeySelector { get; set; } + public Func SortKeySelector { get; set; } - public Func PathGroupSelector { get; set; } + public Func PathGroupSelector { get; set; } - public bool InferSecuritySchemes { get; set; } + public bool InferSecuritySchemes { get; set; } - public Func, IDictionary> SecuritySchemesSelector { get; set; } + public Func, IDictionary> SecuritySchemesSelector { get; set; } - public bool DescribeAllParametersInCamelCase { get; set; } + public bool DescribeAllParametersInCamelCase { get; set; } - public List Servers { get; set; } + public List Servers { get; set; } - public IDictionary SecuritySchemes { get; set; } + public IDictionary SecuritySchemes { get; set; } - public IList SecurityRequirements { get; set; } + public IList SecurityRequirements { get; set; } - public IComparer SchemaComparer { get; set; } + public IComparer SchemaComparer { get; set; } - public IList ParameterFilters { get; set; } + public IList ParameterFilters { get; set; } - public IList ParameterAsyncFilters { get; set; } + public IList ParameterAsyncFilters { get; set; } - public List RequestBodyFilters { get; set; } + public List RequestBodyFilters { get; set; } - public IList RequestBodyAsyncFilters { get; set; } + public IList RequestBodyAsyncFilters { get; set; } - public List OperationFilters { get; set; } + public List OperationFilters { get; set; } - public IList OperationAsyncFilters { get; set; } + public IList OperationAsyncFilters { get; set; } - public IList DocumentFilters { get; set; } + public IList DocumentFilters { get; set; } - public IList DocumentAsyncFilters { get; set; } + public IList DocumentAsyncFilters { get; set; } - public string XmlCommentEndOfLine { get; set; } + public string XmlCommentEndOfLine { get; set; } - private bool DefaultDocInclusionPredicate(string documentName, ApiDescription apiDescription) - { - return apiDescription.GroupName == null || apiDescription.GroupName == documentName; - } + private bool DefaultDocInclusionPredicate(string documentName, ApiDescription apiDescription) + { + return apiDescription.GroupName == null || apiDescription.GroupName == documentName; + } - private string DefaultOperationIdSelector(ApiDescription apiDescription) - { - var actionDescriptor = apiDescription.ActionDescriptor; - - // Resolve the operation ID from the route name and fallback to the - // endpoint name if no route name is available. This allows us to - // generate operation IDs for endpoints that are defined using - // minimal APIs. -#if (!NETSTANDARD2_0) - return - actionDescriptor.AttributeRouteInfo?.Name - ?? (actionDescriptor.EndpointMetadata?.LastOrDefault(m => m is IEndpointNameMetadata) as IEndpointNameMetadata)?.EndpointName; + private string DefaultOperationIdSelector(ApiDescription apiDescription) + { + var actionDescriptor = apiDescription.ActionDescriptor; + + // Resolve the operation ID from the route name and fallback to the + // endpoint name if no route name is available. This allows us to + // generate operation IDs for endpoints that are defined using + // minimal APIs. +#if NET + return + actionDescriptor.AttributeRouteInfo?.Name + ?? (actionDescriptor.EndpointMetadata?.LastOrDefault(m => m is IEndpointNameMetadata) as IEndpointNameMetadata)?.EndpointName; #else - return actionDescriptor.AttributeRouteInfo?.Name; + return actionDescriptor.AttributeRouteInfo?.Name; #endif - } + } - private IList DefaultTagsSelector(ApiDescription apiDescription) - { -#if (!NET6_0_OR_GREATER) - return new[] { apiDescription.ActionDescriptor.RouteValues["controller"] }; + private IList DefaultTagsSelector(ApiDescription apiDescription) + { +#if !NET + return new[] { apiDescription.ActionDescriptor.RouteValues["controller"] }; #else - var actionDescriptor = apiDescription.ActionDescriptor; - var tagsMetadata = actionDescriptor.EndpointMetadata?.LastOrDefault(m => m is ITagsMetadata) as ITagsMetadata; - if (tagsMetadata != null) - { - return new List(tagsMetadata.Tags); - } - return new[] { apiDescription.ActionDescriptor.RouteValues["controller"] }; -#endif - } - - private string DefaultSortKeySelector(ApiDescription apiDescription) + var actionDescriptor = apiDescription.ActionDescriptor; + var tagsMetadata = actionDescriptor.EndpointMetadata?.LastOrDefault(m => m is ITagsMetadata) as ITagsMetadata; + if (tagsMetadata != null) { - return TagsSelector(apiDescription).First(); + return new List(tagsMetadata.Tags); } + return new[] { apiDescription.ActionDescriptor.RouteValues["controller"] }; +#endif + } - private static string DefaultPathGroupSelector(ApiDescription apiDescription) - { - return apiDescription.RelativePathSansParameterConstraints(); - } + private string DefaultSortKeySelector(ApiDescription apiDescription) + { + return TagsSelector(apiDescription).First(); + } + + private static string DefaultPathGroupSelector(ApiDescription apiDescription) + { + return apiDescription.RelativePathSansParameterConstraints(); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/MethodInfoExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/MethodInfoExtensions.cs index c08de1ef54..ebc2a86e37 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/MethodInfoExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/MethodInfoExtensions.cs @@ -1,81 +1,80 @@ using System.Reflection; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public static class MethodInfoExtensions { - public static class MethodInfoExtensions + public static MethodInfo GetUnderlyingGenericTypeMethod(this MethodInfo constructedTypeMethod) { - public static MethodInfo GetUnderlyingGenericTypeMethod(this MethodInfo constructedTypeMethod) - { - var constructedType = constructedTypeMethod.DeclaringType; - var genericTypeDefinition = constructedType.GetGenericTypeDefinition(); - var genericArguments = constructedType.GenericTypeArguments; + var constructedType = constructedTypeMethod.DeclaringType; + var genericTypeDefinition = constructedType.GetGenericTypeDefinition(); + var genericArguments = constructedType.GenericTypeArguments; - var constructedTypeParameters = constructedTypeMethod.GetParameters(); + var constructedTypeParameters = constructedTypeMethod.GetParameters(); - // Retrieve list of candidate methods that match name and parameter count - var candidateMethods = genericTypeDefinition.GetMethods() - .Where(m => + // Retrieve list of candidate methods that match name and parameter count + var candidateMethods = genericTypeDefinition.GetMethods() + .Where(m => + { + var genericTypeDefinitionParameters = m.GetParameters(); + if (m.Name == constructedTypeMethod.Name && genericTypeDefinitionParameters.Length == constructedTypeParameters.Length) { - var genericTypeDefinitionParameters = m.GetParameters(); - if (m.Name == constructedTypeMethod.Name && genericTypeDefinitionParameters.Length == constructedTypeParameters.Length) + for (var i = 0; i < genericTypeDefinitionParameters.Length; i++) { - for (var i = 0; i < genericTypeDefinitionParameters.Length; i++) + if (genericTypeDefinitionParameters[i].ParameterType.IsArray && constructedTypeParameters[i].ParameterType.IsArray) { - if (genericTypeDefinitionParameters[i].ParameterType.IsArray && constructedTypeParameters[i].ParameterType.IsArray) + var genericTypeDefinitionElement = genericTypeDefinitionParameters[i].ParameterType.GetElementType(); + var constructedTypeDefinitionElement = constructedTypeParameters[i].ParameterType.GetElementType(); + if (genericTypeDefinitionElement.IsGenericParameter && genericArguments.Any(p => p == constructedTypeDefinitionElement)) { - var genericTypeDefinitionElement = genericTypeDefinitionParameters[i].ParameterType.GetElementType(); - var constructedTypeDefinitionElement = constructedTypeParameters[i].ParameterType.GetElementType(); - if (genericTypeDefinitionElement.IsGenericParameter && genericArguments.Any(p => p == constructedTypeDefinitionElement)) - { - continue; - } - else if (genericTypeDefinitionElement != constructedTypeDefinitionElement) - { - return false; - } + continue; } - else if (genericTypeDefinitionParameters[i].ParameterType.IsConstructedGenericType && constructedTypeParameters[i].ParameterType.IsConstructedGenericType) + else if (genericTypeDefinitionElement != constructedTypeDefinitionElement) { - if (genericTypeDefinitionParameters[i].ParameterType.GetGenericTypeDefinition() != constructedTypeParameters[i].ParameterType.GetGenericTypeDefinition()) - { - return false; - } - var genericTypeDefinitionArguments = genericTypeDefinitionParameters[i].ParameterType.GetGenericArguments(); - var constructedDefinitionArguments = constructedTypeParameters[i].ParameterType.GetGenericArguments(); - if (genericTypeDefinitionArguments.Length != constructedDefinitionArguments.Length) - { - return false; - } - for (var j = 0; j < genericTypeDefinitionArguments.Length; j++) - { - if (genericTypeDefinitionArguments[j].IsGenericParameter && genericArguments.Any(p => p == constructedDefinitionArguments[j])) - { - continue; - } - else if (genericTypeDefinitionArguments[j] != constructedDefinitionArguments[j]) - { - return false; - } - } - continue; + return false; } - else if (genericTypeDefinitionParameters[i].ParameterType.IsGenericParameter && genericArguments.Any(p => p == constructedTypeParameters[i].ParameterType)) + } + else if (genericTypeDefinitionParameters[i].ParameterType.IsConstructedGenericType && constructedTypeParameters[i].ParameterType.IsConstructedGenericType) + { + if (genericTypeDefinitionParameters[i].ParameterType.GetGenericTypeDefinition() != constructedTypeParameters[i].ParameterType.GetGenericTypeDefinition()) { - continue; + return false; } - else if (genericTypeDefinitionParameters[i].ParameterType != constructedTypeParameters[i].ParameterType) + var genericTypeDefinitionArguments = genericTypeDefinitionParameters[i].ParameterType.GetGenericArguments(); + var constructedDefinitionArguments = constructedTypeParameters[i].ParameterType.GetGenericArguments(); + if (genericTypeDefinitionArguments.Length != constructedDefinitionArguments.Length) { return false; } + for (var j = 0; j < genericTypeDefinitionArguments.Length; j++) + { + if (genericTypeDefinitionArguments[j].IsGenericParameter && genericArguments.Any(p => p == constructedDefinitionArguments[j])) + { + continue; + } + else if (genericTypeDefinitionArguments[j] != constructedDefinitionArguments[j]) + { + return false; + } + } + continue; + } + else if (genericTypeDefinitionParameters[i].ParameterType.IsGenericParameter && genericArguments.Any(p => p == constructedTypeParameters[i].ParameterType)) + { + continue; + } + else if (genericTypeDefinitionParameters[i].ParameterType != constructedTypeParameters[i].ParameterType) + { + return false; } - return true; } - return false; - }); + return true; + } + return false; + }); - // If inconclusive, just return null - return (candidateMethods.Count() == 1) ? candidateMethods.First() : null; - } + // If inconclusive, just return null + return (candidateMethods.Count() == 1) ? candidateMethods.First() : null; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs index 959daf156b..04a5fd168f 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsDocumentFilter.cs @@ -2,59 +2,58 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class XmlCommentsDocumentFilter : IDocumentFilter { - public class XmlCommentsDocumentFilter : IDocumentFilter - { - private const string SummaryTag = "summary"; + private const string SummaryTag = "summary"; - private readonly IReadOnlyDictionary _xmlDocMembers; - private readonly SwaggerGeneratorOptions _options; + private readonly IReadOnlyDictionary _xmlDocMembers; + private readonly SwaggerGeneratorOptions _options; - public XmlCommentsDocumentFilter(XPathDocument xmlDoc) - : this(xmlDoc, null) - { - } + public XmlCommentsDocumentFilter(XPathDocument xmlDoc) + : this(xmlDoc, null) + { + } - public XmlCommentsDocumentFilter(XPathDocument xmlDoc, SwaggerGeneratorOptions options) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), options) - { - } + public XmlCommentsDocumentFilter(XPathDocument xmlDoc, SwaggerGeneratorOptions options) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), options) + { + } - internal XmlCommentsDocumentFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) - { - _xmlDocMembers = xmlDocMembers; - _options = options; - } + internal XmlCommentsDocumentFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) + { + _xmlDocMembers = xmlDocMembers; + _options = options; + } - public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + // Collect (unique) controller names and types in a dictionary + var controllerNamesAndTypes = context.ApiDescriptions + .Select(apiDesc => new { ApiDesc = apiDesc, ActionDesc = apiDesc.ActionDescriptor as ControllerActionDescriptor }) + .Where(x => x.ActionDesc != null) + .GroupBy(x => _options?.TagsSelector(x.ApiDesc).FirstOrDefault() ?? x.ActionDesc.ControllerName) + .Select(group => new KeyValuePair(group.Key, group.First().ActionDesc.ControllerTypeInfo.AsType())); + + foreach (var nameAndType in controllerNamesAndTypes) { - // Collect (unique) controller names and types in a dictionary - var controllerNamesAndTypes = context.ApiDescriptions - .Select(apiDesc => new { ApiDesc = apiDesc, ActionDesc = apiDesc.ActionDescriptor as ControllerActionDescriptor }) - .Where(x => x.ActionDesc != null) - .GroupBy(x => _options?.TagsSelector(x.ApiDesc).FirstOrDefault() ?? x.ActionDesc.ControllerName) - .Select(group => new KeyValuePair(group.Key, group.First().ActionDesc.ControllerTypeInfo.AsType())); - - foreach (var nameAndType in controllerNamesAndTypes) + var memberName = XmlCommentsNodeNameHelper.GetMemberNameForType(nameAndType.Value); + + if (!_xmlDocMembers.TryGetValue(memberName, out var typeNode)) { - var memberName = XmlCommentsNodeNameHelper.GetMemberNameForType(nameAndType.Value); + continue; + } - if (!_xmlDocMembers.TryGetValue(memberName, out var typeNode)) - { - continue; - } + var summaryNode = typeNode.SelectFirstChild(SummaryTag); + if (summaryNode != null) + { + swaggerDoc.Tags ??= new List(); - var summaryNode = typeNode.SelectFirstChild(SummaryTag); - if (summaryNode != null) + swaggerDoc.Tags.Add(new OpenApiTag { - swaggerDoc.Tags ??= new List(); - - swaggerDoc.Tags.Add(new OpenApiTag - { - Name = nameAndType.Key, - Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine) - }); - } + Name = nameAndType.Key, + Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine) + }); } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsExampleHelper.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsExampleHelper.cs index c92d60c239..759988f1b0 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsExampleHelper.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsExampleHelper.cs @@ -1,24 +1,23 @@ using System.Text.Json; using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +internal static class XmlCommentsExampleHelper { - internal static class XmlCommentsExampleHelper + public static Microsoft.OpenApi.Any.IOpenApiAny Create( + SchemaRepository schemaRepository, + OpenApiSchema schema, + string exampleString) { - public static Microsoft.OpenApi.Any.IOpenApiAny Create( - SchemaRepository schemaRepository, - OpenApiSchema schema, - string exampleString) - { - var isStringType = - schema?.ResolveType(schemaRepository) == JsonSchemaTypes.String && - !string.Equals(exampleString, "null"); + var isStringType = + schema?.ResolveType(schemaRepository) == JsonSchemaTypes.String && + !string.Equals(exampleString, "null"); - var exampleAsJson = isStringType - ? JsonSerializer.Serialize(exampleString) - : exampleString; + var exampleAsJson = isStringType + ? JsonSerializer.Serialize(exampleString) + : exampleString; - return JsonModelFactory.CreateFromJson(exampleAsJson); - } + return JsonModelFactory.CreateFromJson(exampleAsJson); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs index 6d67d1f284..7d5886e747 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsNodeNameHelper.cs @@ -1,100 +1,99 @@ using System.Reflection; using System.Text; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class XmlCommentsNodeNameHelper { - public class XmlCommentsNodeNameHelper + public static string GetMemberNameForMethod(MethodInfo method) { - public static string GetMemberNameForMethod(MethodInfo method) - { - var builder = new StringBuilder("M:"); + var builder = new StringBuilder("M:"); - builder.Append(QualifiedNameFor(method.DeclaringType)); - builder.Append($".{method.Name}"); + builder.Append(QualifiedNameFor(method.DeclaringType)); + builder.Append($".{method.Name}"); - var parameters = method.GetParameters(); - if (parameters.Any()) + var parameters = method.GetParameters(); + if (parameters.Any()) + { + var parametersNames = parameters.Select(p => { - var parametersNames = parameters.Select(p => - { - return p.ParameterType.IsGenericParameter - ? $"`{p.ParameterType.GenericParameterPosition}" - : QualifiedNameFor(p.ParameterType, expandGenericArgs: true); - }); - builder.Append($"({string.Join(",", parametersNames)})"); - } - - return builder.ToString(); + return p.ParameterType.IsGenericParameter + ? $"`{p.ParameterType.GenericParameterPosition}" + : QualifiedNameFor(p.ParameterType, expandGenericArgs: true); + }); + builder.Append($"({string.Join(",", parametersNames)})"); } - public static string GetMemberNameForType(Type type) - { - var builder = new StringBuilder("T:"); - builder.Append(QualifiedNameFor(type)); + return builder.ToString(); + } - return builder.ToString(); - } + public static string GetMemberNameForType(Type type) + { + var builder = new StringBuilder("T:"); + builder.Append(QualifiedNameFor(type)); - public static string GetMemberNameForFieldOrProperty(MemberInfo fieldOrPropertyInfo) - { - var builder = new StringBuilder(((fieldOrPropertyInfo.MemberType & MemberTypes.Field) != 0) ? "F:" : "P:"); - builder.Append(QualifiedNameFor(fieldOrPropertyInfo.DeclaringType)); - builder.Append($".{fieldOrPropertyInfo.Name}"); + return builder.ToString(); + } - return builder.ToString(); - } + public static string GetMemberNameForFieldOrProperty(MemberInfo fieldOrPropertyInfo) + { + var builder = new StringBuilder(((fieldOrPropertyInfo.MemberType & MemberTypes.Field) != 0) ? "F:" : "P:"); + builder.Append(QualifiedNameFor(fieldOrPropertyInfo.DeclaringType)); + builder.Append($".{fieldOrPropertyInfo.Name}"); - private static string QualifiedNameFor(Type type, bool expandGenericArgs = false) + return builder.ToString(); + } + + private static string QualifiedNameFor(Type type, bool expandGenericArgs = false) + { + if (type.IsArray) { - if (type.IsArray) - { - var elementType = type.GetElementType(); - return elementType.IsGenericParameter ? $"`{elementType.GenericParameterPosition}[]" : $"{QualifiedNameFor(type.GetElementType(), expandGenericArgs)}[]"; - } + var elementType = type.GetElementType(); + return elementType.IsGenericParameter ? $"`{elementType.GenericParameterPosition}[]" : $"{QualifiedNameFor(type.GetElementType(), expandGenericArgs)}[]"; + } - var builder = new StringBuilder(); + var builder = new StringBuilder(); - if (!string.IsNullOrEmpty(type.Namespace)) - builder.Append($"{type.Namespace}."); + if (!string.IsNullOrEmpty(type.Namespace)) + builder.Append($"{type.Namespace}."); - if (type.IsNested) - { - builder.Append($"{string.Join(".", GetNestedTypeNames(type))}."); - } + if (type.IsNested) + { + builder.Append($"{string.Join(".", GetNestedTypeNames(type))}."); + } - if (type.IsConstructedGenericType && expandGenericArgs) - { - var nameSansGenericArgs = type.Name.Split('`').First(); - builder.Append(nameSansGenericArgs); - - var genericArgsNames = type.GetGenericArguments().Select(t => - { - return t.IsGenericParameter - ? $"`{t.GenericParameterPosition}" - : QualifiedNameFor(t, true); - }); - - builder.Append($"{{{string.Join(",", genericArgsNames)}}}"); - } - else + if (type.IsConstructedGenericType && expandGenericArgs) + { + var nameSansGenericArgs = type.Name.Split('`').First(); + builder.Append(nameSansGenericArgs); + + var genericArgsNames = type.GetGenericArguments().Select(t => { - builder.Append(type.Name); - } + return t.IsGenericParameter + ? $"`{t.GenericParameterPosition}" + : QualifiedNameFor(t, true); + }); - return builder.ToString(); + builder.Append($"{{{string.Join(",", genericArgsNames)}}}"); } - - private static IEnumerable GetNestedTypeNames(Type type) + else { - if (!type.IsNested || type.DeclaringType == null) yield break; + builder.Append(type.Name); + } - foreach (var nestedTypeName in GetNestedTypeNames(type.DeclaringType)) - { - yield return nestedTypeName; - } + return builder.ToString(); + } - yield return type.DeclaringType.Name; + private static IEnumerable GetNestedTypeNames(Type type) + { + if (!type.IsNested || type.DeclaringType == null) yield break; + + foreach (var nestedTypeName in GetNestedTypeNames(type.DeclaringType)) + { + yield return nestedTypeName; } + + yield return type.DeclaringType.Name; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs index f0cf2aacad..2e3a160026 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsOperationFilter.cs @@ -3,80 +3,79 @@ using System.Reflection; using System.Xml.XPath; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class XmlCommentsOperationFilter : IOperationFilter { - public class XmlCommentsOperationFilter : IOperationFilter - { - private readonly IReadOnlyDictionary _xmlDocMembers; - private readonly SwaggerGeneratorOptions _options; + private readonly IReadOnlyDictionary _xmlDocMembers; + private readonly SwaggerGeneratorOptions _options; - public XmlCommentsOperationFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null) - { - } + public XmlCommentsOperationFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null) + { + } - [ActivatorUtilitiesConstructor] - internal XmlCommentsOperationFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) - { - _xmlDocMembers = xmlDocMembers; - _options = options; - } + [ActivatorUtilitiesConstructor] + internal XmlCommentsOperationFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) + { + _xmlDocMembers = xmlDocMembers; + _options = options; + } - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - if (context.MethodInfo == null) return; + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (context.MethodInfo == null) return; - // If method is from a constructed generic type, look for comments from the generic type method - var targetMethod = context.MethodInfo.DeclaringType.IsConstructedGenericType - ? context.MethodInfo.GetUnderlyingGenericTypeMethod() - : context.MethodInfo; + // If method is from a constructed generic type, look for comments from the generic type method + var targetMethod = context.MethodInfo.DeclaringType.IsConstructedGenericType + ? context.MethodInfo.GetUnderlyingGenericTypeMethod() + : context.MethodInfo; - if (targetMethod == null) return; + if (targetMethod == null) return; - ApplyControllerTags(operation, targetMethod.DeclaringType); - ApplyMethodTags(operation, targetMethod); - } + ApplyControllerTags(operation, targetMethod.DeclaringType); + ApplyMethodTags(operation, targetMethod); + } - private void ApplyControllerTags(OpenApiOperation operation, Type controllerType) - { - var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(controllerType); + private void ApplyControllerTags(OpenApiOperation operation, Type controllerType) + { + var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(controllerType); - if (!_xmlDocMembers.TryGetValue(typeMemberName, out var methodNode)) return; + if (!_xmlDocMembers.TryGetValue(typeMemberName, out var methodNode)) return; - var responseNodes = methodNode.SelectChildren("response"); - ApplyResponseTags(operation, responseNodes); - } + var responseNodes = methodNode.SelectChildren("response"); + ApplyResponseTags(operation, responseNodes); + } - private void ApplyMethodTags(OpenApiOperation operation, MethodInfo methodInfo) - { - var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(methodInfo); + private void ApplyMethodTags(OpenApiOperation operation, MethodInfo methodInfo) + { + var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(methodInfo); - if (!_xmlDocMembers.TryGetValue(methodMemberName, out var methodNode)) return; + if (!_xmlDocMembers.TryGetValue(methodMemberName, out var methodNode)) return; - var summaryNode = methodNode.SelectFirstChild("summary"); - if (summaryNode != null) - operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine); + var summaryNode = methodNode.SelectFirstChild("summary"); + if (summaryNode != null) + operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine); - var remarksNode = methodNode.SelectFirstChild("remarks"); - if (remarksNode != null) - operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml, _options?.XmlCommentEndOfLine); + var remarksNode = methodNode.SelectFirstChild("remarks"); + if (remarksNode != null) + operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml, _options?.XmlCommentEndOfLine); - var responseNodes = methodNode.SelectChildren("response"); - ApplyResponseTags(operation, responseNodes); - } + var responseNodes = methodNode.SelectChildren("response"); + ApplyResponseTags(operation, responseNodes); + } - private void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator responseNodes) + private void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator responseNodes) + { + while (responseNodes.MoveNext()) { - while (responseNodes.MoveNext()) + var code = responseNodes.Current.GetAttribute("code"); + if (!operation.Responses.TryGetValue(code, out var response)) { - var code = responseNodes.Current.GetAttribute("code"); - if (!operation.Responses.TryGetValue(code, out var response)) - { - response = new OpenApiResponse(); - operation.Responses[code] = response; - } - - response.Description = XmlCommentsTextHelper.Humanize(responseNodes.Current.InnerXml, _options?.XmlCommentEndOfLine); + response = new OpenApiResponse(); + operation.Responses[code] = response; } + + response.Description = XmlCommentsTextHelper.Humanize(responseNodes.Current.InnerXml, _options?.XmlCommentEndOfLine); } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs index cf93752040..1116b84cab 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsParameterFilter.cs @@ -3,81 +3,80 @@ using System.Reflection; using System.Xml.XPath; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class XmlCommentsParameterFilter : IParameterFilter { - public class XmlCommentsParameterFilter : IParameterFilter + private readonly IReadOnlyDictionary _xmlDocMembers; + private readonly SwaggerGeneratorOptions _options; + + public XmlCommentsParameterFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null) { - private readonly IReadOnlyDictionary _xmlDocMembers; - private readonly SwaggerGeneratorOptions _options; + } - public XmlCommentsParameterFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null) - { - } + [ActivatorUtilitiesConstructor] + internal XmlCommentsParameterFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) + { + _xmlDocMembers = xmlDocMembers; + _options = options; + } - [ActivatorUtilitiesConstructor] - internal XmlCommentsParameterFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) + public void Apply(OpenApiParameter parameter, ParameterFilterContext context) + { + if (context.PropertyInfo != null) { - _xmlDocMembers = xmlDocMembers; - _options = options; + ApplyPropertyTags(parameter, context); } - - public void Apply(OpenApiParameter parameter, ParameterFilterContext context) + else if (context.ParameterInfo != null) { - if (context.PropertyInfo != null) - { - ApplyPropertyTags(parameter, context); - } - else if (context.ParameterInfo != null) - { - ApplyParamTags(parameter, context); - } + ApplyParamTags(parameter, context); } + } - private void ApplyPropertyTags(OpenApiParameter parameter, ParameterFilterContext context) - { - var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.PropertyInfo); + private void ApplyPropertyTags(OpenApiParameter parameter, ParameterFilterContext context) + { + var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.PropertyInfo); - if (!_xmlDocMembers.TryGetValue(propertyMemberName, out var propertyNode)) return; + if (!_xmlDocMembers.TryGetValue(propertyMemberName, out var propertyNode)) return; - var summaryNode = propertyNode.SelectFirstChild("summary"); - if (summaryNode != null) - { - parameter.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine); - parameter.Schema.Description = null; // no need to duplicate - } + var summaryNode = propertyNode.SelectFirstChild("summary"); + if (summaryNode != null) + { + parameter.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine); + parameter.Schema.Description = null; // no need to duplicate + } - var exampleNode = propertyNode.SelectFirstChild("example"); - if (exampleNode == null) return; + var exampleNode = propertyNode.SelectFirstChild("example"); + if (exampleNode == null) return; - parameter.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, parameter.Schema, exampleNode.ToString()); - } + parameter.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, parameter.Schema, exampleNode.ToString()); + } - private void ApplyParamTags(OpenApiParameter parameter, ParameterFilterContext context) - { - if (!(context.ParameterInfo.Member is MethodInfo methodInfo)) return; + private void ApplyParamTags(OpenApiParameter parameter, ParameterFilterContext context) + { + if (!(context.ParameterInfo.Member is MethodInfo methodInfo)) return; - // If method is from a constructed generic type, look for comments from the generic type method - var targetMethod = methodInfo.DeclaringType.IsConstructedGenericType - ? methodInfo.GetUnderlyingGenericTypeMethod() - : methodInfo; + // If method is from a constructed generic type, look for comments from the generic type method + var targetMethod = methodInfo.DeclaringType.IsConstructedGenericType + ? methodInfo.GetUnderlyingGenericTypeMethod() + : methodInfo; - if (targetMethod == null) return; + if (targetMethod == null) return; - var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod); + var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod); - if (!_xmlDocMembers.TryGetValue(methodMemberName, out var propertyNode)) return; + if (!_xmlDocMembers.TryGetValue(methodMemberName, out var propertyNode)) return; - XPathNavigator paramNode = propertyNode.SelectFirstChildWithAttribute("param", "name", context.ParameterInfo.Name); + XPathNavigator paramNode = propertyNode.SelectFirstChildWithAttribute("param", "name", context.ParameterInfo.Name); - if (paramNode != null) - { - parameter.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml, _options?.XmlCommentEndOfLine); + if (paramNode != null) + { + parameter.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml, _options?.XmlCommentEndOfLine); - var example = paramNode.GetAttribute("example"); - if (string.IsNullOrEmpty(example)) return; + var example = paramNode.GetAttribute("example"); + if (string.IsNullOrEmpty(example)) return; - parameter.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, parameter.Schema, example); - } + parameter.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, parameter.Schema, example); } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs index 6ae0ce3fa3..7318972bb7 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsRequestBodyFilter.cs @@ -3,176 +3,175 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class XmlCommentsRequestBodyFilter : IRequestBodyFilter { - public class XmlCommentsRequestBodyFilter : IRequestBodyFilter + private readonly IReadOnlyDictionary _xmlDocMembers; + private readonly SwaggerGeneratorOptions _options; + + public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null) { - private readonly IReadOnlyDictionary _xmlDocMembers; - private readonly SwaggerGeneratorOptions _options; + } - public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null) - { - } + [ActivatorUtilitiesConstructor] + internal XmlCommentsRequestBodyFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) + { + _xmlDocMembers = xmlDocMembers; + _options = options; + } - [ActivatorUtilitiesConstructor] - internal XmlCommentsRequestBodyFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) - { - _xmlDocMembers = xmlDocMembers; - _options = options; - } + public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context) + { + var bodyParameterDescription = context.BodyParameterDescription; - public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context) + if (bodyParameterDescription is not null) { - var bodyParameterDescription = context.BodyParameterDescription; - - if (bodyParameterDescription is not null) + var propertyInfo = bodyParameterDescription.PropertyInfo(); + if (propertyInfo is not null) { - var propertyInfo = bodyParameterDescription.PropertyInfo(); - if (propertyInfo is not null) - { - ApplyPropertyTagsForBody(requestBody, context, propertyInfo); - } - else - { - var parameterInfo = bodyParameterDescription.ParameterInfo(); - if (parameterInfo is not null) - { - ApplyParamTagsForBody(requestBody, context, parameterInfo); - } - } + ApplyPropertyTagsForBody(requestBody, context, propertyInfo); } else { - var numberOfFromForm = context.FormParameterDescriptions?.Count() ?? 0; - if (requestBody.Content?.Count is 0 || numberOfFromForm is 0) + var parameterInfo = bodyParameterDescription.ParameterInfo(); + if (parameterInfo is not null) { - return; + ApplyParamTagsForBody(requestBody, context, parameterInfo); } + } + } + else + { + var numberOfFromForm = context.FormParameterDescriptions?.Count() ?? 0; + if (requestBody.Content?.Count is 0 || numberOfFromForm is 0) + { + return; + } - foreach (var formParameter in context.FormParameterDescriptions) + foreach (var formParameter in context.FormParameterDescriptions) + { + if (formParameter.Name is null || formParameter.PropertyInfo() is not null) { - if (formParameter.Name is null || formParameter.PropertyInfo() is not null) - { - continue; - } + continue; + } - var parameterFromForm = formParameter.ParameterInfo(); - if (parameterFromForm is null) - { - continue; - } + var parameterFromForm = formParameter.ParameterInfo(); + if (parameterFromForm is null) + { + continue; + } - foreach (var item in requestBody.Content.Values) + foreach (var item in requestBody.Content.Values) + { + if (item?.Schema?.Properties is { } properties + && (properties.TryGetValue(formParameter.Name, out var value) || properties.TryGetValue(formParameter.Name.ToCamelCase(), out value))) { - if (item?.Schema?.Properties is { } properties - && (properties.TryGetValue(formParameter.Name, out var value) || properties.TryGetValue(formParameter.Name.ToCamelCase(), out value))) + var (summary, example) = GetParamTags(parameterFromForm); + value.Description ??= summary; + if (!string.IsNullOrEmpty(example)) { - var (summary, example) = GetParamTags(parameterFromForm); - value.Description ??= summary; - if (!string.IsNullOrEmpty(example)) - { - value.Example ??= XmlCommentsExampleHelper.Create(context.SchemaRepository, value, example); - } + value.Example ??= XmlCommentsExampleHelper.Create(context.SchemaRepository, value, example); } } } } } + } + + private (string summary, string example) GetPropertyTags(PropertyInfo propertyInfo) + { + var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(propertyInfo); + if (!_xmlDocMembers.TryGetValue(propertyMemberName, out var propertyNode)) + { + return (null, null); + } - private (string summary, string example) GetPropertyTags(PropertyInfo propertyInfo) + string summary = null; + var summaryNode = propertyNode.SelectFirstChild("summary"); + if (summaryNode is not null) { - var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(propertyInfo); - if (!_xmlDocMembers.TryGetValue(propertyMemberName, out var propertyNode)) - { - return (null, null); - } + summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine); + } - string summary = null; - var summaryNode = propertyNode.SelectFirstChild("summary"); - if (summaryNode is not null) - { - summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine); - } + var exampleNode = propertyNode.SelectFirstChild("example"); - var exampleNode = propertyNode.SelectFirstChild("example"); + return (summary, exampleNode?.ToString()); + } - return (summary, exampleNode?.ToString()); - } + private void ApplyPropertyTagsForBody(OpenApiRequestBody requestBody, RequestBodyFilterContext context, PropertyInfo propertyInfo) + { + var (summary, example) = GetPropertyTags(propertyInfo); - private void ApplyPropertyTagsForBody(OpenApiRequestBody requestBody, RequestBodyFilterContext context, PropertyInfo propertyInfo) + if (summary is not null) { - var (summary, example) = GetPropertyTags(propertyInfo); + requestBody.Description = summary; + } - if (summary is not null) - { - requestBody.Description = summary; - } + if (requestBody.Content?.Count is 0) + { + return; + } - if (requestBody.Content?.Count is 0) - { - return; - } + foreach (var mediaType in requestBody.Content.Values) + { + mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example); + } + } - foreach (var mediaType in requestBody.Content.Values) - { - mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example); - } + private (string summary, string example) GetParamTags(ParameterInfo parameterInfo) + { + if (parameterInfo.Member is not MethodInfo methodInfo) + { + return (null, null); } - private (string summary, string example) GetParamTags(ParameterInfo parameterInfo) + var targetMethod = methodInfo.DeclaringType.IsConstructedGenericType + ? methodInfo.GetUnderlyingGenericTypeMethod() + : methodInfo; + + if (targetMethod is null) { - if (parameterInfo.Member is not MethodInfo methodInfo) - { - return (null, null); - } + return (null, null); + } - var targetMethod = methodInfo.DeclaringType.IsConstructedGenericType - ? methodInfo.GetUnderlyingGenericTypeMethod() - : methodInfo; + var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod); - if (targetMethod is null) - { - return (null, null); - } + if (!_xmlDocMembers.TryGetValue(methodMemberName, out var propertyNode)) + { + return (null, null); + } - var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod); + var paramNode = propertyNode.SelectFirstChildWithAttribute("param", "name", parameterInfo.Name); - if (!_xmlDocMembers.TryGetValue(methodMemberName, out var propertyNode)) - { - return (null, null); - } + if (paramNode is null) + { + return (null, null); + } - var paramNode = propertyNode.SelectFirstChildWithAttribute("param", "name", parameterInfo.Name); + var summary = XmlCommentsTextHelper.Humanize(paramNode.InnerXml, _options?.XmlCommentEndOfLine); + var example = paramNode.GetAttribute("example"); - if (paramNode is null) - { - return (null, null); - } + return (summary, example); + } - var summary = XmlCommentsTextHelper.Humanize(paramNode.InnerXml, _options?.XmlCommentEndOfLine); - var example = paramNode.GetAttribute("example"); + private void ApplyParamTagsForBody(OpenApiRequestBody requestBody, RequestBodyFilterContext context, ParameterInfo parameterInfo) + { + var (summary, example) = GetParamTags(parameterInfo); - return (summary, example); + if (summary is not null) + { + requestBody.Description = summary; } - private void ApplyParamTagsForBody(OpenApiRequestBody requestBody, RequestBodyFilterContext context, ParameterInfo parameterInfo) + if (requestBody.Content?.Count is 0 || string.IsNullOrEmpty(example)) { - var (summary, example) = GetParamTags(parameterInfo); - - if (summary is not null) - { - requestBody.Description = summary; - } - - if (requestBody.Content?.Count is 0 || string.IsNullOrEmpty(example)) - { - return; - } + return; + } - foreach (var mediaType in requestBody.Content.Values) - { - mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example); - } + foreach (var mediaType in requestBody.Content.Values) + { + mediaType.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, mediaType.Schema, example); } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs index 55e66d7691..ee122c1ec5 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs @@ -2,93 +2,92 @@ using Microsoft.OpenApi.Models; using System.Xml.XPath; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public class XmlCommentsSchemaFilter : ISchemaFilter { - public class XmlCommentsSchemaFilter : ISchemaFilter + private readonly IReadOnlyDictionary _xmlDocMembers; + private readonly SwaggerGeneratorOptions _options; + + public XmlCommentsSchemaFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null) { - private readonly IReadOnlyDictionary _xmlDocMembers; - private readonly SwaggerGeneratorOptions _options; + } - public XmlCommentsSchemaFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null) - { - } + [ActivatorUtilitiesConstructor] + internal XmlCommentsSchemaFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) + { + _xmlDocMembers = xmlDocMembers; + _options = options; + } - [ActivatorUtilitiesConstructor] - internal XmlCommentsSchemaFilter(IReadOnlyDictionary xmlDocMembers, SwaggerGeneratorOptions options) - { - _xmlDocMembers = xmlDocMembers; - _options = options; - } + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + ApplyTypeTags(schema, context.Type); - public void Apply(OpenApiSchema schema, SchemaFilterContext context) + if (context.MemberInfo != null) { - ApplyTypeTags(schema, context.Type); - - if (context.MemberInfo != null) - { - ApplyMemberTags(schema, context); - } + ApplyMemberTags(schema, context); } + } - private void ApplyTypeTags(OpenApiSchema schema, Type type) - { - var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(type); - - if (!_xmlDocMembers.TryGetValue(typeMemberName, out var memberNode)) return; + private void ApplyTypeTags(OpenApiSchema schema, Type type) + { + var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(type); - var typeSummaryNode = memberNode.SelectFirstChild("summary"); + if (!_xmlDocMembers.TryGetValue(typeMemberName, out var memberNode)) return; - if (typeSummaryNode != null) - { - schema.Description = XmlCommentsTextHelper.Humanize(typeSummaryNode.InnerXml, _options?.XmlCommentEndOfLine); - } - } + var typeSummaryNode = memberNode.SelectFirstChild("summary"); - private void ApplyMemberTags(OpenApiSchema schema, SchemaFilterContext context) + if (typeSummaryNode != null) { - var fieldOrPropertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.MemberInfo); + schema.Description = XmlCommentsTextHelper.Humanize(typeSummaryNode.InnerXml, _options?.XmlCommentEndOfLine); + } + } - var recordTypeName = XmlCommentsNodeNameHelper.GetMemberNameForType(context.MemberInfo.DeclaringType); + private void ApplyMemberTags(OpenApiSchema schema, SchemaFilterContext context) + { + var fieldOrPropertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.MemberInfo); - if (_xmlDocMembers.TryGetValue(recordTypeName, out var recordTypeNode)) - { - XPathNavigator recordDefaultConstructorProperty = recordTypeNode.SelectFirstChildWithAttribute("param", "name", context.MemberInfo.Name); + var recordTypeName = XmlCommentsNodeNameHelper.GetMemberNameForType(context.MemberInfo.DeclaringType); - if (recordDefaultConstructorProperty != null) - { - var summaryNode = recordDefaultConstructorProperty.Value; - if (summaryNode != null) - { - schema.Description = XmlCommentsTextHelper.Humanize(summaryNode, _options?.XmlCommentEndOfLine); - } - - var example = recordDefaultConstructorProperty.GetAttribute("example"); - if (!string.IsNullOrEmpty(example)) - { - TrySetExample(schema, context, example); - } - } - } + if (_xmlDocMembers.TryGetValue(recordTypeName, out var recordTypeNode)) + { + XPathNavigator recordDefaultConstructorProperty = recordTypeNode.SelectFirstChildWithAttribute("param", "name", context.MemberInfo.Name); - if (_xmlDocMembers.TryGetValue(fieldOrPropertyMemberName, out var fieldOrPropertyNode)) + if (recordDefaultConstructorProperty != null) { - var summaryNode = fieldOrPropertyNode.SelectFirstChild("summary"); + var summaryNode = recordDefaultConstructorProperty.Value; if (summaryNode != null) { - schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine); + schema.Description = XmlCommentsTextHelper.Humanize(summaryNode, _options?.XmlCommentEndOfLine); } - var exampleNode = fieldOrPropertyNode.SelectFirstChild("example"); - TrySetExample(schema, context, exampleNode?.Value); + var example = recordDefaultConstructorProperty.GetAttribute("example"); + if (!string.IsNullOrEmpty(example)) + { + TrySetExample(schema, context, example); + } } } - private static void TrySetExample(OpenApiSchema schema, SchemaFilterContext context, string example) + if (_xmlDocMembers.TryGetValue(fieldOrPropertyMemberName, out var fieldOrPropertyNode)) { - if (example == null) - return; + var summaryNode = fieldOrPropertyNode.SelectFirstChild("summary"); + if (summaryNode != null) + { + schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine); + } - schema.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, schema, example); + var exampleNode = fieldOrPropertyNode.SelectFirstChild("example"); + TrySetExample(schema, context, exampleNode?.Value); } } + + private static void TrySetExample(OpenApiSchema schema, SchemaFilterContext context, string example) + { + if (example == null) + return; + + schema.Example = XmlCommentsExampleHelper.Create(context.SchemaRepository, schema, example); + } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs index adee7084e6..1a29cce6ca 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs @@ -2,227 +2,226 @@ using System.Text; using System.Text.RegularExpressions; -namespace Swashbuckle.AspNetCore.SwaggerGen +namespace Swashbuckle.AspNetCore.SwaggerGen; + +public static partial class XmlCommentsTextHelper { - public static partial class XmlCommentsTextHelper + public static string Humanize(string text) { - public static string Humanize(string text) - { - return Humanize(text, null); - } - - public static string Humanize(string text, string xmlCommentEndOfLine) - { - if (text == null) - throw new ArgumentNullException(nameof(text)); - - //Call DecodeXml at last to avoid entities like < and > to break valid xml - - return text - .NormalizeIndentation(xmlCommentEndOfLine) - .HumanizeRefTags() - .HumanizeHrefTags() - .HumanizeCodeTags() - .HumanizeMultilineCodeTags(xmlCommentEndOfLine) - .HumanizeParaTags() - .HumanizeBrTags(xmlCommentEndOfLine) // must be called after HumanizeParaTags() so that it replaces any additional
tags - .DecodeXml(); - } + return Humanize(text, null); + } - private static string NormalizeIndentation(this string text, string xmlCommentEndOfLine) - { - var lines = text.Split(["\r\n", "\n"], StringSplitOptions.None); - string padding = GetCommonLeadingWhitespace(lines); + public static string Humanize(string text, string xmlCommentEndOfLine) + { + if (text == null) + throw new ArgumentNullException(nameof(text)); + + //Call DecodeXml at last to avoid entities like < and > to break valid xml + + return text + .NormalizeIndentation(xmlCommentEndOfLine) + .HumanizeRefTags() + .HumanizeHrefTags() + .HumanizeCodeTags() + .HumanizeMultilineCodeTags(xmlCommentEndOfLine) + .HumanizeParaTags() + .HumanizeBrTags(xmlCommentEndOfLine) // must be called after HumanizeParaTags() so that it replaces any additional
tags + .DecodeXml(); + } - int padLen = padding?.Length ?? 0; + private static string NormalizeIndentation(this string text, string xmlCommentEndOfLine) + { + var lines = text.Split(["\r\n", "\n"], StringSplitOptions.None); + string padding = GetCommonLeadingWhitespace(lines); - // remove leading padding from each line - for (int i = 0, l = lines.Length; i < l; ++i) - { - string line = lines[i].TrimEnd('\r'); // remove trailing '\r' + int padLen = padding?.Length ?? 0; - if (padLen != 0 && line.Length >= padLen && line.Substring(0, padLen) == padding) - line = line.Substring(padLen); + // remove leading padding from each line + for (int i = 0, l = lines.Length; i < l; ++i) + { + string line = lines[i].TrimEnd('\r'); // remove trailing '\r' - lines[i] = line; - } + if (padLen != 0 && line.Length >= padLen && line.Substring(0, padLen) == padding) + line = line.Substring(padLen); - // remove leading empty lines, but not all leading padding - // remove all trailing whitespace, regardless - return string.Join(EndOfLine(xmlCommentEndOfLine), lines.SkipWhile(string.IsNullOrWhiteSpace)).TrimEnd(); + lines[i] = line; } - private static string GetCommonLeadingWhitespace(string[] lines) - { - if (null == lines) - throw new ArgumentException("lines"); - - if (lines.Length == 0) - return null; + // remove leading empty lines, but not all leading padding + // remove all trailing whitespace, regardless + return string.Join(EndOfLine(xmlCommentEndOfLine), lines.SkipWhile(string.IsNullOrWhiteSpace)).TrimEnd(); + } - string[] nonEmptyLines = lines - .Where(x => !string.IsNullOrWhiteSpace(x)) - .ToArray(); + private static string GetCommonLeadingWhitespace(string[] lines) + { + if (null == lines) + throw new ArgumentException("lines"); - if (nonEmptyLines.Length < 1) - return null; + if (lines.Length == 0) + return null; - int padLen = 0; + string[] nonEmptyLines = lines + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToArray(); - // use the first line as a seed, and see what is shared over all nonEmptyLines - string seed = nonEmptyLines[0]; - for (int i = 0, l = seed.Length; i < l; ++i) - { - if (!char.IsWhiteSpace(seed, i)) - break; + if (nonEmptyLines.Length < 1) + return null; - if (nonEmptyLines.Any(line => line[i] != seed[i])) - break; + int padLen = 0; - ++padLen; - } + // use the first line as a seed, and see what is shared over all nonEmptyLines + string seed = nonEmptyLines[0]; + for (int i = 0, l = seed.Length; i < l; ++i) + { + if (!char.IsWhiteSpace(seed, i)) + break; - if (padLen > 0) - return seed.Substring(0, padLen); + if (nonEmptyLines.Any(line => line[i] != seed[i])) + break; - return null; + ++padLen; } - private static string HumanizeRefTags(this string text) - { - return RefTag().Replace(text, (match) => match.Groups["display"].Value); - } + if (padLen > 0) + return seed.Substring(0, padLen); - private static string HumanizeHrefTags(this string text) - { - return HrefTag().Replace(text, m => $"[{m.Groups[2].Value}]({m.Groups[1].Value})"); - } + return null; + } - private static string HumanizeCodeTags(this string text) - { - return CodeTag().Replace(text, (match) => "`" + match.Groups["display"].Value + "`"); - } + private static string HumanizeRefTags(this string text) + { + return RefTag().Replace(text, (match) => match.Groups["display"].Value); + } + + private static string HumanizeHrefTags(this string text) + { + return HrefTag().Replace(text, m => $"[{m.Groups[2].Value}]({m.Groups[1].Value})"); + } - private static string HumanizeMultilineCodeTags(this string text, string xmlCommentEndOfLine) + private static string HumanizeCodeTags(this string text) + { + return CodeTag().Replace(text, (match) => "`" + match.Groups["display"].Value + "`"); + } + + private static string HumanizeMultilineCodeTags(this string text, string xmlCommentEndOfLine) + { + return MultilineCodeTag().Replace(text, match => { - return MultilineCodeTag().Replace(text, match => + var codeText = match.Groups["display"].Value; + if (LineBreaks().IsMatch(codeText)) { - var codeText = match.Groups["display"].Value; - if (LineBreaks().IsMatch(codeText)) + var builder = new StringBuilder().Append("```"); + if (!codeText.StartsWith("\r") && !codeText.StartsWith("\n")) { - var builder = new StringBuilder().Append("```"); - if (!codeText.StartsWith("\r") && !codeText.StartsWith("\n")) - { - builder.Append(EndOfLine(xmlCommentEndOfLine)); - } - - builder.Append(RemoveCommonLeadingWhitespace(codeText, xmlCommentEndOfLine)); - if (!codeText.EndsWith("\n")) - { - builder.Append(EndOfLine(xmlCommentEndOfLine)); - } - - builder.Append("```"); - return DoubleUpLineBreaks().Replace(builder.ToString(), EndOfLine(xmlCommentEndOfLine)); + builder.Append(EndOfLine(xmlCommentEndOfLine)); } - return $"```{codeText}```"; - }); - } + builder.Append(RemoveCommonLeadingWhitespace(codeText, xmlCommentEndOfLine)); + if (!codeText.EndsWith("\n")) + { + builder.Append(EndOfLine(xmlCommentEndOfLine)); + } - private static string HumanizeParaTags(this string text) - { - return ParaTag().Replace(text, match => "
" + match.Groups["display"].Value.Trim()); - } + builder.Append("```"); + return DoubleUpLineBreaks().Replace(builder.ToString(), EndOfLine(xmlCommentEndOfLine)); + } - private static string HumanizeBrTags(this string text, string xmlCommentEndOfLine) - { - return BrTag().Replace(text, _ => EndOfLine(xmlCommentEndOfLine)); - } + return $"```{codeText}```"; + }); + } - private static string DecodeXml(this string text) - { - return WebUtility.HtmlDecode(text); - } + private static string HumanizeParaTags(this string text) + { + return ParaTag().Replace(text, match => "
" + match.Groups["display"].Value.Trim()); + } - private static string RemoveCommonLeadingWhitespace(string input, string xmlCommentEndOfLine) - { - var lines = input.Split(["\r\n", "\n"], StringSplitOptions.None); - var padding = GetCommonLeadingWhitespace(lines); - if (string.IsNullOrEmpty(padding)) - { - return input; - } + private static string HumanizeBrTags(this string text, string xmlCommentEndOfLine) + { + return BrTag().Replace(text, _ => EndOfLine(xmlCommentEndOfLine)); + } - var minLeadingSpaces = padding.Length; - var builder = new StringBuilder(); - foreach (var line in lines) - { - builder.Append(string.IsNullOrWhiteSpace(line) - ? line - : line.Substring(minLeadingSpaces)); - builder.Append(EndOfLine(xmlCommentEndOfLine)); - } + private static string DecodeXml(this string text) + { + return WebUtility.HtmlDecode(text); + } - return builder.ToString(); + private static string RemoveCommonLeadingWhitespace(string input, string xmlCommentEndOfLine) + { + var lines = input.Split(["\r\n", "\n"], StringSplitOptions.None); + var padding = GetCommonLeadingWhitespace(lines); + if (string.IsNullOrEmpty(padding)) + { + return input; } - internal static string EndOfLine(string xmlCommentEndOfLine) + var minLeadingSpaces = padding.Length; + var builder = new StringBuilder(); + foreach (var line in lines) { - return xmlCommentEndOfLine ?? Environment.NewLine; + builder.Append(string.IsNullOrWhiteSpace(line) + ? line + : line.Substring(minLeadingSpaces)); + builder.Append(EndOfLine(xmlCommentEndOfLine)); } - private const string RefTagPattern = @"<(see|paramref) (name|cref|langword)=""([TPF]{1}:)?(?.+?)"" ?/>"; - private const string CodeTagPattern = @"(?.+?)"; - private const string MultilineCodeTagPattern = @"(?.+?)"; - private const string ParaTagPattern = @"(?.+?)"; - private const string HrefPattern = @"(.*)<\/see>"; - private const string BrPattern = @"(
)"; // handles
,
,
- private const string LineBreaksPattern = @"\r?\n"; - private const string DoubleUpLineBreaksPattern = @"(\r?\n){2,}"; + return builder.ToString(); + } + + internal static string EndOfLine(string xmlCommentEndOfLine) + { + return xmlCommentEndOfLine ?? Environment.NewLine; + } -#if NET7_0_OR_GREATER - [GeneratedRegex(RefTagPattern)] - private static partial Regex RefTag(); + private const string RefTagPattern = @"<(see|paramref) (name|cref|langword)=""([TPF]{1}:)?(?.+?)"" ?/>"; + private const string CodeTagPattern = @"(?.+?)"; + private const string MultilineCodeTagPattern = @"(?.+?)"; + private const string ParaTagPattern = @"(?.+?)"; + private const string HrefPattern = @"(.*)<\/see>"; + private const string BrPattern = @"(
)"; // handles
,
,
+ private const string LineBreaksPattern = @"\r?\n"; + private const string DoubleUpLineBreaksPattern = @"(\r?\n){2,}"; - [GeneratedRegex(CodeTagPattern)] - private static partial Regex CodeTag(); +#if NET + [GeneratedRegex(RefTagPattern)] + private static partial Regex RefTag(); - [GeneratedRegex(MultilineCodeTagPattern, RegexOptions.Singleline)] - private static partial Regex MultilineCodeTag(); + [GeneratedRegex(CodeTagPattern)] + private static partial Regex CodeTag(); - [GeneratedRegex(ParaTagPattern, RegexOptions.Singleline)] - private static partial Regex ParaTag(); + [GeneratedRegex(MultilineCodeTagPattern, RegexOptions.Singleline)] + private static partial Regex MultilineCodeTag(); - [GeneratedRegex(HrefPattern)] - private static partial Regex HrefTag(); + [GeneratedRegex(ParaTagPattern, RegexOptions.Singleline)] + private static partial Regex ParaTag(); - [GeneratedRegex(BrPattern)] - private static partial Regex BrTag(); + [GeneratedRegex(HrefPattern)] + private static partial Regex HrefTag(); - [GeneratedRegex(LineBreaksPattern)] - private static partial Regex LineBreaks(); + [GeneratedRegex(BrPattern)] + private static partial Regex BrTag(); - [GeneratedRegex(DoubleUpLineBreaksPattern)] - private static partial Regex DoubleUpLineBreaks(); + [GeneratedRegex(LineBreaksPattern)] + private static partial Regex LineBreaks(); + + [GeneratedRegex(DoubleUpLineBreaksPattern)] + private static partial Regex DoubleUpLineBreaks(); #else - private static readonly Regex _refTag = new(RefTagPattern); - private static readonly Regex _codeTag = new(CodeTagPattern); - private static readonly Regex _multilineCodeTag = new(MultilineCodeTagPattern, RegexOptions.Singleline); - private static readonly Regex _paraTag = new(ParaTagPattern, RegexOptions.Singleline); - private static readonly Regex _hrefTag = new(HrefPattern); - private static readonly Regex _brTag = new(BrPattern); - private static readonly Regex _lineBreaks = new(LineBreaksPattern); - private static readonly Regex _doubleUpLineBreaks = new(DoubleUpLineBreaksPattern); - - private static Regex RefTag() => _refTag; - private static Regex CodeTag() => _codeTag; - private static Regex MultilineCodeTag() => _multilineCodeTag; - private static Regex ParaTag() => _paraTag; - private static Regex HrefTag() => _hrefTag; - private static Regex BrTag() => _brTag; - private static Regex LineBreaks() => _lineBreaks; - private static Regex DoubleUpLineBreaks() => _doubleUpLineBreaks; + private static readonly Regex _refTag = new(RefTagPattern); + private static readonly Regex _codeTag = new(CodeTagPattern); + private static readonly Regex _multilineCodeTag = new(MultilineCodeTagPattern, RegexOptions.Singleline); + private static readonly Regex _paraTag = new(ParaTagPattern, RegexOptions.Singleline); + private static readonly Regex _hrefTag = new(HrefPattern); + private static readonly Regex _brTag = new(BrPattern); + private static readonly Regex _lineBreaks = new(LineBreaksPattern); + private static readonly Regex _doubleUpLineBreaks = new(DoubleUpLineBreaksPattern); + + private static Regex RefTag() => _refTag; + private static Regex CodeTag() => _codeTag; + private static Regex MultilineCodeTag() => _multilineCodeTag; + private static Regex ParaTag() => _paraTag; + private static Regex HrefTag() => _hrefTag; + private static Regex BrTag() => _brTag; + private static Regex LineBreaks() => _lineBreaks; + private static Regex DoubleUpLineBreaks() => _doubleUpLineBreaks; #endif - } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/JavascriptStringEnumConverter.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/JavascriptStringEnumConverter.cs index 3d9a3fd60e..84c90c0dd0 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/JavascriptStringEnumConverter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/JavascriptStringEnumConverter.cs @@ -1,5 +1,4 @@ -#if NET6_0_OR_GREATER -using System; +#if NET using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; @@ -7,11 +6,7 @@ namespace Swashbuckle.AspNetCore.SwaggerUI; internal sealed class JavascriptStringEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum>() : -#if NET8_0_OR_GREATER JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false) -#else - JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false) -#endif where TEnum : struct, Enum { } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/JavascriptStringEnumEnumerableConverter.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/JavascriptStringEnumEnumerableConverter.cs index 4f5ac37958..bd9029a369 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/JavascriptStringEnumEnumerableConverter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/JavascriptStringEnumEnumerableConverter.cs @@ -1,6 +1,4 @@ -#if NET6_0_OR_GREATER -using System; -using System.Collections.Generic; +#if NET using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs index ee102f2cb1..b7db72cb72 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs @@ -3,44 +3,43 @@ using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.SwaggerUI; -#if NETSTANDARD +#if !NET using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; #endif -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public static class SwaggerUIBuilderExtensions { - public static class SwaggerUIBuilderExtensions + /// + /// Register the SwaggerUI middleware with provided options + /// + public static IApplicationBuilder UseSwaggerUI(this IApplicationBuilder app, SwaggerUIOptions options) + { + return app.UseMiddleware(options); + } + + /// + /// Register the SwaggerUI middleware with optional setup action for DI-injected options + /// + public static IApplicationBuilder UseSwaggerUI( + this IApplicationBuilder app, + Action setupAction = null) { - /// - /// Register the SwaggerUI middleware with provided options - /// - public static IApplicationBuilder UseSwaggerUI(this IApplicationBuilder app, SwaggerUIOptions options) + SwaggerUIOptions options; + using (var scope = app.ApplicationServices.CreateScope()) { - return app.UseMiddleware(options); + options = scope.ServiceProvider.GetRequiredService>().Value; + setupAction?.Invoke(options); } - /// - /// Register the SwaggerUI middleware with optional setup action for DI-injected options - /// - public static IApplicationBuilder UseSwaggerUI( - this IApplicationBuilder app, - Action setupAction = null) + // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults + if (options.ConfigObject.Urls == null) { - SwaggerUIOptions options; - using (var scope = app.ApplicationServices.CreateScope()) - { - options = scope.ServiceProvider.GetRequiredService>().Value; - setupAction?.Invoke(options); - } - - // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults - if (options.ConfigObject.Urls == null) - { - var hostingEnv = app.ApplicationServices.GetRequiredService(); - options.ConfigObject.Urls = [new UrlDescriptor { Name = $"{hostingEnv.ApplicationName} v1", Url = "v1/swagger.json" }]; - } - - return app.UseSwaggerUI(options); + var hostingEnv = app.ApplicationServices.GetRequiredService(); + options.ConfigObject.Urls = [new UrlDescriptor { Name = $"{hostingEnv.ApplicationName} v1", Url = "v1/swagger.json" }]; } + + return app.UseSwaggerUI(options); } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs index 51e9fbe31d..0cc8d27962 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs @@ -1,6 +1,4 @@ -#if NET6_0_OR_GREATER -using System; -using System.Collections.Generic; +#if NET using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -32,15 +30,11 @@ namespace Swashbuckle.AspNetCore.SwaggerUI; [JsonSerializable(typeof(JsonArray))] [JsonSerializable(typeof(JsonObject))] [JsonSerializable(typeof(JsonDocument))] -#if NET7_0_OR_GREATER [JsonSerializable(typeof(DateOnly))] [JsonSerializable(typeof(TimeOnly))] -#endif -#if NET8_0_OR_GREATER [JsonSerializable(typeof(Half))] [JsonSerializable(typeof(Int128))] [JsonSerializable(typeof(UInt128))] -#endif [JsonSourceGenerationOptions( DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs index d39fbe245e..d25240304f 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs @@ -12,217 +12,212 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -#if NETSTANDARD +#if !NET using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; #endif -namespace Swashbuckle.AspNetCore.SwaggerUI +namespace Swashbuckle.AspNetCore.SwaggerUI; + +internal sealed partial class SwaggerUIMiddleware { - internal sealed partial class SwaggerUIMiddleware - { - private const string EmbeddedFileNamespace = "Swashbuckle.AspNetCore.SwaggerUI.node_modules.swagger_ui_dist"; + private const string EmbeddedFileNamespace = "Swashbuckle.AspNetCore.SwaggerUI.node_modules.swagger_ui_dist"; - private readonly SwaggerUIOptions _options; - private readonly StaticFileMiddleware _staticFileMiddleware; - private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly SwaggerUIOptions _options; + private readonly StaticFileMiddleware _staticFileMiddleware; + private readonly JsonSerializerOptions _jsonSerializerOptions; - public SwaggerUIMiddleware( - RequestDelegate next, - IWebHostEnvironment hostingEnv, - ILoggerFactory loggerFactory, - SwaggerUIOptions options) - { - _options = options ?? new SwaggerUIOptions(); + public SwaggerUIMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + SwaggerUIOptions options) + { + _options = options ?? new SwaggerUIOptions(); - _staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options); + _staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options); - if (options.JsonSerializerOptions != null) - { - _jsonSerializerOptions = options.JsonSerializerOptions; - } -#if !NET6_0_OR_GREATER - else + if (options.JsonSerializerOptions != null) + { + _jsonSerializerOptions = options.JsonSerializerOptions; + } +#if !NET + else + { + _jsonSerializerOptions = new JsonSerializerOptions() { - _jsonSerializerOptions = new JsonSerializerOptions() - { -#if NET5_0_OR_GREATER - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, -#else - IgnoreNullValues = true, -#endif - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false) } - }; - } -#endif + IgnoreNullValues = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false) } + }; } +#endif + } + + public async Task Invoke(HttpContext httpContext) + { + var httpMethod = httpContext.Request.Method; - public async Task Invoke(HttpContext httpContext) + if (HttpMethods.IsGet(httpMethod)) { - var httpMethod = httpContext.Request.Method; + var path = httpContext.Request.Path.Value; - if (HttpMethods.IsGet(httpMethod)) + // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL + if (Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) { - var path = httpContext.Request.Path.Value; - - // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL - if (Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase)) - { - // Use relative redirect to support proxy environments - var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") - ? "index.html" - : $"{path.Split('/').Last()}/index.html"; - - RespondWithRedirect(httpContext.Response, relativeIndexUrl); - return; - } - - var match = Regex.Match(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?(index.(html|js))$", RegexOptions.IgnoreCase); - - if (match.Success) - { - await RespondWithFile(httpContext.Response, match.Groups[1].Value); - return; - } - - var pattern = $"^/?{Regex.Escape(_options.RoutePrefix)}/{_options.SwaggerDocumentUrlsPath}/?$"; - if (Regex.IsMatch(path, pattern, RegexOptions.IgnoreCase)) - { - await RespondWithDocumentUrls(httpContext.Response); - return; - } + // Use relative redirect to support proxy environments + var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") + ? "index.html" + : $"{path.Split('/').Last()}/index.html"; + + RespondWithRedirect(httpContext.Response, relativeIndexUrl); + return; } - await _staticFileMiddleware.Invoke(httpContext); - } + var match = Regex.Match(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?(index.(html|js))$", RegexOptions.IgnoreCase); - private static StaticFileMiddleware CreateStaticFileMiddleware( - RequestDelegate next, - IWebHostEnvironment hostingEnv, - ILoggerFactory loggerFactory, - SwaggerUIOptions options) - { - var staticFileOptions = new StaticFileOptions + if (match.Success) { - RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}", - FileProvider = new EmbeddedFileProvider(typeof(SwaggerUIMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace), - }; + await RespondWithFile(httpContext.Response, match.Groups[1].Value); + return; + } - return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory); + var pattern = $"^/?{Regex.Escape(_options.RoutePrefix)}/{_options.SwaggerDocumentUrlsPath}/?$"; + if (Regex.IsMatch(path, pattern, RegexOptions.IgnoreCase)) + { + await RespondWithDocumentUrls(httpContext.Response); + return; + } } - private static void RespondWithRedirect(HttpResponse response, string location) + await _staticFileMiddleware.Invoke(httpContext); + } + + private static StaticFileMiddleware CreateStaticFileMiddleware( + RequestDelegate next, + IWebHostEnvironment hostingEnv, + ILoggerFactory loggerFactory, + SwaggerUIOptions options) + { + var staticFileOptions = new StaticFileOptions { - response.StatusCode = StatusCodes.Status301MovedPermanently; -#if NET6_0_OR_GREATER - response.Headers.Location = location; + RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}", + FileProvider = new EmbeddedFileProvider(typeof(SwaggerUIMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace), + }; + + return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory); + } + + private static void RespondWithRedirect(HttpResponse response, string location) + { + response.StatusCode = StatusCodes.Status301MovedPermanently; +#if NET + response.Headers.Location = location; #else - response.Headers["Location"] = location; + response.Headers["Location"] = location; #endif - } + } - private async Task RespondWithFile(HttpResponse response, string fileName) + private async Task RespondWithFile(HttpResponse response, string fileName) + { + response.StatusCode = 200; + + Stream stream; + + if (fileName == "index.js") { - response.StatusCode = 200; + response.ContentType = "application/javascript;charset=utf-8"; + stream = ResourceHelper.GetEmbeddedResource(fileName); + } + else + { + response.ContentType = "text/html;charset=utf-8"; + stream = _options.IndexStream(); + } - Stream stream; + using (stream) + { + using var reader = new StreamReader(stream); - if (fileName == "index.js") + // Inject arguments before writing to response + var content = new StringBuilder(await reader.ReadToEndAsync()); + foreach (var entry in GetIndexArguments()) { - response.ContentType = "application/javascript;charset=utf-8"; - stream = ResourceHelper.GetEmbeddedResource(fileName); + content.Replace(entry.Key, entry.Value); } - else - { - response.ContentType = "text/html;charset=utf-8"; - stream = _options.IndexStream(); - } - - using (stream) - { - using var reader = new StreamReader(stream); - - // Inject arguments before writing to response - var content = new StringBuilder(await reader.ReadToEndAsync()); - foreach (var entry in GetIndexArguments()) - { - content.Replace(entry.Key, entry.Value); - } - await response.WriteAsync(content.ToString(), Encoding.UTF8); - } + await response.WriteAsync(content.ToString(), Encoding.UTF8); } + } -#if NET5_0_OR_GREATER - [UnconditionalSuppressMessage( - "AOT", - "IL2026:RequiresUnreferencedCode", - Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] - [UnconditionalSuppressMessage( - "AOT", - "IL3050:RequiresDynamicCode", - Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] +#if NET + [UnconditionalSuppressMessage( + "AOT", + "IL2026:RequiresUnreferencedCode", + Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050:RequiresDynamicCode", + Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] #endif - private async Task RespondWithDocumentUrls(HttpResponse response) - { - response.StatusCode = 200; + private async Task RespondWithDocumentUrls(HttpResponse response) + { + response.StatusCode = 200; - response.ContentType = "application/javascript;charset=utf-8"; - string json = "[]"; + response.ContentType = "application/javascript;charset=utf-8"; + string json = "[]"; -#if NET6_0_OR_GREATER - if (_jsonSerializerOptions is null) - { - var l = new List(_options.ConfigObject.Urls); - json = JsonSerializer.Serialize(l, SwaggerUIOptionsJsonContext.Default.ListUrlDescriptor); - } +#if NET + if (_jsonSerializerOptions is null) + { + var l = new List(_options.ConfigObject.Urls); + json = JsonSerializer.Serialize(l, SwaggerUIOptionsJsonContext.Default.ListUrlDescriptor); + } #endif - json ??= JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions); + json ??= JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions); - await response.WriteAsync(json, Encoding.UTF8); - } + await response.WriteAsync(json, Encoding.UTF8); + } -#if NET5_0_OR_GREATER - [UnconditionalSuppressMessage( - "AOT", - "IL2026:RequiresUnreferencedCode", - Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] - [UnconditionalSuppressMessage( - "AOT", - "IL3050:RequiresDynamicCode", - Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] +#if NET + [UnconditionalSuppressMessage( + "AOT", + "IL2026:RequiresUnreferencedCode", + Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050:RequiresDynamicCode", + Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")] #endif - private Dictionary GetIndexArguments() - { - string configObject = null; - string oauthConfigObject = null; - string interceptors = null; + private Dictionary GetIndexArguments() + { + string configObject = null; + string oauthConfigObject = null; + string interceptors = null; -#if NET6_0_OR_GREATER - if (_jsonSerializerOptions is null) - { - configObject = JsonSerializer.Serialize(_options.ConfigObject, SwaggerUIOptionsJsonContext.Default.ConfigObject); - oauthConfigObject = JsonSerializer.Serialize(_options.OAuthConfigObject, SwaggerUIOptionsJsonContext.Default.OAuthConfigObject); - interceptors = JsonSerializer.Serialize(_options.Interceptors, SwaggerUIOptionsJsonContext.Default.InterceptorFunctions); - } +#if NET + if (_jsonSerializerOptions is null) + { + configObject = JsonSerializer.Serialize(_options.ConfigObject, SwaggerUIOptionsJsonContext.Default.ConfigObject); + oauthConfigObject = JsonSerializer.Serialize(_options.OAuthConfigObject, SwaggerUIOptionsJsonContext.Default.OAuthConfigObject); + interceptors = JsonSerializer.Serialize(_options.Interceptors, SwaggerUIOptionsJsonContext.Default.InterceptorFunctions); + } #endif - configObject ??= JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions); - oauthConfigObject ??= JsonSerializer.Serialize(_options.OAuthConfigObject, _jsonSerializerOptions); - interceptors ??= JsonSerializer.Serialize(_options.Interceptors, _jsonSerializerOptions); + configObject ??= JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions); + oauthConfigObject ??= JsonSerializer.Serialize(_options.OAuthConfigObject, _jsonSerializerOptions); + interceptors ??= JsonSerializer.Serialize(_options.Interceptors, _jsonSerializerOptions); - return new Dictionary() - { - { "%(DocumentTitle)", _options.DocumentTitle }, - { "%(HeadContent)", _options.HeadContent }, - { "%(StylesPath)", _options.StylesPath }, - { "%(ScriptBundlePath)", _options.ScriptBundlePath }, - { "%(ScriptPresetsPath)", _options.ScriptPresetsPath }, - { "%(ConfigObject)", configObject }, - { "%(OAuthConfigObject)", oauthConfigObject }, - { "%(Interceptors)", interceptors }, - }; - } + return new Dictionary() + { + { "%(DocumentTitle)", _options.DocumentTitle }, + { "%(HeadContent)", _options.HeadContent }, + { "%(StylesPath)", _options.StylesPath }, + { "%(ScriptBundlePath)", _options.ScriptBundlePath }, + { "%(ScriptPresetsPath)", _options.ScriptPresetsPath }, + { "%(ConfigObject)", configObject }, + { "%(OAuthConfigObject)", oauthConfigObject }, + { "%(Interceptors)", interceptors }, + }; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs index 12b1386d1c..ca2372530d 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs @@ -1,331 +1,330 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Swashbuckle.AspNetCore.SwaggerUI +namespace Swashbuckle.AspNetCore.SwaggerUI; + +public class SwaggerUIOptions +{ + /// + /// Gets or sets a route prefix for accessing the swagger-ui + /// + public string RoutePrefix { get; set; } = "swagger"; + + /// + /// Gets or sets a Stream function for retrieving the swagger-ui page + /// + public Func IndexStream { get; set; } = () => ResourceHelper.GetEmbeddedResource("index.html"); + + /// + /// Gets or sets a title for the swagger-ui page + /// + public string DocumentTitle { get; set; } = "Swagger UI"; + + /// + /// Gets or sets additional content to place in the head of the swagger-ui page + /// + public string HeadContent { get; set; } = ""; + + /// + /// Gets the JavaScript config object, represented as JSON, that will be passed to the SwaggerUI + /// + public ConfigObject ConfigObject { get; set; } = new ConfigObject(); + + /// + /// Gets the JavaScript config object, represented as JSON, that will be passed to the initOAuth method + /// + public OAuthConfigObject OAuthConfigObject { get; set; } = new OAuthConfigObject(); + + /// + /// Gets the interceptor functions that define client-side request/response interceptors + /// + public InterceptorFunctions Interceptors { get; set; } = new InterceptorFunctions(); + + /// + /// Gets or sets the optional JSON serialization options to use to serialize options to the HTML document. + /// + public JsonSerializerOptions JsonSerializerOptions { get; set; } + + /// + /// Gets or sets the path or URL to the Swagger UI JavaScript bundle file. + /// + public string ScriptBundlePath { get; set; } = "./swagger-ui-bundle.js"; + + /// + /// Gets or sets the path or URL to the Swagger UI JavaScript standalone presets file. + /// + public string ScriptPresetsPath { get; set; } = "./swagger-ui-standalone-preset.js"; + + /// + /// Gets or sets the path or URL to the Swagger UI CSS file. + /// + public string StylesPath { get; set; } = "./swagger-ui.css"; + + /// + /// Gets or sets whether to expose the ConfigObject.Urls object via an + /// HTTP endpoint with the URL specified by + /// so that external code can auto-discover all Swagger documents. + /// + public bool ExposeSwaggerDocumentUrlsRoute { get; set; } = false; + + /// + /// Gets or sets the relative URL path to the route that exposes the values of the configured values. + /// + public string SwaggerDocumentUrlsPath { get; set; } = "documentUrls"; +} + +public class ConfigObject { - public class SwaggerUIOptions - { - /// - /// Gets or sets a route prefix for accessing the swagger-ui - /// - public string RoutePrefix { get; set; } = "swagger"; - - /// - /// Gets or sets a Stream function for retrieving the swagger-ui page - /// - public Func IndexStream { get; set; } = () => ResourceHelper.GetEmbeddedResource("index.html"); - - /// - /// Gets or sets a title for the swagger-ui page - /// - public string DocumentTitle { get; set; } = "Swagger UI"; - - /// - /// Gets or sets additional content to place in the head of the swagger-ui page - /// - public string HeadContent { get; set; } = ""; - - /// - /// Gets the JavaScript config object, represented as JSON, that will be passed to the SwaggerUI - /// - public ConfigObject ConfigObject { get; set; } = new ConfigObject(); - - /// - /// Gets the JavaScript config object, represented as JSON, that will be passed to the initOAuth method - /// - public OAuthConfigObject OAuthConfigObject { get; set; } = new OAuthConfigObject(); - - /// - /// Gets the interceptor functions that define client-side request/response interceptors - /// - public InterceptorFunctions Interceptors { get; set; } = new InterceptorFunctions(); - - /// - /// Gets or sets the optional JSON serialization options to use to serialize options to the HTML document. - /// - public JsonSerializerOptions JsonSerializerOptions { get; set; } - - /// - /// Gets or sets the path or URL to the Swagger UI JavaScript bundle file. - /// - public string ScriptBundlePath { get; set; } = "./swagger-ui-bundle.js"; - - /// - /// Gets or sets the path or URL to the Swagger UI JavaScript standalone presets file. - /// - public string ScriptPresetsPath { get; set; } = "./swagger-ui-standalone-preset.js"; - - /// - /// Gets or sets the path or URL to the Swagger UI CSS file. - /// - public string StylesPath { get; set; } = "./swagger-ui.css"; - - /// - /// Gets or sets whether to expose the ConfigObject.Urls object via an - /// HTTP endpoint with the URL specified by - /// so that external code can auto-discover all Swagger documents. - /// - public bool ExposeSwaggerDocumentUrlsRoute { get; set; } = false; - - /// - /// Gets or sets the relative URL path to the route that exposes the values of the configured values. - /// - public string SwaggerDocumentUrlsPath { get; set; } = "documentUrls"; - } - - public class ConfigObject - { - /// - /// One or more Swagger JSON endpoints (url and name) to power the UI - /// - [JsonPropertyName("urls")] - public IEnumerable Urls { get; set; } = null; - - /// - /// If set to true, enables deep linking for tags and operations - /// - [JsonPropertyName("deepLinking")] - public bool DeepLinking { get; set; } = false; - - /// - /// If set to true, it persists authorization data and it would not be lost on browser close/refresh - /// - [JsonPropertyName("persistAuthorization")] - public bool PersistAuthorization { get; set; } = false; - - /// - /// Controls the display of operationId in operations list - /// - [JsonPropertyName("displayOperationId")] - public bool DisplayOperationId { get; set; } = false; - - /// - /// The default expansion depth for models (set to -1 completely hide the models) - /// - [JsonPropertyName("defaultModelsExpandDepth")] - public int DefaultModelsExpandDepth { get; set; } = 1; - - /// - /// The default expansion depth for the model on the model-example section - /// - [JsonPropertyName("defaultModelExpandDepth")] - public int DefaultModelExpandDepth { get; set; } = 1; - - /// - /// Controls how the model is shown when the API is first rendered. - /// (The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links) - /// -#if NET6_0_OR_GREATER - [JsonConverter(typeof(JavascriptStringEnumConverter))] + /// + /// One or more Swagger JSON endpoints (url and name) to power the UI + /// + [JsonPropertyName("urls")] + public IEnumerable Urls { get; set; } = null; + + /// + /// If set to true, enables deep linking for tags and operations + /// + [JsonPropertyName("deepLinking")] + public bool DeepLinking { get; set; } = false; + + /// + /// If set to true, it persists authorization data and it would not be lost on browser close/refresh + /// + [JsonPropertyName("persistAuthorization")] + public bool PersistAuthorization { get; set; } = false; + + /// + /// Controls the display of operationId in operations list + /// + [JsonPropertyName("displayOperationId")] + public bool DisplayOperationId { get; set; } = false; + + /// + /// The default expansion depth for models (set to -1 completely hide the models) + /// + [JsonPropertyName("defaultModelsExpandDepth")] + public int DefaultModelsExpandDepth { get; set; } = 1; + + /// + /// The default expansion depth for the model on the model-example section + /// + [JsonPropertyName("defaultModelExpandDepth")] + public int DefaultModelExpandDepth { get; set; } = 1; + + /// + /// Controls how the model is shown when the API is first rendered. + /// (The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links) + /// +#if NET + [JsonConverter(typeof(JavascriptStringEnumConverter))] #endif - [JsonPropertyName("defaultModelRendering")] - public ModelRendering DefaultModelRendering { get; set; } = ModelRendering.Example; - - /// - /// Controls the display of the request duration (in milliseconds) for Try-It-Out requests - /// - [JsonPropertyName("displayRequestDuration")] - public bool DisplayRequestDuration { get; set; } = false; - - /// - /// Controls the default expansion setting for the operations and tags. - /// It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing) - /// -#if NET6_0_OR_GREATER - [JsonConverter(typeof(JavascriptStringEnumConverter))] + [JsonPropertyName("defaultModelRendering")] + public ModelRendering DefaultModelRendering { get; set; } = ModelRendering.Example; + + /// + /// Controls the display of the request duration (in milliseconds) for Try-It-Out requests + /// + [JsonPropertyName("displayRequestDuration")] + public bool DisplayRequestDuration { get; set; } = false; + + /// + /// Controls the default expansion setting for the operations and tags. + /// It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing) + /// +#if NET + [JsonConverter(typeof(JavascriptStringEnumConverter))] #endif - [JsonPropertyName("docExpansion")] - public DocExpansion DocExpansion { get; set; } = DocExpansion.List; - - /// - /// If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations - /// that are shown. Can be an empty string or specific value, in which case filtering will be enabled using that - /// value as the filter expression. Filtering is case sensitive matching the filter expression anywhere inside the tag - /// - [JsonPropertyName("filter")] - public string Filter { get; set; } = null; - - /// - /// If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations - /// - [JsonPropertyName("maxDisplayedTags")] - public int? MaxDisplayedTags { get; set; } = null; - - /// - /// Controls the display of vendor extension (x-) fields and values for Operations, Parameters, and Schema - /// - [JsonPropertyName("showExtensions")] - public bool ShowExtensions { get; set; } = false; - - /// - /// Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters - /// - [JsonPropertyName("showCommonExtensions")] - public bool ShowCommonExtensions { get; set; } = false; - - /// - /// OAuth redirect URL - /// - [JsonPropertyName("oauth2RedirectUrl")] - public string OAuth2RedirectUrl { get; set; } = null; - - /// - /// List of HTTP methods that have the Try it out feature enabled. - /// An empty array disables Try it out for all operations. This does not filter the operations from the display - /// -#if NET6_0_OR_GREATER - [JsonConverter(typeof(JavascriptStringEnumEnumerableConverter))] + [JsonPropertyName("docExpansion")] + public DocExpansion DocExpansion { get; set; } = DocExpansion.List; + + /// + /// If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations + /// that are shown. Can be an empty string or specific value, in which case filtering will be enabled using that + /// value as the filter expression. Filtering is case sensitive matching the filter expression anywhere inside the tag + /// + [JsonPropertyName("filter")] + public string Filter { get; set; } = null; + + /// + /// If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations + /// + [JsonPropertyName("maxDisplayedTags")] + public int? MaxDisplayedTags { get; set; } = null; + + /// + /// Controls the display of vendor extension (x-) fields and values for Operations, Parameters, and Schema + /// + [JsonPropertyName("showExtensions")] + public bool ShowExtensions { get; set; } = false; + + /// + /// Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters + /// + [JsonPropertyName("showCommonExtensions")] + public bool ShowCommonExtensions { get; set; } = false; + + /// + /// OAuth redirect URL + /// + [JsonPropertyName("oauth2RedirectUrl")] + public string OAuth2RedirectUrl { get; set; } = null; + + /// + /// List of HTTP methods that have the Try it out feature enabled. + /// An empty array disables Try it out for all operations. This does not filter the operations from the display + /// +#if NET + [JsonConverter(typeof(JavascriptStringEnumEnumerableConverter))] #endif - [JsonPropertyName("supportedSubmitMethods")] - public IEnumerable SupportedSubmitMethods { get; set; } = -#if NET5_0_OR_GREATER - Enum.GetValues(); + [JsonPropertyName("supportedSubmitMethods")] + public IEnumerable SupportedSubmitMethods { get; set; } = +#if NET + Enum.GetValues(); #else - Enum.GetValues(typeof(SubmitMethod)).Cast(); + Enum.GetValues(typeof(SubmitMethod)).Cast(); #endif - /// - /// Controls whether the "Try it out" section should be enabled by default. - /// - [JsonPropertyName("tryItOutEnabled")] - public bool TryItOutEnabled { get; set; } - - /// - /// By default, Swagger-UI attempts to validate specs against swagger.io's online validator. - /// You can use this parameter to set a different validator URL, for example for locally deployed validators (Validator Badge). - /// Setting it to null will disable validation - /// - [JsonPropertyName("validatorUrl")] - public string ValidatorUrl { get; set; } = null; - - /// - /// Any custom plugins' function names. - /// - [JsonPropertyName("plugins")] - public IList Plugins { get; set; } = null; - - [JsonExtensionData] - public Dictionary AdditionalItems { get; set; } = []; - } - - public class UrlDescriptor - { - [JsonPropertyName("url")] - public string Url { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - } - - public enum ModelRendering - { - Example, - Model - } - - public enum DocExpansion - { - List, - Full, - None - } - - public enum SubmitMethod - { - Get, - Put, - Post, - Delete, - Options, - Head, - Patch, - Trace - } - - public class OAuthConfigObject - { - /// - /// Default username for OAuth2 password flow. - /// - public string Username { get; set; } = null; - - /// - /// Default clientId - /// - [JsonPropertyName("clientId")] - public string ClientId { get; set; } = null; - - /// - /// Default clientSecret - /// - /// Setting this exposes the client secrets in inline javascript in the swagger-ui generated html. - [JsonPropertyName("clientSecret")] - public string ClientSecret { get; set; } = null; - - /// - /// Realm query parameter (for oauth1) added to authorizationUrl and tokenUrl - /// - [JsonPropertyName("realm")] - public string Realm { get; set; } = null; - - /// - /// Application name, displayed in authorization popup - /// - [JsonPropertyName("appName")] - public string AppName { get; set; } = null; - - /// - /// Scope separator for passing scopes, encoded before calling, default value is a space (encoded value %20) - /// - [JsonPropertyName("scopeSeparator")] - public string ScopeSeparator { get; set; } = " "; - - /// - /// String array of initially selected oauth scopes, default is empty array - /// - [JsonPropertyName("scopes")] - public IEnumerable Scopes { get; set; } = []; - - /// - /// Additional query parameters added to authorizationUrl and tokenUrl - /// - [JsonPropertyName("additionalQueryStringParams")] - public Dictionary AdditionalQueryStringParams { get; set; } = null; - - /// - /// Only activated for the accessCode flow. During the authorization_code request to the tokenUrl, - /// pass the Client Password using the HTTP Basic Authentication scheme - /// (Authorization header with Basic base64encode(client_id + client_secret)) - /// - [JsonPropertyName("useBasicAuthenticationWithAccessCodeGrant")] - public bool UseBasicAuthenticationWithAccessCodeGrant { get; set; } = false; - - /// - /// Only applies to authorizatonCode flows. Proof Key for Code Exchange brings enhanced security for OAuth public clients. - /// The default is false - /// - [JsonPropertyName("usePkceWithAuthorizationCodeGrant")] - public bool UsePkceWithAuthorizationCodeGrant { get; set; } = false; - } - - public class InterceptorFunctions - { - /// - /// MUST be a valid Javascript function. - /// Function to intercept remote definition, "Try it out", and OAuth 2.0 requests. - /// Accepts one argument requestInterceptor(request) and must return the modified request, or a Promise that resolves to the modified request. - /// Ex: "function (req) { req.headers['MyCustomHeader'] = 'CustomValue'; return req; }" - /// - [JsonPropertyName("RequestInterceptorFunction")] - public string RequestInterceptorFunction { get; set; } - - /// - /// MUST be a valid Javascript function. - /// Function to intercept remote definition, "Try it out", and OAuth 2.0 responses. - /// Accepts one argument responseInterceptor(response) and must return the modified response, or a Promise that resolves to the modified response. - /// Ex: "function (res) { console.log(res); return res; }" - /// - [JsonPropertyName("ResponseInterceptorFunction")] - public string ResponseInterceptorFunction { get; set; } - } + /// + /// Controls whether the "Try it out" section should be enabled by default. + /// + [JsonPropertyName("tryItOutEnabled")] + public bool TryItOutEnabled { get; set; } + + /// + /// By default, Swagger-UI attempts to validate specs against swagger.io's online validator. + /// You can use this parameter to set a different validator URL, for example for locally deployed validators (Validator Badge). + /// Setting it to null will disable validation + /// + [JsonPropertyName("validatorUrl")] + public string ValidatorUrl { get; set; } = null; + + /// + /// Any custom plugins' function names. + /// + [JsonPropertyName("plugins")] + public IList Plugins { get; set; } = null; + + [JsonExtensionData] + public Dictionary AdditionalItems { get; set; } = []; +} + +public class UrlDescriptor +{ + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } +} + +public enum ModelRendering +{ + Example, + Model +} + +public enum DocExpansion +{ + List, + Full, + None +} + +public enum SubmitMethod +{ + Get, + Put, + Post, + Delete, + Options, + Head, + Patch, + Trace +} + +public class OAuthConfigObject +{ + /// + /// Default username for OAuth2 password flow. + /// + public string Username { get; set; } = null; + + /// + /// Default clientId + /// + [JsonPropertyName("clientId")] + public string ClientId { get; set; } = null; + + /// + /// Default clientSecret + /// + /// Setting this exposes the client secrets in inline javascript in the swagger-ui generated html. + [JsonPropertyName("clientSecret")] + public string ClientSecret { get; set; } = null; + + /// + /// Realm query parameter (for oauth1) added to authorizationUrl and tokenUrl + /// + [JsonPropertyName("realm")] + public string Realm { get; set; } = null; + + /// + /// Application name, displayed in authorization popup + /// + [JsonPropertyName("appName")] + public string AppName { get; set; } = null; + + /// + /// Scope separator for passing scopes, encoded before calling, default value is a space (encoded value %20) + /// + [JsonPropertyName("scopeSeparator")] + public string ScopeSeparator { get; set; } = " "; + + /// + /// String array of initially selected oauth scopes, default is empty array + /// + [JsonPropertyName("scopes")] + public IEnumerable Scopes { get; set; } = []; + + /// + /// Additional query parameters added to authorizationUrl and tokenUrl + /// + [JsonPropertyName("additionalQueryStringParams")] + public Dictionary AdditionalQueryStringParams { get; set; } = null; + + /// + /// Only activated for the accessCode flow. During the authorization_code request to the tokenUrl, + /// pass the Client Password using the HTTP Basic Authentication scheme + /// (Authorization header with Basic base64encode(client_id + client_secret)) + /// + [JsonPropertyName("useBasicAuthenticationWithAccessCodeGrant")] + public bool UseBasicAuthenticationWithAccessCodeGrant { get; set; } = false; + + /// + /// Only applies to authorizatonCode flows. Proof Key for Code Exchange brings enhanced security for OAuth public clients. + /// The default is false + /// + [JsonPropertyName("usePkceWithAuthorizationCodeGrant")] + public bool UsePkceWithAuthorizationCodeGrant { get; set; } = false; +} + +public class InterceptorFunctions +{ + /// + /// MUST be a valid Javascript function. + /// Function to intercept remote definition, "Try it out", and OAuth 2.0 requests. + /// Accepts one argument requestInterceptor(request) and must return the modified request, or a Promise that resolves to the modified request. + /// Ex: "function (req) { req.headers['MyCustomHeader'] = 'CustomValue'; return req; }" + /// + [JsonPropertyName("RequestInterceptorFunction")] + public string RequestInterceptorFunction { get; set; } + + /// + /// MUST be a valid Javascript function. + /// Function to intercept remote definition, "Try it out", and OAuth 2.0 responses. + /// Accepts one argument responseInterceptor(response) and must return the modified response, or a Promise that resolves to the modified response. + /// Ex: "function (res) { console.log(res); return res; }" + /// + [JsonPropertyName("ResponseInterceptorFunction")] + public string ResponseInterceptorFunction { get; set; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs index 2d37495c07..f88c5d36e6 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs @@ -1,345 +1,344 @@ using System.Text; using Swashbuckle.AspNetCore.SwaggerUI; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public static class SwaggerUIOptionsExtensions { - public static class SwaggerUIOptionsExtensions - { - /// - /// Injects additional CSS stylesheets into the index.html page - /// - /// - /// A path to the stylesheet - i.e. the link "href" attribute - /// The target media - i.e. the link "media" attribute - public static void InjectStylesheet(this SwaggerUIOptions options, string path, string media = "screen") - { - var builder = new StringBuilder(options.HeadContent); - builder.AppendLine($""); - options.HeadContent = builder.ToString(); - } - - /// - /// Injects additional Javascript files into the index.html page - /// - /// - /// A path to the javascript - i.e. the script "src" attribute - /// The script type - i.e. the script "type" attribute - public static void InjectJavascript(this SwaggerUIOptions options, string path, string type = "text/javascript") - { - var builder = new StringBuilder(options.HeadContent); - builder.AppendLine($""); - options.HeadContent = builder.ToString(); - } - - /// - /// Adds Swagger JSON endpoints. Can be fully-qualified or relative to the UI page - /// - /// - /// Can be fully qualified or relative to the current host - /// The description that appears in the document selector drop-down - public static void SwaggerEndpoint(this SwaggerUIOptions options, string url, string name) - { - var urls = new List(options.ConfigObject.Urls ?? []) - { - new() { Url = url, Name = name } - }; - options.ConfigObject.Urls = urls; - } - - /// - /// Enables deep linking for tags and operations - /// - /// - public static void EnableDeepLinking(this SwaggerUIOptions options) - { - options.ConfigObject.DeepLinking = true; - } - /// - /// Enables persist authorization data - /// - /// - public static void EnablePersistAuthorization(this SwaggerUIOptions options) - { - options.ConfigObject.PersistAuthorization = true; - } - - /// - /// Controls the display of operationId in operations list - /// - /// - public static void DisplayOperationId(this SwaggerUIOptions options) - { - options.ConfigObject.DisplayOperationId = true; - } - - /// - /// The default expansion depth for models (set to -1 completely hide the models) - /// - /// - /// - public static void DefaultModelsExpandDepth(this SwaggerUIOptions options, int depth) - { - options.ConfigObject.DefaultModelsExpandDepth = depth; - } - - /// - /// The default expansion depth for the model on the model-example section - /// - /// - /// - public static void DefaultModelExpandDepth(this SwaggerUIOptions options, int depth) - { - options.ConfigObject.DefaultModelExpandDepth = depth; - } - - /// - /// Controls how the model is shown when the API is first rendered. - /// (The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links.) - /// - /// - /// - public static void DefaultModelRendering(this SwaggerUIOptions options, ModelRendering modelRendering) - { - options.ConfigObject.DefaultModelRendering = modelRendering; - } - - /// - /// Controls the display of the request duration (in milliseconds) for Try-It-Out requests - /// - /// - public static void DisplayRequestDuration(this SwaggerUIOptions options) - { - options.ConfigObject.DisplayRequestDuration = true; - } - - /// - /// Controls the default expansion setting for the operations and tags. - /// It can be 'List' (expands only the tags), 'Full' (expands the tags and operations) or 'None' (expands nothing) - /// - /// - /// - public static void DocExpansion(this SwaggerUIOptions options, DocExpansion docExpansion) - { - options.ConfigObject.DocExpansion = docExpansion; - } - - /// - /// Enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. - /// If an expression is provided it will be used and applied initially. - /// Filtering is case sensitive matching the filter expression anywhere inside the tag - /// - /// - /// - public static void EnableFilter(this SwaggerUIOptions options, string expression = null) - { - options.ConfigObject.Filter = expression ?? ""; - } - - /// - /// Enables the "Try it out" section by default. - /// - /// - public static void EnableTryItOutByDefault(this SwaggerUIOptions options) - { - options.ConfigObject.TryItOutEnabled = true; - } - - /// - /// Limits the number of tagged operations displayed to at most this many. The default is to show all operations - /// - /// - /// - public static void MaxDisplayedTags(this SwaggerUIOptions options, int count) - { - options.ConfigObject.MaxDisplayedTags = count; - } - - /// - /// Controls the display of vendor extension (x-) fields and values for Operations, Parameters, and Schema - /// - /// - public static void ShowExtensions(this SwaggerUIOptions options) - { - options.ConfigObject.ShowExtensions = true; - } - - /// - /// Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters - /// - /// - public static void ShowCommonExtensions(this SwaggerUIOptions options) - { - options.ConfigObject.ShowCommonExtensions = true; - } - - /// - /// List of HTTP methods that have the Try it out feature enabled. An empty array disables Try it out for all operations. - /// This does not filter the operations from the display - /// - /// - /// - public static void SupportedSubmitMethods(this SwaggerUIOptions options, params SubmitMethod[] submitMethods) - { - options.ConfigObject.SupportedSubmitMethods = submitMethods; - } - - /// - /// OAuth redirect URL - /// - /// - /// - public static void OAuth2RedirectUrl(this SwaggerUIOptions options, string url) - { - options.ConfigObject.OAuth2RedirectUrl = url; - } + /// + /// Injects additional CSS stylesheets into the index.html page + /// + /// + /// A path to the stylesheet - i.e. the link "href" attribute + /// The target media - i.e. the link "media" attribute + public static void InjectStylesheet(this SwaggerUIOptions options, string path, string media = "screen") + { + var builder = new StringBuilder(options.HeadContent); + builder.AppendLine($""); + options.HeadContent = builder.ToString(); + } - [Obsolete("The validator is disabled by default. Use EnableValidator to enable it")] - public static void ValidatorUrl(this SwaggerUIOptions options, string url) - { - options.ConfigObject.ValidatorUrl = url; - } - - /// - /// You can use this parameter to enable the swagger-ui's built-in validator (badge) functionality - /// Setting it to null will disable validation - /// - /// - /// - public static void EnableValidator(this SwaggerUIOptions options, string url = "https://online.swagger.io/validator") - { - options.ConfigObject.ValidatorUrl = url; - } - - /// - /// Default clientId - /// - /// - /// - public static void OAuthClientId(this SwaggerUIOptions options, string value) - { - options.OAuthConfigObject.ClientId = value; - } - - /// - /// Default userName - /// - /// - /// - public static void OAuthUsername(this SwaggerUIOptions options, string value) - { - options.OAuthConfigObject.Username = value; - } - - /// - /// Default clientSecret - /// - /// - /// - /// Setting this exposes the client secrets in inline javascript in the swagger-ui generated html. - public static void OAuthClientSecret(this SwaggerUIOptions options, string value) - { - options.OAuthConfigObject.ClientSecret = value; - } - - /// - /// realm query parameter (for oauth1) added to authorizationUrl and tokenUrl - /// - /// - /// - public static void OAuthRealm(this SwaggerUIOptions options, string value) - { - options.OAuthConfigObject.Realm = value; - } - - /// - /// Application name, displayed in authorization popup - /// - /// - /// - public static void OAuthAppName(this SwaggerUIOptions options, string value) - { - options.OAuthConfigObject.AppName = value; - } - - /// - /// Scope separator for passing scopes, encoded before calling, default value is a space (encoded value %20) - /// - /// - /// - public static void OAuthScopeSeparator(this SwaggerUIOptions options, string value) - { - options.OAuthConfigObject.ScopeSeparator = value; - } + /// + /// Injects additional Javascript files into the index.html page + /// + /// + /// A path to the javascript - i.e. the script "src" attribute + /// The script type - i.e. the script "type" attribute + public static void InjectJavascript(this SwaggerUIOptions options, string path, string type = "text/javascript") + { + var builder = new StringBuilder(options.HeadContent); + builder.AppendLine($""); + options.HeadContent = builder.ToString(); + } - /// - /// String array of initially selected oauth scopes, default is empty array - /// - public static void OAuthScopes(this SwaggerUIOptions options, params string[] scopes) - { - options.OAuthConfigObject.Scopes = scopes; - } - - /// - /// Additional query parameters added to authorizationUrl and tokenUrl - /// - /// - /// - public static void OAuthAdditionalQueryStringParams( - this SwaggerUIOptions options, - Dictionary value) - { - options.OAuthConfigObject.AdditionalQueryStringParams = value; - } - - /// - /// Only activated for the accessCode flow. During the authorization_code request to the tokenUrl, - /// pass the Client Password using the HTTP Basic Authentication scheme (Authorization header with - /// Basic base64encoded[client_id:client_secret]). The default is false - /// - /// - public static void OAuthUseBasicAuthenticationWithAccessCodeGrant(this SwaggerUIOptions options) - { - options.OAuthConfigObject.UseBasicAuthenticationWithAccessCodeGrant = true; - } - - /// - /// Only applies to authorizatonCode flows. Proof Key for Code Exchange brings enhanced security for OAuth public clients. - /// The default is false - /// - /// - public static void OAuthUsePkce(this SwaggerUIOptions options) - { - options.OAuthConfigObject.UsePkceWithAuthorizationCodeGrant = true; - } - - /// - /// Function to intercept remote definition, "Try it out", and OAuth 2.0 requests. - /// - /// - /// MUST be a valid Javascript function: (request: SwaggerRequest) => SwaggerRequest - public static void UseRequestInterceptor(this SwaggerUIOptions options, string value) - { - options.Interceptors.RequestInterceptorFunction = value; - } - - /// - /// Function to intercept remote definition, "Try it out", and OAuth 2.0 responses. - /// - /// - /// MUST be a valid Javascript function: (response: SwaggerResponse ) => SwaggerResponse - public static void UseResponseInterceptor(this SwaggerUIOptions options, string value) - { - options.Interceptors.ResponseInterceptorFunction = value; - } - - /// - /// Function to enable the option to expose the available - /// Swagger document urls to external parties. - /// - /// - public static void EnableSwaggerDocumentUrlsEndpoint(this SwaggerUIOptions options) + /// + /// Adds Swagger JSON endpoints. Can be fully-qualified or relative to the UI page + /// + /// + /// Can be fully qualified or relative to the current host + /// The description that appears in the document selector drop-down + public static void SwaggerEndpoint(this SwaggerUIOptions options, string url, string name) + { + var urls = new List(options.ConfigObject.Urls ?? []) { - options.ExposeSwaggerDocumentUrlsRoute = true; - } + new() { Url = url, Name = name } + }; + options.ConfigObject.Urls = urls; + } + + /// + /// Enables deep linking for tags and operations + /// + /// + public static void EnableDeepLinking(this SwaggerUIOptions options) + { + options.ConfigObject.DeepLinking = true; + } + /// + /// Enables persist authorization data + /// + /// + public static void EnablePersistAuthorization(this SwaggerUIOptions options) + { + options.ConfigObject.PersistAuthorization = true; + } + + /// + /// Controls the display of operationId in operations list + /// + /// + public static void DisplayOperationId(this SwaggerUIOptions options) + { + options.ConfigObject.DisplayOperationId = true; + } + + /// + /// The default expansion depth for models (set to -1 completely hide the models) + /// + /// + /// + public static void DefaultModelsExpandDepth(this SwaggerUIOptions options, int depth) + { + options.ConfigObject.DefaultModelsExpandDepth = depth; + } + + /// + /// The default expansion depth for the model on the model-example section + /// + /// + /// + public static void DefaultModelExpandDepth(this SwaggerUIOptions options, int depth) + { + options.ConfigObject.DefaultModelExpandDepth = depth; + } + + /// + /// Controls how the model is shown when the API is first rendered. + /// (The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links.) + /// + /// + /// + public static void DefaultModelRendering(this SwaggerUIOptions options, ModelRendering modelRendering) + { + options.ConfigObject.DefaultModelRendering = modelRendering; + } + + /// + /// Controls the display of the request duration (in milliseconds) for Try-It-Out requests + /// + /// + public static void DisplayRequestDuration(this SwaggerUIOptions options) + { + options.ConfigObject.DisplayRequestDuration = true; + } + + /// + /// Controls the default expansion setting for the operations and tags. + /// It can be 'List' (expands only the tags), 'Full' (expands the tags and operations) or 'None' (expands nothing) + /// + /// + /// + public static void DocExpansion(this SwaggerUIOptions options, DocExpansion docExpansion) + { + options.ConfigObject.DocExpansion = docExpansion; + } + + /// + /// Enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. + /// If an expression is provided it will be used and applied initially. + /// Filtering is case sensitive matching the filter expression anywhere inside the tag + /// + /// + /// + public static void EnableFilter(this SwaggerUIOptions options, string expression = null) + { + options.ConfigObject.Filter = expression ?? ""; + } + + /// + /// Enables the "Try it out" section by default. + /// + /// + public static void EnableTryItOutByDefault(this SwaggerUIOptions options) + { + options.ConfigObject.TryItOutEnabled = true; + } + + /// + /// Limits the number of tagged operations displayed to at most this many. The default is to show all operations + /// + /// + /// + public static void MaxDisplayedTags(this SwaggerUIOptions options, int count) + { + options.ConfigObject.MaxDisplayedTags = count; + } + + /// + /// Controls the display of vendor extension (x-) fields and values for Operations, Parameters, and Schema + /// + /// + public static void ShowExtensions(this SwaggerUIOptions options) + { + options.ConfigObject.ShowExtensions = true; + } + + /// + /// Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters + /// + /// + public static void ShowCommonExtensions(this SwaggerUIOptions options) + { + options.ConfigObject.ShowCommonExtensions = true; + } + + /// + /// List of HTTP methods that have the Try it out feature enabled. An empty array disables Try it out for all operations. + /// This does not filter the operations from the display + /// + /// + /// + public static void SupportedSubmitMethods(this SwaggerUIOptions options, params SubmitMethod[] submitMethods) + { + options.ConfigObject.SupportedSubmitMethods = submitMethods; + } + + /// + /// OAuth redirect URL + /// + /// + /// + public static void OAuth2RedirectUrl(this SwaggerUIOptions options, string url) + { + options.ConfigObject.OAuth2RedirectUrl = url; + } + + [Obsolete("The validator is disabled by default. Use EnableValidator to enable it")] + public static void ValidatorUrl(this SwaggerUIOptions options, string url) + { + options.ConfigObject.ValidatorUrl = url; + } + + /// + /// You can use this parameter to enable the swagger-ui's built-in validator (badge) functionality + /// Setting it to null will disable validation + /// + /// + /// + public static void EnableValidator(this SwaggerUIOptions options, string url = "https://online.swagger.io/validator") + { + options.ConfigObject.ValidatorUrl = url; + } + + /// + /// Default clientId + /// + /// + /// + public static void OAuthClientId(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.ClientId = value; + } + + /// + /// Default userName + /// + /// + /// + public static void OAuthUsername(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.Username = value; + } + + /// + /// Default clientSecret + /// + /// + /// + /// Setting this exposes the client secrets in inline javascript in the swagger-ui generated html. + public static void OAuthClientSecret(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.ClientSecret = value; + } + + /// + /// realm query parameter (for oauth1) added to authorizationUrl and tokenUrl + /// + /// + /// + public static void OAuthRealm(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.Realm = value; + } + + /// + /// Application name, displayed in authorization popup + /// + /// + /// + public static void OAuthAppName(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.AppName = value; + } + + /// + /// Scope separator for passing scopes, encoded before calling, default value is a space (encoded value %20) + /// + /// + /// + public static void OAuthScopeSeparator(this SwaggerUIOptions options, string value) + { + options.OAuthConfigObject.ScopeSeparator = value; + } + + /// + /// String array of initially selected oauth scopes, default is empty array + /// + public static void OAuthScopes(this SwaggerUIOptions options, params string[] scopes) + { + options.OAuthConfigObject.Scopes = scopes; + } + + /// + /// Additional query parameters added to authorizationUrl and tokenUrl + /// + /// + /// + public static void OAuthAdditionalQueryStringParams( + this SwaggerUIOptions options, + Dictionary value) + { + options.OAuthConfigObject.AdditionalQueryStringParams = value; + } + + /// + /// Only activated for the accessCode flow. During the authorization_code request to the tokenUrl, + /// pass the Client Password using the HTTP Basic Authentication scheme (Authorization header with + /// Basic base64encoded[client_id:client_secret]). The default is false + /// + /// + public static void OAuthUseBasicAuthenticationWithAccessCodeGrant(this SwaggerUIOptions options) + { + options.OAuthConfigObject.UseBasicAuthenticationWithAccessCodeGrant = true; + } + + /// + /// Only applies to authorizatonCode flows. Proof Key for Code Exchange brings enhanced security for OAuth public clients. + /// The default is false + /// + /// + public static void OAuthUsePkce(this SwaggerUIOptions options) + { + options.OAuthConfigObject.UsePkceWithAuthorizationCodeGrant = true; + } + + /// + /// Function to intercept remote definition, "Try it out", and OAuth 2.0 requests. + /// + /// + /// MUST be a valid Javascript function: (request: SwaggerRequest) => SwaggerRequest + public static void UseRequestInterceptor(this SwaggerUIOptions options, string value) + { + options.Interceptors.RequestInterceptorFunction = value; + } + + /// + /// Function to intercept remote definition, "Try it out", and OAuth 2.0 responses. + /// + /// + /// MUST be a valid Javascript function: (response: SwaggerResponse ) => SwaggerResponse + public static void UseResponseInterceptor(this SwaggerUIOptions options, string value) + { + options.Interceptors.ResponseInterceptorFunction = value; + } + + /// + /// Function to enable the option to expose the available + /// Swagger document urls to external parties. + /// + /// + public static void EnableSwaggerDocumentUrlsEndpoint(this SwaggerUIOptions options) + { + options.ExposeSwaggerDocumentUrlsRoute = true; } } diff --git a/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsDocumentFilterTests.cs b/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsDocumentFilterTests.cs index 5c561d5cfa..9349fe26eb 100644 --- a/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsDocumentFilterTests.cs +++ b/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsDocumentFilterTests.cs @@ -3,30 +3,29 @@ using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.TestSupport; -namespace Swashbuckle.AspNetCore.Annotations.Test +namespace Swashbuckle.AspNetCore.Annotations.Test; + +public class AnnotationsDocumentFilterTests { - public class AnnotationsDocumentFilterTests + [Fact] + public void Apply_CreatesMetadataForControllerNameTag_FromSwaggerTagAttribute() { - [Fact] - public void Apply_CreatesMetadataForControllerNameTag_FromSwaggerTagAttribute() - { - var document = new OpenApiDocument(); - var apiDescription = ApiDescriptionFactory.Create(c => nameof(c.ActionWithNoAttributes)); - var filterContext = new DocumentFilterContext( - apiDescriptions: new[] { apiDescription }, - schemaGenerator: null, - schemaRepository: null); + var document = new OpenApiDocument(); + var apiDescription = ApiDescriptionFactory.Create(c => nameof(c.ActionWithNoAttributes)); + var filterContext = new DocumentFilterContext( + apiDescriptions: new[] { apiDescription }, + schemaGenerator: null, + schemaRepository: null); - Subject().Apply(document, filterContext); + Subject().Apply(document, filterContext); - var tag = document.Tags.Single(t => t.Name == "FakeControllerWithSwaggerAnnotations"); - Assert.Equal("Description for FakeControllerWithSwaggerAnnotations", tag.Description); - Assert.Equal("http://tempuri.org/", tag.ExternalDocs.Url.ToString()); - } + var tag = document.Tags.Single(t => t.Name == "FakeControllerWithSwaggerAnnotations"); + Assert.Equal("Description for FakeControllerWithSwaggerAnnotations", tag.Description); + Assert.Equal("http://tempuri.org/", tag.ExternalDocs.Url.ToString()); + } - private AnnotationsDocumentFilter Subject() - { - return new AnnotationsDocumentFilter(); - } + private AnnotationsDocumentFilter Subject() + { + return new AnnotationsDocumentFilter(); } } diff --git a/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsOperationFilterTests.cs b/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsOperationFilterTests.cs index 2a2b03a5b4..3478d7b284 100644 --- a/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsOperationFilterTests.cs +++ b/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsOperationFilterTests.cs @@ -6,165 +6,164 @@ using Swashbuckle.AspNetCore.SwaggerGen; using Xunit; -namespace Swashbuckle.AspNetCore.Annotations.Test +namespace Swashbuckle.AspNetCore.Annotations.Test; + +public class AnnotationsOperationFilterTests { - public class AnnotationsOperationFilterTests + [Fact] + public void Apply_EnrichesOperationMetadata_IfActionDecoratedWithSwaggerOperationAttribute() { - [Fact] - public void Apply_EnrichesOperationMetadata_IfActionDecoratedWithSwaggerOperationAttribute() - { - var operation = new OpenApiOperation(); - var methodInfo = typeof(FakeControllerWithSwaggerAnnotations) - .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerOperationAttribute)); - var filterContext = new OperationFilterContext( - apiDescription: null, - schemaRegistry: null, - schemaRepository: null, - methodInfo: methodInfo); - - Subject().Apply(operation, filterContext); - - Assert.Equal("Summary for ActionWithSwaggerOperationAttribute", operation.Summary); - Assert.Equal("Description for ActionWithSwaggerOperationAttribute", operation.Description); - Assert.Equal("actionWithSwaggerOperationAttribute", operation.OperationId); - Assert.Equal(["foobar"], [.. operation.Tags.Select(t => t.Name)]); - } - - [Fact] - public void Apply_EnrichesResponseMetadata_IfActionDecoratedWithSwaggerResponseAttribute() + var operation = new OpenApiOperation(); + var methodInfo = typeof(FakeControllerWithSwaggerAnnotations) + .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerOperationAttribute)); + var filterContext = new OperationFilterContext( + apiDescription: null, + schemaRegistry: null, + schemaRepository: null, + methodInfo: methodInfo); + + Subject().Apply(operation, filterContext); + + Assert.Equal("Summary for ActionWithSwaggerOperationAttribute", operation.Summary); + Assert.Equal("Description for ActionWithSwaggerOperationAttribute", operation.Description); + Assert.Equal("actionWithSwaggerOperationAttribute", operation.OperationId); + Assert.Equal(["foobar"], [.. operation.Tags.Select(t => t.Name)]); + } + + [Fact] + public void Apply_EnrichesResponseMetadata_IfActionDecoratedWithSwaggerResponseAttribute() + { + var operation = new OpenApiOperation { - var operation = new OpenApiOperation + Responses = new OpenApiResponses { - Responses = new OpenApiResponses - { - { "204", new OpenApiResponse { } }, - { "400", new OpenApiResponse { } }, - } - }; - var methodInfo = typeof(FakeControllerWithSwaggerAnnotations) - .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerResponseAttributes)); - var filterContext = new OperationFilterContext( - apiDescription: null, - schemaRegistry: null, - schemaRepository: null, - methodInfo: methodInfo); - - Subject().Apply(operation, filterContext); - - Assert.Equal(["204", "400", "500"], [.. operation.Responses.Keys]); - var response1 = operation.Responses["204"]; - Assert.Equal("Description for 204 response", response1.Description); - var response2 = operation.Responses["400"]; - Assert.Equal("Description for 400 response", response2.Description); - var response3 = operation.Responses["500"]; - Assert.Equal("Description for 500 response", response3.Description); - } - - [Fact] - public void Apply_EnrichesResponseMetadata_IfActionDecoratedWithSwaggerResponseContentTypesAttribute() + { "204", new OpenApiResponse { } }, + { "400", new OpenApiResponse { } }, + } + }; + var methodInfo = typeof(FakeControllerWithSwaggerAnnotations) + .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerResponseAttributes)); + var filterContext = new OperationFilterContext( + apiDescription: null, + schemaRegistry: null, + schemaRepository: null, + methodInfo: methodInfo); + + Subject().Apply(operation, filterContext); + + Assert.Equal(["204", "400", "500"], [.. operation.Responses.Keys]); + var response1 = operation.Responses["204"]; + Assert.Equal("Description for 204 response", response1.Description); + var response2 = operation.Responses["400"]; + Assert.Equal("Description for 400 response", response2.Description); + var response3 = operation.Responses["500"]; + Assert.Equal("Description for 500 response", response3.Description); + } + + [Fact] + public void Apply_EnrichesResponseMetadata_IfActionDecoratedWithSwaggerResponseContentTypesAttribute() + { + var operation = new OpenApiOperation { - var operation = new OpenApiOperation + Responses = new OpenApiResponses { - Responses = new OpenApiResponses - { - { "200", new OpenApiResponse { } }, - } - }; - var methodInfo = typeof(FakeControllerWithSwaggerAnnotations) - .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerResponseContentTypesAttributes)); - var filterContext = new OperationFilterContext( - apiDescription: null, - schemaRegistry: new SchemaGenerator(new SchemaGeneratorOptions(), new JsonSerializerDataContractResolver(new JsonSerializerOptions())), - schemaRepository: new SchemaRepository(), - methodInfo: methodInfo); - - Subject().Apply(operation, filterContext); - - Assert.Equal(["200", "500"], [.. operation.Responses.Keys]); - var response1 = operation.Responses["200"]; - Assert.Equal("Description for 200 response", response1.Description); - Assert.NotNull(response1.Content); - Assert.Equal(2, response1.Content.Count); - var jsonContent = response1.Content.First(); - var xmlContent = response1.Content.Last(); - Assert.Equal("application/json", jsonContent.Key); - Assert.Equal(JsonSchemaTypes.String, jsonContent.Value.Schema.Type); - Assert.Equal("application/xml", xmlContent.Key); - Assert.Equal(JsonSchemaTypes.String, xmlContent.Value.Schema.Type); - } - - [Fact] - public void Apply_DelegatesToSpecifiedFilter_IfControllerDecoratedWithSwaggerOperationFilterAttribute() - { - var operation = new OpenApiOperation(); - var methodInfo = typeof(FakeControllerWithSwaggerAnnotations).GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithNoAttributes)); - var filterContext = new OperationFilterContext( - apiDescription: null, - schemaRegistry: null, - schemaRepository: null, - methodInfo: methodInfo); + { "200", new OpenApiResponse { } }, + } + }; + var methodInfo = typeof(FakeControllerWithSwaggerAnnotations) + .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerResponseContentTypesAttributes)); + var filterContext = new OperationFilterContext( + apiDescription: null, + schemaRegistry: new SchemaGenerator(new SchemaGeneratorOptions(), new JsonSerializerDataContractResolver(new JsonSerializerOptions())), + schemaRepository: new SchemaRepository(), + methodInfo: methodInfo); + + Subject().Apply(operation, filterContext); + + Assert.Equal(["200", "500"], [.. operation.Responses.Keys]); + var response1 = operation.Responses["200"]; + Assert.Equal("Description for 200 response", response1.Description); + Assert.NotNull(response1.Content); + Assert.Equal(2, response1.Content.Count); + var jsonContent = response1.Content.First(); + var xmlContent = response1.Content.Last(); + Assert.Equal("application/json", jsonContent.Key); + Assert.Equal(JsonSchemaTypes.String, jsonContent.Value.Schema.Type); + Assert.Equal("application/xml", xmlContent.Key); + Assert.Equal(JsonSchemaTypes.String, xmlContent.Value.Schema.Type); + } - Subject().Apply(operation, filterContext); + [Fact] + public void Apply_DelegatesToSpecifiedFilter_IfControllerDecoratedWithSwaggerOperationFilterAttribute() + { + var operation = new OpenApiOperation(); + var methodInfo = typeof(FakeControllerWithSwaggerAnnotations).GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithNoAttributes)); + var filterContext = new OperationFilterContext( + apiDescription: null, + schemaRegistry: null, + schemaRepository: null, + methodInfo: methodInfo); - Assert.NotEmpty(operation.Extensions); - } + Subject().Apply(operation, filterContext); - [Fact] - public void Apply_DelegatesToSpecifiedFilter_IfActionDecoratedWithSwaggerOperationFilterAttribute() - { - var operation = new OpenApiOperation(); - var methodInfo = typeof(FakeControllerWithSwaggerAnnotations).GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerOperationFilterAttribute)); - var filterContext = new OperationFilterContext( - apiDescription: null, - schemaRegistry: null, - schemaRepository: null, - methodInfo: methodInfo); + Assert.NotEmpty(operation.Extensions); + } + + [Fact] + public void Apply_DelegatesToSpecifiedFilter_IfActionDecoratedWithSwaggerOperationFilterAttribute() + { + var operation = new OpenApiOperation(); + var methodInfo = typeof(FakeControllerWithSwaggerAnnotations).GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerOperationFilterAttribute)); + var filterContext = new OperationFilterContext( + apiDescription: null, + schemaRegistry: null, + schemaRepository: null, + methodInfo: methodInfo); - Subject().Apply(operation, filterContext); + Subject().Apply(operation, filterContext); - Assert.NotEmpty(operation.Extensions); - } + Assert.NotEmpty(operation.Extensions); + } - [Fact] - public void Apply_EnrichesOperationMetadata_IfMinimalActionDecoratedWithSwaggerOperationAttribute() + [Fact] + public void Apply_EnrichesOperationMetadata_IfMinimalActionDecoratedWithSwaggerOperationAttribute() + { + var operationAttribute = new SwaggerOperationAttribute("Summary for ActionWithSwaggerOperationAttribute") { - var operationAttribute = new SwaggerOperationAttribute("Summary for ActionWithSwaggerOperationAttribute") - { - Description = "Description for ActionWithSwaggerOperationAttribute", - OperationId = "actionWithSwaggerOperationAttribute", - Tags = ["foobar"] - }; + Description = "Description for ActionWithSwaggerOperationAttribute", + OperationId = "actionWithSwaggerOperationAttribute", + Tags = ["foobar"] + }; - var action = RequestDelegateFactory.Create((string parameter) => "{}"); + var action = RequestDelegateFactory.Create((string parameter) => "{}"); - var operation = new OpenApiOperation(); - var methodInfo = action.RequestDelegate.Method; + var operation = new OpenApiOperation(); + var methodInfo = action.RequestDelegate.Method; - var apiDescription = new ApiDescription() - { - ActionDescriptor = new ActionDescriptor() - { - EndpointMetadata = [operationAttribute] - } - }; - - var filterContext = new OperationFilterContext( - apiDescription: apiDescription, - schemaRegistry: null, - schemaRepository: null, - methodInfo: methodInfo); - - Subject().Apply(operation, filterContext); - - Assert.Equal("Summary for ActionWithSwaggerOperationAttribute", operation.Summary); - Assert.Equal("Description for ActionWithSwaggerOperationAttribute", operation.Description); - Assert.Equal("actionWithSwaggerOperationAttribute", operation.OperationId); - Assert.Equal(["foobar"], [.. operation.Tags.Select(t => t.Name)]); - } - - private static AnnotationsOperationFilter Subject() + var apiDescription = new ApiDescription() { - return new(); - } + ActionDescriptor = new ActionDescriptor() + { + EndpointMetadata = [operationAttribute] + } + }; + + var filterContext = new OperationFilterContext( + apiDescription: apiDescription, + schemaRegistry: null, + schemaRepository: null, + methodInfo: methodInfo); + + Subject().Apply(operation, filterContext); + + Assert.Equal("Summary for ActionWithSwaggerOperationAttribute", operation.Summary); + Assert.Equal("Description for ActionWithSwaggerOperationAttribute", operation.Description); + Assert.Equal("actionWithSwaggerOperationAttribute", operation.OperationId); + Assert.Equal(["foobar"], [.. operation.Tags.Select(t => t.Name)]); + } + + private static AnnotationsOperationFilter Subject() + { + return new(); } } diff --git a/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsParameterFilterTests.cs b/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsParameterFilterTests.cs index ab3a71c883..48af7a7665 100644 --- a/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsParameterFilterTests.cs +++ b/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsParameterFilterTests.cs @@ -3,67 +3,66 @@ using Xunit; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.Annotations.Test +namespace Swashbuckle.AspNetCore.Annotations.Test; + +public class AnnotationsParameterFilterTests { - public class AnnotationsParameterFilterTests + [Fact] + public void Apply_EnrichesParameterMetadata_IfParameterDecoratedWithSwaggerParameterAttribute() { - [Fact] - public void Apply_EnrichesParameterMetadata_IfParameterDecoratedWithSwaggerParameterAttribute() - { - var parameter = new OpenApiParameter { }; - var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) - .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerParameterAttribute)) - .GetParameters()[0]; - var filterContext = new ParameterFilterContext( - apiParameterDescription: null, - schemaGenerator: null, - schemaRepository: null, - parameterInfo: parameterInfo); + var parameter = new OpenApiParameter { }; + var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) + .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerParameterAttribute)) + .GetParameters()[0]; + var filterContext = new ParameterFilterContext( + apiParameterDescription: null, + schemaGenerator: null, + schemaRepository: null, + parameterInfo: parameterInfo); - Subject().Apply(parameter, filterContext); + Subject().Apply(parameter, filterContext); - Assert.Equal("Description for param", parameter.Description); - Assert.True(parameter.Required); - } + Assert.Equal("Description for param", parameter.Description); + Assert.True(parameter.Required); + } - [Fact] - public void Apply_EnrichesParameterMetadata_IfPropertyDecoratedWithSwaggerParameterAttribute() - { - var parameter = new OpenApiParameter(); - var propertyInfo = typeof(SwaggerAnnotatedType).GetProperty(nameof(SwaggerAnnotatedType.StringWithSwaggerParameterAttribute)); - var filterContext = new ParameterFilterContext( - apiParameterDescription: new ApiParameterDescription(), - schemaGenerator: null, - schemaRepository: null, - propertyInfo: propertyInfo); + [Fact] + public void Apply_EnrichesParameterMetadata_IfPropertyDecoratedWithSwaggerParameterAttribute() + { + var parameter = new OpenApiParameter(); + var propertyInfo = typeof(SwaggerAnnotatedType).GetProperty(nameof(SwaggerAnnotatedType.StringWithSwaggerParameterAttribute)); + var filterContext = new ParameterFilterContext( + apiParameterDescription: new ApiParameterDescription(), + schemaGenerator: null, + schemaRepository: null, + propertyInfo: propertyInfo); - Subject().Apply(parameter, filterContext); + Subject().Apply(parameter, filterContext); - Assert.Equal("Description for StringWithSwaggerParameterAttribute", parameter.Description); - Assert.True(parameter.Required); - } + Assert.Equal("Description for StringWithSwaggerParameterAttribute", parameter.Description); + Assert.True(parameter.Required); + } - [Fact] - public void Apply_DoesNotModifyTheRequiredFlag_IfNotSpecifiedWithSwaggerParameterAttribute() - { - var parameter = new OpenApiParameter { Required = true }; - var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) - .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerParameterAttributeDescriptionOnly)) - .GetParameters()[0]; - var filterContext = new ParameterFilterContext( - apiParameterDescription: null, - schemaGenerator: null, - schemaRepository: null, - parameterInfo: parameterInfo); + [Fact] + public void Apply_DoesNotModifyTheRequiredFlag_IfNotSpecifiedWithSwaggerParameterAttribute() + { + var parameter = new OpenApiParameter { Required = true }; + var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) + .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerParameterAttributeDescriptionOnly)) + .GetParameters()[0]; + var filterContext = new ParameterFilterContext( + apiParameterDescription: null, + schemaGenerator: null, + schemaRepository: null, + parameterInfo: parameterInfo); - Subject().Apply(parameter, filterContext); + Subject().Apply(parameter, filterContext); - Assert.True(parameter.Required); - } + Assert.True(parameter.Required); + } - private AnnotationsParameterFilter Subject() - { - return new AnnotationsParameterFilter(); - } + private AnnotationsParameterFilter Subject() + { + return new AnnotationsParameterFilter(); } } diff --git a/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsRequestBodyFilterTests.cs b/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsRequestBodyFilterTests.cs index 0ab2d4bed4..fc2a93f672 100644 --- a/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsRequestBodyFilterTests.cs +++ b/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsRequestBodyFilterTests.cs @@ -8,91 +8,90 @@ using Swashbuckle.AspNetCore.TestSupport; using Xunit; -namespace Swashbuckle.AspNetCore.Annotations.Test +namespace Swashbuckle.AspNetCore.Annotations.Test; + +public class AnnotationsRequestBodyFilterTests { - public class AnnotationsRequestBodyFilterTests + [Fact] + public void Apply_EnrichesRequestBodyMetadata_IfControllerParameterDecoratedWithSwaggerRequestBodyAttribute() { - [Fact] - public void Apply_EnrichesRequestBodyMetadata_IfControllerParameterDecoratedWithSwaggerRequestBodyAttribute() + var requestBody = new OpenApiRequestBody(); + var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) + .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerRequestBodyAttribute)) + .GetParameters()[0]; + var bodyParameterDescription = new ApiParameterDescription { - var requestBody = new OpenApiRequestBody(); - var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) - .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerRequestBodyAttribute)) - .GetParameters()[0]; - var bodyParameterDescription = new ApiParameterDescription - { - ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo } - }; - var context = new RequestBodyFilterContext(bodyParameterDescription, null, null, null); + ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo } + }; + var context = new RequestBodyFilterContext(bodyParameterDescription, null, null, null); - Subject().Apply(requestBody, context); + Subject().Apply(requestBody, context); - Assert.Equal("Description for param", requestBody.Description); - Assert.True(requestBody.Required); - } + Assert.Equal("Description for param", requestBody.Description); + Assert.True(requestBody.Required); + } - [Fact] - public void Apply_EnrichesRequestBodyMetadata_IfEndpointParameterDecoratedWithSwaggerRequestBodyAttribute() + [Fact] + public void Apply_EnrichesRequestBodyMetadata_IfEndpointParameterDecoratedWithSwaggerRequestBodyAttribute() + { + var requestBody = new OpenApiRequestBody(); + var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) + .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerRequestBodyAttribute)) + .GetParameters()[0]; + var bodyParameterDescription = new ApiParameterDescription { - var requestBody = new OpenApiRequestBody(); - var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) - .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerRequestBodyAttribute)) - .GetParameters()[0]; - var bodyParameterDescription = new ApiParameterDescription - { - ParameterDescriptor = new CustomParameterDescriptor { ParameterInfo = parameterInfo } - }; - var context = new RequestBodyFilterContext(bodyParameterDescription, null, null, null); + ParameterDescriptor = new CustomParameterDescriptor { ParameterInfo = parameterInfo } + }; + var context = new RequestBodyFilterContext(bodyParameterDescription, null, null, null); - Subject().Apply(requestBody, context); + Subject().Apply(requestBody, context); - Assert.Equal("Description for param", requestBody.Description); - Assert.True(requestBody.Required); - } + Assert.Equal("Description for param", requestBody.Description); + Assert.True(requestBody.Required); + } - [Fact] - public void Apply_EnrichesParameterMetadata_IfPropertyDecoratedWithSwaggerRequestBodyAttribute() + [Fact] + public void Apply_EnrichesParameterMetadata_IfPropertyDecoratedWithSwaggerRequestBodyAttribute() + { + var requestBody = new OpenApiRequestBody(); + var bodyParameterDescription = new ApiParameterDescription { - var requestBody = new OpenApiRequestBody(); - var bodyParameterDescription = new ApiParameterDescription - { - ModelMetadata = ModelMetadataFactory.CreateForProperty(typeof(SwaggerAnnotatedType), nameof(SwaggerAnnotatedType.StringWithSwaggerRequestBodyAttribute)) - }; - var context = new RequestBodyFilterContext(bodyParameterDescription, null, null, null); + ModelMetadata = ModelMetadataFactory.CreateForProperty(typeof(SwaggerAnnotatedType), nameof(SwaggerAnnotatedType.StringWithSwaggerRequestBodyAttribute)) + }; + var context = new RequestBodyFilterContext(bodyParameterDescription, null, null, null); - Subject().Apply(requestBody, context); - - Assert.Equal("Description for StringWithSwaggerRequestBodyAttribute", requestBody.Description); - Assert.True(requestBody.Required); - } + Subject().Apply(requestBody, context); + + Assert.Equal("Description for StringWithSwaggerRequestBodyAttribute", requestBody.Description); + Assert.True(requestBody.Required); + } - [Fact] - public void Apply_DoesNotModifyTheRequiredFlag_IfNotSpecifiedWithSwaggerParameterAttribute() - { + [Fact] + public void Apply_DoesNotModifyTheRequiredFlag_IfNotSpecifiedWithSwaggerParameterAttribute() + { - var requestBody = new OpenApiRequestBody { Required = true }; - var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) - .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerRequestbodyAttributeDescriptionOnly)) - .GetParameters()[0]; - var bodyParameterDescription = new ApiParameterDescription - { - ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo } - }; - var context = new RequestBodyFilterContext(bodyParameterDescription, null, null, null); + var requestBody = new OpenApiRequestBody { Required = true }; + var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) + .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerRequestbodyAttributeDescriptionOnly)) + .GetParameters()[0]; + var bodyParameterDescription = new ApiParameterDescription + { + ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo } + }; + var context = new RequestBodyFilterContext(bodyParameterDescription, null, null, null); - Subject().Apply(requestBody, context); + Subject().Apply(requestBody, context); - Assert.True(requestBody.Required); - } + Assert.True(requestBody.Required); + } - private AnnotationsRequestBodyFilter Subject() - { - return new AnnotationsRequestBodyFilter(); - } + private AnnotationsRequestBodyFilter Subject() + { + return new AnnotationsRequestBodyFilter(); + } - private sealed class CustomParameterDescriptor : ParameterDescriptor, IParameterInfoParameterDescriptor - { - public ParameterInfo ParameterInfo { get; set; } - } + private sealed class CustomParameterDescriptor : ParameterDescriptor, IParameterInfoParameterDescriptor + { + public ParameterInfo ParameterInfo { get; set; } } } diff --git a/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsSchemaFilterTests.cs b/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsSchemaFilterTests.cs index e99bf8255b..69136a9b55 100644 --- a/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsSchemaFilterTests.cs +++ b/test/Swashbuckle.AspNetCore.Annotations.Test/AnnotationsSchemaFilterTests.cs @@ -3,110 +3,109 @@ using Swashbuckle.AspNetCore.SwaggerGen; using Xunit; -namespace Swashbuckle.AspNetCore.Annotations.Test +namespace Swashbuckle.AspNetCore.Annotations.Test; + +public class AnnotationsSchemaFilterTests { - public class AnnotationsSchemaFilterTests + [Theory] + [InlineData(typeof(SwaggerAnnotatedType))] + [InlineData(typeof(SwaggerAnnotatedStruct))] + public void Apply_EnrichesSchemaMetadata_IfTypeDecoratedWithSwaggerSchemaAttribute(Type type) + { + var schema = new OpenApiSchema(); + var context = new SchemaFilterContext(type: type, schemaGenerator: null, schemaRepository: null); + + Subject().Apply(schema, context); + + Assert.Equal($"Description for {type.Name}", schema.Description); + Assert.Equal(new[] { "StringWithSwaggerSchemaAttribute" }, schema.Required); + Assert.Equal($"Title for {type.Name}", schema.Title); + } + + [Fact] + public void Apply_EnrichesSchemaMetadata_IfParameterDecoratedWithSwaggerSchemaAttribute() + { + var schema = new OpenApiSchema(); + var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) + .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerSchemaAttribute)) + .GetParameters()[0]; + var context = new SchemaFilterContext( + type: parameterInfo.ParameterType, + schemaGenerator: null, + schemaRepository: null, + parameterInfo: parameterInfo); + + Subject().Apply(schema, context); + + Assert.Equal($"Description for param", schema.Description); + Assert.Equal("date", schema.Format); + } + + [Theory] + [InlineData(typeof(SwaggerAnnotatedType), nameof(SwaggerAnnotatedType.StringWithSwaggerSchemaAttribute), true, true, false)] + [InlineData(typeof(SwaggerAnnotatedStruct), nameof(SwaggerAnnotatedStruct.StringWithSwaggerSchemaAttribute), true, true, false)] + public void Apply_EnrichesSchemaMetadata_IfPropertyDecoratedWithSwaggerSchemaAttribute( + Type declaringType, + string propertyName, + bool expectedReadOnly, + bool expectedWriteOnly, + bool expectedNullable) + { + var schema = new OpenApiSchema { Nullable = true }; + var propertyInfo = declaringType + .GetProperty(propertyName); + var context = new SchemaFilterContext( + type: propertyInfo.PropertyType, + schemaGenerator: null, + schemaRepository: null, + memberInfo: propertyInfo); + + Subject().Apply(schema, context); + + Assert.Equal($"Description for {propertyName}", schema.Description); + Assert.Equal("date", schema.Format); + Assert.Equal(expectedReadOnly, schema.ReadOnly); + Assert.Equal(expectedWriteOnly, schema.WriteOnly); + Assert.Equal(expectedNullable, schema.Nullable); + } + + [Fact] + public void Apply_DoesNotModifyFlags_IfNotSpecifiedWithSwaggerSchemaAttribute() + { + var schema = new OpenApiSchema { ReadOnly = true, WriteOnly = true, Nullable = true }; + var propertyInfo = typeof(SwaggerAnnotatedType) + .GetProperty(nameof(SwaggerAnnotatedType.StringWithSwaggerSchemaAttributeDescriptionOnly)); + var context = new SchemaFilterContext( + type: propertyInfo.PropertyType, + schemaGenerator: null, + schemaRepository: null, + memberInfo: propertyInfo); + + Subject().Apply(schema, context); + + Assert.True(schema.ReadOnly); + Assert.True(schema.WriteOnly); + Assert.True(schema.Nullable); + } + + [Theory] + [InlineData(typeof(SwaggerAnnotatedType))] + [InlineData(typeof(SwaggerAnnotatedStruct))] + public void Apply_DelegatesToSpecifiedFilter_IfTypeDecoratedWithFilterAttribute(Type type) + { + var schema = new OpenApiSchema(); + var context = new SchemaFilterContext(type: type, schemaGenerator: null, schemaRepository: null); + + Subject().Apply(schema, context); + + Assert.NotEmpty(schema.Extensions); + } + + private static AnnotationsSchemaFilter Subject() { - [Theory] - [InlineData(typeof(SwaggerAnnotatedType))] - [InlineData(typeof(SwaggerAnnotatedStruct))] - public void Apply_EnrichesSchemaMetadata_IfTypeDecoratedWithSwaggerSchemaAttribute(Type type) - { - var schema = new OpenApiSchema(); - var context = new SchemaFilterContext(type: type, schemaGenerator: null, schemaRepository: null); - - Subject().Apply(schema, context); - - Assert.Equal($"Description for {type.Name}", schema.Description); - Assert.Equal(new[] { "StringWithSwaggerSchemaAttribute" }, schema.Required); - Assert.Equal($"Title for {type.Name}", schema.Title); - } - - [Fact] - public void Apply_EnrichesSchemaMetadata_IfParameterDecoratedWithSwaggerSchemaAttribute() - { - var schema = new OpenApiSchema(); - var parameterInfo = typeof(FakeControllerWithSwaggerAnnotations) - .GetMethod(nameof(FakeControllerWithSwaggerAnnotations.ActionWithSwaggerSchemaAttribute)) - .GetParameters()[0]; - var context = new SchemaFilterContext( - type: parameterInfo.ParameterType, - schemaGenerator: null, - schemaRepository: null, - parameterInfo: parameterInfo); - - Subject().Apply(schema, context); - - Assert.Equal($"Description for param", schema.Description); - Assert.Equal("date", schema.Format); - } - - [Theory] - [InlineData(typeof(SwaggerAnnotatedType), nameof(SwaggerAnnotatedType.StringWithSwaggerSchemaAttribute), true, true, false)] - [InlineData(typeof(SwaggerAnnotatedStruct), nameof(SwaggerAnnotatedStruct.StringWithSwaggerSchemaAttribute), true, true, false)] - public void Apply_EnrichesSchemaMetadata_IfPropertyDecoratedWithSwaggerSchemaAttribute( - Type declaringType, - string propertyName, - bool expectedReadOnly, - bool expectedWriteOnly, - bool expectedNullable) - { - var schema = new OpenApiSchema { Nullable = true }; - var propertyInfo = declaringType - .GetProperty(propertyName); - var context = new SchemaFilterContext( - type: propertyInfo.PropertyType, - schemaGenerator: null, - schemaRepository: null, - memberInfo: propertyInfo); - - Subject().Apply(schema, context); - - Assert.Equal($"Description for {propertyName}", schema.Description); - Assert.Equal("date", schema.Format); - Assert.Equal(expectedReadOnly, schema.ReadOnly); - Assert.Equal(expectedWriteOnly, schema.WriteOnly); - Assert.Equal(expectedNullable, schema.Nullable); - } - - [Fact] - public void Apply_DoesNotModifyFlags_IfNotSpecifiedWithSwaggerSchemaAttribute() - { - var schema = new OpenApiSchema { ReadOnly = true, WriteOnly = true, Nullable = true }; - var propertyInfo = typeof(SwaggerAnnotatedType) - .GetProperty(nameof(SwaggerAnnotatedType.StringWithSwaggerSchemaAttributeDescriptionOnly)); - var context = new SchemaFilterContext( - type: propertyInfo.PropertyType, - schemaGenerator: null, - schemaRepository: null, - memberInfo: propertyInfo); - - Subject().Apply(schema, context); - - Assert.True(schema.ReadOnly); - Assert.True(schema.WriteOnly); - Assert.True(schema.Nullable); - } - - [Theory] - [InlineData(typeof(SwaggerAnnotatedType))] - [InlineData(typeof(SwaggerAnnotatedStruct))] - public void Apply_DelegatesToSpecifiedFilter_IfTypeDecoratedWithFilterAttribute(Type type) - { - var schema = new OpenApiSchema(); - var context = new SchemaFilterContext(type: type, schemaGenerator: null, schemaRepository: null); - - Subject().Apply(schema, context); - - Assert.NotEmpty(schema.Extensions); - } - - private static AnnotationsSchemaFilter Subject() - { - // A service provider is required from .NET 8 onwards. - // See https://learn.microsoft.com/dotnet/core/compatibility/extensions/8.0/activatorutilities-createinstance-null-provider. - var serviceProvider = new ServiceCollection().BuildServiceProvider(); - return new AnnotationsSchemaFilter(serviceProvider); - } + // A service provider is required from .NET 8 onwards. + // See https://learn.microsoft.com/dotnet/core/compatibility/extensions/8.0/activatorutilities-createinstance-null-provider. + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + return new AnnotationsSchemaFilter(serviceProvider); } } diff --git a/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/FakeControllerWithSwaggerAnnotations.cs b/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/FakeControllerWithSwaggerAnnotations.cs index 87af3c17b3..ecc0efbbfc 100644 --- a/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/FakeControllerWithSwaggerAnnotations.cs +++ b/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/FakeControllerWithSwaggerAnnotations.cs @@ -1,58 +1,57 @@ using Microsoft.AspNetCore.Mvc; -namespace Swashbuckle.AspNetCore.Annotations.Test +namespace Swashbuckle.AspNetCore.Annotations.Test; + +[SwaggerOperationFilter(typeof(VendorExtensionsOperationFilter))] +[SwaggerResponse(500, "Description for 500 response", typeof(IDictionary))] +[SwaggerTag("Description for FakeControllerWithSwaggerAnnotations", "http://tempuri.org")] +internal class FakeControllerWithSwaggerAnnotations { - [SwaggerOperationFilter(typeof(VendorExtensionsOperationFilter))] - [SwaggerResponse(500, "Description for 500 response", typeof(IDictionary))] - [SwaggerTag("Description for FakeControllerWithSwaggerAnnotations", "http://tempuri.org")] - internal class FakeControllerWithSwaggerAnnotations + [SwaggerOperation("Summary for ActionWithSwaggerOperationAttribute", + Description = "Description for ActionWithSwaggerOperationAttribute", + OperationId = "actionWithSwaggerOperationAttribute", + Tags = new[] { "foobar" } + )] + public void ActionWithSwaggerOperationAttribute() + { } + + public void ActionWithSwaggerParameterAttribute( + [SwaggerParameter("Description for param", Required = true)] string param) + { } + + public void ActionWithSwaggerParameterAttributeDescriptionOnly( + [SwaggerParameter("Description for param")] string param) + { } + + public void ActionWithSwaggerSchemaAttribute( + [SwaggerSchema("Description for param", Format = "date")] string param) + { } + + public void ActionWithSwaggerRequestBodyAttribute( + [SwaggerRequestBody("Description for param", Required = true)] string param) + { } + + public void ActionWithSwaggerRequestbodyAttributeDescriptionOnly( + [SwaggerRequestBody("Description for param")] string param) + { } + + [SwaggerResponse(204, "Description for 204 response")] + [SwaggerResponse(400, "Description for 400 response", typeof(IDictionary))] + public IActionResult ActionWithSwaggerResponseAttributes() + { + throw new NotImplementedException(); + } + + [SwaggerResponse(200, "Description for 200 response", typeof(string), "application/json", "application/xml")] + public IActionResult ActionWithSwaggerResponseContentTypesAttributes() { - [SwaggerOperation("Summary for ActionWithSwaggerOperationAttribute", - Description = "Description for ActionWithSwaggerOperationAttribute", - OperationId = "actionWithSwaggerOperationAttribute", - Tags = new[] { "foobar" } - )] - public void ActionWithSwaggerOperationAttribute() - { } - - public void ActionWithSwaggerParameterAttribute( - [SwaggerParameter("Description for param", Required = true)] string param) - { } - - public void ActionWithSwaggerParameterAttributeDescriptionOnly( - [SwaggerParameter("Description for param")] string param) - { } - - public void ActionWithSwaggerSchemaAttribute( - [SwaggerSchema("Description for param", Format = "date")] string param) - { } - - public void ActionWithSwaggerRequestBodyAttribute( - [SwaggerRequestBody("Description for param", Required = true)] string param) - { } - - public void ActionWithSwaggerRequestbodyAttributeDescriptionOnly( - [SwaggerRequestBody("Description for param")] string param) - { } - - [SwaggerResponse(204, "Description for 204 response")] - [SwaggerResponse(400, "Description for 400 response", typeof(IDictionary))] - public IActionResult ActionWithSwaggerResponseAttributes() - { - throw new NotImplementedException(); - } - - [SwaggerResponse(200, "Description for 200 response", typeof(string), "application/json", "application/xml")] - public IActionResult ActionWithSwaggerResponseContentTypesAttributes() - { - throw new NotImplementedException(); - } - - public void ActionWithNoAttributes() - { } - - [SwaggerOperationFilter(typeof(VendorExtensionsOperationFilter))] - public void ActionWithSwaggerOperationFilterAttribute() - { } + throw new NotImplementedException(); } + + public void ActionWithNoAttributes() + { } + + [SwaggerOperationFilter(typeof(VendorExtensionsOperationFilter))] + public void ActionWithSwaggerOperationFilterAttribute() + { } } diff --git a/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/SwaggerAnnotatedType.cs b/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/SwaggerAnnotatedType.cs index 54f363fc39..b88959c84d 100644 --- a/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/SwaggerAnnotatedType.cs +++ b/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/SwaggerAnnotatedType.cs @@ -1,27 +1,26 @@ -namespace Swashbuckle.AspNetCore.Annotations.Test +namespace Swashbuckle.AspNetCore.Annotations.Test; + +[SwaggerSchema("Description for SwaggerAnnotatedType", Required = new[] { "StringWithSwaggerSchemaAttribute" }, Title = "Title for SwaggerAnnotatedType")] +[SwaggerSchemaFilter(typeof(VendorExtensionsSchemaFilter))] +public class SwaggerAnnotatedType { - [SwaggerSchema("Description for SwaggerAnnotatedType", Required = new[] { "StringWithSwaggerSchemaAttribute" }, Title = "Title for SwaggerAnnotatedType")] - [SwaggerSchemaFilter(typeof(VendorExtensionsSchemaFilter))] - public class SwaggerAnnotatedType - { - [SwaggerSchema("Description for StringWithSwaggerSchemaAttribute", Format = "date", ReadOnly = true, WriteOnly = true, Nullable = false)] - public string StringWithSwaggerSchemaAttribute { get; set; } + [SwaggerSchema("Description for StringWithSwaggerSchemaAttribute", Format = "date", ReadOnly = true, WriteOnly = true, Nullable = false)] + public string StringWithSwaggerSchemaAttribute { get; set; } - [SwaggerSchema("Description for StringWithSwaggerSchemaAttributeDescriptionOnly")] - public string StringWithSwaggerSchemaAttributeDescriptionOnly { get; set; } + [SwaggerSchema("Description for StringWithSwaggerSchemaAttributeDescriptionOnly")] + public string StringWithSwaggerSchemaAttributeDescriptionOnly { get; set; } - [SwaggerParameter("Description for StringWithSwaggerParameterAttribute", Required = true)] - public string StringWithSwaggerParameterAttribute { get; set; } + [SwaggerParameter("Description for StringWithSwaggerParameterAttribute", Required = true)] + public string StringWithSwaggerParameterAttribute { get; set; } - [SwaggerRequestBody("Description for StringWithSwaggerRequestBodyAttribute", Required = true)] - public string StringWithSwaggerRequestBodyAttribute { get; set; } - } + [SwaggerRequestBody("Description for StringWithSwaggerRequestBodyAttribute", Required = true)] + public string StringWithSwaggerRequestBodyAttribute { get; set; } +} - [SwaggerSchema("Description for SwaggerAnnotatedStruct", Required = new[] { "StringWithSwaggerSchemaAttribute" }, Title = "Title for SwaggerAnnotatedStruct")] - [SwaggerSchemaFilter(typeof(VendorExtensionsSchemaFilter))] - public struct SwaggerAnnotatedStruct - { - [SwaggerSchema("Description for StringWithSwaggerSchemaAttribute", Format = "date", ReadOnly = true, WriteOnly = true, Nullable = false)] - public string StringWithSwaggerSchemaAttribute { get; set; } - } -} \ No newline at end of file +[SwaggerSchema("Description for SwaggerAnnotatedStruct", Required = new[] { "StringWithSwaggerSchemaAttribute" }, Title = "Title for SwaggerAnnotatedStruct")] +[SwaggerSchemaFilter(typeof(VendorExtensionsSchemaFilter))] +public struct SwaggerAnnotatedStruct +{ + [SwaggerSchema("Description for StringWithSwaggerSchemaAttribute", Format = "date", ReadOnly = true, WriteOnly = true, Nullable = false)] + public string StringWithSwaggerSchemaAttribute { get; set; } +} diff --git a/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/VendorExtensionsOperationFilter.cs b/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/VendorExtensionsOperationFilter.cs index 5675d2f82b..245281c4f4 100644 --- a/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/VendorExtensionsOperationFilter.cs +++ b/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/VendorExtensionsOperationFilter.cs @@ -2,13 +2,12 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.Annotations.Test +namespace Swashbuckle.AspNetCore.Annotations.Test; + +public class VendorExtensionsOperationFilter : IOperationFilter { - public class VendorExtensionsOperationFilter : IOperationFilter + public void Apply(OpenApiOperation operation, OperationFilterContext context) { - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - operation.Extensions.Add("X-property1", new OpenApiString("value")); - } + operation.Extensions.Add("X-property1", new OpenApiString("value")); } } diff --git a/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/VendorExtensionsSchemaFilter.cs b/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/VendorExtensionsSchemaFilter.cs index 9ed2c62554..778e553035 100644 --- a/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/VendorExtensionsSchemaFilter.cs +++ b/test/Swashbuckle.AspNetCore.Annotations.Test/Fixtures/VendorExtensionsSchemaFilter.cs @@ -2,13 +2,12 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -namespace Swashbuckle.AspNetCore.Annotations.Test +namespace Swashbuckle.AspNetCore.Annotations.Test; + +public class VendorExtensionsSchemaFilter : ISchemaFilter { - public class VendorExtensionsSchemaFilter : ISchemaFilter + public void Apply(OpenApiSchema schema, SchemaFilterContext context) { - public void Apply(OpenApiSchema schema, SchemaFilterContext context) - { - schema.Extensions.Add("X-property1", new OpenApiString("value")); - } + schema.Extensions.Add("X-property1", new OpenApiString("value")); } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.ApiTesting.Test/ApiTestRunnerBaseTests.cs b/test/Swashbuckle.AspNetCore.ApiTesting.Test/ApiTestRunnerBaseTests.cs index db632a06fc..37a17b7f24 100644 --- a/test/Swashbuckle.AspNetCore.ApiTesting.Test/ApiTestRunnerBaseTests.cs +++ b/test/Swashbuckle.AspNetCore.ApiTesting.Test/ApiTestRunnerBaseTests.cs @@ -1,171 +1,170 @@ using Microsoft.OpenApi.Models; using Xunit; -namespace Swashbuckle.AspNetCore.ApiTesting.Test +namespace Swashbuckle.AspNetCore.ApiTesting.Test; + +public class ApiTestRunnerBaseTests { - public class ApiTestRunnerBaseTests + [Fact] + public async Task TestAsync_ThrowsException_IfDocumentNotFound() { - [Fact] - public async Task TestAsync_ThrowsException_IfDocumentNotFound() - { - var exception = await Assert.ThrowsAsync(() => Subject().TestAsync( - "v1", - "GetProducts", - "200", - new HttpRequestMessage(), - CreateHttpClient())); + var exception = await Assert.ThrowsAsync(() => Subject().TestAsync( + "v1", + "GetProducts", + "200", + new HttpRequestMessage(), + CreateHttpClient())); - Assert.Equal("Document with name 'v1' not found", exception.Message); - } + Assert.Equal("Document with name 'v1' not found", exception.Message); + } - [Fact] - public async Task TestAsync_ThrowsException_IfOperationNotFound() + [Fact] + public async Task TestAsync_ThrowsException_IfOperationNotFound() + { + var subject = new FakeApiTestRunner(); + subject.Configure(c => { - var subject = new FakeApiTestRunner(); - subject.Configure(c => - { - c.OpenApiDocs.Add("v1", new OpenApiDocument()); - }); - - var exception = await Assert.ThrowsAsync(() => subject.TestAsync( - "v1", - "GetProducts", - "200", - new HttpRequestMessage(), - CreateHttpClient())); + c.OpenApiDocs.Add("v1", new OpenApiDocument()); + }); + + var exception = await Assert.ThrowsAsync(() => subject.TestAsync( + "v1", + "GetProducts", + "200", + new HttpRequestMessage(), + CreateHttpClient())); - Assert.Equal("Operation with id 'GetProducts' not found in OpenAPI document 'v1'", exception.Message); - } + Assert.Equal("Operation with id 'GetProducts' not found in OpenAPI document 'v1'", exception.Message); + } - [Theory] - [InlineData("/api/products", "200", "Required parameter 'param' is not present")] - [InlineData("/api/products?param=foo", "200", null)] - public async Task TestAsync_ThrowsException_IfExpectedStatusCodeIs2xxAndRequestDoesNotMatchSpec( - string requestUri, - string statusCode, - string expectedExceptionMessage) + [Theory] + [InlineData("/api/products", "200", "Required parameter 'param' is not present")] + [InlineData("/api/products?param=foo", "200", null)] + public async Task TestAsync_ThrowsException_IfExpectedStatusCodeIs2xxAndRequestDoesNotMatchSpec( + string requestUri, + string statusCode, + string expectedExceptionMessage) + { + var subject = new FakeApiTestRunner(); + subject.Configure(c => { - var subject = new FakeApiTestRunner(); - subject.Configure(c => + c.OpenApiDocs.Add("v1", new OpenApiDocument { - c.OpenApiDocs.Add("v1", new OpenApiDocument + Paths = new OpenApiPaths { - Paths = new OpenApiPaths + ["/api/products"] = new OpenApiPathItem { - ["/api/products"] = new OpenApiPathItem + Operations = new Dictionary { - Operations = new Dictionary + [OperationType.Get] = new OpenApiOperation { - [OperationType.Get] = new OpenApiOperation + OperationId = "GetProducts", + Parameters = new List { - OperationId = "GetProducts", - Parameters = new List + new OpenApiParameter { - new OpenApiParameter - { - Name = "param", - Required = true, - In = ParameterLocation.Query - } - }, - Responses = new OpenApiResponses - { - [ "200" ] = new OpenApiResponse() + Name = "param", + Required = true, + In = ParameterLocation.Query } + }, + Responses = new OpenApiResponses + { + [ "200" ] = new OpenApiResponse() } } } } - }); + } }); + }); - var exception = await Record.ExceptionAsync(() => subject.TestAsync( - "v1", - "GetProducts", - statusCode, - new HttpRequestMessage - { - RequestUri = new Uri(requestUri, UriKind.Relative), - Method = HttpMethod.Get - }, - CreateHttpClient())); - - Assert.Equal(expectedExceptionMessage, exception?.Message); - } + var exception = await Record.ExceptionAsync(() => subject.TestAsync( + "v1", + "GetProducts", + statusCode, + new HttpRequestMessage + { + RequestUri = new Uri(requestUri, UriKind.Relative), + Method = HttpMethod.Get + }, + CreateHttpClient())); - [Theory] - [InlineData("/api/products", "400", "Status code '200' does not match expected value '400'")] - [InlineData("/api/products?param=foo", "200", null)] - public async Task TestAsync_ThrowsException_IfResponseDoesNotMatchSpec( - string requestUri, - string statusCode, - string expectedExceptionMessage) + Assert.Equal(expectedExceptionMessage, exception?.Message); + } + + [Theory] + [InlineData("/api/products", "400", "Status code '200' does not match expected value '400'")] + [InlineData("/api/products?param=foo", "200", null)] + public async Task TestAsync_ThrowsException_IfResponseDoesNotMatchSpec( + string requestUri, + string statusCode, + string expectedExceptionMessage) + { + var subject = new FakeApiTestRunner(); + subject.Configure(c => { - var subject = new FakeApiTestRunner(); - subject.Configure(c => + c.OpenApiDocs.Add("v1", new OpenApiDocument { - c.OpenApiDocs.Add("v1", new OpenApiDocument + Paths = new OpenApiPaths { - Paths = new OpenApiPaths + ["/api/products"] = new OpenApiPathItem { - ["/api/products"] = new OpenApiPathItem + Operations = new Dictionary { - Operations = new Dictionary + [OperationType.Get] = new OpenApiOperation { - [OperationType.Get] = new OpenApiOperation + OperationId = "GetProducts", + Responses = new OpenApiResponses { - OperationId = "GetProducts", - Responses = new OpenApiResponses - { - [ "400" ] = new OpenApiResponse(), - [ "200" ] = new OpenApiResponse() - } + [ "400" ] = new OpenApiResponse(), + [ "200" ] = new OpenApiResponse() } } } } - }); + } }); + }); - var exception = await Record.ExceptionAsync(() => subject.TestAsync( - "v1", - "GetProducts", - statusCode, - new HttpRequestMessage - { - RequestUri = new Uri(requestUri, UriKind.Relative), - Method = HttpMethod.Get - }, - CreateHttpClient())); - - Assert.Equal(expectedExceptionMessage, exception?.Message); - } + var exception = await Record.ExceptionAsync(() => subject.TestAsync( + "v1", + "GetProducts", + statusCode, + new HttpRequestMessage + { + RequestUri = new Uri(requestUri, UriKind.Relative), + Method = HttpMethod.Get + }, + CreateHttpClient())); - private FakeApiTestRunner Subject() - { - return new FakeApiTestRunner(); - } + Assert.Equal(expectedExceptionMessage, exception?.Message); + } - private HttpClient CreateHttpClient() - { - var client = new HttpClient(new FakeHttpMessageHandler()); - client.BaseAddress = new Uri("http://tempuri.org"); - return client; - } + private FakeApiTestRunner Subject() + { + return new FakeApiTestRunner(); } - internal class FakeApiTestRunner : ApiTestRunnerBase + private HttpClient CreateHttpClient() { - public FakeApiTestRunner() - { - } + var client = new HttpClient(new FakeHttpMessageHandler()); + client.BaseAddress = new Uri("http://tempuri.org"); + return client; } +} - internal class FakeHttpMessageHandler : HttpMessageHandler +internal class FakeApiTestRunner : ApiTestRunnerBase +{ + public FakeApiTestRunner() { - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return Task.FromResult(new HttpResponseMessage()); - } + } +} + +internal class FakeHttpMessageHandler : HttpMessageHandler +{ + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage()); } } diff --git a/test/Swashbuckle.AspNetCore.ApiTesting.Test/JsonValidatorTests.cs b/test/Swashbuckle.AspNetCore.ApiTesting.Test/JsonValidatorTests.cs index 2039f6b6b3..aa133d07da 100644 --- a/test/Swashbuckle.AspNetCore.ApiTesting.Test/JsonValidatorTests.cs +++ b/test/Swashbuckle.AspNetCore.ApiTesting.Test/JsonValidatorTests.cs @@ -4,654 +4,653 @@ using JsonSchemaType = string; -namespace Swashbuckle.AspNetCore.ApiTesting.Test +namespace Swashbuckle.AspNetCore.ApiTesting.Test; + +public class JsonValidatorTests { - public class JsonValidatorTests - { - public static TheoryData Validate_ReturnsError_IfInstanceNotOfExpectedTypeData => - new() - { - { JsonSchemaTypes.Null, "{}", false, "Path: . Instance is not of type 'null'" }, - { JsonSchemaTypes.Null, "null", true, null }, - { JsonSchemaTypes.Boolean, "'foobar'", false, "Path: . Instance is not of type 'boolean'" }, - { JsonSchemaTypes.Boolean, "true", true, null }, - { JsonSchemaTypes.Object, "'foobar'", false, "Path: . Instance is not of type 'object'" }, - { JsonSchemaTypes.Object, "{}", true, null }, - { JsonSchemaTypes.Array, "'foobar'", false, "Path: . Instance is not of type 'array'" }, - { JsonSchemaTypes.Array, "[]", true, null }, - { JsonSchemaTypes.Number, "'foobar'", false, "Path: . Instance is not of type 'number'" }, - { JsonSchemaTypes.Number, "1", true, null }, - { JsonSchemaTypes.String, "{}", false, "Path: . Instance is not of type 'string'" }, - { JsonSchemaTypes.String, "'foobar'", true, null }, - }; - - [Theory] - [MemberData(nameof(Validate_ReturnsError_IfInstanceNotOfExpectedTypeData))] - public void Validate_ReturnsError_IfInstanceNotOfExpectedType( - JsonSchemaType schemaType, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + public static TheoryData Validate_ReturnsError_IfInstanceNotOfExpectedTypeData => + new() { - var openApiSchema = new OpenApiSchema { Type = schemaType }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + { JsonSchemaTypes.Null, "{}", false, "Path: . Instance is not of type 'null'" }, + { JsonSchemaTypes.Null, "null", true, null }, + { JsonSchemaTypes.Boolean, "'foobar'", false, "Path: . Instance is not of type 'boolean'" }, + { JsonSchemaTypes.Boolean, "true", true, null }, + { JsonSchemaTypes.Object, "'foobar'", false, "Path: . Instance is not of type 'object'" }, + { JsonSchemaTypes.Object, "{}", true, null }, + { JsonSchemaTypes.Array, "'foobar'", false, "Path: . Instance is not of type 'array'" }, + { JsonSchemaTypes.Array, "[]", true, null }, + { JsonSchemaTypes.Number, "'foobar'", false, "Path: . Instance is not of type 'number'" }, + { JsonSchemaTypes.Number, "1", true, null }, + { JsonSchemaTypes.String, "{}", false, "Path: . Instance is not of type 'string'" }, + { JsonSchemaTypes.String, "'foobar'", true, null }, + }; + + [Theory] + [MemberData(nameof(Validate_ReturnsError_IfInstanceNotOfExpectedTypeData))] + public void Validate_ReturnsError_IfInstanceNotOfExpectedType( + JsonSchemaType schemaType, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { Type = schemaType }; + var instance = JToken.Parse(instanceText); - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(5.0, "9", false, "Path: . Number is not evenly divisible by multipleOf")] - [InlineData(5.0, "10", true, null)] - public void Validate_ReturnsError_IfNumberNotEvenlyDivisibleByMultipleOf( - decimal schemaMultipleOf, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) - { - var openApiSchema = new OpenApiSchema { Type = JsonSchemaTypes.Number, MultipleOf = schemaMultipleOf }; - var instance = JToken.Parse(instanceText); + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(10.0, "10.1", false, "Path: . Number is greater than maximum")] - [InlineData(10.0, "10.0", true, null)] - public void Validate_ReturnsError_IfNumberGreaterThanMaximum( - decimal schemaMaximum, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) - { - var openApiSchema = new OpenApiSchema { Type = JsonSchemaTypes.Number, Maximum = schemaMaximum }; - var instance = JToken.Parse(instanceText); + [Theory] + [InlineData(5.0, "9", false, "Path: . Number is not evenly divisible by multipleOf")] + [InlineData(5.0, "10", true, null)] + public void Validate_ReturnsError_IfNumberNotEvenlyDivisibleByMultipleOf( + decimal schemaMultipleOf, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { Type = JsonSchemaTypes.Number, MultipleOf = schemaMultipleOf }; + var instance = JToken.Parse(instanceText); - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(10.0, "10.0", false, "Path: . Number is greater than, or equal to, maximum")] - [InlineData(10.0, "9.9", true, null)] - public void Validate_ReturnsError_IfNumberGreaterThanOrEqualToMaximumAndExclusiveMaximumSet( - decimal schemaMaximum, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) - { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Number, - Maximum = schemaMaximum, - ExclusiveMaximum = true - }; - var instance = JToken.Parse(instanceText); + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + [Theory] + [InlineData(10.0, "10.1", false, "Path: . Number is greater than maximum")] + [InlineData(10.0, "10.0", true, null)] + public void Validate_ReturnsError_IfNumberGreaterThanMaximum( + decimal schemaMaximum, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { Type = JsonSchemaTypes.Number, Maximum = schemaMaximum }; + var instance = JToken.Parse(instanceText); - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(10.0, "9.9", false, "Path: . Number is less than minimum")] - [InlineData(10.0, "10.0", true, null)] - public void Validate_ReturnsError_IfNumberLessThanMinimum( - decimal schemaMinimum, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) - { - var openApiSchema = new OpenApiSchema { Type = JsonSchemaTypes.Number, Minimum = schemaMinimum }; - var instance = JToken.Parse(instanceText); + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(10.0, "10.0", false, "Path: . Number is less than, or equal to, minimum")] - [InlineData(10.0, "10.1", true, null)] - public void Validate_ReturnsError_IfNumberLessThanOrEqualToMinimumAndExclusiveMinimumSet( - decimal schemaMinimum, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData(10.0, "10.0", false, "Path: . Number is greater than, or equal to, maximum")] + [InlineData(10.0, "9.9", true, null)] + public void Validate_ReturnsError_IfNumberGreaterThanOrEqualToMaximumAndExclusiveMaximumSet( + decimal schemaMaximum, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Number, - Minimum = schemaMinimum, - ExclusiveMinimum = true - }; - var instance = JToken.Parse(instanceText); + Type = JsonSchemaTypes.Number, + Maximum = schemaMaximum, + ExclusiveMaximum = true + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + [Theory] + [InlineData(10.0, "9.9", false, "Path: . Number is less than minimum")] + [InlineData(10.0, "10.0", true, null)] + public void Validate_ReturnsError_IfNumberLessThanMinimum( + decimal schemaMinimum, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { Type = JsonSchemaTypes.Number, Minimum = schemaMinimum }; + var instance = JToken.Parse(instanceText); - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(5, "'123456'", false, "Path: . String length is greater than maxLength")] - [InlineData(5, "'12345'", true, null)] - public void Validate_ReturnsError_IfStringLengthGreaterThanMaxLength( - int schemaMaxLength, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) - { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.String, - MaxLength = schemaMaxLength - }; - var instance = JToken.Parse(instanceText); + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(5, "'1234'", false, "Path: . String length is less than minLength" )] - [InlineData(5, "'12345'", true, null)] - public void Validate_ReturnsError_IfStringLengthLessThanMinLength( - int schemaMinLength, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData(10.0, "10.0", false, "Path: . Number is less than, or equal to, minimum")] + [InlineData(10.0, "10.1", true, null)] + public void Validate_ReturnsError_IfNumberLessThanOrEqualToMinimumAndExclusiveMinimumSet( + decimal schemaMinimum, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.String, - MinLength = schemaMinLength - }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + Type = JsonSchemaTypes.Number, + Minimum = schemaMinimum, + ExclusiveMinimum = true + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData("^[a-z]{3}$", "'aa1'", false, "Path: . String does not match pattern")] - [InlineData("^[a-z]{3}$", "'aaz'", true, null)] - public void Validate_ReturnsError_IfStringDoesNotMatchPattern( - string schemaPattern, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData(5, "'123456'", false, "Path: . String length is greater than maxLength")] + [InlineData(5, "'12345'", true, null)] + public void Validate_ReturnsError_IfStringLengthGreaterThanMaxLength( + int schemaMaxLength, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.String, - Pattern = schemaPattern - }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); - - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } + Type = JsonSchemaTypes.String, + MaxLength = schemaMaxLength + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - public static TheoryData Validate_ReturnsError_IfArrayItemDoesNotMatchItemsSchemaData => - new() - { - { JsonSchemaTypes.Boolean, "[ true, 'foo' ]", false, "Path: [1]. Instance is not of type 'boolean'" }, - { JsonSchemaTypes.Number, "[ 123, 'foo' ]", false, "Path: [1]. Instance is not of type 'number'" }, - { JsonSchemaTypes.Boolean, "[ true, false ]", true, null }, - }; - - [Theory] - [MemberData(nameof(Validate_ReturnsError_IfArrayItemDoesNotMatchItemsSchemaData))] - public void Validate_ReturnsError_IfArrayItemDoesNotMatchItemsSchema( - JsonSchemaType itemsSchemaType, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData(5, "'1234'", false, "Path: . String length is less than minLength" )] + [InlineData(5, "'12345'", true, null)] + public void Validate_ReturnsError_IfStringLengthLessThanMinLength( + int schemaMinLength, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Array, - Items = new OpenApiSchema { Type = itemsSchemaType } - }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + Type = JsonSchemaTypes.String, + MinLength = schemaMinLength + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(2, "[ 1, 2, 3 ]", false, "Path: . Array size is greater than maxItems")] - [InlineData(2, "[ 1, 2 ]", true, null)] - public void Validate_ReturnsError_IfArraySizeGreaterThanMaxItems( - int schemaMaxItems, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData("^[a-z]{3}$", "'aa1'", false, "Path: . String does not match pattern")] + [InlineData("^[a-z]{3}$", "'aaz'", true, null)] + public void Validate_ReturnsError_IfStringDoesNotMatchPattern( + string schemaPattern, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Array, - MaxItems = schemaMaxItems - }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + Type = JsonSchemaTypes.String, + Pattern = schemaPattern + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(2, "[ 1 ]", false, "Path: . Array size is less than minItems")] - [InlineData(2, "[ 1, 2 ]", true, null)] - public void Validate_ReturnsError_IfArraySizeLessThanMinItems( - int schemaMinItems, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + public static TheoryData Validate_ReturnsError_IfArrayItemDoesNotMatchItemsSchemaData => + new() { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Array, - MinItems = schemaMinItems - }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); - - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData("[ 1, 1, 3 ]", false, "Path: . Array does not contain uniqueItems")] - [InlineData("[ 1, 2, 3 ]", true, null)] - public void Validate_ReturnsError_IfArrayDoesNotContainUniqueItemsAndUniqueItemsSet( - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + { JsonSchemaTypes.Boolean, "[ true, 'foo' ]", false, "Path: [1]. Instance is not of type 'boolean'" }, + { JsonSchemaTypes.Number, "[ 123, 'foo' ]", false, "Path: [1]. Instance is not of type 'number'" }, + { JsonSchemaTypes.Boolean, "[ true, false ]", true, null }, + }; + + [Theory] + [MemberData(nameof(Validate_ReturnsError_IfArrayItemDoesNotMatchItemsSchemaData))] + public void Validate_ReturnsError_IfArrayItemDoesNotMatchItemsSchema( + JsonSchemaType itemsSchemaType, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Array, - UniqueItems = true - }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + Type = JsonSchemaTypes.Array, + Items = new OpenApiSchema { Type = itemsSchemaType } + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(1, "{ \"id\": 1, \"name\": \"foo\" }", false, "Path: . Number of properties is greater than maxProperties")] - [InlineData(1, "{ \"id\": 1 }", true, null)] - public void Validate_ReturnsError_IfNumberOfPropertiesGreaterThanMaxProperties( - int schemaMaxProperties, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData(2, "[ 1, 2, 3 ]", false, "Path: . Array size is greater than maxItems")] + [InlineData(2, "[ 1, 2 ]", true, null)] + public void Validate_ReturnsError_IfArraySizeGreaterThanMaxItems( + int schemaMaxItems, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Object, - MaxProperties = schemaMaxProperties - }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + Type = JsonSchemaTypes.Array, + MaxItems = schemaMaxItems + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(2, "{ \"id\": 1 }", false, "Path: . Number of properties is less than minProperties")] - [InlineData(2, "{ \"id\": 1, \"name\": \"foo\" }", true, null)] - public void Validate_ReturnsError_IfNumberOfPropertiesLessThanMinProperties( - int schemaMinProperties, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData(2, "[ 1 ]", false, "Path: . Array size is less than minItems")] + [InlineData(2, "[ 1, 2 ]", true, null)] + public void Validate_ReturnsError_IfArraySizeLessThanMinItems( + int schemaMinItems, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Object, - MinProperties = schemaMinProperties - }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + Type = JsonSchemaTypes.Array, + MinItems = schemaMinItems + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(new[] { "id", "name" }, "{ \"id\": 1 }", false, "Path: . Required property(s) not present")] - [InlineData(new[] { "id", "name" }, "{ \"id\": 1, \"name\": \"foo\" }", true, null)] - public void Validate_ReturnsError_IfRequiredPropertyNotPresent( - string[] schemaRequired, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData("[ 1, 1, 3 ]", false, "Path: . Array does not contain uniqueItems")] + [InlineData("[ 1, 2, 3 ]", true, null)] + public void Validate_ReturnsError_IfArrayDoesNotContainUniqueItemsAndUniqueItemsSet( + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Object, - Required = new SortedSet(schemaRequired) - }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); - - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } + Type = JsonSchemaTypes.Array, + UniqueItems = true + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - public static TheoryData Validate_ReturnsError_IfKnownPropertyDoesNotMatchPropertySchemaData => - new() - { - { JsonSchemaTypes.Number, "{ \"id\": \"foo\" }", false, "Path: id. Instance is not of type 'number'" }, - { JsonSchemaTypes.String, "{ \"id\": 123 }", false, "Path: id. Instance is not of type 'string'" }, - { JsonSchemaTypes.Number, "{ \"id\": 123 }", true, null }, - }; - - [Theory] - [MemberData(nameof(Validate_ReturnsError_IfKnownPropertyDoesNotMatchPropertySchemaData))] - public void Validate_ReturnsError_IfKnownPropertyDoesNotMatchPropertySchema( - JsonSchemaType propertySchemaType, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData(1, "{ \"id\": 1, \"name\": \"foo\" }", false, "Path: . Number of properties is greater than maxProperties")] + [InlineData(1, "{ \"id\": 1 }", true, null)] + public void Validate_ReturnsError_IfNumberOfPropertiesGreaterThanMaxProperties( + int schemaMaxProperties, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Object, - Properties = new Dictionary - { - [ "id" ] = new OpenApiSchema { Type = propertySchemaType } - } - }; - var instance = JToken.Parse(instanceText); - - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); - - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } + Type = JsonSchemaTypes.Object, + MaxProperties = schemaMaxProperties + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - public static TheoryData Validate_ReturnsError_IfAdditionalPropertyDoesNotMatchAdditionalPropertiesSchemaData => - new() - { - { JsonSchemaTypes.Number, "{ \"id\": \"foo\" }", false, "Path: id. Instance is not of type 'number'" }, - { JsonSchemaTypes.String, "{ \"name\": 123 }", false, "Path: name. Instance is not of type 'string'" }, - { JsonSchemaTypes.Number, "{ \"description\": 123 }", true, null }, - }; - - [Theory] - [MemberData(nameof(Validate_ReturnsError_IfAdditionalPropertyDoesNotMatchAdditionalPropertiesSchemaData))] - public void Validate_ReturnsError_IfAdditionalPropertyDoesNotMatchAdditionalPropertiesSchema( - JsonSchemaType additionalPropertiesType, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData(2, "{ \"id\": 1 }", false, "Path: . Number of properties is less than minProperties")] + [InlineData(2, "{ \"id\": 1, \"name\": \"foo\" }", true, null)] + public void Validate_ReturnsError_IfNumberOfPropertiesLessThanMinProperties( + int schemaMinProperties, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - Type = JsonSchemaTypes.Object, - AdditionalProperties = new OpenApiSchema { Type = additionalPropertiesType } - }; - var instance = JToken.Parse(instanceText); + Type = JsonSchemaTypes.Object, + MinProperties = schemaMinProperties + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + [Theory] + [InlineData(new[] { "id", "name" }, "{ \"id\": 1 }", false, "Path: . Required property(s) not present")] + [InlineData(new[] { "id", "name" }, "{ \"id\": 1, \"name\": \"foo\" }", true, null)] + public void Validate_ReturnsError_IfRequiredPropertyNotPresent( + string[] schemaRequired, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema + { + Type = JsonSchemaTypes.Object, + Required = new SortedSet(schemaRequired) + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData(false, "{ \"id\": \"foo\" }", false, "Path: . Additional properties not allowed")] - [InlineData(true, "{ \"id\": \"foo\" }", true, null)] - public void Validate_ReturnsError_IfAdditionalPropertiesPresentAndAdditionalPropertiesAllowedUnset( - bool additionalPropertiesAllowed, - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + public static TheoryData Validate_ReturnsError_IfKnownPropertyDoesNotMatchPropertySchemaData => + new() + { + { JsonSchemaTypes.Number, "{ \"id\": \"foo\" }", false, "Path: id. Instance is not of type 'number'" }, + { JsonSchemaTypes.String, "{ \"id\": 123 }", false, "Path: id. Instance is not of type 'string'" }, + { JsonSchemaTypes.Number, "{ \"id\": 123 }", true, null }, + }; + + [Theory] + [MemberData(nameof(Validate_ReturnsError_IfKnownPropertyDoesNotMatchPropertySchemaData))] + public void Validate_ReturnsError_IfKnownPropertyDoesNotMatchPropertySchema( + JsonSchemaType propertySchemaType, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema + Type = JsonSchemaTypes.Object, + Properties = new Dictionary { - Type = JsonSchemaTypes.Object, - AdditionalPropertiesAllowed = additionalPropertiesAllowed - }; - var instance = JToken.Parse(instanceText); + [ "id" ] = new OpenApiSchema { Type = propertySchemaType } + } + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + public static TheoryData Validate_ReturnsError_IfAdditionalPropertyDoesNotMatchAdditionalPropertiesSchemaData => + new() + { + { JsonSchemaTypes.Number, "{ \"id\": \"foo\" }", false, "Path: id. Instance is not of type 'number'" }, + { JsonSchemaTypes.String, "{ \"name\": 123 }", false, "Path: name. Instance is not of type 'string'" }, + { JsonSchemaTypes.Number, "{ \"description\": 123 }", true, null }, + }; + + [Theory] + [MemberData(nameof(Validate_ReturnsError_IfAdditionalPropertyDoesNotMatchAdditionalPropertiesSchemaData))] + public void Validate_ReturnsError_IfAdditionalPropertyDoesNotMatchAdditionalPropertiesSchema( + JsonSchemaType additionalPropertiesType, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema + { + Type = JsonSchemaTypes.Object, + AdditionalProperties = new OpenApiSchema { Type = additionalPropertiesType } + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData("{ \"p1\": 1, \"p2\": 2 }", false, "Path: . Required property(s) not present (allOf[2])")] - [InlineData("{ \"p1\": 1, \"p2\": 2, \"p3\": 3 }", true, null)] - public void Validate_ReturnsError_IfInstanceDoesNotMatchAllSchemasSpecifiedByAllOf( - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData(false, "{ \"id\": \"foo\" }", false, "Path: . Additional properties not allowed")] + [InlineData(true, "{ \"id\": \"foo\" }", true, null)] + public void Validate_ReturnsError_IfAdditionalPropertiesPresentAndAdditionalPropertiesAllowedUnset( + bool additionalPropertiesAllowed, + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - AllOf = - [ - new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p1" } }, - new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p2" } }, - new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p3" } } - ] - }; - var instance = JToken.Parse(instanceText); + Type = JsonSchemaTypes.Object, + AdditionalPropertiesAllowed = additionalPropertiesAllowed + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + [Theory] + [InlineData("{ \"p1\": 1, \"p2\": 2 }", false, "Path: . Required property(s) not present (allOf[2])")] + [InlineData("{ \"p1\": 1, \"p2\": 2, \"p3\": 3 }", true, null)] + public void Validate_ReturnsError_IfInstanceDoesNotMatchAllSchemasSpecifiedByAllOf( + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema + { + AllOf = + [ + new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p1" } }, + new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p2" } }, + new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p3" } } + ] + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData("{}", false, "Path: . Required property(s) not present (anyOf[0])")] - [InlineData("{ \"p1\": 1 }", true, null)] - public void Validate_ReturnsError_IfInstanceDoesNotMatchAnySchemaSpecifiedByAnyOf( - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData("{}", false, "Path: . Required property(s) not present (anyOf[0])")] + [InlineData("{ \"p1\": 1 }", true, null)] + public void Validate_ReturnsError_IfInstanceDoesNotMatchAnySchemaSpecifiedByAnyOf( + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema - { - AnyOf = - [ - new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p1" } }, - new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p2" } }, - new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p3" } } - ] - }; - var instance = JToken.Parse(instanceText); + AnyOf = + [ + new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p1" } }, + new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p2" } }, + new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p3" } } + ] + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - var returnValue = Subject().Validate( - openApiSchema, - new OpenApiDocument(), - instance, - out IEnumerable errorMessages); + [Theory] + [InlineData("{}", false, "Path: . Required property(s) not present (oneOf[0])")] + [InlineData("{ \"p1\": 1, \"p2\": 2 }", false, "Path: . Instance matches multiple schemas in oneOf array")] + [InlineData("{ \"p1\": 1 }", true, null)] + public void Validate_ReturnsError_IfInstanceDoesNotMatchExactlyOneSchemaSpecifiedByOneOf( + string instanceText, + bool expectedReturnValue, + string expectedErrorMessage) + { + var openApiSchema = new OpenApiSchema + { + OneOf = + [ + new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p1" } }, + new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p2" } }, + new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p3" } } + ] + }; + var instance = JToken.Parse(instanceText); + + var returnValue = Subject().Validate( + openApiSchema, + new OpenApiDocument(), + instance, + out IEnumerable errorMessages); + + Assert.Equal(expectedReturnValue, returnValue); + Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); + } - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData("{}", false, "Path: . Required property(s) not present (oneOf[0])")] - [InlineData("{ \"p1\": 1, \"p2\": 2 }", false, "Path: . Instance matches multiple schemas in oneOf array")] - [InlineData("{ \"p1\": 1 }", true, null)] - public void Validate_ReturnsError_IfInstanceDoesNotMatchExactlyOneSchemaSpecifiedByOneOf( - string instanceText, - bool expectedReturnValue, - string expectedErrorMessage) + [Theory] + [InlineData("foo", "Invalid Reference identifier 'foo'.")] + [InlineData("ref", null)] + public void Validate_SupportsReferencedSchemas_IfDefinedInProvidedOpenApiDocument( + string referenceId, + string expectedExceptionMessage) + { + var openApiSchema = new OpenApiSchema { - var openApiSchema = new OpenApiSchema + Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = referenceId } + }; + var openApiDocument = new OpenApiDocument + { + Components = new OpenApiComponents { - OneOf = - [ - new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p1" } }, - new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p2" } }, - new OpenApiSchema { Type = JsonSchemaTypes.Object, Required = new SortedSet { "p3" } } - ] - }; - var instance = JToken.Parse(instanceText); + Schemas = new Dictionary + { + ["ref"] = new OpenApiSchema { Type = JsonSchemaTypes.Number } + } + } + }; + var instance = JToken.Parse("1"); + var exception = Record.Exception(() => + { var returnValue = Subject().Validate( openApiSchema, - new OpenApiDocument(), + openApiDocument, instance, out IEnumerable errorMessages); - Assert.Equal(expectedReturnValue, returnValue); - Assert.Equal(expectedErrorMessage, errorMessages.FirstOrDefault()); - } - - [Theory] - [InlineData("foo", "Invalid Reference identifier 'foo'.")] - [InlineData("ref", null)] - public void Validate_SupportsReferencedSchemas_IfDefinedInProvidedOpenApiDocument( - string referenceId, - string expectedExceptionMessage) - { - var openApiSchema = new OpenApiSchema - { - Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = referenceId } - }; - var openApiDocument = new OpenApiDocument - { - Components = new OpenApiComponents - { - Schemas = new Dictionary - { - ["ref"] = new OpenApiSchema { Type = JsonSchemaTypes.Number } - } - } - }; - var instance = JToken.Parse("1"); - - var exception = Record.Exception(() => - { - var returnValue = Subject().Validate( - openApiSchema, - openApiDocument, - instance, - out IEnumerable errorMessages); - - Assert.True(returnValue); - }); - - Assert.Equal(expectedExceptionMessage, exception?.Message); - } + Assert.True(returnValue); + }); - private static JsonValidator Subject() => new(); + Assert.Equal(expectedExceptionMessage, exception?.Message); } + + private static JsonValidator Subject() => new(); } diff --git a/test/Swashbuckle.AspNetCore.ApiTesting.Test/RequestValidatorTests.cs b/test/Swashbuckle.AspNetCore.ApiTesting.Test/RequestValidatorTests.cs index 70a75b3f5b..e2adff58bb 100644 --- a/test/Swashbuckle.AspNetCore.ApiTesting.Test/RequestValidatorTests.cs +++ b/test/Swashbuckle.AspNetCore.ApiTesting.Test/RequestValidatorTests.cs @@ -4,395 +4,394 @@ using JsonSchemaType = string; -namespace Swashbuckle.AspNetCore.ApiTesting.Test +namespace Swashbuckle.AspNetCore.ApiTesting.Test; + +public class RequestValidatorTests { - public class RequestValidatorTests + [Theory] + [InlineData("/api/foobar", "/api/products", "Request URI '/api/foobar' does not match specified template '/api/products'")] + [InlineData("/api/products", "/api/products", null)] + public void Validate_ThrowsException_IfUriDoesNotMatchPathTemplate( + string uriString, + string pathTemplate, + string expectedErrorMessage) { - [Theory] - [InlineData("/api/foobar", "/api/products", "Request URI '/api/foobar' does not match specified template '/api/products'")] - [InlineData("/api/products", "/api/products", null)] - public void Validate_ThrowsException_IfUriDoesNotMatchPathTemplate( - string uriString, - string pathTemplate, - string expectedErrorMessage) + var openApiDocument = DocumentWithOperation(pathTemplate, OperationType.Get, new OpenApiOperation()); + var request = new HttpRequestMessage { - var openApiDocument = DocumentWithOperation(pathTemplate, OperationType.Get, new OpenApiOperation()); - var request = new HttpRequestMessage - { - RequestUri = new Uri(uriString, UriKind.Relative), - }; + RequestUri = new Uri(uriString, UriKind.Relative), + }; - var exception = Record.Exception(() => - { - Subject().Validate(request, openApiDocument, pathTemplate, OperationType.Get); - }); - - Assert.Equal(expectedErrorMessage, exception?.Message); - } - - [Theory] - [InlineData("POST", OperationType.Get, "Request method 'POST' does not match specified operation type 'Get'")] - [InlineData("GET", OperationType.Get, null)] - public void Validate_ThrowsException_IfMethodDoesNotMatchOperationType( - string methodString, - OperationType operationType, - string expectedErrorMessage) + var exception = Record.Exception(() => { - var openApiDocument = DocumentWithOperation("/api/products", operationType, new OpenApiOperation()); - var request = new HttpRequestMessage - { - RequestUri = new Uri("/api/products", UriKind.Relative), - Method = new HttpMethod(methodString) - }; + Subject().Validate(request, openApiDocument, pathTemplate, OperationType.Get); + }); - var exception = Record.Exception(() => - { - Subject().Validate(request, openApiDocument, "/api/products", operationType); - }); - - Assert.Equal(expectedErrorMessage, exception?.Message); - } - - [Theory] - [InlineData("/api/products", "Required parameter 'param' is not present")] - [InlineData("/api/products?param=foo", null)] - public void Validate_ThrowsException_IfRequiredQueryParameterIsNotPresent( - string uriString, - string expectedErrorMessage) + Assert.Equal(expectedErrorMessage, exception?.Message); + } + + [Theory] + [InlineData("POST", OperationType.Get, "Request method 'POST' does not match specified operation type 'Get'")] + [InlineData("GET", OperationType.Get, null)] + public void Validate_ThrowsException_IfMethodDoesNotMatchOperationType( + string methodString, + OperationType operationType, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products", operationType, new OpenApiOperation()); + var request = new HttpRequestMessage { - var openApiDocument = DocumentWithOperation("/api/products", OperationType.Get, new OpenApiOperation - { - Parameters = new List - { - new OpenApiParameter - { - Name = "param", - In = ParameterLocation.Query, - Schema = new OpenApiSchema { Type = JsonSchemaTypes.String }, - Required = true - } - } - }); - var request = new HttpRequestMessage - { - RequestUri = new Uri(uriString, UriKind.Relative), - Method = HttpMethod.Get - }; + RequestUri = new Uri("/api/products", UriKind.Relative), + Method = new HttpMethod(methodString) + }; - var exception = Record.Exception(() => - { - Subject().Validate(request, openApiDocument, "/api/products", OperationType.Get); - }); - - Assert.Equal(expectedErrorMessage, exception?.Message); - } - - [Theory] - [InlineData(null, "Required parameter 'test-header' is not present")] - [InlineData("foo", null)] - public void Validate_ThrowsException_IfRequiredHeaderParameterIsNotPresent( - string parameterValue, - string expectedErrorMessage) + var exception = Record.Exception(() => + { + Subject().Validate(request, openApiDocument, "/api/products", operationType); + }); + + Assert.Equal(expectedErrorMessage, exception?.Message); + } + + [Theory] + [InlineData("/api/products", "Required parameter 'param' is not present")] + [InlineData("/api/products?param=foo", null)] + public void Validate_ThrowsException_IfRequiredQueryParameterIsNotPresent( + string uriString, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products", OperationType.Get, new OpenApiOperation { - var openApiDocument = DocumentWithOperation("/api/products", OperationType.Get, new OpenApiOperation + Parameters = new List { - Parameters = new List + new OpenApiParameter { - new OpenApiParameter - { - Name = "test-header", - In = ParameterLocation.Header, - Schema = new OpenApiSchema { Type = JsonSchemaTypes.String }, - Required = true - } + Name = "param", + In = ParameterLocation.Query, + Schema = new OpenApiSchema { Type = JsonSchemaTypes.String }, + Required = true } - }); - var request = new HttpRequestMessage - { - RequestUri = new Uri("/api/products", UriKind.Relative), - Method = HttpMethod.Get, - }; - if (parameterValue != null) request.Headers.Add("test-header", parameterValue); + } + }); + var request = new HttpRequestMessage + { + RequestUri = new Uri(uriString, UriKind.Relative), + Method = HttpMethod.Get + }; - var exception = Record.Exception(() => - { - Subject().Validate(request, openApiDocument, "/api/products", OperationType.Get); - }); + var exception = Record.Exception(() => + { + Subject().Validate(request, openApiDocument, "/api/products", OperationType.Get); + }); - Assert.Equal(expectedErrorMessage, exception?.Message); - } + Assert.Equal(expectedErrorMessage, exception?.Message); + } - public static TheoryData PathParameterTypeMismatchData => new() + [Theory] + [InlineData(null, "Required parameter 'test-header' is not present")] + [InlineData("foo", null)] + public void Validate_ThrowsException_IfRequiredHeaderParameterIsNotPresent( + string parameterValue, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products", OperationType.Get, new OpenApiOperation + { + Parameters = new List + { + new OpenApiParameter + { + Name = "test-header", + In = ParameterLocation.Header, + Schema = new OpenApiSchema { Type = JsonSchemaTypes.String }, + Required = true + } + } + }); + var request = new HttpRequestMessage { - { "/api/products/foo", JsonSchemaTypes.Boolean, "Parameter 'param' is not of type 'boolean'" }, - { "/api/products/foo", JsonSchemaTypes.Number, "Parameter 'param' is not of type 'number'" }, - { "/api/products/true", JsonSchemaTypes.Boolean, null }, - { "/api/products/1", JsonSchemaTypes.Number, null }, - { "/api/products/foo", JsonSchemaTypes.String, null } + RequestUri = new Uri("/api/products", UriKind.Relative), + Method = HttpMethod.Get, }; + if (parameterValue != null) request.Headers.Add("test-header", parameterValue); - [Theory] - [MemberData(nameof(PathParameterTypeMismatchData))] - public void Validate_ThrowsException_IfPathParameterIsNotOfSpecifiedType( - string uriString, - JsonSchemaType specifiedType, - string expectedErrorMessage) + var exception = Record.Exception(() => { - var openApiDocument = DocumentWithOperation("/api/products/{param}", OperationType.Get, new OpenApiOperation + Subject().Validate(request, openApiDocument, "/api/products", OperationType.Get); + }); + + Assert.Equal(expectedErrorMessage, exception?.Message); + } + + public static TheoryData PathParameterTypeMismatchData => new() + { + { "/api/products/foo", JsonSchemaTypes.Boolean, "Parameter 'param' is not of type 'boolean'" }, + { "/api/products/foo", JsonSchemaTypes.Number, "Parameter 'param' is not of type 'number'" }, + { "/api/products/true", JsonSchemaTypes.Boolean, null }, + { "/api/products/1", JsonSchemaTypes.Number, null }, + { "/api/products/foo", JsonSchemaTypes.String, null } + }; + + [Theory] + [MemberData(nameof(PathParameterTypeMismatchData))] + public void Validate_ThrowsException_IfPathParameterIsNotOfSpecifiedType( + string uriString, + JsonSchemaType specifiedType, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products/{param}", OperationType.Get, new OpenApiOperation + { + Parameters = new List { - Parameters = new List + new OpenApiParameter { - new OpenApiParameter - { - Name = "param", - In = ParameterLocation.Path, - Schema = new OpenApiSchema { Type = specifiedType } - } + Name = "param", + In = ParameterLocation.Path, + Schema = new OpenApiSchema { Type = specifiedType } } - }); - var request = new HttpRequestMessage - { - RequestUri = new Uri(uriString, UriKind.Relative), - Method = HttpMethod.Get - }; + } + }); + var request = new HttpRequestMessage + { + RequestUri = new Uri(uriString, UriKind.Relative), + Method = HttpMethod.Get + }; - var exception = Record.Exception(() => - { - Subject().Validate(request, openApiDocument, "/api/products/{param}", OperationType.Get); - }); + var exception = Record.Exception(() => + { + Subject().Validate(request, openApiDocument, "/api/products/{param}", OperationType.Get); + }); - Assert.Equal(expectedErrorMessage, exception?.Message); - } + Assert.Equal(expectedErrorMessage, exception?.Message); + } #nullable enable - public static TheoryData QueryParameterTypeMismatchData => new() - { - { "/api/products?param=foo", JsonSchemaTypes.Boolean, null, "Parameter 'param' is not of type 'boolean'" }, - { "/api/products?param=foo", JsonSchemaTypes.Number, null, "Parameter 'param' is not of type 'number'" }, - { "/api/products?param=true", JsonSchemaTypes.Boolean, null, null }, - { "/api/products?param=1", JsonSchemaTypes.Number, null, null }, - { "/api/products?param=foo", JsonSchemaTypes.String, null, null }, - { "/api/products?param=1¶m=2", JsonSchemaTypes.Array, JsonSchemaTypes.Number, null }, - { "/api/products?param=1¶m=foo", JsonSchemaTypes.Array, JsonSchemaTypes.Number, "Parameter 'param' is not of type 'array[number]'" }, - }; - - [Theory] - [MemberData(nameof(QueryParameterTypeMismatchData))] - public void Validate_ThrowsException_IfQueryParameterIsNotOfSpecifiedType( - string path, - JsonSchemaType specifiedType, - JsonSchemaType? specifiedItemsType, - string? expectedErrorMessage) + public static TheoryData QueryParameterTypeMismatchData => new() + { + { "/api/products?param=foo", JsonSchemaTypes.Boolean, null, "Parameter 'param' is not of type 'boolean'" }, + { "/api/products?param=foo", JsonSchemaTypes.Number, null, "Parameter 'param' is not of type 'number'" }, + { "/api/products?param=true", JsonSchemaTypes.Boolean, null, null }, + { "/api/products?param=1", JsonSchemaTypes.Number, null, null }, + { "/api/products?param=foo", JsonSchemaTypes.String, null, null }, + { "/api/products?param=1¶m=2", JsonSchemaTypes.Array, JsonSchemaTypes.Number, null }, + { "/api/products?param=1¶m=foo", JsonSchemaTypes.Array, JsonSchemaTypes.Number, "Parameter 'param' is not of type 'array[number]'" }, + }; + + [Theory] + [MemberData(nameof(QueryParameterTypeMismatchData))] + public void Validate_ThrowsException_IfQueryParameterIsNotOfSpecifiedType( + string path, + JsonSchemaType specifiedType, + JsonSchemaType? specifiedItemsType, + string? expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products", OperationType.Get, new OpenApiOperation { - var openApiDocument = DocumentWithOperation("/api/products", OperationType.Get, new OpenApiOperation + Parameters = new List { - Parameters = new List + new OpenApiParameter { - new OpenApiParameter + Name = "param", + In = ParameterLocation.Query, + Schema = new OpenApiSchema { - Name = "param", - In = ParameterLocation.Query, - Schema = new OpenApiSchema - { - Type = specifiedType, - Items = specifiedItemsType != null ? new OpenApiSchema { Type = specifiedItemsType } : null - } + Type = specifiedType, + Items = specifiedItemsType != null ? new OpenApiSchema { Type = specifiedItemsType } : null } } - }); - var request = new HttpRequestMessage - { - RequestUri = new Uri(path, UriKind.Relative), - Method = HttpMethod.Get - }; - - var exception = Record.Exception(() => - { - Subject().Validate(request, openApiDocument, "/api/products", OperationType.Get); - }); - - Assert.Equal(expectedErrorMessage, exception?.Message); - } - - public static TheoryData HeaderParameterTypeMismatchData => new() + } + }); + var request = new HttpRequestMessage { - { "foo", JsonSchemaTypes.Boolean, null, "Parameter 'test-header' is not of type 'boolean'" }, - { "foo", JsonSchemaTypes.Number, null, "Parameter 'test-header' is not of type 'number'" }, - { "true", JsonSchemaTypes.Boolean, null, null }, - { "1", JsonSchemaTypes.Number, null, null }, - { "foo", JsonSchemaTypes.String, null, null }, - { "1,2", JsonSchemaTypes.Array, JsonSchemaTypes.Number, null }, - { "1,foo", JsonSchemaTypes.Array, JsonSchemaTypes.Number, "Parameter 'test-header' is not of type 'array[number]'" }, + RequestUri = new Uri(path, UriKind.Relative), + Method = HttpMethod.Get }; - [Theory] - [MemberData(nameof(HeaderParameterTypeMismatchData))] - public void Validate_ThrowsException_IfHeaderParameterIsNotOfSpecifiedType( - string parameterValue, - JsonSchemaType specifiedType, - JsonSchemaType? specifiedItemsType, - string? expectedErrorMessage) + var exception = Record.Exception(() => { - var openApiDocument = DocumentWithOperation("/api/products", OperationType.Get, new OpenApiOperation + Subject().Validate(request, openApiDocument, "/api/products", OperationType.Get); + }); + + Assert.Equal(expectedErrorMessage, exception?.Message); + } + + public static TheoryData HeaderParameterTypeMismatchData => new() + { + { "foo", JsonSchemaTypes.Boolean, null, "Parameter 'test-header' is not of type 'boolean'" }, + { "foo", JsonSchemaTypes.Number, null, "Parameter 'test-header' is not of type 'number'" }, + { "true", JsonSchemaTypes.Boolean, null, null }, + { "1", JsonSchemaTypes.Number, null, null }, + { "foo", JsonSchemaTypes.String, null, null }, + { "1,2", JsonSchemaTypes.Array, JsonSchemaTypes.Number, null }, + { "1,foo", JsonSchemaTypes.Array, JsonSchemaTypes.Number, "Parameter 'test-header' is not of type 'array[number]'" }, + }; + + [Theory] + [MemberData(nameof(HeaderParameterTypeMismatchData))] + public void Validate_ThrowsException_IfHeaderParameterIsNotOfSpecifiedType( + string parameterValue, + JsonSchemaType specifiedType, + JsonSchemaType? specifiedItemsType, + string? expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products", OperationType.Get, new OpenApiOperation + { + Parameters = new List { - Parameters = new List + new OpenApiParameter { - new OpenApiParameter + Name = "test-header", + In = ParameterLocation.Header, + Schema = new OpenApiSchema { - Name = "test-header", - In = ParameterLocation.Header, - Schema = new OpenApiSchema - { - Type = specifiedType, - Items = (specifiedItemsType != null) ? new OpenApiSchema { Type = specifiedItemsType } : null - } + Type = specifiedType, + Items = (specifiedItemsType != null) ? new OpenApiSchema { Type = specifiedItemsType } : null } } - }); - var request = new HttpRequestMessage - { - RequestUri = new Uri("/api/products", UriKind.Relative), - Method = HttpMethod.Get - }; - if (parameterValue != null) request.Headers.Add("test-header", parameterValue); + } + }); + var request = new HttpRequestMessage + { + RequestUri = new Uri("/api/products", UriKind.Relative), + Method = HttpMethod.Get + }; + if (parameterValue != null) request.Headers.Add("test-header", parameterValue); - var exception = Record.Exception(() => - { - Subject().Validate(request, openApiDocument, "/api/products", OperationType.Get); - }); + var exception = Record.Exception(() => + { + Subject().Validate(request, openApiDocument, "/api/products", OperationType.Get); + }); - Assert.Equal(expectedErrorMessage, exception?.Message); - } + Assert.Equal(expectedErrorMessage, exception?.Message); + } #nullable restore - [Theory] - [InlineData(null, "Required content is not present")] - [InlineData("foo", null)] - public void Validate_ThrowsException_IfRequiredContentIsNotPresent( - string contentString, - string expectedErrorMessage) + [Theory] + [InlineData(null, "Required content is not present")] + [InlineData("foo", null)] + public void Validate_ThrowsException_IfRequiredContentIsNotPresent( + string contentString, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products", OperationType.Post, new OpenApiOperation { - var openApiDocument = DocumentWithOperation("/api/products", OperationType.Post, new OpenApiOperation + RequestBody = new OpenApiRequestBody { - RequestBody = new OpenApiRequestBody + Required = true, + Content = new Dictionary { - Required = true, - Content = new Dictionary - { - [ "text/plain" ] = new OpenApiMediaType() - } + [ "text/plain" ] = new OpenApiMediaType() } - }); - var request = new HttpRequestMessage - { - RequestUri = new Uri("/api/products", UriKind.Relative), - Method = HttpMethod.Post - }; - if (contentString != null) request.Content = new StringContent(contentString); + } + }); + var request = new HttpRequestMessage + { + RequestUri = new Uri("/api/products", UriKind.Relative), + Method = HttpMethod.Post + }; + if (contentString != null) request.Content = new StringContent(contentString); - var exception = Record.Exception(() => - { - Subject().Validate(request, openApiDocument, "/api/products", OperationType.Post); - }); - - Assert.Equal(expectedErrorMessage, exception?.Message); - } - - [Theory] - [InlineData("application/foo", "Content media type 'application/foo' is not specified")] - [InlineData("application/json", null)] - public void Validate_ThrowsException_IfContentMediaTypeIsNotSpecified( - string mediaType, - string expectedErrorMessage) + var exception = Record.Exception(() => + { + Subject().Validate(request, openApiDocument, "/api/products", OperationType.Post); + }); + + Assert.Equal(expectedErrorMessage, exception?.Message); + } + + [Theory] + [InlineData("application/foo", "Content media type 'application/foo' is not specified")] + [InlineData("application/json", null)] + public void Validate_ThrowsException_IfContentMediaTypeIsNotSpecified( + string mediaType, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products", OperationType.Post, new OpenApiOperation { - var openApiDocument = DocumentWithOperation("/api/products", OperationType.Post, new OpenApiOperation + RequestBody = new OpenApiRequestBody { - RequestBody = new OpenApiRequestBody + Content = new Dictionary { - Content = new Dictionary - { - [ "application/json" ] = new OpenApiMediaType() - } + [ "application/json" ] = new OpenApiMediaType() } - }); - var request = new HttpRequestMessage - { - RequestUri = new Uri("/api/products", UriKind.Relative), - Method = HttpMethod.Post, - Content = new StringContent("{\"foo\":\"bar\"}", Encoding.UTF8, mediaType) - }; + } + }); + var request = new HttpRequestMessage + { + RequestUri = new Uri("/api/products", UriKind.Relative), + Method = HttpMethod.Post, + Content = new StringContent("{\"foo\":\"bar\"}", Encoding.UTF8, mediaType) + }; - var exception = Record.Exception(() => - { - Subject().Validate(request, openApiDocument, "/api/products", OperationType.Post); - }); - - Assert.Equal(expectedErrorMessage, exception?.Message); - } - - [Theory] - [InlineData("{\"prop1\":\"foo\"}", "Content does not match spec. Path: . Required property(s) not present")] - [InlineData("{\"prop1\":\"foo\",\"prop2\":\"bar\"}", null)] - public void Validate_DelegatesContentValidationToInjectedContentValidators( - string jsonString, - string expectedErrorMessage) + var exception = Record.Exception(() => { - var openApiDocument = DocumentWithOperation("/api/products", OperationType.Post, new OpenApiOperation + Subject().Validate(request, openApiDocument, "/api/products", OperationType.Post); + }); + + Assert.Equal(expectedErrorMessage, exception?.Message); + } + + [Theory] + [InlineData("{\"prop1\":\"foo\"}", "Content does not match spec. Path: . Required property(s) not present")] + [InlineData("{\"prop1\":\"foo\",\"prop2\":\"bar\"}", null)] + public void Validate_DelegatesContentValidationToInjectedContentValidators( + string jsonString, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products", OperationType.Post, new OpenApiOperation + { + RequestBody = new OpenApiRequestBody { - RequestBody = new OpenApiRequestBody + Content = new Dictionary { - Content = new Dictionary + [ "application/json" ] = new OpenApiMediaType { - [ "application/json" ] = new OpenApiMediaType + Schema = new OpenApiSchema { - Schema = new OpenApiSchema - { - Type = JsonSchemaTypes.Object, - Required = new SortedSet { "prop1", "prop2" } - } + Type = JsonSchemaTypes.Object, + Required = new SortedSet { "prop1", "prop2" } } } } - }); - var request = new HttpRequestMessage - { - RequestUri = new Uri("/api/products", UriKind.Relative), - Method = HttpMethod.Post, - Content = new StringContent(jsonString, Encoding.UTF8, "application/json") - }; + } + }); + var request = new HttpRequestMessage + { + RequestUri = new Uri("/api/products", UriKind.Relative), + Method = HttpMethod.Post, + Content = new StringContent(jsonString, Encoding.UTF8, "application/json") + }; - var exception = Record.Exception(() => - { - Subject([new JsonContentValidator()]).Validate(request, openApiDocument, "/api/products", OperationType.Post); - }); + var exception = Record.Exception(() => + { + Subject([new JsonContentValidator()]).Validate(request, openApiDocument, "/api/products", OperationType.Post); + }); - Assert.Equal(expectedErrorMessage, exception?.Message); - } + Assert.Equal(expectedErrorMessage, exception?.Message); + } - private static OpenApiDocument DocumentWithOperation(string pathTemplate, OperationType operationType, OpenApiOperation operationSpec) + private static OpenApiDocument DocumentWithOperation(string pathTemplate, OperationType operationType, OpenApiOperation operationSpec) + { + return new OpenApiDocument { - return new OpenApiDocument + Paths = new OpenApiPaths { - Paths = new OpenApiPaths + [pathTemplate] = new OpenApiPathItem { - [pathTemplate] = new OpenApiPathItem + Operations = new Dictionary { - Operations = new Dictionary - { - [operationType] = operationSpec - } + [operationType] = operationSpec } - }, - Components = new OpenApiComponents - { - Schemas = new Dictionary() } - }; - } + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary() + } + }; + } - private static RequestValidator Subject(IEnumerable contentValidators = null) - { - return new RequestValidator(contentValidators ?? []); - } + private static RequestValidator Subject(IEnumerable contentValidators = null) + { + return new RequestValidator(contentValidators ?? []); } } diff --git a/test/Swashbuckle.AspNetCore.ApiTesting.Test/ResponseValidatorTests.cs b/test/Swashbuckle.AspNetCore.ApiTesting.Test/ResponseValidatorTests.cs index 76e8f47a2b..86ef205636 100644 --- a/test/Swashbuckle.AspNetCore.ApiTesting.Test/ResponseValidatorTests.cs +++ b/test/Swashbuckle.AspNetCore.ApiTesting.Test/ResponseValidatorTests.cs @@ -5,261 +5,260 @@ using JsonSchemaType = string; -namespace Swashbuckle.AspNetCore.ApiTesting.Test +namespace Swashbuckle.AspNetCore.ApiTesting.Test; + +public class ResponseValidatorTests { - public class ResponseValidatorTests + [Theory] + [InlineData(HttpStatusCode.InternalServerError, "Status code '500' does not match expected value '200'")] + [InlineData(HttpStatusCode.OK, null)] + public void Validate_ThrowsException_IfStatusCodeDifferentToExpectedResponseCode( + HttpStatusCode statusCode, + string expectedErrorMessage) { - [Theory] - [InlineData(HttpStatusCode.InternalServerError, "Status code '500' does not match expected value '200'")] - [InlineData(HttpStatusCode.OK, null)] - public void Validate_ThrowsException_IfStatusCodeDifferentToExpectedResponseCode( - HttpStatusCode statusCode, - string expectedErrorMessage) + var openApiDocument = DocumentWithOperation("/api/products", OperationType.Get, new OpenApiOperation { - var openApiDocument = DocumentWithOperation("/api/products", OperationType.Get, new OpenApiOperation - { - Responses = new OpenApiResponses - { - [ "200" ] = new OpenApiResponse(), - [ "500" ] = new OpenApiResponse() - } - }); - var response = new HttpResponseMessage + Responses = new OpenApiResponses { - StatusCode = statusCode - }; + [ "200" ] = new OpenApiResponse(), + [ "500" ] = new OpenApiResponse() + } + }); + var response = new HttpResponseMessage + { + StatusCode = statusCode + }; - var exception = Record.Exception(() => - { - Subject().Validate(response, openApiDocument, "/api/products", OperationType.Get, "200"); - }); + var exception = Record.Exception(() => + { + Subject().Validate(response, openApiDocument, "/api/products", OperationType.Get, "200"); + }); - Assert.Equal(expectedErrorMessage, exception?.Message); - } + Assert.Equal(expectedErrorMessage, exception?.Message); + } - [Theory] - [InlineData(null, "Required header 'test-header' is not present")] - [InlineData("foo", null)] - public void Validate_ThrowsException_IfRequiredHeaderIsNotPresent( - string headerValue, - string expectedErrorMessage) + [Theory] + [InlineData(null, "Required header 'test-header' is not present")] + [InlineData("foo", null)] + public void Validate_ThrowsException_IfRequiredHeaderIsNotPresent( + string headerValue, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products", OperationType.Post, new OpenApiOperation { - var openApiDocument = DocumentWithOperation("/api/products", OperationType.Post, new OpenApiOperation + Responses = new OpenApiResponses { - Responses = new OpenApiResponses + [ "201" ] = new OpenApiResponse { - [ "201" ] = new OpenApiResponse + Headers = new Dictionary { - Headers = new Dictionary + [ "test-header" ] = new OpenApiHeader { - [ "test-header" ] = new OpenApiHeader - { - Required = true - } + Required = true } } } - }); - var response = new HttpResponseMessage - { - StatusCode = HttpStatusCode.Created - }; - if (headerValue != null) response.Headers.Add("test-header", headerValue); + } + }); + var response = new HttpResponseMessage + { + StatusCode = HttpStatusCode.Created + }; + if (headerValue != null) response.Headers.Add("test-header", headerValue); - var exception = Record.Exception(() => - { - Subject().Validate(response, openApiDocument, "/api/products", OperationType.Post, "201"); - }); + var exception = Record.Exception(() => + { + Subject().Validate(response, openApiDocument, "/api/products", OperationType.Post, "201"); + }); - Assert.Equal(expectedErrorMessage, exception?.Message); - } + Assert.Equal(expectedErrorMessage, exception?.Message); + } #nullable enable - public static TheoryData HeaderTypeValidationData => new() - { - { "foo", JsonSchemaTypes.Boolean, null, "Header 'test-header' is not of type 'boolean'" }, - { "foo", JsonSchemaTypes.Number, null, "Header 'test-header' is not of type 'number'" }, - { "true", JsonSchemaTypes.Boolean, null, null }, - { "1", JsonSchemaTypes.Number, null, null }, - { "foo", JsonSchemaTypes.String, null, null }, - { "1,2", JsonSchemaTypes.Array, JsonSchemaTypes.Number, null }, - { "1,foo", JsonSchemaTypes.Array, JsonSchemaTypes.Number, "Header 'test-header' is not of type 'array[number]'" }, - }; + public static TheoryData HeaderTypeValidationData => new() + { + { "foo", JsonSchemaTypes.Boolean, null, "Header 'test-header' is not of type 'boolean'" }, + { "foo", JsonSchemaTypes.Number, null, "Header 'test-header' is not of type 'number'" }, + { "true", JsonSchemaTypes.Boolean, null, null }, + { "1", JsonSchemaTypes.Number, null, null }, + { "foo", JsonSchemaTypes.String, null, null }, + { "1,2", JsonSchemaTypes.Array, JsonSchemaTypes.Number, null }, + { "1,foo", JsonSchemaTypes.Array, JsonSchemaTypes.Number, "Header 'test-header' is not of type 'array[number]'" }, + }; - [Theory] - [MemberData(nameof(HeaderTypeValidationData))] - public void Validate_ThrowsException_IfHeaderIsNotOfSpecifiedType( - string headerValue, - JsonSchemaType specifiedType, - JsonSchemaType? specifiedItemsType, - string? expectedErrorMessage) + [Theory] + [MemberData(nameof(HeaderTypeValidationData))] + public void Validate_ThrowsException_IfHeaderIsNotOfSpecifiedType( + string headerValue, + JsonSchemaType specifiedType, + JsonSchemaType? specifiedItemsType, + string? expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products", OperationType.Post, new OpenApiOperation { - var openApiDocument = DocumentWithOperation("/api/products", OperationType.Post, new OpenApiOperation + Responses = new OpenApiResponses { - Responses = new OpenApiResponses + [ "201" ] = new OpenApiResponse { - [ "201" ] = new OpenApiResponse + Headers = new Dictionary { - Headers = new Dictionary + [ "test-header" ] = new OpenApiHeader { - [ "test-header" ] = new OpenApiHeader + Schema = new OpenApiSchema { - Schema = new OpenApiSchema - { - Type = specifiedType, - Items = specifiedItemsType != null ? new OpenApiSchema { Type = specifiedItemsType } : null - } + Type = specifiedType, + Items = specifiedItemsType != null ? new OpenApiSchema { Type = specifiedItemsType } : null } } } } - }); - var response = new HttpResponseMessage - { - StatusCode = HttpStatusCode.Created - }; - response.Headers.Add("test-header", headerValue); + } + }); + var response = new HttpResponseMessage + { + StatusCode = HttpStatusCode.Created + }; + response.Headers.Add("test-header", headerValue); - var exception = Record.Exception(() => - { - Subject().Validate(response, openApiDocument, "/api/products", OperationType.Post, "201"); - }); + var exception = Record.Exception(() => + { + Subject().Validate(response, openApiDocument, "/api/products", OperationType.Post, "201"); + }); - Assert.Equal(expectedErrorMessage, exception?.Message); - } + Assert.Equal(expectedErrorMessage, exception?.Message); + } #nullable restore - [Theory] - [InlineData(null, "Expected content is not present")] - [InlineData("foo", null)] - public void Validate_ThrowsException_IfExpectedContentIsNotPresent( - string contentString, - string expectedErrorMessage) + [Theory] + [InlineData(null, "Expected content is not present")] + [InlineData("foo", null)] + public void Validate_ThrowsException_IfExpectedContentIsNotPresent( + string contentString, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products/1", OperationType.Get, new OpenApiOperation { - var openApiDocument = DocumentWithOperation("/api/products/1", OperationType.Get, new OpenApiOperation + Responses = new OpenApiResponses { - Responses = new OpenApiResponses + [ "200" ] = new OpenApiResponse { - [ "200" ] = new OpenApiResponse + Content = new Dictionary { - Content = new Dictionary - { - ["text/plain"] = new OpenApiMediaType() - } + ["text/plain"] = new OpenApiMediaType() } } - }); - var response = new HttpResponseMessage - { - }; - if (contentString != null) response.Content = new StringContent(contentString); + } + }); + var response = new HttpResponseMessage + { + }; + if (contentString != null) response.Content = new StringContent(contentString); - var exception = Record.Exception(() => - { - Subject().Validate(response, openApiDocument, "/api/products/1", OperationType.Get, "200"); - }); + var exception = Record.Exception(() => + { + Subject().Validate(response, openApiDocument, "/api/products/1", OperationType.Get, "200"); + }); - Assert.Equal(expectedErrorMessage, exception?.Message); - } + Assert.Equal(expectedErrorMessage, exception?.Message); + } - [Theory] - [InlineData("application/foo", "Content media type 'application/foo' is not specified")] - [InlineData("application/json", null)] - public void Validate_ThrowsException_IfContentMediaTypeIsNotSpecified( - string mediaType, - string expectedErrorMessage) + [Theory] + [InlineData("application/foo", "Content media type 'application/foo' is not specified")] + [InlineData("application/json", null)] + public void Validate_ThrowsException_IfContentMediaTypeIsNotSpecified( + string mediaType, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products/1", OperationType.Get, new OpenApiOperation { - var openApiDocument = DocumentWithOperation("/api/products/1", OperationType.Get, new OpenApiOperation + Responses = new OpenApiResponses { - Responses = new OpenApiResponses + [ "200" ] = new OpenApiResponse { - [ "200" ] = new OpenApiResponse + Content = new Dictionary { - Content = new Dictionary - { - ["application/json"] = new OpenApiMediaType() - } + ["application/json"] = new OpenApiMediaType() } } - }); - var response = new HttpResponseMessage - { - Content = new StringContent("{\"foo\":\"bar\"}", Encoding.UTF8, mediaType) - }; + } + }); + var response = new HttpResponseMessage + { + Content = new StringContent("{\"foo\":\"bar\"}", Encoding.UTF8, mediaType) + }; - var exception = Record.Exception(() => - { - Subject().Validate(response, openApiDocument, "/api/products/1", OperationType.Get, "200"); - }); + var exception = Record.Exception(() => + { + Subject().Validate(response, openApiDocument, "/api/products/1", OperationType.Get, "200"); + }); - Assert.Equal(expectedErrorMessage, exception?.Message); - } + Assert.Equal(expectedErrorMessage, exception?.Message); + } - [Theory] - [InlineData("{\"prop1\":\"foo\"}", "Content does not match spec. Path: . Required property(s) not present")] - [InlineData("{\"prop1\":\"foo\",\"prop2\":\"bar\"}", null)] - public void Validate_DelegatesContentValidationToInjectedContentValidators( - string jsonString, - string expectedErrorMessage) + [Theory] + [InlineData("{\"prop1\":\"foo\"}", "Content does not match spec. Path: . Required property(s) not present")] + [InlineData("{\"prop1\":\"foo\",\"prop2\":\"bar\"}", null)] + public void Validate_DelegatesContentValidationToInjectedContentValidators( + string jsonString, + string expectedErrorMessage) + { + var openApiDocument = DocumentWithOperation("/api/products/1", OperationType.Get, new OpenApiOperation { - var openApiDocument = DocumentWithOperation("/api/products/1", OperationType.Get, new OpenApiOperation + Responses = new OpenApiResponses { - Responses = new OpenApiResponses + [ "200" ] = new OpenApiResponse { - [ "200" ] = new OpenApiResponse + Content = new Dictionary { - Content = new Dictionary + ["application/json"] = new OpenApiMediaType { - ["application/json"] = new OpenApiMediaType + Schema = new OpenApiSchema { - Schema = new OpenApiSchema - { - Type = JsonSchemaTypes.Object, - Required = new SortedSet { "prop1", "prop2" } - } + Type = JsonSchemaTypes.Object, + Required = new SortedSet { "prop1", "prop2" } } } } } - }); - var response = new HttpResponseMessage - { - Content = new StringContent(jsonString, Encoding.UTF8, "application/json") - }; + } + }); + var response = new HttpResponseMessage + { + Content = new StringContent(jsonString, Encoding.UTF8, "application/json") + }; - var exception = Record.Exception(() => - { - Subject([new JsonContentValidator()]).Validate(response, openApiDocument, "/api/products/1", OperationType.Get, "200"); - }); + var exception = Record.Exception(() => + { + Subject([new JsonContentValidator()]).Validate(response, openApiDocument, "/api/products/1", OperationType.Get, "200"); + }); - Assert.Equal(expectedErrorMessage, exception?.Message); - } + Assert.Equal(expectedErrorMessage, exception?.Message); + } - private static OpenApiDocument DocumentWithOperation(string pathTemplate, OperationType operationType, OpenApiOperation operationSpec) + private static OpenApiDocument DocumentWithOperation(string pathTemplate, OperationType operationType, OpenApiOperation operationSpec) + { + return new OpenApiDocument { - return new OpenApiDocument + Paths = new OpenApiPaths { - Paths = new OpenApiPaths + [pathTemplate] = new OpenApiPathItem { - [pathTemplate] = new OpenApiPathItem + Operations = new Dictionary { - Operations = new Dictionary - { - [operationType] = operationSpec - } + [operationType] = operationSpec } - }, - Components = new OpenApiComponents - { - Schemas = new Dictionary() } - }; - } + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary() + } + }; + } - private static ResponseValidator Subject(IEnumerable contentValidators = null) - { - return new ResponseValidator(contentValidators ?? []); - } + private static ResponseValidator Subject(IEnumerable contentValidators = null) + { + return new ResponseValidator(contentValidators ?? []); } } diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs index 2190c078be..774223f9b4 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs @@ -1,108 +1,107 @@ using Xunit; -namespace Swashbuckle.AspNetCore.Cli.Test +namespace Swashbuckle.AspNetCore.Cli.Test; + +public static class CommandRunnerTests { - public static class CommandRunnerTests + [Fact] + public static void Run_ParsesArgumentsAndExecutesCommands_AccordingToConfiguredMetadata() { - [Fact] - public static void Run_ParsesArgumentsAndExecutesCommands_AccordingToConfiguredMetadata() - { - var receivedValues = new List(); - var subject = new CommandRunner("test", "a test", new StringWriter()); - subject.SubCommand("cmd1", "", c => { - c.Option("--opt1", ""); - c.Option("--opt2", "", true); - c.Argument("arg1", ""); - c.OnRun((namedArgs) => - { - receivedValues.Add(namedArgs["--opt1"]); - receivedValues.Add(namedArgs["--opt2"]); - receivedValues.Add(namedArgs["arg1"]); - return 2; - }); + var receivedValues = new List(); + var subject = new CommandRunner("test", "a test", new StringWriter()); + subject.SubCommand("cmd1", "", c => { + c.Option("--opt1", ""); + c.Option("--opt2", "", true); + c.Argument("arg1", ""); + c.OnRun((namedArgs) => + { + receivedValues.Add(namedArgs["--opt1"]); + receivedValues.Add(namedArgs["--opt2"]); + receivedValues.Add(namedArgs["arg1"]); + return 2; }); - subject.SubCommand("cmd2", "", c => { - c.Option("--opt1", ""); - c.Option("--opt2", "", true); - c.Argument("arg1", ""); - c.OnRun((namedArgs) => - { - receivedValues.Add(namedArgs["--opt1"]); - receivedValues.Add(namedArgs["--opt2"]); - receivedValues.Add(namedArgs["arg1"]); - return 3; - }); + }); + subject.SubCommand("cmd2", "", c => { + c.Option("--opt1", ""); + c.Option("--opt2", "", true); + c.Argument("arg1", ""); + c.OnRun((namedArgs) => + { + receivedValues.Add(namedArgs["--opt1"]); + receivedValues.Add(namedArgs["--opt2"]); + receivedValues.Add(namedArgs["arg1"]); + return 3; }); + }); - var cmd1ExitCode = subject.Run(["cmd1", "--opt1", "foo", "--opt2", "bar"]); - var cmd2ExitCode = subject.Run(["cmd2", "--opt1", "blah", "--opt2", "dblah"]); + var cmd1ExitCode = subject.Run(["cmd1", "--opt1", "foo", "--opt2", "bar"]); + var cmd2ExitCode = subject.Run(["cmd2", "--opt1", "blah", "--opt2", "dblah"]); - Assert.Equal(2, cmd1ExitCode); - Assert.Equal(3, cmd2ExitCode); - Assert.Equal(["foo", null, "bar", "blah", null, "dblah"], [.. receivedValues]); - } + Assert.Equal(2, cmd1ExitCode); + Assert.Equal(3, cmd2ExitCode); + Assert.Equal(["foo", null, "bar", "blah", null, "dblah"], [.. receivedValues]); + } - [Fact] - public static void Run_PrintsAvailableCommands_WhenUnexpectedCommandIsProvided() - { - var output = new StringWriter(); - var subject = new CommandRunner("test", "a test", output); - subject.SubCommand("cmd", "does something", c => { - }); + [Fact] + public static void Run_PrintsAvailableCommands_WhenUnexpectedCommandIsProvided() + { + var output = new StringWriter(); + var subject = new CommandRunner("test", "a test", output); + subject.SubCommand("cmd", "does something", c => { + }); - var exitCode = subject.Run(["foo"]); + var exitCode = subject.Run(["foo"]); - Assert.StartsWith("a test", output.ToString()); - Assert.Contains("Commands:", output.ToString()); - Assert.Contains("cmd: does something", output.ToString()); - } + Assert.StartsWith("a test", output.ToString()); + Assert.Contains("Commands:", output.ToString()); + Assert.Contains("cmd: does something", output.ToString()); + } - [Fact] - public static void Run_PrintsAvailableCommands_WhenHelpOptionIsProvided() - { - var output = new StringWriter(); - var subject = new CommandRunner("test", "a test", output); - subject.SubCommand("cmd", "does something", c => { - }); + [Fact] + public static void Run_PrintsAvailableCommands_WhenHelpOptionIsProvided() + { + var output = new StringWriter(); + var subject = new CommandRunner("test", "a test", output); + subject.SubCommand("cmd", "does something", c => { + }); - var exitCode = subject.Run(["--help"]); + var exitCode = subject.Run(["--help"]); - Assert.StartsWith("a test", output.ToString()); - Assert.Contains("Commands:", output.ToString()); - Assert.Contains("cmd: does something", output.ToString()); - } + Assert.StartsWith("a test", output.ToString()); + Assert.Contains("Commands:", output.ToString()); + Assert.Contains("cmd: does something", output.ToString()); + } - [Theory] - [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt2", "foo" }, true)] - [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1" }, true)] - [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1", "--opt2" }, true)] - [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1", "foo" }, false)] - [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd" }, true)] - [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "--opt1" }, true)] - [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "foo", "bar" }, true)] - [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "foo" }, false)] - public static void Run_PrintsCommandUsage_WhenUnexpectedArgumentsAreProvided( - string[] optionNames, - string[] argNames, - string[] providedArgs, - bool shouldPrintUsage) + [Theory] + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt2", "foo" }, true)] + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1" }, true)] + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1", "--opt2" }, true)] + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1", "foo" }, false)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd" }, true)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "--opt1" }, true)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "foo", "bar" }, true)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "foo" }, false)] + public static void Run_PrintsCommandUsage_WhenUnexpectedArgumentsAreProvided( + string[] optionNames, + string[] argNames, + string[] providedArgs, + bool shouldPrintUsage) + { + var output = new StringWriter(); + var subject = new CommandRunner("test", "a test", output); + subject.SubCommand("cmd", "a command", c => { - var output = new StringWriter(); - var subject = new CommandRunner("test", "a test", output); - subject.SubCommand("cmd", "a command", c => - { - foreach (var name in optionNames) - c.Option(name, ""); - foreach (var name in argNames) - c.Argument(name, ""); - }); + foreach (var name in optionNames) + c.Option(name, ""); + foreach (var name in argNames) + c.Argument(name, ""); + }); - subject.Run(providedArgs); + subject.Run(providedArgs); - if (shouldPrintUsage) - Assert.StartsWith("Usage: test cmd", output.ToString()); - else - Assert.Empty(output.ToString()); - } + if (shouldPrintUsage) + Assert.StartsWith("Usage: test cmd", output.ToString()); + else + Assert.Empty(output.ToString()); } } diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs index f26c4920b8..bf9fe4e494 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -3,288 +3,287 @@ using Swashbuckle.AspNetCore.TestSupport.Utilities; using Xunit; -namespace Swashbuckle.AspNetCore.Cli.Test +namespace Swashbuckle.AspNetCore.Cli.Test; + +public static class ToolTests { - public static class ToolTests + [Fact] + public static void Can_Output_Swagger_Document_Names() { - [Fact] - public static void Can_Output_Swagger_Document_Names() - { - var result = RunToStringCommand((outputPath) => - [ - "list", - "--output", - outputPath, - Path.Combine(Directory.GetCurrentDirectory(), "MultipleVersions.dll") - ], nameof(Can_Output_Swagger_Document_Names)); - var expected = $"\"1.0\"{Environment.NewLine}\"2.0\"{Environment.NewLine}"; - Assert.Equal(expected, result); - } - - [Fact] - public static void Throws_When_Startup_Assembly_Does_Not_Exist() - { - string[] args = ["tofile", "--output", "swagger.json", "--openapiversion", "2.0", "./does_not_exist.dll", "v1"]; - Assert.Throws(() => Program.Main(args)); - } - - [Theory] - [InlineData("a")] - [InlineData("1.9")] - [InlineData("3.2")] - public static void Error_When_OpenApiVersion_Is_Not_Supported(string version) - { - string[] args = - [ - "tofile", - "--output", - "swagger.json", - "--openapiversion", - version, - Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), - "v1" - ]; - Assert.NotEqual(0, Program.Main(args)); - } - - [Fact] - public static void Can_Generate_Swagger_Json_v2_OpenApiVersion() - { - using var document = RunToJsonCommand((outputPath) => - [ - "tofile", - "--output", - outputPath, - "--openapiversion", - "2.0", - Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), - "v1" - ]); - - Assert.Equal("2.0", document.RootElement.GetProperty("swagger").GetString()); - - // verify one of the endpoints - var paths = document.RootElement.GetProperty("paths"); - var productsPath = paths.GetProperty("/products"); - Assert.True(productsPath.TryGetProperty("post", out _)); - } - - [Fact] - public static void Can_Generate_Swagger_Json_v2_SerializeAsV2() - { - using var document = RunToJsonCommand((outputPath) => - [ - "tofile", - "--output", - outputPath, - "--serializeasv2", - Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), - "v1" - ]); - - Assert.Equal("2.0", document.RootElement.GetProperty("swagger").GetString()); - - // verify one of the endpoints - var paths = document.RootElement.GetProperty("paths"); - var productsPath = paths.GetProperty("/products"); - Assert.True(productsPath.TryGetProperty("post", out _)); - } + var result = RunToStringCommand((outputPath) => + [ + "list", + "--output", + outputPath, + Path.Combine(Directory.GetCurrentDirectory(), "MultipleVersions.dll") + ], nameof(Can_Output_Swagger_Document_Names)); + var expected = $"\"1.0\"{Environment.NewLine}\"2.0\"{Environment.NewLine}"; + Assert.Equal(expected, result); + } - [Fact] - public static void Can_Generate_Swagger_Json_v3() - { - using var document = RunToJsonCommand((outputPath) => - [ - "tofile", - "--output", - outputPath, - Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), - "v1" - ]); + [Fact] + public static void Throws_When_Startup_Assembly_Does_Not_Exist() + { + string[] args = ["tofile", "--output", "swagger.json", "--openapiversion", "2.0", "./does_not_exist.dll", "v1"]; + Assert.Throws(() => Program.Main(args)); + } - Assert.StartsWith("3.0.", document.RootElement.GetProperty("openapi").GetString()); + [Theory] + [InlineData("a")] + [InlineData("1.9")] + [InlineData("3.2")] + public static void Error_When_OpenApiVersion_Is_Not_Supported(string version) + { + string[] args = + [ + "tofile", + "--output", + "swagger.json", + "--openapiversion", + version, + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]; + Assert.NotEqual(0, Program.Main(args)); + } - // verify one of the endpoints - var paths = document.RootElement.GetProperty("paths"); - var productsPath = paths.GetProperty("/products"); - Assert.True(productsPath.TryGetProperty("post", out _)); - } + [Fact] + public static void Can_Generate_Swagger_Json_v2_OpenApiVersion() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--openapiversion", + "2.0", + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]); + + Assert.Equal("2.0", document.RootElement.GetProperty("swagger").GetString()); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var productsPath = paths.GetProperty("/products"); + Assert.True(productsPath.TryGetProperty("post", out _)); + } - [Fact] - public static void Can_Generate_Swagger_Json_v3_OpenApiVersion() - { - using var document = RunToJsonCommand((outputPath) => - [ - "tofile", - "--output", - outputPath, - "--openapiversion", - "3.0", - Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), - "v1" - ]); + [Fact] + public static void Can_Generate_Swagger_Json_v2_SerializeAsV2() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--serializeasv2", + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]); + + Assert.Equal("2.0", document.RootElement.GetProperty("swagger").GetString()); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var productsPath = paths.GetProperty("/products"); + Assert.True(productsPath.TryGetProperty("post", out _)); + } - Assert.StartsWith("3.0.", document.RootElement.GetProperty("openapi").GetString()); + [Fact] + public static void Can_Generate_Swagger_Json_v3() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]); + + Assert.StartsWith("3.0.", document.RootElement.GetProperty("openapi").GetString()); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var productsPath = paths.GetProperty("/products"); + Assert.True(productsPath.TryGetProperty("post", out _)); + } - // verify one of the endpoints - var paths = document.RootElement.GetProperty("paths"); - var productsPath = paths.GetProperty("/products"); - Assert.True(productsPath.TryGetProperty("post", out _)); - } + [Fact] + public static void Can_Generate_Swagger_Json_v3_OpenApiVersion() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--openapiversion", + "3.0", + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]); + + Assert.StartsWith("3.0.", document.RootElement.GetProperty("openapi").GetString()); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var productsPath = paths.GetProperty("/products"); + Assert.True(productsPath.TryGetProperty("post", out _)); + } - [Fact] - public static void Overwrites_Existing_File() + [Fact] + public static void Overwrites_Existing_File() + { + using var document = RunToJsonCommand((outputPath) => { - using var document = RunToJsonCommand((outputPath) => - { - File.WriteAllText(outputPath, new string('x', 100_000)); - - return - [ - "tofile", - "--output", - outputPath, - "--openapiversion", - "2.0", - Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), - "v1" - ]; - }); - - // verify one of the endpoints - var paths = document.RootElement.GetProperty("paths"); - var productsPath = paths.GetProperty("/products"); - Assert.True(productsPath.TryGetProperty("post", out _)); - } + File.WriteAllText(outputPath, new string('x', 100_000)); - [Fact] - public static void CustomDocumentSerializer_Writes_Custom_V2_Document() - { - using var document = RunToJsonCommand((outputPath) => + return [ "tofile", "--output", outputPath, "--openapiversion", "2.0", - Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), - "v1" - ]); - - // verify that the custom serializer wrote the swagger info - var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); - Assert.Equal("DocumentSerializerTest2.0", swaggerInfo); - } - - [Fact] - public static void CustomDocumentSerializer_Writes_Custom_V3_Document() - { - using var document = RunToJsonCommand((outputPath) => - [ - "tofile", - "--output", - outputPath, - Path.Combine(Directory.GetCurrentDirectory(), - "CustomDocumentSerializer.dll"), + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" - ]); + ]; + }); - // verify that the custom serializer wrote the swagger info - var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); - Assert.Equal("DocumentSerializerTest3.0", swaggerInfo); - } + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var productsPath = paths.GetProperty("/products"); + Assert.True(productsPath.TryGetProperty("post", out _)); + } - [Fact] - public static void Can_Generate_Swagger_Json_ForTopLevelApp() - { - using var document = RunToJsonCommand((outputPath) => - [ - "tofile", - "--output", - outputPath, - "--openapiversion", - "2.0", - Path.Combine(Directory.GetCurrentDirectory(), "MinimalApp.dll"), - "v1" - ]); + [Fact] + public static void CustomDocumentSerializer_Writes_Custom_V2_Document() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--openapiversion", + "2.0", + Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), + "v1" + ]); + + // verify that the custom serializer wrote the swagger info + var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); + Assert.Equal("DocumentSerializerTest2.0", swaggerInfo); + } - // verify one of the endpoints - var paths = document.RootElement.GetProperty("paths"); - var path = paths.GetProperty("/WeatherForecast"); - Assert.True(path.TryGetProperty("get", out _)); - } + [Fact] + public static void CustomDocumentSerializer_Writes_Custom_V3_Document() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + Path.Combine(Directory.GetCurrentDirectory(), + "CustomDocumentSerializer.dll"), + "v1" + ]); + + // verify that the custom serializer wrote the swagger info + var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); + Assert.Equal("DocumentSerializerTest3.0", swaggerInfo); + } - [Fact] - public static void Does_Not_Run_Crashing_HostedService() - { - using var document = RunToJsonCommand((outputPath) => - [ - "tofile", - "--output", - outputPath, - Path.Combine(Directory.GetCurrentDirectory(), "MinimalAppWithHostedServices.dll"), - "v1" - ]); + [Fact] + public static void Can_Generate_Swagger_Json_ForTopLevelApp() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--openapiversion", + "2.0", + Path.Combine(Directory.GetCurrentDirectory(), "MinimalApp.dll"), + "v1" + ]); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var path = paths.GetProperty("/WeatherForecast"); + Assert.True(path.TryGetProperty("get", out _)); + } - // verify one of the endpoints - var paths = document.RootElement.GetProperty("paths"); - var path = paths.GetProperty("/ShouldContain"); - Assert.True(path.TryGetProperty("get", out _)); - } + [Fact] + public static void Does_Not_Run_Crashing_HostedService() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + Path.Combine(Directory.GetCurrentDirectory(), "MinimalAppWithHostedServices.dll"), + "v1" + ]); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var path = paths.GetProperty("/ShouldContain"); + Assert.True(path.TryGetProperty("get", out _)); + } - [Fact] - public static void Creates_New_Folder_Path() - { - using var document = RunToJsonCommand(outputPath => - [ - "tofile", - "--output", - outputPath, - "--openapiversion", - "2.0", - Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), - "v1" - ], GenerateRandomString(5)); + [Fact] + public static void Creates_New_Folder_Path() + { + using var document = RunToJsonCommand(outputPath => + [ + "tofile", + "--output", + outputPath, + "--openapiversion", + "2.0", + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ], GenerateRandomString(5)); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var productsPath = paths.GetProperty("/products"); + Assert.True(productsPath.TryGetProperty("post", out _)); + } - // verify one of the endpoints - var paths = document.RootElement.GetProperty("paths"); - var productsPath = paths.GetProperty("/products"); - Assert.True(productsPath.TryGetProperty("post", out _)); - } + private static string RunToStringCommand(Func setup, string subOutputPath = default) + { + using var temporaryDirectory = new TemporaryDirectory(); - private static string RunToStringCommand(Func setup, string subOutputPath = default) - { - using var temporaryDirectory = new TemporaryDirectory(); + var outputPath = !string.IsNullOrEmpty(subOutputPath) + ? Path.Combine(temporaryDirectory.Path, subOutputPath, "swagger.json") + : Path.Combine(temporaryDirectory.Path, "swagger.json"); - var outputPath = !string.IsNullOrEmpty(subOutputPath) - ? Path.Combine(temporaryDirectory.Path, subOutputPath, "swagger.json") - : Path.Combine(temporaryDirectory.Path, "swagger.json"); + string[] args = setup(outputPath); - string[] args = setup(outputPath); + Assert.Equal(0, Program.Main(args)); - Assert.Equal(0, Program.Main(args)); + return File.ReadAllText(outputPath); + } - return File.ReadAllText(outputPath); - } + private static JsonDocument RunToJsonCommand(Func setup, string subOutputPath = default) + { + string json = RunToStringCommand(setup, subOutputPath); + return JsonDocument.Parse(json); + } - private static JsonDocument RunToJsonCommand(Func setup, string subOutputPath = default) - { - string json = RunToStringCommand(setup, subOutputPath); - return JsonDocument.Parse(json); - } + private static string GenerateRandomString(int length) + { + const string Choices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var result = new StringBuilder(length); - private static string GenerateRandomString(int length) + for (int i = 0; i < length; i++) { - const string Choices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - var result = new StringBuilder(length); - - for (int i = 0; i < length; i++) - { - result.Append(Choices[Random.Shared.Next(Choices.Length)]); - } - - return result.ToString(); + result.Append(Choices[Random.Shared.Next(Choices.Length)]); } + return result.ToString(); } + } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/CustomDocumentSerializerTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/CustomDocumentSerializerTests.cs index 9dfbbc5c7d..05b3b43e2a 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/CustomDocumentSerializerTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/CustomDocumentSerializerTests.cs @@ -5,108 +5,107 @@ using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.Swagger; -namespace Swashbuckle.AspNetCore.IntegrationTests +namespace Swashbuckle.AspNetCore.IntegrationTests; + +[Collection("TestSite")] +public class CustomDocumentSerializerTests { - [Collection("TestSite")] - public class CustomDocumentSerializerTests + [Fact] + public async Task TestSite_Writes_Custom_V3_Document() { - [Fact] - public async Task TestSite_Writes_Custom_V3_Document() - { - var testSite = new TestSite(typeof(CustomDocumentSerializer.Startup)); - var client = testSite.BuildClient(); + var testSite = new TestSite(typeof(CustomDocumentSerializer.Startup)); + var client = testSite.BuildClient(); - var swaggerResponse = await client.GetAsync($"/swagger/v1/swagger.json"); + var swaggerResponse = await client.GetAsync($"/swagger/v1/swagger.json"); - swaggerResponse.EnsureSuccessStatusCode(); - var contentStream = await swaggerResponse.Content.ReadAsStreamAsync(); - using var document = JsonDocument.Parse(contentStream); + swaggerResponse.EnsureSuccessStatusCode(); + var contentStream = await swaggerResponse.Content.ReadAsStreamAsync(); + using var document = JsonDocument.Parse(contentStream); - // verify that the custom serializer wrote the swagger info - var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); - Assert.Equal("DocumentSerializerTest3.0", swaggerInfo); - } + // verify that the custom serializer wrote the swagger info + var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); + Assert.Equal("DocumentSerializerTest3.0", swaggerInfo); + } - [Fact] - public async Task TestSite_Writes_Custom_V2_Document() - { - var testSite = new TestSite(typeof(CustomDocumentSerializer.Startup)); - var client = testSite.BuildClient(); + [Fact] + public async Task TestSite_Writes_Custom_V2_Document() + { + var testSite = new TestSite(typeof(CustomDocumentSerializer.Startup)); + var client = testSite.BuildClient(); - var swaggerResponse = await client.GetAsync($"/swagger/v1/swaggerv2.json"); + var swaggerResponse = await client.GetAsync($"/swagger/v1/swaggerv2.json"); - swaggerResponse.EnsureSuccessStatusCode(); - var contentStream = await swaggerResponse.Content.ReadAsStreamAsync(); - using var document = JsonDocument.Parse(contentStream); + swaggerResponse.EnsureSuccessStatusCode(); + var contentStream = await swaggerResponse.Content.ReadAsStreamAsync(); + using var document = JsonDocument.Parse(contentStream); - // verify that the custom serializer wrote the swagger info - var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); - Assert.Equal("DocumentSerializerTest2.0", swaggerInfo); - } + // verify that the custom serializer wrote the swagger info + var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); + Assert.Equal("DocumentSerializerTest2.0", swaggerInfo); + } - [Fact] - public async Task DocumentProvider_Writes_Custom_V3_Document() - { - var testSite = new TestSite(typeof(CustomDocumentSerializer.Startup)); - var server = testSite.BuildServer(); - var services = server.Host.Services; + [Fact] + public async Task DocumentProvider_Writes_Custom_V3_Document() + { + var testSite = new TestSite(typeof(CustomDocumentSerializer.Startup)); + var server = testSite.BuildServer(); + var services = server.Host.Services; - var documentProvider = services.GetService(); - using var stream = new MemoryStream(); + var documentProvider = services.GetService(); + using var stream = new MemoryStream(); - using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 2048, leaveOpen: true)) - { - await documentProvider.GenerateAsync("v1", writer); - await writer.FlushAsync(); - } + using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 2048, leaveOpen: true)) + { + await documentProvider.GenerateAsync("v1", writer); + await writer.FlushAsync(); + } - stream.Position = 0L; + stream.Position = 0L; - using var document = JsonDocument.Parse(stream); + using var document = JsonDocument.Parse(stream); - // verify that the custom serializer wrote the swagger info - var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); - Assert.Equal("DocumentSerializerTest3.0", swaggerInfo); - } + // verify that the custom serializer wrote the swagger info + var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); + Assert.Equal("DocumentSerializerTest3.0", swaggerInfo); + } - [Fact] - public async Task DocumentProvider_Writes_Custom_V2_Document() - { - await DocumentProviderWritesCustomV2Document( - (options) => options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); - } + [Fact] + public async Task DocumentProvider_Writes_Custom_V2_Document() + { + await DocumentProviderWritesCustomV2Document( + (options) => options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); + } - [Obsolete] - [Fact] - public async Task DocumentProvider_Writes_Custom_V2_Document_SerializeAsV2() - => await DocumentProviderWritesCustomV2Document((options) => options.SerializeAsV2 = true); + [Obsolete] + [Fact] + public async Task DocumentProvider_Writes_Custom_V2_Document_SerializeAsV2() + => await DocumentProviderWritesCustomV2Document((options) => options.SerializeAsV2 = true); - private static async Task DocumentProviderWritesCustomV2Document(Action configure) - { - var testSite = new TestSite(typeof(CustomDocumentSerializer.Startup)); - var server = testSite.BuildServer(); - var services = server.Host.Services; + private static async Task DocumentProviderWritesCustomV2Document(Action configure) + { + var testSite = new TestSite(typeof(CustomDocumentSerializer.Startup)); + var server = testSite.BuildServer(); + var services = server.Host.Services; - var documentProvider = services.GetService(); - var options = services.GetService>(); + var documentProvider = services.GetService(); + var options = services.GetService>(); - configure(options.Value); + configure(options.Value); - using var stream = new MemoryStream(); + using var stream = new MemoryStream(); - using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 2048, leaveOpen: true)) - { - await documentProvider.GenerateAsync("v1", writer); - await writer.FlushAsync(); - } + using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 2048, leaveOpen: true)) + { + await documentProvider.GenerateAsync("v1", writer); + await writer.FlushAsync(); + } - stream.Position = 0L; + stream.Position = 0L; - using var document = JsonDocument.Parse(stream); + using var document = JsonDocument.Parse(stream); - // verify that the custom serializer wrote the swagger info - var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); - Assert.Equal("DocumentSerializerTest2.0", swaggerInfo); - } + // verify that the custom serializer wrote the swagger info + var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); + Assert.Equal("DocumentSerializerTest2.0", swaggerInfo); } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/DocumentProviderTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/DocumentProviderTests.cs index 63a6081270..f3aaeb3b41 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/DocumentProviderTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/DocumentProviderTests.cs @@ -2,68 +2,67 @@ using Microsoft.Extensions.ApiDescriptions; using Swashbuckle.AspNetCore.Swagger; -namespace Swashbuckle.AspNetCore.IntegrationTests +namespace Swashbuckle.AspNetCore.IntegrationTests; + +[Collection("TestSite")] +public class DocumentProviderTests { - [Collection("TestSite")] - public class DocumentProviderTests + [Theory] + [InlineData(typeof(Basic.Startup), new[] { "v1" })] + [InlineData(typeof(CustomUIConfig.Startup), new[] { "v1" })] + [InlineData(typeof(CustomUIIndex.Startup), new[] { "v1" })] + [InlineData(typeof(GenericControllers.Startup), new[] { "v1" })] + [InlineData(typeof(MultipleVersions.Startup), new[] { "1.0", "2.0" })] + [InlineData(typeof(OAuth2Integration.Startup), new[] { "v1" })] + public void DocumentProvider_ExposesAllDocumentNames(Type startupType, string[] expectedNames) { - [Theory] - [InlineData(typeof(Basic.Startup), new[] { "v1" })] - [InlineData(typeof(CustomUIConfig.Startup), new[] { "v1" })] - [InlineData(typeof(CustomUIIndex.Startup), new[] { "v1" })] - [InlineData(typeof(GenericControllers.Startup), new[] { "v1" })] - [InlineData(typeof(MultipleVersions.Startup), new[] { "1.0", "2.0" })] - [InlineData(typeof(OAuth2Integration.Startup), new[] { "v1" })] - public void DocumentProvider_ExposesAllDocumentNames(Type startupType, string[] expectedNames) - { - var testSite = new TestSite(startupType); - var server = testSite.BuildServer(); - var services = server.Host.Services; - var documentProvider = (IDocumentProvider)services.GetService(typeof(IDocumentProvider)); + var testSite = new TestSite(startupType); + var server = testSite.BuildServer(); + var services = server.Host.Services; + var documentProvider = (IDocumentProvider)services.GetService(typeof(IDocumentProvider)); - var documentNames = documentProvider.GetDocumentNames(); - - Assert.Equal(expectedNames, documentNames); - } + var documentNames = documentProvider.GetDocumentNames(); - [Theory] - [InlineData(typeof(Basic.Startup), "v1")] - [InlineData(typeof(CustomUIConfig.Startup), "v1")] - [InlineData(typeof(CustomUIIndex.Startup), "v1")] - [InlineData(typeof(GenericControllers.Startup), "v1")] - [InlineData(typeof(MultipleVersions.Startup), "2.0")] - [InlineData(typeof(OAuth2Integration.Startup), "v1")] - public async Task DocumentProvider_ExposesGeneratedSwagger(Type startupType, string documentName) - { - var testSite = new TestSite(startupType); - var server = testSite.BuildServer(); - var services = server.Host.Services; + Assert.Equal(expectedNames, documentNames); + } - var documentProvider = (IDocumentProvider)services.GetService(typeof(IDocumentProvider)); - using var stream = new MemoryStream(); - using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 2048, leaveOpen: true)) - { - await documentProvider.GenerateAsync(documentName, writer); - await writer.FlushAsync(); - } + [Theory] + [InlineData(typeof(Basic.Startup), "v1")] + [InlineData(typeof(CustomUIConfig.Startup), "v1")] + [InlineData(typeof(CustomUIIndex.Startup), "v1")] + [InlineData(typeof(GenericControllers.Startup), "v1")] + [InlineData(typeof(MultipleVersions.Startup), "2.0")] + [InlineData(typeof(OAuth2Integration.Startup), "v1")] + public async Task DocumentProvider_ExposesGeneratedSwagger(Type startupType, string documentName) + { + var testSite = new TestSite(startupType); + var server = testSite.BuildServer(); + var services = server.Host.Services; - stream.Position = 0L; - var (_, diagnostic) = await OpenApiDocumentLoader.LoadWithDiagnosticsAsync(stream); - Assert.NotNull(diagnostic); - Assert.Empty(diagnostic.Errors); + var documentProvider = (IDocumentProvider)services.GetService(typeof(IDocumentProvider)); + using var stream = new MemoryStream(); + using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 2048, leaveOpen: true)) + { + await documentProvider.GenerateAsync(documentName, writer); + await writer.FlushAsync(); } - [Fact] - public async Task DocumentProvider_ThrowsUnknownDocument_IfUnknownDocumentName() - { - var testSite = new TestSite(typeof(Basic.Startup)); - var server = testSite.BuildServer(); - var services = server.Host.Services; + stream.Position = 0L; + var (_, diagnostic) = await OpenApiDocumentLoader.LoadWithDiagnosticsAsync(stream); + Assert.NotNull(diagnostic); + Assert.Empty(diagnostic.Errors); + } - var documentProvider = (IDocumentProvider)services.GetService(typeof(IDocumentProvider)); - using var writer = new StringWriter(); - await Assert.ThrowsAsync( - () => documentProvider.GenerateAsync("NotADocument", writer)); - } + [Fact] + public async Task DocumentProvider_ThrowsUnknownDocument_IfUnknownDocumentName() + { + var testSite = new TestSite(typeof(Basic.Startup)); + var server = testSite.BuildServer(); + var services = server.Host.Services; + + var documentProvider = (IDocumentProvider)services.GetService(typeof(IDocumentProvider)); + using var writer = new StringWriter(); + await Assert.ThrowsAsync( + () => documentProvider.GenerateAsync("NotADocument", writer)); } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs index a01fca06a4..08819b3b6a 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs @@ -3,148 +3,147 @@ using Swashbuckle.AspNetCore.ReDoc; using ReDocApp = ReDoc; -namespace Swashbuckle.AspNetCore.IntegrationTests +namespace Swashbuckle.AspNetCore.IntegrationTests; + +[Collection("TestSite")] +public class ReDocIntegrationTests { - [Collection("TestSite")] - public class ReDocIntegrationTests + [Fact] + public async Task RoutePrefix_RedirectsToIndexUrl() + { + var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); + + var response = await client.GetAsync("/api-docs"); + + Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode); + Assert.Equal("api-docs/index.html", response.Headers.Location.ToString()); + } + + [Fact] + public async Task IndexUrl_ReturnsEmbeddedVersionOfTheRedocUI() + { + var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); + + var htmlResponse = await client.GetAsync("/api-docs/index.html"); + var cssResponse = await client.GetAsync("/api-docs/index.css"); + var jsResponse = await client.GetAsync("/api-docs/redoc.standalone.js"); + + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); + } + + [Fact] + public async Task RedocMiddleware_ReturnsInitializerScript() + { + var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); + + var response = await client.GetAsync("/api-docs/index.js"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("Redoc.init", content); + Assert.DoesNotContain("%(DocumentTitle)", content); + Assert.DoesNotContain("%(HeadContent)", content); + Assert.DoesNotContain("%(SpecUrl)", content); + Assert.DoesNotContain("%(ConfigObject)", content); + } + + [Fact] + public async Task IndexUrl_IgnoresUrlCase() + { + var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); + + var htmlResponse = await client.GetAsync("/Api-Docs/index.html"); + var cssResponse = await client.GetAsync("/Api-Docs/index.css"); + var jsInitResponse = await client.GetAsync("/Api-Docs/index.js"); + var jsRedocResponse = await client.GetAsync("/Api-Docs/redoc.standalone.js"); + + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, jsInitResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, jsRedocResponse.StatusCode); + } + + [Theory] + [InlineData("/redoc/1.0/index.html", "/redoc/1.0/index.js", "/swagger/1.0/swagger.json")] + [InlineData("/redoc/2.0/index.html", "/redoc/2.0/index.js", "/swagger/2.0/swagger.json")] + public async Task RedocMiddleware_CanBeConfiguredMultipleTimes(string htmlUrl, string jsUrl, string swaggerPath) + { + var client = new TestSite(typeof(MultipleVersions.Startup)).BuildClient(); + + var htmlResponse = await client.GetAsync(htmlUrl); + var jsResponse = await client.GetAsync(jsUrl); + var content = await jsResponse.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); + Assert.Contains(swaggerPath, content); + } + + [Fact] + public void ReDocOptions_Extensions() { - [Fact] - public async Task RoutePrefix_RedirectsToIndexUrl() - { - var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); - - var response = await client.GetAsync("/api-docs"); - - Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode); - Assert.Equal("api-docs/index.html", response.Headers.Location.ToString()); - } - - [Fact] - public async Task IndexUrl_ReturnsEmbeddedVersionOfTheRedocUI() - { - var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); - - var htmlResponse = await client.GetAsync("/api-docs/index.html"); - var cssResponse = await client.GetAsync("/api-docs/index.css"); - var jsResponse = await client.GetAsync("/api-docs/redoc.standalone.js"); - - Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); - } - - [Fact] - public async Task RedocMiddleware_ReturnsInitializerScript() - { - var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); - - var response = await client.GetAsync("/api-docs/index.js"); - var content = await response.Content.ReadAsStringAsync(); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains("Redoc.init", content); - Assert.DoesNotContain("%(DocumentTitle)", content); - Assert.DoesNotContain("%(HeadContent)", content); - Assert.DoesNotContain("%(SpecUrl)", content); - Assert.DoesNotContain("%(ConfigObject)", content); - } - - [Fact] - public async Task IndexUrl_IgnoresUrlCase() - { - var client = new TestSite(typeof(ReDocApp.Startup)).BuildClient(); - - var htmlResponse = await client.GetAsync("/Api-Docs/index.html"); - var cssResponse = await client.GetAsync("/Api-Docs/index.css"); - var jsInitResponse = await client.GetAsync("/Api-Docs/index.js"); - var jsRedocResponse = await client.GetAsync("/Api-Docs/redoc.standalone.js"); - - Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, jsInitResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, jsRedocResponse.StatusCode); - } - - [Theory] - [InlineData("/redoc/1.0/index.html", "/redoc/1.0/index.js", "/swagger/1.0/swagger.json")] - [InlineData("/redoc/2.0/index.html", "/redoc/2.0/index.js", "/swagger/2.0/swagger.json")] - public async Task RedocMiddleware_CanBeConfiguredMultipleTimes(string htmlUrl, string jsUrl, string swaggerPath) - { - var client = new TestSite(typeof(MultipleVersions.Startup)).BuildClient(); - - var htmlResponse = await client.GetAsync(htmlUrl); - var jsResponse = await client.GetAsync(jsUrl); - var content = await jsResponse.Content.ReadAsStringAsync(); - - Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); - Assert.Contains(swaggerPath, content); - } - - [Fact] - public void ReDocOptions_Extensions() - { - // Arrange - var options = new ReDocOptions(); - - // Act and Assert - Assert.NotNull(options.IndexStream); - Assert.Null(options.JsonSerializerOptions); - Assert.Null(options.SpecUrl); - Assert.Equal("API Docs", options.DocumentTitle); - Assert.Equal(string.Empty, options.HeadContent); - Assert.Equal("api-docs", options.RoutePrefix); - - Assert.NotNull(options.ConfigObject); - Assert.NotNull(options.ConfigObject.AdditionalItems); - Assert.Empty(options.ConfigObject.AdditionalItems); - Assert.Null(options.ConfigObject.ScrollYOffset); - Assert.Equal("all", options.ConfigObject.ExpandResponses); - Assert.False(options.ConfigObject.DisableSearch); - Assert.False(options.ConfigObject.HideDownloadButton); - Assert.False(options.ConfigObject.HideHostname); - Assert.False(options.ConfigObject.HideLoading); - Assert.False(options.ConfigObject.NativeScrollbars); - Assert.False(options.ConfigObject.NoAutoAuth); - Assert.False(options.ConfigObject.OnlyRequiredInSamples); - Assert.False(options.ConfigObject.PathInMiddlePanel); - Assert.False(options.ConfigObject.RequiredPropsFirst); - Assert.False(options.ConfigObject.SortPropsAlphabetically); - Assert.False(options.ConfigObject.UntrustedSpec); - - // Act - options.DisableSearch(); - options.EnableUntrustedSpec(); - options.ExpandResponses("response"); - options.HideDownloadButton(); - options.HideHostname(); - options.HideLoading(); - options.InjectStylesheet("custom.css", "screen and (max-width: 700px)"); - options.NativeScrollbars(); - options.NoAutoAuth(); - options.OnlyRequiredInSamples(); - options.PathInMiddlePanel(); - options.RequiredPropsFirst(); - options.ScrollYOffset(42); - options.SortPropsAlphabetically(); - options.SpecUrl("spec.json"); - - // Assert - Assert.Equal("" + Environment.NewLine, options.HeadContent); - Assert.Equal("spec.json", options.SpecUrl); - Assert.Equal("response", options.ConfigObject.ExpandResponses); - Assert.Equal(42, options.ConfigObject.ScrollYOffset); - Assert.True(options.ConfigObject.DisableSearch); - Assert.True(options.ConfigObject.HideDownloadButton); - Assert.True(options.ConfigObject.HideHostname); - Assert.True(options.ConfigObject.HideLoading); - Assert.True(options.ConfigObject.NativeScrollbars); - Assert.True(options.ConfigObject.NoAutoAuth); - Assert.True(options.ConfigObject.OnlyRequiredInSamples); - Assert.True(options.ConfigObject.PathInMiddlePanel); - Assert.True(options.ConfigObject.RequiredPropsFirst); - Assert.True(options.ConfigObject.SortPropsAlphabetically); - Assert.True(options.ConfigObject.UntrustedSpec); - } + // Arrange + var options = new ReDocOptions(); + + // Act and Assert + Assert.NotNull(options.IndexStream); + Assert.Null(options.JsonSerializerOptions); + Assert.Null(options.SpecUrl); + Assert.Equal("API Docs", options.DocumentTitle); + Assert.Equal(string.Empty, options.HeadContent); + Assert.Equal("api-docs", options.RoutePrefix); + + Assert.NotNull(options.ConfigObject); + Assert.NotNull(options.ConfigObject.AdditionalItems); + Assert.Empty(options.ConfigObject.AdditionalItems); + Assert.Null(options.ConfigObject.ScrollYOffset); + Assert.Equal("all", options.ConfigObject.ExpandResponses); + Assert.False(options.ConfigObject.DisableSearch); + Assert.False(options.ConfigObject.HideDownloadButton); + Assert.False(options.ConfigObject.HideHostname); + Assert.False(options.ConfigObject.HideLoading); + Assert.False(options.ConfigObject.NativeScrollbars); + Assert.False(options.ConfigObject.NoAutoAuth); + Assert.False(options.ConfigObject.OnlyRequiredInSamples); + Assert.False(options.ConfigObject.PathInMiddlePanel); + Assert.False(options.ConfigObject.RequiredPropsFirst); + Assert.False(options.ConfigObject.SortPropsAlphabetically); + Assert.False(options.ConfigObject.UntrustedSpec); + + // Act + options.DisableSearch(); + options.EnableUntrustedSpec(); + options.ExpandResponses("response"); + options.HideDownloadButton(); + options.HideHostname(); + options.HideLoading(); + options.InjectStylesheet("custom.css", "screen and (max-width: 700px)"); + options.NativeScrollbars(); + options.NoAutoAuth(); + options.OnlyRequiredInSamples(); + options.PathInMiddlePanel(); + options.RequiredPropsFirst(); + options.ScrollYOffset(42); + options.SortPropsAlphabetically(); + options.SpecUrl("spec.json"); + + // Assert + Assert.Equal("" + Environment.NewLine, options.HeadContent); + Assert.Equal("spec.json", options.SpecUrl); + Assert.Equal("response", options.ConfigObject.ExpandResponses); + Assert.Equal(42, options.ConfigObject.ScrollYOffset); + Assert.True(options.ConfigObject.DisableSearch); + Assert.True(options.ConfigObject.HideDownloadButton); + Assert.True(options.ConfigObject.HideHostname); + Assert.True(options.ConfigObject.HideLoading); + Assert.True(options.ConfigObject.NativeScrollbars); + Assert.True(options.ConfigObject.NoAutoAuth); + Assert.True(options.ConfigObject.OnlyRequiredInSamples); + Assert.True(options.ConfigObject.PathInMiddlePanel); + Assert.True(options.ConfigObject.RequiredPropsFirst); + Assert.True(options.ConfigObject.SortPropsAlphabetically); + Assert.True(options.ConfigObject.UntrustedSpec); } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerAndSwaggerUIIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerAndSwaggerUIIntegrationTests.cs index 215b52b647..7b7c845940 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerAndSwaggerUIIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerAndSwaggerUIIntegrationTests.cs @@ -1,23 +1,22 @@ using System.Net; using Microsoft.AspNetCore.Mvc.Testing; -namespace Swashbuckle.AspNetCore.IntegrationTests +namespace Swashbuckle.AspNetCore.IntegrationTests; + +public class SwaggerAndSwaggerUIIntegrationTests { - public class SwaggerAndSwaggerUIIntegrationTests + [Theory] + [InlineData("/swagger/index.html", "text/html")] + [InlineData("/swagger/v1.json", "application/json")] + [InlineData("/swagger/v1.yaml", "text/yaml")] + [InlineData("/swagger/v1.yml", "text/yaml")] + public async Task SwaggerDocWithoutSubdirectory(string path, string mediaType) { - [Theory] - [InlineData("/swagger/index.html", "text/html")] - [InlineData("/swagger/v1.json", "application/json")] - [InlineData("/swagger/v1.yaml", "text/yaml")] - [InlineData("/swagger/v1.yml", "text/yaml")] - public async Task SwaggerDocWithoutSubdirectory(string path, string mediaType) - { - var client = new WebApplicationFactory().CreateClient(); + var client = new WebApplicationFactory().CreateClient(); - var response = await client.GetAsync(path); + var response = await client.GetAsync(path); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(mediaType, response.Content.Headers.ContentType?.MediaType); - } + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(mediaType, response.Content.Headers.ContentType?.MediaType); } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs index 8394eae0ea..080b0b1576 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs @@ -1,210 +1,209 @@ using System.Globalization; using System.Reflection; -#if NET8_0_OR_GREATER +#if NET #endif using System.Text; using System.Text.Json; using Microsoft.OpenApi.Any; using ReDocApp = ReDoc; -namespace Swashbuckle.AspNetCore.IntegrationTests +namespace Swashbuckle.AspNetCore.IntegrationTests; + +[Collection("TestSite")] +public class SwaggerIntegrationTests { - [Collection("TestSite")] - public class SwaggerIntegrationTests + [Theory] + [InlineData(typeof(Basic.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(CliExample.Startup), "/swagger/v1/swagger_net8.0.json")] + [InlineData(typeof(ConfigFromFile.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(CustomUIConfig.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(CustomUIIndex.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(GenericControllers.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(MultipleVersions.Startup), "/swagger/1.0/swagger.json")] + [InlineData(typeof(MultipleVersions.Startup), "/swagger/2.0/swagger.json")] + [InlineData(typeof(NSwagClientExample.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(OAuth2Integration.Startup), "/resource-server/swagger/v1/swagger.json")] + [InlineData(typeof(ReDocApp.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(TestFirst.Startup), "/swagger/v1-generated/openapi.json")] + public async Task SwaggerEndpoint_ReturnsValidSwaggerJson( + Type startupType, + string swaggerRequestUri) { - [Theory] - [InlineData(typeof(Basic.Startup), "/swagger/v1/swagger.json")] - [InlineData(typeof(CliExample.Startup), "/swagger/v1/swagger_net8.0.json")] - [InlineData(typeof(ConfigFromFile.Startup), "/swagger/v1/swagger.json")] - [InlineData(typeof(CustomUIConfig.Startup), "/swagger/v1/swagger.json")] - [InlineData(typeof(CustomUIIndex.Startup), "/swagger/v1/swagger.json")] - [InlineData(typeof(GenericControllers.Startup), "/swagger/v1/swagger.json")] - [InlineData(typeof(MultipleVersions.Startup), "/swagger/1.0/swagger.json")] - [InlineData(typeof(MultipleVersions.Startup), "/swagger/2.0/swagger.json")] - [InlineData(typeof(NSwagClientExample.Startup), "/swagger/v1/swagger.json")] - [InlineData(typeof(OAuth2Integration.Startup), "/resource-server/swagger/v1/swagger.json")] - [InlineData(typeof(ReDocApp.Startup), "/swagger/v1/swagger.json")] - [InlineData(typeof(TestFirst.Startup), "/swagger/v1-generated/openapi.json")] - public async Task SwaggerEndpoint_ReturnsValidSwaggerJson( - Type startupType, - string swaggerRequestUri) - { - var testSite = new TestSite(startupType); - using var client = testSite.BuildClient(); + var testSite = new TestSite(startupType); + using var client = testSite.BuildClient(); - await AssertValidSwaggerJson(client, swaggerRequestUri); - } + await AssertValidSwaggerJson(client, swaggerRequestUri); + } - [Fact] - public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_ForAutofaq() - { - var testSite = new TestSiteAutofaq(typeof(CliExampleWithFactory.Startup)); - using var client = testSite.BuildClient(); + [Fact] + public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_ForAutofaq() + { + var testSite = new TestSiteAutofaq(typeof(CliExampleWithFactory.Startup)); + using var client = testSite.BuildClient(); - await AssertValidSwaggerJson(client, "/swagger/v1/swagger_net8.0.json"); - } + await AssertValidSwaggerJson(client, "/swagger/v1/swagger_net8.0.json"); + } - [Fact] - public async Task SwaggerEndpoint_ReturnsNotFound_IfUnknownSwaggerDocument() - { - var testSite = new TestSite(typeof(Basic.Startup)); - using var client = testSite.BuildClient(); + [Fact] + public async Task SwaggerEndpoint_ReturnsNotFound_IfUnknownSwaggerDocument() + { + var testSite = new TestSite(typeof(Basic.Startup)); + using var client = testSite.BuildClient(); - using var swaggerResponse = await client.GetAsync("/swagger/v2/swagger.json"); + using var swaggerResponse = await client.GetAsync("/swagger/v2/swagger.json"); - Assert.Equal(System.Net.HttpStatusCode.NotFound, swaggerResponse.StatusCode); - } + Assert.Equal(System.Net.HttpStatusCode.NotFound, swaggerResponse.StatusCode); + } - [Fact] - public async Task SwaggerEndpoint_DoesNotReturnByteOrderMark() - { - var testSite = new TestSite(typeof(Basic.Startup)); - using var client = testSite.BuildClient(); + [Fact] + public async Task SwaggerEndpoint_DoesNotReturnByteOrderMark() + { + var testSite = new TestSite(typeof(Basic.Startup)); + using var client = testSite.BuildClient(); - using var swaggerResponse = await client.GetAsync("/swagger/v1/swagger.json"); + using var swaggerResponse = await client.GetAsync("/swagger/v1/swagger.json"); - swaggerResponse.EnsureSuccessStatusCode(); - var contentBytes = await swaggerResponse.Content.ReadAsByteArrayAsync(); - var bomBytes = Encoding.UTF8.GetPreamble(); - Assert.NotEqual(bomBytes, contentBytes.Take(bomBytes.Length)); - } + swaggerResponse.EnsureSuccessStatusCode(); + var contentBytes = await swaggerResponse.Content.ReadAsByteArrayAsync(); + var bomBytes = Encoding.UTF8.GetPreamble(); + Assert.NotEqual(bomBytes, contentBytes.Take(bomBytes.Length)); + } + + [Theory] + [InlineData("en-US")] + [InlineData("sv-SE")] + public async Task SwaggerEndpoint_ReturnsCorrectPriceExample_ForDifferentCultures(string culture) + { + var testSite = new TestSite(typeof(Basic.Startup)); + using var client = testSite.BuildClient(); - [Theory] - [InlineData("en-US")] - [InlineData("sv-SE")] - public async Task SwaggerEndpoint_ReturnsCorrectPriceExample_ForDifferentCultures(string culture) + using var swaggerResponse = await client.GetAsync($"/swagger/v1/swagger.json?culture={culture}"); + + swaggerResponse.EnsureSuccessStatusCode(); + using var contentStream = await swaggerResponse.Content.ReadAsStreamAsync(); + var currentCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + try { - var testSite = new TestSite(typeof(Basic.Startup)); - using var client = testSite.BuildClient(); - - using var swaggerResponse = await client.GetAsync($"/swagger/v1/swagger.json?culture={culture}"); - - swaggerResponse.EnsureSuccessStatusCode(); - using var contentStream = await swaggerResponse.Content.ReadAsStreamAsync(); - var currentCulture = CultureInfo.CurrentCulture; - CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; - try - { - var openApiDocument = await OpenApiDocumentLoader.LoadAsync(contentStream); - var example = openApiDocument.Components.Schemas["Product"].Example; - var exampleObject = Assert.IsType(example); - double price = Assert.IsType(exampleObject["price"]).Value; - Assert.Equal(14.37, price); - } - finally - { - CultureInfo.CurrentCulture = currentCulture; - } + var openApiDocument = await OpenApiDocumentLoader.LoadAsync(contentStream); + var example = openApiDocument.Components.Schemas["Product"].Example; + var exampleObject = Assert.IsType(example); + double price = Assert.IsType(exampleObject["price"]).Value; + Assert.Equal(14.37, price); } - - [Theory] - [InlineData("/swagger/v1/swagger.json", "openapi", "3.0.4")] - [InlineData("/swagger/v1/swaggerv2.json", "swagger", "2.0")] - public async Task SwaggerMiddleware_CanBeConfiguredMultipleTimes( - string swaggerUrl, - string expectedVersionProperty, - string expectedVersionValue) + finally { - using var client = new TestSite(typeof(Basic.Startup)).BuildClient(); + CultureInfo.CurrentCulture = currentCulture; + } + } - using var response = await client.GetAsync(swaggerUrl); + [Theory] + [InlineData("/swagger/v1/swagger.json", "openapi", "3.0.4")] + [InlineData("/swagger/v1/swaggerv2.json", "swagger", "2.0")] + public async Task SwaggerMiddleware_CanBeConfiguredMultipleTimes( + string swaggerUrl, + string expectedVersionProperty, + string expectedVersionValue) + { + using var client = new TestSite(typeof(Basic.Startup)).BuildClient(); - response.EnsureSuccessStatusCode(); - using var contentStream = await response.Content.ReadAsStreamAsync(); + using var response = await client.GetAsync(swaggerUrl); - var json = await JsonSerializer.DeserializeAsync(contentStream); - Assert.Equal(expectedVersionValue, json.GetProperty(expectedVersionProperty).GetString()); - } + response.EnsureSuccessStatusCode(); + using var contentStream = await response.Content.ReadAsStreamAsync(); - [Theory] - [InlineData(typeof(MinimalApp.Program), "/swagger/v1/swagger.json")] - [InlineData(typeof(TopLevelSwaggerDoc.Program), "/swagger/v1.json")] - [InlineData(typeof(MvcWithNullable.Program), "/swagger/v1/swagger.json")] - [InlineData(typeof(WebApi.Program), "/swagger/v1/swagger.json")] - [InlineData(typeof(WebApi.Aot.Program), "/swagger/v1/swagger.json")] - public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_Without_Startup( - Type entryPointType, - string swaggerRequestUri) - { - await SwaggerEndpointReturnsValidSwaggerJson(entryPointType, swaggerRequestUri); - } + var json = await JsonSerializer.DeserializeAsync(contentStream); + Assert.Equal(expectedVersionValue, json.GetProperty(expectedVersionProperty).GetString()); + } - [Fact] - public async Task TypesAreRenderedCorrectly() - { - using var application = new TestApplication(); - using var client = application.CreateDefaultClient(); + [Theory] + [InlineData(typeof(MinimalApp.Program), "/swagger/v1/swagger.json")] + [InlineData(typeof(TopLevelSwaggerDoc.Program), "/swagger/v1.json")] + [InlineData(typeof(MvcWithNullable.Program), "/swagger/v1/swagger.json")] + [InlineData(typeof(WebApi.Program), "/swagger/v1/swagger.json")] + [InlineData(typeof(WebApi.Aot.Program), "/swagger/v1/swagger.json")] + public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_Without_Startup( + Type entryPointType, + string swaggerRequestUri) + { + await SwaggerEndpointReturnsValidSwaggerJson(entryPointType, swaggerRequestUri); + } - using var response = await client.GetAsync("/swagger/v1/swagger.json"); + [Fact] + public async Task TypesAreRenderedCorrectly() + { + using var application = new TestApplication(); + using var client = application.CreateDefaultClient(); - var content = await response.Content.ReadAsStringAsync(); + using var response = await client.GetAsync("/swagger/v1/swagger.json"); - Assert.True(response.IsSuccessStatusCode, content); + var content = await response.Content.ReadAsStringAsync(); - using var swaggerResponse = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + Assert.True(response.IsSuccessStatusCode, content); - var weatherForecase = swaggerResponse.RootElement - .GetProperty("components") - .GetProperty("schemas") - .GetProperty("WeatherForecast"); + using var swaggerResponse = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); - Assert.Equal("object", weatherForecase.GetProperty("type").GetString()); + var weatherForecase = swaggerResponse.RootElement + .GetProperty("components") + .GetProperty("schemas") + .GetProperty("WeatherForecast"); - var properties = weatherForecase.GetProperty("properties"); - Assert.Equal(4, properties.EnumerateObject().Count()); + Assert.Equal("object", weatherForecase.GetProperty("type").GetString()); - Assert.Multiple( - [ - () => Assert.Equal("string", properties.GetProperty("date").GetProperty("type").GetString()), - () => Assert.Equal("date", properties.GetProperty("date").GetProperty("format").GetString()), - () => Assert.Equal("integer", properties.GetProperty("temperatureC").GetProperty("type").GetString()), - () => Assert.Equal("int32", properties.GetProperty("temperatureC").GetProperty("format").GetString()), - () => Assert.Equal("string", properties.GetProperty("summary").GetProperty("type").GetString()), - () => Assert.True(properties.GetProperty("summary").GetProperty("nullable").GetBoolean()), - () => Assert.Equal("integer", properties.GetProperty("temperatureF").GetProperty("type").GetString()), - () => Assert.Equal("int32", properties.GetProperty("temperatureF").GetProperty("format").GetString()), - () => Assert.True(properties.GetProperty("temperatureF").GetProperty("readOnly").GetBoolean()), - ]); - } + var properties = weatherForecase.GetProperty("properties"); + Assert.Equal(4, properties.EnumerateObject().Count()); - private static async Task SwaggerEndpointReturnsValidSwaggerJson(Type entryPointType, string swaggerRequestUri) - { - using var client = GetHttpClientForTestApplication(entryPointType); - await AssertValidSwaggerJson(client, swaggerRequestUri); - } + Assert.Multiple( + [ + () => Assert.Equal("string", properties.GetProperty("date").GetProperty("type").GetString()), + () => Assert.Equal("date", properties.GetProperty("date").GetProperty("format").GetString()), + () => Assert.Equal("integer", properties.GetProperty("temperatureC").GetProperty("type").GetString()), + () => Assert.Equal("int32", properties.GetProperty("temperatureC").GetProperty("format").GetString()), + () => Assert.Equal("string", properties.GetProperty("summary").GetProperty("type").GetString()), + () => Assert.True(properties.GetProperty("summary").GetProperty("nullable").GetBoolean()), + () => Assert.Equal("integer", properties.GetProperty("temperatureF").GetProperty("type").GetString()), + () => Assert.Equal("int32", properties.GetProperty("temperatureF").GetProperty("format").GetString()), + () => Assert.True(properties.GetProperty("temperatureF").GetProperty("readOnly").GetBoolean()), + ]); + } - internal static HttpClient GetHttpClientForTestApplication(Type entryPointType) - { - var applicationType = typeof(TestApplication<>).MakeGenericType(entryPointType); - var application = (IDisposable)Activator.CreateInstance(applicationType); - Assert.NotNull(application); + private static async Task SwaggerEndpointReturnsValidSwaggerJson(Type entryPointType, string swaggerRequestUri) + { + using var client = GetHttpClientForTestApplication(entryPointType); + await AssertValidSwaggerJson(client, swaggerRequestUri); + } - var createClientMethod = applicationType - .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .FirstOrDefault(m => m.Name == "CreateDefaultClient" && m.GetParameters().Length == 1) - ?? throw new InvalidOperationException($"The method CreateDefaultClient was not found on TestApplication<{entryPointType.FullName}>."); + internal static HttpClient GetHttpClientForTestApplication(Type entryPointType) + { + var applicationType = typeof(TestApplication<>).MakeGenericType(entryPointType); + var application = (IDisposable)Activator.CreateInstance(applicationType); + Assert.NotNull(application); - // Pass null for DelegatingHandler[] - var parameters = new object[] { null }; + var createClientMethod = applicationType + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .FirstOrDefault(m => m.Name == "CreateDefaultClient" && m.GetParameters().Length == 1) + ?? throw new InvalidOperationException($"The method CreateDefaultClient was not found on TestApplication<{entryPointType.FullName}>."); - var clientObject = (IDisposable)createClientMethod.Invoke(application, parameters); - if (clientObject is not HttpClient client) - { - throw new InvalidOperationException($"The method CreateDefaultClient on TestApplication<{entryPointType.FullName}> did not return an HttpClient."); - } + // Pass null for DelegatingHandler[] + var parameters = new object[] { null }; - return client; + var clientObject = (IDisposable)createClientMethod.Invoke(application, parameters); + if (clientObject is not HttpClient client) + { + throw new InvalidOperationException($"The method CreateDefaultClient on TestApplication<{entryPointType.FullName}> did not return an HttpClient."); } - private static async Task AssertValidSwaggerJson(HttpClient client, string swaggerRequestUri) - { - using var swaggerResponse = await client.GetAsync(swaggerRequestUri); + return client; + } - Assert.True(swaggerResponse.IsSuccessStatusCode, $"IsSuccessStatusCode is false. Response: '{await swaggerResponse.Content.ReadAsStringAsync()}'"); - using var contentStream = await swaggerResponse.Content.ReadAsStreamAsync(); - var (_, diagnostic) = await OpenApiDocumentLoader.LoadWithDiagnosticsAsync(contentStream); - Assert.NotNull(diagnostic); - Assert.Empty(diagnostic.Errors); - } + private static async Task AssertValidSwaggerJson(HttpClient client, string swaggerRequestUri) + { + using var swaggerResponse = await client.GetAsync(swaggerRequestUri); + + Assert.True(swaggerResponse.IsSuccessStatusCode, $"IsSuccessStatusCode is false. Response: '{await swaggerResponse.Content.ReadAsStringAsync()}'"); + using var contentStream = await swaggerResponse.Content.ReadAsStreamAsync(); + var (_, diagnostic) = await OpenApiDocumentLoader.LoadWithDiagnosticsAsync(contentStream); + Assert.NotNull(diagnostic); + Assert.Empty(diagnostic.Errors); } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs index 5259e9eaf5..1c53a060bf 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs @@ -1,173 +1,172 @@ using System.Net; -namespace Swashbuckle.AspNetCore.IntegrationTests +namespace Swashbuckle.AspNetCore.IntegrationTests; + +[Collection("TestSite")] +public class SwaggerUIIntegrationTests { - [Collection("TestSite")] - public class SwaggerUIIntegrationTests + [Theory] + [InlineData(typeof(Basic.Startup), "/", "index.html")] + [InlineData(typeof(CustomUIConfig.Startup), "/swagger", "swagger/index.html")] + [InlineData(typeof(CustomUIConfig.Startup), "/swagger/", "index.html")] + public async Task RoutePrefix_RedirectsToPathRelativeIndexUrl( + Type startupType, + string requestPath, + string expectedRedirectPath) { - [Theory] - [InlineData(typeof(Basic.Startup), "/", "index.html")] - [InlineData(typeof(CustomUIConfig.Startup), "/swagger", "swagger/index.html")] - [InlineData(typeof(CustomUIConfig.Startup), "/swagger/", "index.html")] - public async Task RoutePrefix_RedirectsToPathRelativeIndexUrl( - Type startupType, - string requestPath, - string expectedRedirectPath) - { - var client = new TestSite(startupType).BuildClient(); + var client = new TestSite(startupType).BuildClient(); - var response = await client.GetAsync(requestPath); + var response = await client.GetAsync(requestPath); - Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode); - Assert.Equal(expectedRedirectPath, response.Headers.Location.ToString()); - } + Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode); + Assert.Equal(expectedRedirectPath, response.Headers.Location.ToString()); + } - [Theory] - [InlineData(typeof(Basic.Startup), "/index.html", "/swagger-ui.js", "/index.css", "/swagger-ui.css")] - [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.html", "/swagger/swagger-ui.js", "swagger/index.css", "/swagger/swagger-ui.css")] - public async Task IndexUrl_ReturnsEmbeddedVersionOfTheSwaggerUI( - Type startupType, - string htmlPath, - string swaggerUijsPath, - string indexCssPath, - string swaggerUiCssPath) - { - var client = new TestSite(startupType).BuildClient(); + [Theory] + [InlineData(typeof(Basic.Startup), "/index.html", "/swagger-ui.js", "/index.css", "/swagger-ui.css")] + [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.html", "/swagger/swagger-ui.js", "swagger/index.css", "/swagger/swagger-ui.css")] + public async Task IndexUrl_ReturnsEmbeddedVersionOfTheSwaggerUI( + Type startupType, + string htmlPath, + string swaggerUijsPath, + string indexCssPath, + string swaggerUiCssPath) + { + var client = new TestSite(startupType).BuildClient(); - var htmlResponse = await client.GetAsync(htmlPath); - Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + var htmlResponse = await client.GetAsync(htmlPath); + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); - var jsResponse = await client.GetAsync(swaggerUijsPath); - Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); + var jsResponse = await client.GetAsync(swaggerUijsPath); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); - var cssResponse = await client.GetAsync(indexCssPath); - Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); + var cssResponse = await client.GetAsync(indexCssPath); + Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); - cssResponse = await client.GetAsync(swaggerUiCssPath); - Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); - } + cssResponse = await client.GetAsync(swaggerUiCssPath); + Assert.Equal(HttpStatusCode.OK, cssResponse.StatusCode); + } - [Theory] - [InlineData(typeof(Basic.Startup), "/index.js")] - [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.js")] - public async Task SwaggerUIMiddleware_ReturnsInitializerScript( - Type startupType, - string indexJsPath) - { - var client = new TestSite(startupType).BuildClient(); - - var jsResponse = await client.GetAsync(indexJsPath); - Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); - - var jsContent = await jsResponse.Content.ReadAsStringAsync(); - Assert.Contains("SwaggerUIBundle", jsContent); - Assert.DoesNotContain("%(DocumentTitle)", jsContent); - Assert.DoesNotContain("%(HeadContent)", jsContent); - Assert.DoesNotContain("%(StylesPath)", jsContent); - Assert.DoesNotContain("%(ScriptBundlePath)", jsContent); - Assert.DoesNotContain("%(ScriptPresetsPath)", jsContent); - Assert.DoesNotContain("%(ConfigObject)", jsContent); - Assert.DoesNotContain("%(OAuthConfigObject)", jsContent); - Assert.DoesNotContain("%(Interceptors)", jsContent); - } + [Theory] + [InlineData(typeof(Basic.Startup), "/index.js")] + [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.js")] + public async Task SwaggerUIMiddleware_ReturnsInitializerScript( + Type startupType, + string indexJsPath) + { + var client = new TestSite(startupType).BuildClient(); + + var jsResponse = await client.GetAsync(indexJsPath); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); + + var jsContent = await jsResponse.Content.ReadAsStringAsync(); + Assert.Contains("SwaggerUIBundle", jsContent); + Assert.DoesNotContain("%(DocumentTitle)", jsContent); + Assert.DoesNotContain("%(HeadContent)", jsContent); + Assert.DoesNotContain("%(StylesPath)", jsContent); + Assert.DoesNotContain("%(ScriptBundlePath)", jsContent); + Assert.DoesNotContain("%(ScriptPresetsPath)", jsContent); + Assert.DoesNotContain("%(ConfigObject)", jsContent); + Assert.DoesNotContain("%(OAuthConfigObject)", jsContent); + Assert.DoesNotContain("%(Interceptors)", jsContent); + } - [Fact] - public async Task IndexUrl_DefinesPlugins() - { - var client = new TestSite(typeof(CustomUIConfig.Startup)).BuildClient(); + [Fact] + public async Task IndexUrl_DefinesPlugins() + { + var client = new TestSite(typeof(CustomUIConfig.Startup)).BuildClient(); - var jsResponse = await client.GetAsync("/swagger/index.js"); - Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); + var jsResponse = await client.GetAsync("/swagger/index.js"); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); - var jsContent = await jsResponse.Content.ReadAsStringAsync(); - Assert.Contains("\"plugins\":[\"customPlugin1\",\"customPlugin2\"]", jsContent); - } + var jsContent = await jsResponse.Content.ReadAsStringAsync(); + Assert.Contains("\"plugins\":[\"customPlugin1\",\"customPlugin2\"]", jsContent); + } - [Fact] - public async Task IndexUrl_DoesntDefinePlugins() - { - var client = new TestSite(typeof(Basic.Startup)).BuildClient(); + [Fact] + public async Task IndexUrl_DoesntDefinePlugins() + { + var client = new TestSite(typeof(Basic.Startup)).BuildClient(); - var jsResponse = await client.GetAsync("/index.js"); - Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); - var jsContent = await jsResponse.Content.ReadAsStringAsync(); - Assert.DoesNotContain("\"plugins\"", jsContent); - } + var jsResponse = await client.GetAsync("/index.js"); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); + var jsContent = await jsResponse.Content.ReadAsStringAsync(); + Assert.DoesNotContain("\"plugins\"", jsContent); + } - [Fact] - public async Task IndexUrl_ReturnsCustomPageTitleAndStylesheets_IfConfigured() - { - var client = new TestSite(typeof(CustomUIConfig.Startup)).BuildClient(); + [Fact] + public async Task IndexUrl_ReturnsCustomPageTitleAndStylesheets_IfConfigured() + { + var client = new TestSite(typeof(CustomUIConfig.Startup)).BuildClient(); - var response = await client.GetAsync("/swagger/index.html"); - var content = await response.Content.ReadAsStringAsync(); + var response = await client.GetAsync("/swagger/index.html"); + var content = await response.Content.ReadAsStringAsync(); - Assert.Contains("CustomUIConfig", content); - Assert.Contains("", content); - } + Assert.Contains("CustomUIConfig", content); + Assert.Contains("", content); + } - [Fact] - public async Task IndexUrl_ReturnsCustomIndexHtml_IfConfigured() - { - var client = new TestSite(typeof(CustomUIIndex.Startup)).BuildClient(); + [Fact] + public async Task IndexUrl_ReturnsCustomIndexHtml_IfConfigured() + { + var client = new TestSite(typeof(CustomUIIndex.Startup)).BuildClient(); - var response = await client.GetAsync("/swagger/index.html"); - var content = await response.Content.ReadAsStringAsync(); + var response = await client.GetAsync("/swagger/index.html"); + var content = await response.Content.ReadAsStringAsync(); - Assert.Contains("Example.com", content); - } + Assert.Contains("Example.com", content); + } - [Fact] - public async Task IndexUrl_ReturnsInterceptors_IfConfigured() - { - var client = new TestSite(typeof(CustomUIConfig.Startup)).BuildClient(); + [Fact] + public async Task IndexUrl_ReturnsInterceptors_IfConfigured() + { + var client = new TestSite(typeof(CustomUIConfig.Startup)).BuildClient(); - var response = await client.GetAsync("/swagger/index.js"); - var content = await response.Content.ReadAsStringAsync(); + var response = await client.GetAsync("/swagger/index.js"); + var content = await response.Content.ReadAsStringAsync(); - Assert.Contains("\"RequestInterceptorFunction\":", content); - Assert.Contains("\"ResponseInterceptorFunction\":", content); - } + Assert.Contains("\"RequestInterceptorFunction\":", content); + Assert.Contains("\"ResponseInterceptorFunction\":", content); + } + + [Theory] + [InlineData("/swagger/index.html", "/swagger/index.js", new[] { "Version 1.0", "Version 2.0" })] + [InlineData("/swagger/1.0/index.html", "/swagger/1.0/index.js", new[] { "Version 1.0" })] + [InlineData("/swagger/2.0/index.html", "/swagger/2.0/index.js", new[] { "Version 2.0" })] + public async Task SwaggerUIMiddleware_CanBeConfiguredMultipleTimes(string htmlUrl, string jsUrl, string[] versions) + { + var client = new TestSite(typeof(MultipleVersions.Startup)).BuildClient(); + + var htmlResponse = await client.GetAsync(htmlUrl); + var jsResponse = await client.GetAsync(jsUrl); + var content = await jsResponse.Content.ReadAsStringAsync(); - [Theory] - [InlineData("/swagger/index.html", "/swagger/index.js", new[] { "Version 1.0", "Version 2.0" })] - [InlineData("/swagger/1.0/index.html", "/swagger/1.0/index.js", new[] { "Version 1.0" })] - [InlineData("/swagger/2.0/index.html", "/swagger/2.0/index.js", new[] { "Version 2.0" })] - public async Task SwaggerUIMiddleware_CanBeConfiguredMultipleTimes(string htmlUrl, string jsUrl, string[] versions) + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); + foreach (var version in versions) { - var client = new TestSite(typeof(MultipleVersions.Startup)).BuildClient(); - - var htmlResponse = await client.GetAsync(htmlUrl); - var jsResponse = await client.GetAsync(jsUrl); - var content = await jsResponse.Content.ReadAsStringAsync(); - - Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, jsResponse.StatusCode); - foreach (var version in versions) - { - Assert.Contains(version, content); - } + Assert.Contains(version, content); } + } - [Theory] - [InlineData(typeof(Basic.Startup), "/index.html", "./swagger-ui.css", "./swagger-ui-bundle.js", "./swagger-ui-standalone-preset.js")] - [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.html", "/ext/custom-stylesheet.css", "/ext/custom-javascript.js", "/ext/custom-javascript.js")] - public async Task IndexUrl_Returns_ExpectedAssetPaths( - Type startupType, - string htmlPath, - string cssPath, - string scriptBundlePath, - string scriptPresetsPath) - { - var client = new TestSite(startupType).BuildClient(); + [Theory] + [InlineData(typeof(Basic.Startup), "/index.html", "./swagger-ui.css", "./swagger-ui-bundle.js", "./swagger-ui-standalone-preset.js")] + [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.html", "/ext/custom-stylesheet.css", "/ext/custom-javascript.js", "/ext/custom-javascript.js")] + public async Task IndexUrl_Returns_ExpectedAssetPaths( + Type startupType, + string htmlPath, + string cssPath, + string scriptBundlePath, + string scriptPresetsPath) + { + var client = new TestSite(startupType).BuildClient(); - var htmlResponse = await client.GetAsync(htmlPath); - Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + var htmlResponse = await client.GetAsync(htmlPath); + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); - var content = await htmlResponse.Content.ReadAsStringAsync(); - Assert.Contains($"", content); - Assert.Contains($"