I've just run into what I'd consider a slightly odd bit of behaviour when writing a C# ASP.NET Core WebAPI controller.
Consider the following controller method, which accepts a JSON body and produces an XML output (for reasons I won't go into):
[Produces("application/xml")]
[Route("api/route/to/data"]
[HttpPost]
public IActionResult GetData([FromBody]IEnumerable<InputData> inputs)
{
return HandleResponse(() => _repository.GetData(inputs));
}
And the following repository method:
public IEnumerable<OutputData> GetData(IEnumerable<InputData> inputs)
{
foreach(var input in inputs)
{
string field1 = input.Field1;
int field2 = input.Field2;
const string sql = @"
SELECT foo.bar, foo.baz
FROM my_table foo
WHERE foo.wubble = :field1
AND foo.flob = :field2";
yield return _repoBase.GetSingleValue<OutputData>(sql, new {field1, field2});
}
}
(The _repoBase.GetSingleValue method is simply a dumb generic wrapper around Dapper's QueryFirstOrDefault method, and doesn't do anything fancy or complicated. Also I'm aware that using a loop in this way is not optimal, but it will do for the purpose of this example.)
If put a breakpoint in the controller, and then call this endpoint from Postman providing a simple JSON data in the body such as:
[{"field1":"wibble", "field2":123456}]
Then the breakpoint is hit, and the repo pulls back data as expected, and execution appears to fall through - however Postman reports that an HTTP 406 Not Acceptable status code was returned as a response, and no data is actually included.
However, if I force the IEnumerable to enumerate into a concrete collection before returning it:
[Produces("application/xml")]
[Route("api/route/to/data"]
[HttpPost]
public IActionResult GetData([FromBody]IEnumerable<InputData> inputs)
{
return HandleResponse(() => _repository.GetData(inputs).ToList());
}
Then I get a 200 OK back along with the data I expected.
I am of course aware of the concept of deferred execution / lazy evaluation when using IEnumerables and particularly yield return
, however I would expect the framework to be intelligent enough to iterate any collections it is provided before spitting them out into the world. Is this actually expected behaviour, or a bug?
At the very least the response code it returns in this case is not very intuitive IMO - it had me checking my middleware setup thinking that I'd forgotten to add an output formatter or something.
Edit 1: Yes, I am wrapping the output into an ObjectResult.
Edit 2: The ObjectResult wrapping is actually done in this method - nothing special:
protected IActionResult HandleResponse<T>(Func<T> resultProvider)
{
try
{
return new ObjectResult(resultProvider());
}
catch (OracleException oex)
{
// DB error handling - omitted for brevity.
// Nothing is done to the collection here.
int errorCode = 500 // This is actually mapped to whatever Oracle error code is returned, e.g. incorrect Oracle password = 403.
return new StatusCodeResult(errorCode);
}
catch (Exception e)
{
_logger.LogError("Unhandled Exception", e);
return new StatusCodeResult(500);
}
}
Belatedly marking this as answered, with thanks to commenters above.
It turns out that this behaviour is specific to XML output formatters; they simply do not seem to support serialization of yield-backed IEnumerable<T>
objects, at least in .NET Core.
I have tried using both XmlSerializerOutputFormatter
and XmlDataContractSerializerOutputFormatter
as output formatters, with the same results.
Substituting either XML output formatter for the standard JSON formatter resolves the problem.
In my case, XML output is required, so as a workaround I am explicitly converting the IEnumerable<T>
to a List<T>
as per my original post. The XML formatters are then able to correctly serialize the collection.