Skip to content

Commit

Permalink
Add native AoT support for ReDoc
Browse files Browse the repository at this point in the history
- Add support for using the ReDoc middleware in native AoT applications.
- Add missing test coverage for ReDoc extensions.
- Fix some code analysis suggestions.
- Fix native AoT test app exception on start-up (see #2951).
  • Loading branch information
martincostello committed Sep 30, 2024
1 parent 9fa161b commit 5230198
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Swashbuckle.AspNetCore.ReDoc.ReDocOptions.JsonSerializerOptions.get -> System.Text.Json.JsonSerializerOptions
Swashbuckle.AspNetCore.ReDoc.ReDocOptions.JsonSerializerOptions.set -> void
60 changes: 48 additions & 12 deletions src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -39,15 +40,25 @@ public ReDocMiddleware(

_staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options);

_jsonSerializerOptions = new JsonSerializerOptions();

#if NET6_0_OR_GREATER
_jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
if (options.JsonSerializerOptions != null)
{
_jsonSerializerOptions = options.JsonSerializerOptions;
}
#if !NET6_0_OR_GREATER
else
{
_jsonSerializerOptions = new JsonSerializerOptions()
{
#if NET5_0_OR_GREATER
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
#else
_jsonSerializerOptions.IgnoreNullValues = true;
IgnoreNullValues = true,
#endif
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false) }
};
}
#endif
_jsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
_jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false));
}

public async Task Invoke(HttpContext httpContext)
Expand Down Expand Up @@ -82,7 +93,7 @@ public async Task Invoke(HttpContext httpContext)
await _staticFileMiddleware.Invoke(httpContext);
}

private StaticFileMiddleware CreateStaticFileMiddleware(
private static StaticFileMiddleware CreateStaticFileMiddleware(
RequestDelegate next,
IWebHostEnvironment hostingEnv,
ILoggerFactory loggerFactory,
Expand All @@ -97,10 +108,14 @@ private StaticFileMiddleware CreateStaticFileMiddleware(
return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory);
}

private void RespondWithRedirect(HttpResponse response, string location)
private static void RespondWithRedirect(HttpResponse response, string location)
{
response.StatusCode = 301;
response.StatusCode = StatusCodes.Status301MovedPermanently;
#if NET6_0_OR_GREATER
response.Headers.Location = location;
#else
response.Headers["Location"] = location;
#endif
}

private async Task RespondWithFile(HttpResponse response, string fileName)
Expand Down Expand Up @@ -138,14 +153,35 @@ private async Task RespondWithFile(HttpResponse response, string fileName)
}
}

private IDictionary<string, string> GetIndexArguments()
#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.")]
#endif
private Dictionary<string, string> GetIndexArguments()
{
string configObject = null;

#if NET6_0_OR_GREATER
if (_jsonSerializerOptions is null)
{
configObject = JsonSerializer.Serialize(_options.ConfigObject, ReDocOptionsJsonContext.Default.ConfigObject);
}
#endif

configObject ??= JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions);

return new Dictionary<string, string>()
{
{ "%(DocumentTitle)", _options.DocumentTitle },
{ "%(HeadContent)", _options.HeadContent },
{ "%(SpecUrl)", _options.SpecUrl },
{ "%(ConfigObject)", JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions) }
{ "%(ConfigObject)", configObject },
};
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/Swashbuckle.AspNetCore.ReDoc/ReDocOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Swashbuckle.AspNetCore.ReDoc
Expand Down Expand Up @@ -32,7 +33,15 @@ public class ReDocOptions
/// </summary>
public string SpecUrl { get; set; } = null;

/// <summary>
/// Gets or sets the <see cref="ConfigObject"/> to use.
/// </summary>
public ConfigObject ConfigObject { get; set; } = new ConfigObject();

/// <summary>
/// Gets or sets the optional <see cref="System.Text.Json.JsonSerializerOptions"/> to use.
/// </summary>
public JsonSerializerOptions JsonSerializerOptions { get; set; }
}

public class ConfigObject
Expand Down
45 changes: 45 additions & 0 deletions src/Swashbuckle.AspNetCore.ReDoc/ReDocOptionsJsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#if NET6_0_OR_GREATER
using System;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

namespace Swashbuckle.AspNetCore.ReDoc;

[JsonSerializable(typeof(ConfigObject))]
// These primitive types are declared for common types that may be used with ConfigObject.AdditionalItems. See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2884.
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(byte))]
[JsonSerializable(typeof(sbyte))]
[JsonSerializable(typeof(short))]
[JsonSerializable(typeof(ushort))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(int?))]
[JsonSerializable(typeof(uint))]
[JsonSerializable(typeof(long))]
[JsonSerializable(typeof(ulong))]
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(double))]
[JsonSerializable(typeof(decimal))]
[JsonSerializable(typeof(char))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(DateTime))]
[JsonSerializable(typeof(DateTimeOffset))]
[JsonSerializable(typeof(TimeSpan))]
[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)]
internal sealed partial class ReDocOptionsJsonContext : JsonSerializerContext;
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<SignAssembly>true</SignAssembly>
<TargetFrameworks>netstandard2.0;netcoreapp3.0;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
<None Remove="index.css" />
Expand Down
8 changes: 6 additions & 2 deletions src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

namespace Swashbuckle.AspNetCore.SwaggerUI
{
public partial class SwaggerUIMiddleware
public class SwaggerUIMiddleware
{
private const string EmbeddedFileNamespace = "Swashbuckle.AspNetCore.SwaggerUI.node_modules.swagger_ui_dist";

Expand Down Expand Up @@ -110,8 +110,12 @@ private static StaticFileMiddleware CreateStaticFileMiddleware(

private static void RespondWithRedirect(HttpResponse response, string location)
{
response.StatusCode = 301;
response.StatusCode = StatusCodes.Status301MovedPermanently;
#if NET6_0_OR_GREATER
response.Headers.Location = location;
#else
response.Headers["Location"] = location;
#endif
}

private async Task RespondWithFile(HttpResponse response, string fileName)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Net;
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Swashbuckle.AspNetCore.ReDoc;
using Xunit;
using ReDocApp = ReDoc;

Expand Down Expand Up @@ -80,5 +83,71 @@ public async Task RedocMiddleware_CanBeConfiguredMultipleTimes(string htmlUrl, s
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("<link href='custom.css' rel='stylesheet' media='screen and (max-width: 700px)' type='text/css' />" + 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);
}
}
}
6 changes: 6 additions & 0 deletions test/WebSites/WebApi.Aot/Program.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.Configure<RouteOptions>(
options => options.SetParameterPolicy<RegexInlineRouteConstraint>("regex"));

builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
Expand Down Expand Up @@ -41,6 +45,8 @@

app.UseSwagger();
app.UseSwaggerUI();
app.UseReDoc();

app.MapSwagger();

app.Run();
Expand Down
1 change: 1 addition & 0 deletions test/WebSites/WebApi.Aot/WebApi.Aot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Swashbuckle.AspNetCore.ReDoc\Swashbuckle.AspNetCore.ReDoc.csproj" />
<ProjectReference Include="..\..\..\src\Swashbuckle.AspNetCore.SwaggerGen\Swashbuckle.AspNetCore.SwaggerGen.csproj" />
<ProjectReference Include="..\..\..\src\Swashbuckle.AspNetCore.SwaggerUI\Swashbuckle.AspNetCore.SwaggerUI.csproj" />
</ItemGroup>
Expand Down

0 comments on commit 5230198

Please sign in to comment.