Skip to content

Commit

Permalink
Support native AoT with SwaggerUI (#2800)
Browse files Browse the repository at this point in the history
Add support for using SwaggerUI with native AoT.

SwaggerGen still does not support native AoT, though it happens to work for simple cases.

Resolves #2550.
  • Loading branch information
martincostello committed May 12, 2024
1 parent 0aac77e commit 61d890c
Show file tree
Hide file tree
Showing 16 changed files with 328 additions and 36 deletions.
15 changes: 15 additions & 0 deletions Swashbuckle.AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomDocumentSerializer",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "test\WebSites\WebApi\WebApi.csproj", "{DE1D77F8-3916-4DEE-A57D-6DDC357F64C6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.Aot", "test\WebSites\WebApi.Aot\WebApi.Aot.csproj", "{07BB09CF-6C6F-4D00-A459-93586345C921}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -271,6 +273,18 @@ Global
{DE1D77F8-3916-4DEE-A57D-6DDC357F64C6}.Release|x64.Build.0 = Release|Any CPU
{DE1D77F8-3916-4DEE-A57D-6DDC357F64C6}.Release|x86.ActiveCfg = Release|Any CPU
{DE1D77F8-3916-4DEE-A57D-6DDC357F64C6}.Release|x86.Build.0 = Release|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Debug|x64.ActiveCfg = Debug|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Debug|x64.Build.0 = Debug|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Debug|x86.ActiveCfg = Debug|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Debug|x86.Build.0 = Debug|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Release|Any CPU.Build.0 = Release|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Release|x64.ActiveCfg = Release|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Release|x64.Build.0 = Release|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Release|x86.ActiveCfg = Release|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -314,6 +328,7 @@ Global
{A0EC16BE-C520-4FCF-BB54-2D79CD255F00} = {3BA087DA-788C-43D6-9D8B-1EF017014A4A}
{B6037A37-4A4F-438D-B18A-0C9D1408EAB2} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB}
{DE1D77F8-3916-4DEE-A57D-6DDC357F64C6} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB}
{07BB09CF-6C6F-4D00-A459-93586345C921} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {36FC6A67-247D-4149-8EDD-79FFD1A75F51}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
<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>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="Microsoft.AspNetCore.Routing" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public DataContract GetDataContractForType(Type type)
{
return DataContract.ForDynamic(
underlyingType: type,
jsonConverter: JsonConverterFunc);
jsonConverter: (value) => JsonConverterFunc(value, type));
}

if (PrimitiveTypesAndFormats.TryGetValue(type, out var primitiveTypeAndFormat))
Expand All @@ -33,16 +33,16 @@ public DataContract GetDataContractForType(Type type)
underlyingType: type,
dataType: primitiveTypeAndFormat.Item1,
dataFormat: primitiveTypeAndFormat.Item2,
jsonConverter: JsonConverterFunc);
jsonConverter: (value) => JsonConverterFunc(value, type));
}

if (type.IsEnum)
{
var enumValues = type.GetEnumValues();

//Test to determine if the serializer will treat as string
// Test to determine if the serializer will treat as string
var serializeAsString = (enumValues.Length > 0)
&& JsonConverterFunc(enumValues.GetValue(0)).StartsWith("\"");
&& JsonConverterFunc(enumValues.GetValue(0), type).StartsWith("\"");

primitiveTypeAndFormat = serializeAsString
? PrimitiveTypesAndFormats[typeof(string)]
Expand All @@ -52,7 +52,7 @@ public DataContract GetDataContractForType(Type type)
underlyingType: type,
dataType: primitiveTypeAndFormat.Item1,
dataFormat: primitiveTypeAndFormat.Item2,
jsonConverter: JsonConverterFunc);
jsonConverter: (value) => JsonConverterFunc(value, type));
}

if (IsSupportedDictionary(type, out Type keyType, out Type valueType))
Expand All @@ -64,7 +64,7 @@ public DataContract GetDataContractForType(Type type)
// This is a special case where we know the possible key values
var enumValuesAsJson = keyType.GetEnumValues()
.Cast<object>()
.Select(value => JsonConverterFunc(value));
.Select(value => JsonConverterFunc(value, keyType));

keys = enumValuesAsJson.Any(json => json.StartsWith("\""))
? enumValuesAsJson.Select(json => json.Replace("\"", string.Empty))
Expand All @@ -75,27 +75,27 @@ public DataContract GetDataContractForType(Type type)
underlyingType: type,
valueType: valueType,
keys: keys,
jsonConverter: JsonConverterFunc);
jsonConverter: (value) => JsonConverterFunc(value, type));
}

if (IsSupportedCollection(type, out Type itemType))
{
return DataContract.ForArray(
underlyingType: type,
itemType: itemType,
jsonConverter: JsonConverterFunc);
jsonConverter: (value) => JsonConverterFunc(value, type));
}

return DataContract.ForObject(
underlyingType: type,
properties: GetDataPropertiesFor(type, out Type extensionDataType),
extensionDataType: extensionDataType,
jsonConverter: JsonConverterFunc);
jsonConverter: (value) => JsonConverterFunc(value, type));
}

private string JsonConverterFunc(object value)
private string JsonConverterFunc(object value, Type type)
{
return JsonSerializer.Serialize(value, _serializerOptions);
return JsonSerializer.Serialize(value, type, _serializerOptions);
}

