Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple types [FromBody] on same method .net core web api

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?

like image 944
Tarek Avatar asked Jan 03 '18 17:01

Tarek


People also ask

Can we declare FromBody attribute on multiple parameters?

The [FromBody] attribute can be applied on only one primitive parameter of an action method. It cannot be applied to multiple primitive parameters of the same action method.

Can we have multiple get methods in Web API?

As mentioned, Web API controller can include multiple Get methods with different parameters and types. Let's add following action methods in StudentController to demonstrate how Web API handles multiple HTTP GET requests.

What is difference between FromForm and FromBody?

[FromForm] - Gets values from posted form fields. [FromBody] - Gets values from the request body.

How do I return multiple values from API?

try with return Ok(new { firstList = firstList, secondList = secondList }); which will return a new object with two properties named firstList and secondList .


1 Answers

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

like image 88
mac Avatar answered Sep 29 '22 17:09

mac