aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

Swagger (Get Index.html) fails when reflection-based serialization has been disabled.

Open emiliovmq opened this issue 1 year ago • 5 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Describe the bug

I'm using .NET 8.0 (8.0.2) and have set up a new simple API (the todo items API). By default, in .NET 8.0, the reflection-based serialization has been disabled for System.Text.Json. Whenever I try to get "swagger/index.html" file from my browser, an exception is thrown.

Expected Behavior

The "swagger/index.html" file is shown as expected.

Steps To Reproduce

builder.Services.AddControllers()
    .AddMvcOptions(options =>
    {
        options.OutputFormatters.RemoveType<StringOutputFormatter>();
        options.OutputFormatters.RemoveType<StreamOutputFormatter>();
        options.OutputFormatters.RemoveType<TextOutputFormatter>();

        options.ReturnHttpNotAcceptable = true;
    }).AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.TypeInfoResolver = SerializationContext.Default;
    });

builder.Services.AddDbContext<TodoContext>(opt =>
    opt.UseInMemoryDatabase("TodoList"));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

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

app.MapControllers();

app.Run();

This is the definition of the SerializationContext for STJ.

/// <summary>
/// Defines the serialization context.
/// </summary>
[JsonSourceGenerationOptions(
    JsonSerializerDefaults.Web,
    UseStringEnumConverter = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(TodoItem))]
[JsonSerializable(typeof(IEnumerable<TodoItem>))]
[JsonSerializable(typeof(ProblemDetails))]
internal partial class SerializationContext : JsonSerializerContext
{
}

Exceptions (if any)

An unhandled exception has occurred while executing the request. System.InvalidOperationException: Reflection-based serialization has been disabled for this application. Either use the source generator APIs or explicitly configure the 'JsonSerializerOptions.TypeInfoResolver' property. at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_JsonSerializerIsReflectionDisabled() at System.Text.Json.JsonSerializerOptions.ConfigureForJsonSerializer() at System.Text.Json.JsonSerializer.GetTypeInfo(JsonSerializerOptions options, Type inputType) at System.Text.Json.JsonSerializer.GetTypeInfo[T](JsonSerializerOptions options) at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options) at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.GetIndexArguments() at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.RespondWithIndexHtml(HttpResponse response) at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext) at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) 12:18:53:684 [Error] Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware [19]: An unhandled exception has occurred while executing the request.

.NET Version

8.0.200

Anything else?

This is the class where the exception is thrown. Please, notice, this class is serializing the classes "ConfigObject", "OAuthConfigObject", and "Interceptors" using the serialization options built in the constructor.

...
using System.Text.Json;
using System.Text.Json.Serialization;
...

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

        private readonly SwaggerUIOptions _options;
        private readonly StaticFileMiddleware _staticFileMiddleware;
        private readonly JsonSerializerOptions _jsonSerializerOptions;

        public SwaggerUIMiddleware(
            RequestDelegate next,
            IWebHostEnvironment hostingEnv,
            ILoggerFactory loggerFactory,
            SwaggerUIOptions options)
        {
            _options = options ?? new SwaggerUIOptions();

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

            _jsonSerializerOptions = new JsonSerializerOptions();
#if NET6_0
            _jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
#else
            _jsonSerializerOptions.IgnoreNullValues = true;
#endif
            _jsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
            _jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false));
        }
...
        private async Task RespondWithIndexHtml(HttpResponse response)
        {
            response.StatusCode = 200;
            response.ContentType = "text/html;charset=utf-8";

            using (var stream = _options.IndexStream())
            {
                using var reader = new StreamReader(stream);

                // Inject arguments before writing to response
                var htmlBuilder = new StringBuilder(await reader.ReadToEndAsync());
                foreach (var entry in GetIndexArguments())
                {
                    htmlBuilder.Replace(entry.Key, entry.Value);
                }

                await response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8);
            }
        }


        // This is the method that fails. STJ 
        private IDictionary<string, string> GetIndexArguments()
        {
            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) },
            };
        }
    }

emiliovmq avatar Mar 05 '24 18:03 emiliovmq

@emiliovmq I believe the issue here is the JsonSerializerOptions that is instantiated by the SwaggerUiMiddleware. I'd recommend filing this issue on Swashbuckle. In general, I don't believe the package is set up for native AoT support.

captainsafia avatar Mar 06 '24 19:03 captainsafia

@captainsafia, thanks for your reply.

Sorry if I don't fully understand your answer, but I have to say that I created the API as a "non webapiaot", in other words, I don't have the <PublishAot>true</PublishAot> setting defined in my project file.

I think this is more a problem related to the fact that as of NET 8.0, the reflection-based serialization has been disabled for System.Text.Json (STJ), that is, all the de/serialization process occurs in the code generated by the SerializationContext (see example above). This SerializationContext is assigned to the ASP.NET pipeline through options.JsonSerializerOptions.TypeInfoResolver = SerializationContext.Default;. This works perfectly for my models/classes, but it fails with any other model/class that is not registered with the SerializationContext, (for which no code is generated).

I am asking for a solution where I can enable reflection-based serialization for those models/classes without needing to make a change request to the Swashbuckle project, and of course, keep using the generated source code for my models/classes.

emiliovmq avatar Mar 06 '24 21:03 emiliovmq

You should be able to achieve that already using the JsonSerializerOptions.TypeInfoResolverChain property:

- options.JsonSerializerOptions.TypeInfoResolver = SerializationContext.Default;
+ options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);

martincostello avatar Mar 07 '24 14:03 martincostello

@martincostello thanks for your comment...unfortunately the change doesn't work.

However, if I enable the reflection-based serialization for STJ (<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault> I'm able to see now the index.html page.

Now the question is...are my classes registered with the SerializationContext still being de/serialized using the generated code? or instead, are they being de/serialized now using reflection? I haven't found a way to debug into the generated source code to verify it's being used...

emiliovmq avatar Mar 07 '24 16:03 emiliovmq

Hi, anything on this...any suggestion???

emiliovmq avatar Mar 16 '24 22:03 emiliovmq

Closing as this is external and tracked in the Swashbuckle repo via https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2550.

captainsafia avatar Apr 22 '24 15:04 captainsafia