diff --git a/VirtoCommerce.Storefront/Infrastructure/Swagger/ApiSchemaOptions.cs b/VirtoCommerce.Storefront/Infrastructure/Swagger/ApiSchemaOptions.cs new file mode 100644 index 0000000..2d10900 --- /dev/null +++ b/VirtoCommerce.Storefront/Infrastructure/Swagger/ApiSchemaOptions.cs @@ -0,0 +1,9 @@ +namespace VirtoCommerce.Storefront.Infrastructure.Swagger +{ + public class ApiSchemaOptions + { + public bool NameAndOrderRequestBody { get; set; } + + public OpenApiSpecificationVersion OpenApiSpecificationVersion { get; set; } + } +} diff --git a/VirtoCommerce.Storefront/Infrastructure/Swagger/ApiSchemaVersion.cs b/VirtoCommerce.Storefront/Infrastructure/Swagger/ApiSchemaVersion.cs new file mode 100644 index 0000000..7775d47 --- /dev/null +++ b/VirtoCommerce.Storefront/Infrastructure/Swagger/ApiSchemaVersion.cs @@ -0,0 +1,8 @@ +namespace VirtoCommerce.Storefront.Infrastructure.Swagger +{ + public enum ApiSchemaVersion + { + V1, + V2 + } +} diff --git a/VirtoCommerce.Storefront/Infrastructure/Swagger/NameAndOrderRequestBodyFilter.cs b/VirtoCommerce.Storefront/Infrastructure/Swagger/NameAndOrderRequestBodyFilter.cs new file mode 100644 index 0000000..d08740b --- /dev/null +++ b/VirtoCommerce.Storefront/Infrastructure/Swagger/NameAndOrderRequestBodyFilter.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace VirtoCommerce.Storefront.Infrastructure.Swagger +{ + public class NameAndOrderRequestBodyFilter: IRequestBodyFilter + { + private readonly SwaggerOptions _swaggerOptions; + + public NameAndOrderRequestBodyFilter(IOptions swaggerOptions) + { + _swaggerOptions = swaggerOptions.Value; + } + + public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context) + { + // Even if it's v2 or request body shouldn't be named + // we want name it because of bugs and different approaches in generation tools + var bodyName = _swaggerOptions.Schema.NameAndOrderRequestBody + ? context.BodyParameterDescription.Name + : "body"; + requestBody.Extensions.Add("x-name", new OpenApiString(bodyName)); + requestBody.Extensions.Add("x-codegen-request-body-name", new OpenApiString(bodyName)); + requestBody.Extensions.Add("x-ms-requestBody-name", new OpenApiString(bodyName)); + + if (_swaggerOptions.Schema.NameAndOrderRequestBody) + { + var bodyPosition = context.BodyParameterDescription.ParameterInfo().Position; + requestBody.Extensions.Add("x-position", new OpenApiInteger(bodyPosition)); + requestBody.Extensions.Add("x-ms-requestBody-index", new OpenApiInteger(bodyPosition)); + } + } + } +} diff --git a/VirtoCommerce.Storefront/Infrastructure/Swagger/NewtonsoftJsonIgnoreFilter.cs b/VirtoCommerce.Storefront/Infrastructure/Swagger/NewtonsoftJsonIgnoreFilter.cs deleted file mode 100644 index 63b1297..0000000 --- a/VirtoCommerce.Storefront/Infrastructure/Swagger/NewtonsoftJsonIgnoreFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Linq; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace VirtoCommerce.Storefront.Infrastructure.Swagger -{ - /// - /// Allows to ignore . - /// - public class NewtonsoftJsonIgnoreFilter : ISchemaFilter - { - public void Apply(OpenApiSchema schema, SchemaFilterContext context) - { - var type = context.Type; - foreach (var prop in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) - .Where(p => p.GetCustomAttributes(typeof(Newtonsoft.Json.JsonIgnoreAttribute), true)?.Any() == true)) - { - var propName = prop.Name[0].ToString().ToLower() + prop.Name.Substring(1); - if (schema?.Properties?.ContainsKey(propName) == true) - schema?.Properties?.Remove(propName); - } - } - } -} diff --git a/VirtoCommerce.Storefront/Infrastructure/Swagger/OpenApiSpecificationVersion.cs b/VirtoCommerce.Storefront/Infrastructure/Swagger/OpenApiSpecificationVersion.cs new file mode 100644 index 0000000..488f5c7 --- /dev/null +++ b/VirtoCommerce.Storefront/Infrastructure/Swagger/OpenApiSpecificationVersion.cs @@ -0,0 +1,8 @@ +namespace VirtoCommerce.Storefront.Infrastructure.Swagger +{ + public enum OpenApiSpecificationVersion + { + V2, + V3 + } +} diff --git a/VirtoCommerce.Storefront/Infrastructure/Swagger/OptionalParametersFilter.cs b/VirtoCommerce.Storefront/Infrastructure/Swagger/OptionalParametersFilter.cs deleted file mode 100644 index d27443a..0000000 --- a/VirtoCommerce.Storefront/Infrastructure/Swagger/OptionalParametersFilter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Linq; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace VirtoCommerce.Storefront.Infrastructure.Swagger -{ - public class OptionalParametersFilter : IOperationFilter - { - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - if (operation.Parameters == null || !operation.Parameters.Any()) - { - return; - } - - var optionalParameters = context.ApiDescription.ParameterDescriptions - .Where(p => p.ParameterDescriptor != null && - ((ControllerParameterDescriptor)p.ParameterDescriptor).ParameterInfo.CustomAttributes.Any(attr => attr.AttributeType == typeof(SwaggerOptionalAttribute))).ToList(); - - foreach (var apiParameter in optionalParameters) - { - var parameter = operation.Parameters.FirstOrDefault(p => p.Name == apiParameter.Name); - if (parameter != null) - { - parameter.Required = false; - } - } - } - } -} diff --git a/VirtoCommerce.Storefront/Infrastructure/Swagger/ParameterOrderFilter.cs b/VirtoCommerce.Storefront/Infrastructure/Swagger/ParameterOrderFilter.cs new file mode 100644 index 0000000..d93b550 --- /dev/null +++ b/VirtoCommerce.Storefront/Infrastructure/Swagger/ParameterOrderFilter.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace VirtoCommerce.Storefront.Infrastructure.Swagger +{ + public class ParameterOrderFilter: IParameterFilter + { + private readonly SwaggerOptions _swaggerOptions; + + public ParameterOrderFilter(IOptions swaggerOptions) + { + _swaggerOptions = swaggerOptions.Value; + } + + public void Apply(OpenApiParameter parameter, ParameterFilterContext context) + { + if (_swaggerOptions.Schema.NameAndOrderRequestBody) + { + // Explicitly specify parameters position + // Position for store and language is unknown and they should be last, so use int.MaxValue relative values + if (context.ParameterInfo != null) + { + parameter.Extensions.Add("x-position", new OpenApiInteger(context.ParameterInfo.Position)); + } + else + { + switch (parameter.Name) + { + case "store": + parameter.Extensions.Add("x-position", new OpenApiInteger(int.MaxValue - 1)); + break; + case "language": + parameter.Extensions.Add("x-position", new OpenApiInteger(int.MaxValue)); + break; + } + } + } + } + } +} diff --git a/VirtoCommerce.Storefront/Infrastructure/Swagger/RequireRequestBodyFilter.cs b/VirtoCommerce.Storefront/Infrastructure/Swagger/RequireRequestBodyFilter.cs new file mode 100644 index 0000000..40754f1 --- /dev/null +++ b/VirtoCommerce.Storefront/Infrastructure/Swagger/RequireRequestBodyFilter.cs @@ -0,0 +1,13 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace VirtoCommerce.Storefront.Infrastructure.Swagger +{ + public class RequireRequestBodyFilter: IRequestBodyFilter + { + public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context) + { + requestBody.Required = true; + } + } +} diff --git a/VirtoCommerce.Storefront/Infrastructure/Swagger/SwaggerOptions.cs b/VirtoCommerce.Storefront/Infrastructure/Swagger/SwaggerOptions.cs new file mode 100644 index 0000000..a46d59c --- /dev/null +++ b/VirtoCommerce.Storefront/Infrastructure/Swagger/SwaggerOptions.cs @@ -0,0 +1,7 @@ +namespace VirtoCommerce.Storefront.Infrastructure.Swagger +{ + public class SwaggerOptions + { + public ApiSchemaOptions Schema { get; set; } + } +} diff --git a/VirtoCommerce.Storefront/Startup.cs b/VirtoCommerce.Storefront/Startup.cs index 29a0627..81fd3b2 100644 --- a/VirtoCommerce.Storefront/Startup.cs +++ b/VirtoCommerce.Storefront/Startup.cs @@ -339,21 +339,25 @@ public void ConfigureServices(IServiceCollection services) options.MaxAge = TimeSpan.FromDays(30); }); + services.Configure(Configuration.GetSection("Swagger").Bind); + // Register the Swagger generator, defining 1 or more Swagger documents services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Storefront REST API documentation", Version = "v1" }); c.IgnoreObsoleteProperties(); c.IgnoreObsoleteActions(); + c.ParameterFilter(); + c.RequestBodyFilter(); + c.RequestBodyFilter(); // To include 401 response type to actions that requires Authorization c.OperationFilter(); c.OperationFilter(); - c.OperationFilter(); - c.OperationFilter(); c.OperationFilter(); + c.OperationFilter(); c.OperationFilter(); c.SchemaFilter(); - c.SchemaFilter(); + c.SchemaGeneratorOptions.UseAllOfToExtendReferenceSchemas = true; // Use method name as operation ID, i.e. ApiAccount.GetOrganization instead of /storefrontapi/account/organization (will be treated as just organization method) c.CustomOperationIds(apiDesc => apiDesc.TryGetMethodInfo(out var methodInfo) ? methodInfo.Name : null); @@ -361,6 +365,7 @@ public void ConfigureServices(IServiceCollection services) // To avoid errors with repeating type names c.CustomSchemaIds(type => (Attribute.GetCustomAttribute(type, typeof(SwaggerSchemaIdAttribute)) as SwaggerSchemaIdAttribute)?.Id ?? type.FriendlyId()); }); + services.AddSwaggerGenNewtonsoftSupport(); services.AddResponseCompression(); @@ -414,7 +419,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) }); // Enable middleware to serve generated Swagger as a JSON endpoint. - app.UseSwagger(c => c.RouteTemplate = "docs/{documentName}/docs.json"); + app.UseSwagger(c => + { + var options = app.ApplicationServices.GetService>().Value; + c.SerializeAsV2 = options.Schema.OpenApiSpecificationVersion == OpenApiSpecificationVersion.V2; + c.RouteTemplate = "docs/{documentName}/docs.json"; + }); var rewriteOptions = new RewriteOptions(); // Load IIS url rewrite rules from external file diff --git a/VirtoCommerce.Storefront/VirtoCommerce.Storefront.csproj b/VirtoCommerce.Storefront/VirtoCommerce.Storefront.csproj index 6570c13..a29ca26 100644 --- a/VirtoCommerce.Storefront/VirtoCommerce.Storefront.csproj +++ b/VirtoCommerce.Storefront/VirtoCommerce.Storefront.csproj @@ -61,6 +61,7 @@ + diff --git a/VirtoCommerce.Storefront/appsettings.json b/VirtoCommerce.Storefront/appsettings.json index 8cde13b..0b10094 100644 --- a/VirtoCommerce.Storefront/appsettings.json +++ b/VirtoCommerce.Storefront/appsettings.json @@ -91,5 +91,11 @@ "SnapshotInLowPriorityThread": true, "ProvideAnonymousTelemetry": true, "FailedRequestLimit": 3 + }, + "Swagger": { + "Schema": { + "NameAndOrderRequestBody": true, + "OpenApiSpecificationVersion": "v3" + } } }