Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use body stream parameter in WebApi controller's action

I currently read input stream from body like this:

public async Task<IActionResult> Post()
{
    byte[] array = new byte[Request.ContentLength.Value];

    using (MemoryStream memoryStream = new MemoryStream(array))
    {
        await Request.Body.CopyToAsync(memoryStream);
    }

    return Ok();
}

I would like to specify the input parameter in the methods signature, due to testing and swagger generation.

Is it possible to specify the input parameter as stream somehow?

public async Task<IActionResult> Post([FromBody]Stream body) ...
like image 940
Liero Avatar asked Mar 29 '17 07:03

Liero


People also ask

Can we use FromBody with Httpget?

Please note that we are able to send [FromBody] parameter in HTTP GET Request input.


2 Answers

You have to create custom attribute and custom parameter binding.

Here is my implementation of FromContent attribute and ContentParameterBinding binding:

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

    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
                                                HttpActionContext actionContext,
                                                CancellationToken cancellationToken)
    {

    }

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

Now you can

    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";
    }

However it doesn't help with swagger :(

like image 89
v-andrew Avatar answered Oct 23 '22 04:10

v-andrew


You have to create your model binder for streams:

public class StreamBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        bindingContext.Result = ModelBindingResult.Success(bindingContext.HttpContext.Request.Body);
        return Task.CompletedTask;
    }
}

Then you should create model binder provider for it:

public class StreamBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Metadata.ModelType == typeof(Stream))
        {
            return new BinderTypeModelBinder(typeof(StreamBinder));
        }

        return null;
    }
}

And register it:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.ModelBinderProviders.Insert(0, new StreamBinderProvider());
    });
}

Usage:

public async Task<IActionResult> Post([FromBody]Stream body) ...
like image 44
user2283070 Avatar answered Oct 23 '22 04:10

user2283070