Search code examples
c#asp.net-coreswaggerswashbuckle.aspnetcoreminimal-apis

How to automatically document TypedResults.Stream responses within NET 8 minimal API


I have the following route mapped in a Mininal API application:

app.MapGet("api/test", async Task<Results<PushStreamHttpResult, NotFound>> (bool fail) => {
    if (fail) return TypedResults.NotFound();
    return TypedResults.Stream(stream => WriteToStream(stream), "text/plain");
}).WithSummary("Test Streaming results");

async Task WriteToStream(Stream stream) {
    // do the writing... not needed for sample code
}

I didn't find a way to generate the proper documentation for this TypedResults type in swagger/OpenAPI UI and schemas. I can do this for other result types:

app.MapGet("api/thisworks", async Task<
    Results<
        Ok<ApiResult>,
        BadRequest<ApiResult>,
        NotFound<ApiResult>>> () => { ... }

Which produces proper documentation for the many return types like this for example:

Proper swagger return types documentation

Is there a way to automatically achieve the same with TypedResults.Stream / PushStreamHttpResult ?


Solution

  • From the swagger docs:

    ApiExplorer (the ASP.NET Core metadata component that Swashbuckle is built on) DOES NOT surface the FileResult type by default and so you need to explicitly tell it to with the Produces attribute:

    [HttpGet("{filename}")]
    [Produces("application/octet-stream", Type = typeof(FileResult))]
    public FileResult GetFile(string fileName)
    

    PushStreamHttpResult is file result too and it seems that the same rules are applied to it also so you can use the Produces call, for example:

    app.MapGet("api/test", async Task<Results<PushStreamHttpResult, NotFound>> (bool fail) =>
        .WithSummary("Test Streaming results")
        .Produces(200, contentType: "text/plain" )
    

    If all PushStreamHttpResult are plain texts then you can try to automate it with operation filter:

    // sample implementation, adjust for production
    public sealed class PushStreamHttpResultFilter : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            var returnType = context.MethodInfo.ReturnType;
    
            if (ContainsPushStream(returnType))
            {
                // adds plain text only if no 200 are defined
                operation.Responses.TryAdd("200", new OpenApiResponse
                {
                    Content = new Dictionary<string, OpenApiMediaType>
                    {
                        ["text/plain"] = new OpenApiMediaType()
                        {
                            Schema = new OpenApiSchema()
                        }
                    }
                });
            }
    
            bool ContainsPushStream(Type type)
            {
                if (type == typeof(PushStreamHttpResult))
                    return true;
                if (!type.IsConstructedGenericType)
                    return false;
                if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Task<>))
                    return ContainsPushStream(type.GetGenericArguments()[0]);
                return type.GetGenericArguments().Any(t => t == typeof(PushStreamHttpResult));
            }
        }
    }
    

    And add it to filters:

    builder.Services.AddSwaggerGen(o =>
    {
        o.OperationFilter<PushStreamHttpResultFilter>();
    });