public bool IsSupportedDictionary(Type type, out Type keyType, out Type valueType)
Expand Down Expand Up @@ -235,7 +235,7 @@ private IEnumerable<DataProperty> GetDataPropertiesFor(Type objectType, out Type
return dataProperties;
}

private static readonly Dictionary<Type, Tuple<DataType, string>> PrimitiveTypesAndFormats = new Dictionary<Type, Tuple<DataType, string>>
private static readonly Dictionary<Type, Tuple<DataType, string>> PrimitiveTypesAndFormats = new()
{
[ typeof(bool) ] = Tuple.Create(DataType.Boolean, (string)null),
[ typeof(byte) ] = Tuple.Create(DataType.Integer, "int32"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#if NET6_0_OR_GREATER
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Swashbuckle.AspNetCore.SwaggerUI;

internal sealed class JavascriptStringEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum>() :
#if NET8_0_OR_GREATER
JsonStringEnumConverter<TEnum>(JsonNamingPolicy.CamelCase, false)
#else
JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false)
#endif
where TEnum : struct, Enum
{
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#if NET6_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Swashbuckle.AspNetCore.SwaggerUI;

internal sealed class JavascriptStringEnumEnumerableConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum>() :
JsonConverterFactory
where TEnum : struct, Enum
{
private readonly JavascriptStringEnumConverter<TEnum> _enumConverter = new();

public override bool CanConvert(Type typeToConvert)
=> typeToConvert.IsAssignableFrom(typeof(IEnumerable<TEnum>));

public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if (!typeToConvert.IsAssignableFrom(typeof(IEnumerable<TEnum>)))
{
throw new NotSupportedException($"The type {typeToConvert} is not supported.");
}

var valueConverter = (JsonConverter<TEnum>)_enumConverter.CreateConverter(typeof(TEnum), options);
return new EnumEnumerableConverter(valueConverter);
}

private sealed class EnumEnumerableConverter(JsonConverter<TEnum> inner) : JsonConverter<IEnumerable<TEnum>>
{
public override IEnumerable<TEnum> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException("Expected start of a JSON array.");
}

var result = new List<TEnum>();

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
return result;
}

result.Add(inner.Read(ref reader, typeof(TEnum), options));
}

throw new JsonException("JSON array is malformed.");
}

public override void Write(Utf8JsonWriter writer, IEnumerable<TEnum> value, JsonSerializerOptions options)
{
writer.WriteStartArray();

foreach (var item in value)
{
inner.Write(writer, item, options);
}

writer.WriteEndArray();
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#if NET6_0_OR_GREATER
using System.Text.Json.Serialization;

namespace Swashbuckle.AspNetCore.SwaggerUI;

[JsonSerializable(typeof(ConfigObject))]
[JsonSerializable(typeof(InterceptorFunctions))]
[JsonSerializable(typeof(OAuthConfigObject))]
[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
internal sealed partial class SwaggerUIOptionsJsonContext : JsonSerializerContext;
#endif
83 changes: 61 additions & 22 deletions src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Text;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.FileProviders;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.AspNetCore.Http.Extensions;
using System.Linq;

#if NETSTANDARD2_0
using IWebHostEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment;
#endif

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

Expand All @@ -41,23 +40,36 @@ public class SwaggerUIMiddleware

_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)
{
var httpMethod = httpContext.Request.Method;
var path = httpContext.Request.Path.Value;

var isGet = HttpMethods.IsGet(httpMethod);

// If the RoutePrefix is requested (with or without trailing slash), redirect to index URL
if (httpMethod == "GET" && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase))
if (isGet && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase))
{
// Use relative redirect to support proxy environments
var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/")
Expand All @@ -68,7 +80,7 @@ public async Task Invoke(HttpContext httpContext)
return;
}

if (httpMethod == "GET" && Regex.IsMatch(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?index.html$", RegexOptions.IgnoreCase))
if (isGet && Regex.IsMatch(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?index.html$", RegexOptions.IgnoreCase))
{
await RespondWithIndexHtml(httpContext.Response);
return;
Expand Down Expand Up @@ -118,15 +130,42 @@ private async Task RespondWithIndexHtml(HttpResponse response)
}
}

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;
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);
}
#endif

configObject ??= JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions);
oauthConfigObject ??= JsonSerializer.Serialize(_options.OAuthConfigObject, _jsonSerializerOptions);
interceptors ??= JsonSerializer.Serialize(_options.Interceptors, _jsonSerializerOptions);

return new Dictionary<string, string>()
{
{ "%(DocumentTitle)", _options.DocumentTitle },
{ "%(HeadContent)", _options.HeadContent },
{ "%(ConfigObject)", JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions) },
{ "%(OAuthConfigObject)", JsonSerializer.Serialize(_options.OAuthConfigObject, _jsonSerializerOptions) },
{ "%(Interceptors)", JsonSerializer.Serialize(_options.Interceptors) },
{ "%(ConfigObject)", configObject },
{ "%(OAuthConfigObject)", oauthConfigObject },
{ "%(Interceptors)", interceptors },
};
}
}
Expand Down

0 comments on commit 61d890c

Please sign in to comment.