Jeroen
Jeroen

Reputation: 63719

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:

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.)

Upvotes: 12

Views: 14138

Answers (4)

Alex from Jitbit
Alex from Jitbit

Reputation: 60626

I love the solution proposed in the accepted answer, and even used it for a while, but now we have the [Consumes] attribute.

And you can even map the two to the same route URL which is great news.

[HttpPost]
[Route("/api/Post")] //same route but different "Consumes"
[Consumes("application/x-www-form-urlencoded")]
public ActionResult Post([FromForm] Data data)
{
    DoStuff();
}

[HttpPost]
[Route("/api/Post")] //same route but different "Consumes"
[Consumes("application/json")]
public ActionResult PostJson([FromBody] Data data)
{
    Post(data); //just call the other action method
}

https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-5.0#define-supported-request-content-types-with-the-consumes-attribute

Upvotes: 14

Shamork.Fu
Shamork.Fu

Reputation: 53

nuget package Toycloud.AspNetCore.Mvc.ModelBinding.BodyAndFormBinding should fits your needs

github

Upvotes: 1

Kirk Larkin
Kirk Larkin

Reputation: 93053

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.

Upvotes: 15

Chris Pratt
Chris Pratt

Reputation: 239270

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.

Upvotes: 7

Related Questions