2

How to perform a data annotation validation for the input as IAsyncEnumerable in ASP.NET Core 7 API controller POST action. I tried to use the following code, but validation didn't occur.

public record struct Car([Required][RegularExpression(@"^[0-9]{8}$")] string? Number);

public class MyController : ControllerBase
{
    [HttpPost("cars")]
    public async IAsyncEnumerable<Car> PostCarsAsync([FromBody] IAsyncEnumerable<Car> cars, CancellationToken ct)
    {
        await foreach (var car in cars)
        {
            yield return car;
        }
    }
}

I tried to pass the following request

[{},{"Number":""},{"Number":"87654321"}]

and got the following response without any validation errors:

[{"Number":null},{"Number":""},{"Number":"87654321"}]

So, the validation of input sequence didn't work.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Why `[FromBody] IAsyncEnumerable`? There's no infinite stream of cars in a POST request. – Panagiotis Kanavos Jan 31 '23 at 09:46
  • A `string?` can't be required. It explicitly says it's OK to be `null`. If you *don't* want null numbers, use `string Number`. I think you need to use `[property:RegularExpression]` and `[property:Required]` too, otherwise the attribute applies only to the constructor argument, not the property – Panagiotis Kanavos Jan 31 '23 at 09:50
  • @PanagiotisKanavos, I don't want to buffer long inputs. I'm going to process input items one by one without extra memory pressure – Anton Plotnikov Jan 31 '23 at 10:07
  • The input comes from the client with the HTTP POST body, you have no control over it. Besides, `IEnumerable` already accesses the input one item at a time. `IAsyncEnumerable` is used to allow asynchronous iteration, something that's *not* needed here – Panagiotis Kanavos Jan 31 '23 at 10:17

1 Answers1

0

First of all you need to fix your Car record by retargeting attributes to property (and possibly removing the Required one and changing the Number to be a non-nullable string):

public record struct Car([property: RegularExpression(@"^[0-9]{8}$")] string Number);
// or
public record struct Car([property: Required][property: RegularExpression(@"^[0-9]{8}$")] string? Number);

Secondary I would argue that this concrete case is conceptually impossible - you need to buffer the whole input (or output) to validate before starting to process it - imagine that you have invalid record at the end of the list - yield return car; should already have started writing to output correct records so what should happen here? And even if you are not returning async enumerable from your method I would say that non-buffering validation is still impossible in the standard ASP.NET Core pipeline (because binding and validation should finish before the action itself). So you need to switch to IEnumerable if you need build-in validation:

public async IAsyncEnumerable<Car> PostCarsAsync([FromBody] IEnumerable<Car> cars, CancellationToken ct)
{
    // ...
} 

Also this open issue hints that possibly IAsynEnumerable should have no effect on buffering (though in my tests with .NET 7 switching to IAsynEnumerable had some effect on memory patterns, at least at first glance).

halfer
  • 19,824
  • 17
  • 99
  • 186
Guru Stron
  • 102,774
  • 10
  • 95
  • 132