Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to combine FromBody and FromForm BindingSource in ASP.NET Core?

I've created a fresh ASP.NET Core 2.1 API project, with a Data dto class and this controller action:

[HttpPost]
public ActionResult<Data> Post([FromForm][FromBody] Data data)
{
    return new ActionResult<Data>(data);
}
public class Data
{
    public string Id { get; set; }
    public string Txt { get; set; }
}

It should echo the data back to the user, nothing fancy. However, only one of the two attributes works, depending on the order.

Here's the test requests:

curl -X POST http://localhost:5000/api/values \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'id=qwer567&txt=text%20from%20x-www-form-urlencoded'

and

curl -X POST http://localhost:5000/api/values \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "abc123",
    "txt": "text from application/json"
}'

I've tried several approaches, all to no avail:

  • Creating a custom child BindingSource, but that's only metadata it seems.
  • Using an attribute [CompositeBindingSource(...)], but the constructor is private and this might not be intended usage
  • Creating an IModelBinder and provider for this, but (1) I might only want this on specific controller actions and (2) it's really a lot of work seemingly to get the two inner model binders (for Body and FormCollection)

So, what is the correct way to combine FromForm and FromBody (or I guess any other combination of sources) attributes into one?

To clarify the reason behind this, and to explain why my question is not a duplicate of this question: I want to know how to have the same URI / Route to support both different types of sending data. (Even though perhaps to some folks' taste, including possibly my own, different routes/uris might be more appropriate.)

like image 232
Jeroen Avatar asked Aug 03 '18 13:08

Jeroen


2 Answers

You might be able to achieve what you're looking for with a custom IActionConstraint:

Conceptually, IActionConstraint is a form of overloading, but instead of overloading methods with the same name, it's overloading between actions that match the same URL.

I've had a bit of a play with this and have come up with the following IActionConstraint implementation:

public class FormContentTypeAttribute : Attribute, IActionConstraint
{
    public int Order => 0;

    public bool Accept(ActionConstraintContext ctx) =>
        ctx.RouteContext.HttpContext.Request.HasFormContentType;
}

As you can see, it's very simple - it's just checking whether or not the incoming HTTP request is of a form content-type. In order to use this, you can attribute the relevant action. Here's a complete example that also includes the idea suggested in this answer, but using your action:

[HttpPost]
[FormContentType]
public ActionResult<Data> PostFromForm([FromForm] Data data) =>
    DoPost(data);

[HttpPost]
public ActionResult<Data> PostFromBody([FromBody] Data data) =>
    DoPost(data);

private ActionResult<Data> DoPost(Data data) =>
    new ActionResult<Data>(data);

[FromBody] is optional above, due to the use of [ApiController], but I've included it to be explicit in the example.

Also from the docs:

...an action with an IActionConstraint is always considered better than an action without.

This means that when the incoming request is not of a form content-type, the FormContentType attribute I've shown will exclude that particular action and therefore use the PostFromBody. Otherwise, if it is of a form content-type, the PostFromForm action will win due to it being "considered better".

I've tested this at a fairly basic level and it does appear to do what you're looking for. There may be cases where it doesn't quite fit so I'd encourage you to have a play with it and see where you can go with it. I fully expect that you may find a case where it falls over completely, but it's an interesting idea to explore nonetheless.

Finally, if you don't like having to use an attribute, it is possible to configure a convention that could e.g. use reflection to find actions with a [FromForm] attribute and automatically add the constraint. There are more details in this excellent post on the topic.

like image 194
Kirk Larkin Avatar answered Nov 15 '22 06:11

Kirk Larkin


You cannot. An action can only accept one or the other. To get around this, you can simply create multiple actions, one with [FromBody] and one without. They'll of course need separate routes as well, since the presence of an attribute is not enough to distinguish overloads. However, you can factor out the body of your action into a private method that both actions can utilize, to at least keep things DRY.

like image 44
Chris Pratt Avatar answered Nov 15 '22 06:11

Chris Pratt