Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
1 task done
emiliovmq opened this issue Mar 5, 2024 · 6 comments
Closed
1 task done
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates External This is an issue in a component not contained in this repository. It is open for tracking purposes. feature-openapi

Comments

@emiliovmq
Copy link

Is there an existing issue for this?

  • 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) },
            };
        }
    }
@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Mar 5, 2024
@captainsafia captainsafia added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi and removed needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically labels Mar 5, 2024
@captainsafia
Copy link
Member

@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 captainsafia added the External This is an issue in a component not contained in this repository. It is open for tracking purposes. label Mar 6, 2024
@emiliovmq
Copy link
Author

emiliovmq commented Mar 6, 2024

@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.

@martincostello
Copy link
Member

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

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

@emiliovmq
Copy link
Author

emiliovmq commented Mar 7, 2024

@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
Copy link
Author

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

@captainsafia
Copy link
Member

Closing as this is external and tracked in the Swashbuckle repo via domaindrivendev/Swashbuckle.AspNetCore#2550.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates External This is an issue in a component not contained in this repository. It is open for tracking purposes. feature-openapi
Projects
None yet
Development

No branches or pull requests

3 participants