diff --git a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentSerializer.cs b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentSerializer.cs
index 7db558cc4e..cf08053034 100644
--- a/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentSerializer.cs
+++ b/src/Swashbuckle.AspNetCore.Swagger/ISwaggerDocumentSerializer.cs
@@ -4,8 +4,18 @@
namespace Swashbuckle.AspNetCore.Swagger
{
+ ///
+ /// Provide an implementation for this interface if you wish to customize how the open api document is exactly written.
+ /// SerializeDocument will be called in that case, instead of the Microsoft built in Serialize methods.
+ ///
public interface ISwaggerDocumentSerializer
{
+ ///
+ /// Called in the places where normally SerializeV2 or SerializeV3 on OpenApiDocument would be called.
+ ///
+ /// The open api document that should be serialized
+ /// The write to which the document needs to be written
+ /// The open api spec to serialize
void SerializeDocument(OpenApiDocument document, IOpenApiWriter writer, OpenApiSpecVersion specVersion);
}
-}
\ No newline at end of file
+}
diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs
index 1a83147be9..1b53bf5c7b 100644
--- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs
+++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs
@@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Writers;
@@ -18,6 +19,12 @@ public class SwaggerMiddleware
private readonly TemplateMatcher _requestMatcher;
private readonly ISwaggerDocumentSerializer _swaggerDocumentSerializer;
+ public SwaggerMiddleware(
+ RequestDelegate next,
+ SwaggerOptions options) : this (next, options, null)
+ {
+ }
+
public SwaggerMiddleware(
RequestDelegate next,
SwaggerOptions options,
@@ -28,7 +35,7 @@ public SwaggerMiddleware(
_requestMatcher = new TemplateMatcher(TemplateParser.Parse(_options.RouteTemplate), new RouteValueDictionary());
// Use IServiceProvider to retrieve the ISwaggerDocumentSerializer, because it is an optional service
- _swaggerDocumentSerializer = serviceProvider.GetService(typeof(ISwaggerDocumentSerializer)) as ISwaggerDocumentSerializer;
+ _swaggerDocumentSerializer = serviceProvider?.GetService();
}
public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs
index 13d8a0476b..974303c793 100644
--- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs
+++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Writers;
using Swashbuckle.AspNetCore.Swagger;
@@ -27,6 +28,13 @@ internal class DocumentProvider : IDocumentProvider
private readonly IAsyncSwaggerProvider _swaggerProvider;
private readonly ISwaggerDocumentSerializer _swaggerDocumentSerializer;
+ public DocumentProvider(
+ IOptions generatorOptions,
+ IOptions options,
+ IAsyncSwaggerProvider swaggerProvider
+ ) : this(generatorOptions, options, swaggerProvider, null)
+ { }
+
public DocumentProvider(
IOptions generatorOptions,
IOptions options,
@@ -39,7 +47,7 @@ IServiceProvider serviceProvider
_swaggerProvider = swaggerProvider;
// Use IServiceProvider to retrieve the ISwaggerDocumentSerializer, because it is an optional service
- _swaggerDocumentSerializer = serviceProvider.GetService(typeof(ISwaggerDocumentSerializer)) as ISwaggerDocumentSerializer;
+ _swaggerDocumentSerializer = serviceProvider?.GetService();
}
public IEnumerable GetDocumentNames()
diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerDocumentSerializerTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerDocumentSerializerTests.cs
new file mode 100644
index 0000000000..94a7af12bb
--- /dev/null
+++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerDocumentSerializerTests.cs
@@ -0,0 +1,1351 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.Swagger;
+using Swashbuckle.AspNetCore.TestSupport;
+using Xunit;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Server.HttpSys;
+using Microsoft.AspNetCore.Authentication;
+
+namespace Swashbuckle.AspNetCore.SwaggerGen.Test
+{
+ public class SwaggerDocumentSerializerTests
+ {
+ [Fact]
+ public void GetSwagger_GeneratesSwaggerDocument_ForApiDescriptionsWithMatchingGroupName()
+ {
+ var subject = Subject(
+ apiDescriptions: new[]
+ {
+ ApiDescriptionFactory.Create(
+ c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "POST", relativePath: "resource"),
+
+ ApiDescriptionFactory.Create(
+ c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "GET", relativePath: "resource"),
+
+ ApiDescriptionFactory.Create(
+ c => nameof(c.ActionWithNoParameters), groupName: "v2", httpMethod: "POST", relativePath: "resource"),
+ },
+ options: new SwaggerGeneratorOptions
+ {
+ SwaggerDocs = new Dictionary
+ {
+ ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" }
+ }
+ }
+ );
+
+ var document = subject.GetSwagger("v1");
+
+ Assert.Equal("V1", document.Info.Version);
+ Assert.Equal("Test API", document.Info.Title);
+ Assert.Equal(new[] { "/resource" }, document.Paths.Keys.ToArray());
+ Assert.Equal(new[] { OperationType.Post, OperationType.Get }, document.Paths["/resource"].Operations.Keys);
+ }
+
+ [Theory]
+ [InlineData("resources/{id}", "/resources/{id}")]
+ [InlineData("resources;secondary={secondary}", "/resources;secondary={secondary}")]
+ [InlineData("resources:deposit", "/resources:deposit")]
+ [InlineData("{category}/{product?}/{sku}", "/{category}/{product}/{sku}")]
+ [InlineData("{area=Home}/{controller:required}/{id=0:int}", "/{area}/{controller}/{id}")]
+ [InlineData("{category}/product/{group?}", "/{category}/product/{group}")]
+ [InlineData("{category:int}/product/{group:range(10, 20)?}", "/{category}/product/{group}")]
+ [InlineData("{person:int}/{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}", "/{person}/{ssn}")]
+ [InlineData("{person:int}/{ssn:regex(^(?=.*kind)(?=.*good).*$)}", "/{person}/{ssn}")]
+ public void GetSwagger_GeneratesSwaggerDocument_ForApiDescriptionsWithConstrainedRelativePaths(string path, string expectedPath)
+ {
+ var subject = Subject(
+ apiDescriptions: new[]
+ {
+ ApiDescriptionFactory.Create(
+ c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "POST", relativePath: path),
+
+ },
+ options: new SwaggerGeneratorOptions
+ {
+ SwaggerDocs = new Dictionary
+ {
+ ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" }
+ }
+ }
+ );
+
+ var document = subject.GetSwagger("v1");
+
+ Assert.Equal("V1", document.Info.Version);
+ Assert.Equal("Test API", document.Info.Title);
+ var (actualPath, _) = Assert.Single(document.Paths);
+ Assert.Equal(expectedPath, actualPath);
+ }
+
+ [Fact]
+ public void GetSwagger_SetsOperationIdToNull_ByDefault()
+ {
+ var subject = Subject(
+ apiDescriptions: new[]
+ {
+ ApiDescriptionFactory.Create(
+ c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "POST", relativePath: "resource"),
+ }
+ );
+
+ var document = subject.GetSwagger("v1");
+
+ Assert.Null(document.Paths["/resource"].Operations[OperationType.Post].OperationId);
+ }
+
+ [Fact]
+ public void GetSwagger_SetsOperationIdToRouteName_IfActionHasRouteNameMetadata()
+ {
+ var subject = Subject(
+ apiDescriptions: new[]
+ {
+ ApiDescriptionFactory.Create(
+ c => nameof(c.ActionWithRouteNameMetadata), groupName: "v1", httpMethod: "POST", relativePath: "resource"),
+ }
+ );
+
+ var document = subject.GetSwagger("v1");
+
+ Assert.Equal("SomeRouteName", document.Paths["/resource"].Operations[OperationType.Post].OperationId);
+ }
+
+ [Fact]
+ public void GetSwagger_SetsOperationIdToEndpointName_IfActionHasEndpointNameMetadata()
+ {
+ var methodInfo = typeof(FakeController).GetMethod(nameof(FakeController.ActionWithParameter));
+ var actionDescriptor = new ActionDescriptor
+ {
+ EndpointMetadata = new List