Multiple types [FromBody] on same method .net core

2020-06-17 07:02发布

I have a controller with one POST method, which will receive an xml string which can be of 2 types. Eg:

[HttpPost("postObj")]
    public async Task<IActionResult> postObj([FromBody]firstClass data)
    {
        if (data != null)...

I would like to be able to bind to multiple types on the same route ([HttpPost("postObj")]) So that I can receive on http://127.0.0.1:5000/api/postObj with firstClass xml in the body, or secondClass xml in the body, and act accordingly.

I tried making another method with the same route but different type like:

    [HttpPost("postObj")]
    public async Task<IActionResult> postObj([FromBody]secondClass data)
    {
        if (data != null)...

but I get "Request matched multiple actions resulting in ambiguity", as expected.

I tried reading the body and doing a check then serializing the xml to the respective object, but that drastically reduced the performance.

I am expecting up to 100 requests per second, and binding using FromBody is giving me that, but manually reading the body and serializing gives me only about 15.

How can I achieve that?

3条回答
在下西门庆
2楼-- · 2020-06-17 07:12

Was playing around with same issue, here is what I end up with:

I wish to have following API:

PATCH /persons/1
{"name": "Alex"}

PATCH /persons/1
{"age": 33}

Also I wish to have separate controller actions, like:

[HttpPatch]
[Route("person/{id:int:min(1)}")]
public void PatchPersonName(int id, [FromBody]PatchPersonName model) {}

[HttpPatch]
[Route("person/{id:int:min(1)}")]
public void PatchPersonAge(int id, [FromBody]PatchPersonAge model) {}

So they can be used by Swashbuckle while generating API documentation.

What is even more important I wish to have built in validation working (which wont work in any other suggested solution).

To make this happen we going to create our own action method selector attribute which will try to deserialize incoming request body and if it will be able to do so then action will be chosen, otherwise next action will be checked.

public class PatchForAttribute : ActionMethodSelectorAttribute
{
    public Type Type { get; }

    public PatchForAttribute(Type type)
    {
        Type = type;
    }

    public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
    {
        routeContext.HttpContext.Request.EnableRewind();
        var body = new StreamReader(routeContext.HttpContext.Request.Body).ReadToEnd();
        try
        {
            JsonConvert.DeserializeObject(body, Type, new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Error });
            return true;
        }
        catch (Exception)
        {
            return false;
        }
        finally
        {
            routeContext.HttpContext.Request.Body.Position = 0;
        }
    }
}

pros: validation is working, no need for third action and/or base model, will work with swashbuckle

cons: for this actions we are reading and deserializing body twice

note: it is important to rewind stream, otherwise anyone else wont be able to read body

and our controller now will look like this:

[HttpPatch]
[Route("person/{id:int:min(1)}")]
[PatchFor(typeof(PatchPersonName))]
public void PatchPersonName(int id, [FromBody]PatchPersonName model) {}

[HttpPatch]
[Route("person/{id:int:min(1)}")]
[PatchFor(typeof(PatchPersonAge))]
public void PatchPersonAge(int id, [FromBody]PatchPersonAge model) {}

Full sample code can be found here

查看更多
祖国的老花朵
3楼-- · 2020-06-17 07:19

As a disclaimer, I consider this a little bit hacky. I'd push for the object that is being sent to you to be changed to one object which can represent either case or to have the two different object types be posted to two different URIs.

With that out of the way, an option to just make it work would be to create a custom IModelBinder by following this tutorial: https://dotnetcoretutorials.com/2016/12/28/custom-model-binders-asp-net-core/

The key here is that the model you're binding will be a base class with your two different classes derived from it.

  • Your custom model binder will choose which one to create based on the input data and do so.
  • Your controller action will take as input the base type and be configured to use your custom binder.
  • Your action will need to check the actual type of the object passed in and process it accordingly.
查看更多
祖国的老花朵
4楼-- · 2020-06-17 07:21

You can't define two actions with same route. Action Selector doesn't consider their parameter types. So, why don't you merge this actions;

public async Task<IActionResult> postObj([FromBody]EntireData data)
{
    if (data.FirstClass != null)
    {
        //Do something
    }
    if (data.SecondClass != null)
    {
        //Do something
    }
}

public class EntireData
{
    public FirstClass  firstClass { get; set; }

    public SecondClass secondClass { get; set; }
}
查看更多
登录 后发表回答