Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Http;

Expand All @@ -12,6 +14,7 @@ internal sealed partial class ReDocMiddleware
{
private static readonly HashSet<string> AllowedHttpMethods = new(StringComparer.OrdinalIgnoreCase) { HttpMethods.Get, HttpMethods.Head };
private static readonly string ReDocVersion = GetReDocVersion();
private static readonly JsonSerializerOptions DefaultJsonSerializerOptions = CreateDefaultJsonSerializerOptions();

private readonly RequestDelegate _next;
private readonly ReDocOptions _options;
Expand All @@ -23,10 +26,7 @@ public ReDocMiddleware(RequestDelegate next, ReDocOptions options)
_next = next;
_options = options ?? new ReDocOptions();

if (options.JsonSerializerOptions != null)
{
_jsonSerializerOptions = options.JsonSerializerOptions;
}
_jsonSerializerOptions = _options.JsonSerializerOptions ?? DefaultJsonSerializerOptions;

var pathPrefix = options.RoutePrefix.StartsWith('/') ? options.RoutePrefix : $"/{options.RoutePrefix}";
_resourceProvider = new(
Expand Down Expand Up @@ -72,6 +72,32 @@ public async Task Invoke(HttpContext httpContext)
await _next(httpContext);
}

[UnconditionalSuppressMessage(
"AOT",
"IL2026:RequiresUnreferencedCode",
Justification = "The reflection-based resolver is only used when dynamic code is supported.")]
[UnconditionalSuppressMessage(
"AOT",
"IL3050:RequiresDynamicCode",
Justification = "The reflection-based resolver is only used when dynamic code is supported.")]
private static JsonSerializerOptions CreateDefaultJsonSerializerOptions()
{
if (!RuntimeFeature.IsDynamicCodeSupported)
{
// Only source-generated metadata can be used with native AoT
return null;
}

// Chain a reflection-based resolver after the source-generated one so that custom
// values in ConfigObject.AdditionalItems (e.g. anonymous types) can be serialized.
return new JsonSerializerOptions(ReDocOptionsJsonContext.Default.Options)
{
TypeInfoResolver = JsonTypeInfoResolver.Combine(
ReDocOptionsJsonContext.Default,
new DefaultJsonTypeInfoResolver()),
};
}

private static string GetReDocVersion()
=> typeof(ReDocMiddleware).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
Expand Down Expand Up @@ -196,11 +222,11 @@ static string GetETag(string text)
[UnconditionalSuppressMessage(
"AOT",
"IL2026:RequiresUnreferencedCode",
Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")]
Justification = "Reflection-based serialization is only used if the user provides their own custom JsonSerializerOptions or when dynamic code is supported.")]
[UnconditionalSuppressMessage(
"AOT",
"IL3050:RequiresDynamicCode",
Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")]
Justification = "Reflection-based serialization is only used if the user provides their own custom JsonSerializerOptions or when dynamic code is supported.")]
private Dictionary<string, string> GetIndexArguments()
{
string configObject = null;
Expand Down
42 changes: 34 additions & 8 deletions src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Http;

Expand All @@ -12,6 +14,7 @@ internal sealed partial class SwaggerUIMiddleware
{
private static readonly HashSet<string> AllowedHttpMethods = new(StringComparer.OrdinalIgnoreCase) { HttpMethods.Get, HttpMethods.Head };
private static readonly string SwaggerUIVersion = GetSwaggerUIVersion();
private static readonly JsonSerializerOptions DefaultJsonSerializerOptions = CreateDefaultJsonSerializerOptions();

private readonly RequestDelegate _next;
private readonly SwaggerUIOptions _options;
Expand All @@ -23,10 +26,7 @@ public SwaggerUIMiddleware(RequestDelegate next, SwaggerUIOptions options)
_next = next;
_options = options ?? new();

if (options.JsonSerializerOptions != null)
{
_jsonSerializerOptions = options.JsonSerializerOptions;
}
_jsonSerializerOptions = _options.JsonSerializerOptions ?? DefaultJsonSerializerOptions;

var pathPrefix = options.RoutePrefix.StartsWith('/') ? options.RoutePrefix : $"/{options.RoutePrefix}";
_resourceProvider = new(
Expand Down Expand Up @@ -82,6 +82,32 @@ public async Task Invoke(HttpContext httpContext)
await _next(httpContext);
}

[UnconditionalSuppressMessage(
"AOT",
"IL2026:RequiresUnreferencedCode",
Justification = "The reflection-based resolver is only used when dynamic code is supported.")]
[UnconditionalSuppressMessage(
"AOT",
"IL3050:RequiresDynamicCode",
Justification = "The reflection-based resolver is only used when dynamic code is supported.")]
private static JsonSerializerOptions CreateDefaultJsonSerializerOptions()
{
if (!RuntimeFeature.IsDynamicCodeSupported)
{
// Only source-generated metadata can be used with native AoT
return null;
}

// Chain a reflection-based resolver after the source-generated one so that custom
// values in ConfigObject.AdditionalItems (e.g. anonymous types) can be serialized.
return new JsonSerializerOptions(SwaggerUIOptionsJsonContext.Default.Options)
{
TypeInfoResolver = JsonTypeInfoResolver.Combine(
SwaggerUIOptionsJsonContext.Default,
new DefaultJsonTypeInfoResolver()),
};
}

private static string GetSwaggerUIVersion()
=> typeof(SwaggerUIMiddleware).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
Expand Down Expand Up @@ -196,11 +222,11 @@ private async Task RespondWithFile(HttpContext context, string fileName)
[UnconditionalSuppressMessage(
"AOT",
"IL2026:RequiresUnreferencedCode",
Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")]
Justification = "Reflection-based serialization is only used if the user provides their own custom JsonSerializerOptions or when dynamic code is supported.")]
[UnconditionalSuppressMessage(
"AOT",
"IL3050:RequiresDynamicCode",
Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")]
Justification = "Reflection-based serialization is only used if the user provides their own custom JsonSerializerOptions or when dynamic code is supported.")]
private async Task RespondWithDocumentUrls(HttpContext context)
{
var response = context.Response;
Expand Down Expand Up @@ -240,11 +266,11 @@ private static string GetETag(string text)
[UnconditionalSuppressMessage(
"AOT",
"IL2026:RequiresUnreferencedCode",
Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")]
Justification = "Reflection-based serialization is only used if the user provides their own custom JsonSerializerOptions or when dynamic code is supported.")]
[UnconditionalSuppressMessage(
"AOT",
"IL3050:RequiresDynamicCode",
Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")]
Justification = "Reflection-based serialization is only used if the user provides their own custom JsonSerializerOptions or when dynamic code is supported.")]
private Dictionary<string, string> GetIndexArguments()
{
string configObject = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,4 +433,31 @@ public async Task ReDocMiddleware_ConfigObject_Is_Not_Spliced_Into_A_String_Lite

Assert.Equal("x');alert(1);//", config.RootElement.GetProperty("theme").GetString());
}

[Fact]
public async Task ReDocMiddleware_Serializes_Custom_Object_In_AdditionalItems()
{
// Arrange
var cancellationToken = TestContext.Current.CancellationToken;

using var server = TestSite.CreateServer((app) => app.UseReDoc((options) =>
{
options.ConfigObject.AdditionalItems["theme"] = new { colors = new { primary = "#086eaa" } };
}));

using var client = server.CreateClient();

// Act
using var response = await client.GetAsync("/api-docs/index.js", cancellationToken);

// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

var body = await response.Content.ReadAsStringAsync(cancellationToken);
outputHelper.WriteLine(body);

using var config = JsonDocument.Parse(body[body.IndexOf('{')..(body.LastIndexOf('}') + 1)]);

Assert.Equal("#086eaa", config.RootElement.GetProperty("theme").GetProperty("colors").GetProperty("primary").GetString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -807,4 +807,37 @@ public async Task SwaggerUIMiddleware_ConfigObject_Is_Not_Spliced_Into_A_String_

Assert.Equal("x');alert(1);//", config.RootElement.GetProperty("urls")[0].GetProperty("name").GetString());
}

[Fact]
public async Task SwaggerUIMiddleware_Serializes_Custom_Object_In_AdditionalItems()
{
// Arrange
var cancellationToken = TestContext.Current.CancellationToken;

using var server = TestSite.CreateServer((app) => app.UseSwaggerUI((options) =>
{
options.ConfigObject.AdditionalItems["theme"] = new { colors = new { primary = "#086eaa" } };
}));

using var client = server.CreateClient();

// Act
using var response = await client.GetAsync("/swagger/index.js", cancellationToken);

// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

var body = await response.Content.ReadAsStringAsync(cancellationToken);

const string Prefix = "var configObject =";

var line = body.Split('\n').First((p) => p.Contains(Prefix));
outputHelper.WriteLine(line);

var json = line[(line.IndexOf(Prefix, StringComparison.Ordinal) + Prefix.Length)..].Trim().TrimEnd(';');

using var config = JsonDocument.Parse(json);

Assert.Equal("#086eaa", config.RootElement.GetProperty("theme").GetProperty("colors").GetProperty("primary").GetString());
}
}
Loading