No1Lives4Ever
No1Lives4Ever

Reputation: 6893

Consume any type of content-type for same endpoint

I have a asp.net core (v2.1) webapi project which expose this function:

[HttpPost]
[Route("v1/do-something")]
public async Task<IActionResult> PostDoSomething(ModelData model)
{
    //...
}

and this model:

public class ModelData
{
    [Required]
    public string Email { get; set; }
}

I want to make this endpoint flexible, in content type perspective. Therefore, it should be OK to send this endpoint diffrent content type in body.

For example, those "BODY" argument will be allowed:

// application/x-www-form-urlencoded
email="[email protected]"

// application/json
{
    "email": "[email protected]"
}

In contrast to the old .net framework, in dotnet core this is not allowed out of the box. I found that I need to add Consume attribute with [FormForm] attribute. But, If I add the [FormForm] attribute the the model argument, it wont work anymore on JSON (for example) - because then it should be [FromBody].

I though that it will be ok to use code like this:

[HttpPost]
[Route("v1/do-something")]
public async Task<IActionResult> PostDoSomething([FromBody] [FromForm] ModelData model)
{
    //...
}

But as you can expect, this code not working.

So, in order to achieve this flexibility I have to duplicate all my endpoint - which sound like a very very bad idea.

[HttpPost]
[Route("v1/do-something")]
[Consume ("application/json")]
public async Task<IActionResult> PostDoSomething([FromBody] ModelData model)
{
    //...
}

[HttpPost]
[Route("v1/do-something")]
[Consume ("application/x-www-form-urlencoded")]
public async Task<IActionResult> PostDoSomething([FromForm] ModelData model)
{
    //...
}

// ... Other content types here ...

It's sound an easy task. But seems like more complicated.

I missed something? How to make an endpoint work in any content type?

Upvotes: 5

Views: 2452

Answers (1)

Shaun Luttin
Shaun Luttin

Reputation: 141662

Here is a custom model binder that binds based on the content type.

public class BodyOrForm : IModelBinder
{
    private readonly IModelBinderFactory factory;

    public BodyOrForm(IModelBinderFactory factory) => this.factory = factory;

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var contentType = 
            bindingContext.ActionContext.HttpContext.Request.ContentType;

        BindingInfo bindingInfo = new BindingInfo();
        if (contentType == "application/json")
        {
            bindingInfo.BindingSource = BindingSource.Body;
        }
        else if (contentType == "application/x-www-form-urlencoded")
        {
            bindingInfo.BindingSource = BindingSource.Form;
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
        }

        var binder = factory.CreateBinder(new ModelBinderFactoryContext
        {
            Metadata = bindingContext.ModelMetadata,
            BindingInfo = bindingInfo,
        });

        await binder.BindModelAsync(bindingContext);
    }
}

Tested with the following action.

[HttpPost]
[Route("api/body-or-form")]
public IActionResult PostDoSomething([ModelBinder(typeof(BodyOrForm))] ModelData model)
{
    return new OkObjectResult(model);
}

Here is a demo is on GitHub.

Upvotes: 6

Related Questions