v-andrew
v-andrew

Reputation: 24181

Swagger does not recognize WebAPI controller parameter with custom attribute [FromContent]

I want to have a custom attribute to parse data as stream and be testable with Swagger.

So I created controller which reads from POST body:

[SwaggerOperation("Create")]
[SwaggerResponse(HttpStatusCode.Created)]
public async Task<string> Post([FromContent]Stream contentStream)
{
    using (StreamReader reader = new StreamReader(contentStream, Encoding.UTF8))
    {
        var str = reader.ReadToEnd();
        Console.WriteLine(str);
    }
    return "OK";
}

How to define stream so it is visible in Swagger UI?

Here is my implementation of FromContent attribute and ContentParameterBinding binding:

public class ContentParameterBinding : HttpParameterBinding
{
    private struct AsyncVoid{}
    public ContentParameterBinding(HttpParameterDescriptor descriptor) : base(descriptor)
    {

    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
                                                HttpActionContext actionContext,
                                                CancellationToken cancellationToken)
    {
        var binding = actionContext.ActionDescriptor.ActionBinding;

        if (binding.ParameterBindings.Length > 1 ||
            actionContext.Request.Method == HttpMethod.Get)
        {
            var taskSource = new TaskCompletionSource<AsyncVoid>();
            taskSource.SetResult(default(AsyncVoid));
            return taskSource.Task as Task;
        }

        var type = binding.ParameterBindings[0].Descriptor.ParameterType;

        if (type == typeof(HttpContent))
        {
            SetValue(actionContext, actionContext.Request.Content);
            var tcs = new TaskCompletionSource<object>();
            tcs.SetResult(actionContext.Request.Content);
            return tcs.Task;
        }
        if (type == typeof(Stream))
        {
            return actionContext.Request.Content
            .ReadAsStreamAsync()
            .ContinueWith((task) =>
            {
                SetValue(actionContext, task.Result);
            });
        }

        throw new InvalidOperationException("Only HttpContent and Stream are supported for [FromContent] parameters");
    }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class FromContentAttribute : ParameterBindingAttribute
{
    public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
    {
        if (parameter == null)
            throw new ArgumentException("Invalid parameter");

        return new ContentParameterBinding(parameter);
    }
}

Update

When I create Stream using [FromBody] is shows correctly in Swagger, however Stream is not initiated and ==null

[SwaggerOperation("Create")]
[SwaggerResponse(HttpStatusCode.Created)]
public async Task<string> Post([FromBody]Stream contentStream)
{
    using (StreamReader reader = new StreamReader(contentStream, Encoding.UTF8))
    {
        var str = reader.ReadToEnd();
        Console.WriteLine(str);
    }
    return "OK";
}

Good Post

So I want to have the same UI but with my custom attribute which let's me have Stream from content.

With my custom attribute it shows without TextArea for the parameter but could be tested using Postman and work correctly and Stream is available Bad  Post

Upvotes: 3

Views: 1532

Answers (2)

Olexander Ivanitskyi
Olexander Ivanitskyi

Reputation: 2240

Try implementing the interface IValueProviderParameterBinding:

public class ContentParameterBinding
    : HttpParameterBinding, IValueProviderParameterBinding
{
    public IEnumerable<ValueProviderFactory> ValueProviderFactories
    {
        get
        {
            return this.Descriptor.Configuration.Services.GetValueProviderFactories();
        }
    }
}

In my case it helped. Also it's generally cleaner as it doesn't inherit FormatterParameterBinding logic, which may not be required.

Upvotes: 1

Andriy Tolstoy
Andriy Tolstoy

Reputation: 6090

Inherit your binding from FormatterParameterBinding class:

public class ContentParameterBinding : FormatterParameterBinding
{
    public ContentParameterBinding(HttpParameterDescriptor descriptor)
            : base(descriptor, 
                   descriptor.Configuration.Formatters, 
                   descriptor.Configuration.Services.GetBodyModelValidator())
    {
    }

    //your code
}

Upvotes: 3

Related Questions