2

I'm trying to write my own Input Formatter which will read the request body, split it by line and pass it into a string array parameter in a controller action.


This works (passing the entire body as a string):

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvcCore(options =>
    {
        options.InputFormatters.Add(new MyInputFormatter());
    }
}


MyInputFormatter.cs

public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
    using (StreamReader reader = new StreamReader(context.HttpContext.Request.Body))
    {
        return InputFormatterResult.Success(await reader.ReadToEndAsync());
    }
}

MyController.cs

[HttpPost("/foo", Name = "Foo")]
public IActionResult Bar([FromBody] string foo)
{
    return Ok(foo);
}

This doesn't work (parameter foo is null):

MyInputFormatter.cs

public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
    List<string> input = new List<string>();

    using (StreamReader reader = new StreamReader(context.HttpContext.Request.Body))
    {
        while (!reader.EndOfStream)
        {
            string line = (await reader.ReadLineAsync()).Trim();
            input.Add(line);
        }
    }

    return InputFormatterResult.Success(input.ToArray());
}

MyController.cs

[HttpPost("/foo", Name = "Foo")]
public IActionResult Bar([FromBody] string[] foo)
{
    return Ok(string.Join(" ", foo));
}

The difference is that in the controller I'm now accepting an array of strings instead of a string and in the formatter I'm reading the input line by line and in the end returning it as an array.


What am I missing? :/


EDIT: How my formatter actually looks, more or less (if it makes any difference):

    public class MyInputFormatter : InputFormatter
    {
        public MyInputFormatter()
        {
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue(MimeType.URI_LIST)); // "text/uri-list"
        }

        public override bool CanRead(InputFormatterContext context)
        {
            if (context == null) throw new ArgumentNullException(nameof(context)); // breakpoint here not reached

            if (context.HttpContext.Request.ContentType == MimeType.URI_LIST)
                return true;

            return false;
        }

        protected override bool CanReadType(Type dataType)
        {
            return typeof(string[]).IsAssignableFrom(dataType); // breakpoint here not reached
        }

        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            List<string> input = new List<string>(); // breakpoint here not reached

            using (StreamReader reader = new StreamReader(context.HttpContext.Request.Body))
            {
                while (!reader.EndOfStream)
                {
                    string line = (await reader.ReadLineAsync()).Trim();

                    if (string.IsNullOrEmpty(line))
                    {
                        continue;
                    }

                    if (!line.StartsWith("foo-"))
                    {
                        return InputFormatterResult.Failure();
                    }

                    input.Add(line.Substring("foo-".Length));
                }
            }

            return InputFormatterResult.Success(input.ToArray());
        }
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
morgoth84
  • 1,070
  • 2
  • 11
  • 25

2 Answers2

0

I've created a test input formatter with your code in the request handler method and it works fine, this is how it looks like:

public class TestInputFormatter : IInputFormatter
{
    public bool CanRead(InputFormatterContext context) => true;

    public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
    {
        List<string> input = new List<string>();

        using (StreamReader reader = new StreamReader(context.HttpContext.Request.Body))
        {
            while (!reader.EndOfStream)
            {
                string line = (await reader.ReadLineAsync()).Trim();
                input.Add(line);
            }
        }

        return InputFormatterResult.Success(input.ToArray());
    }
}

I see only one point which could be wrong in your code - the registration of your input formatter. Documentation says: Formatters are evaluated in the order you insert them. The first one takes precedence. Try to register it like that:

options.InputFormatters.Insert(0, new TestInputFormatter());

It works in my test project with exactly such registration. Because when you call options.InputFormatters.Add it will be added to the end of the input formatters collection and probably your request will be handled by some other input formatter which is located first in that collection.

Vitalii Ilchenko
  • 578
  • 1
  • 3
  • 7
  • It's not the formatter order. :/ I've been debugging it and it's like the formatter isn't used at all. No formatters are being used. It doesn't even run any CanRead or CanReadType methods, it just goes straight to the controller action with a null value. I've tested it with a debugger and breakpoints. What else could cause that kind of behavior? What determines if a formatter will be tried out at all or not? I've added a content type in SupportedMediaTypes in my formatter and I've used the exact same value in the Content-Type header in the request, but still it doesn't trigger. – morgoth84 Aug 06 '19 at 06:54
  • I've added at the bottom how my formatter really looks. I don't think it makes much difference, but just in case. – morgoth84 Aug 06 '19 at 07:16
  • 1
    The problem is not in your `InputFilter` implementation or it's registration in the filters collection (because it has it's own Content-Type). I've tested your `MyInputFormatter` and it works fine. You've missed something else and it's hard to say what without a whole project sources. – Vitalii Ilchenko Aug 06 '19 at 09:17
  • Yeah, I see what you mean. I've created a fresh mvc project and added just the controller action and the formatter inside and it works. :/ I'll try poking around to see what's the problem... – morgoth84 Aug 06 '19 at 09:56
0

I figured out what was the problem in the end. I had a custom ModelBinder which was interfering, capturing anything that isn't a string and an implementation of a custom interface (which is used for other post data). That's why it worked for strings and other input payloads (implementations of the interface), but not a string array. That binder was supposed to be used for query parameters (to be able to process custom types), but ended up triggering for this POST payload as well.

morgoth84
  • 1,070
  • 2
  • 11
  • 25