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

13,931

Solution 1

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

Solution 2

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; }
}
Share:
13,931
Admin
Author by

Admin

Updated on July 26, 2022

Comments

  • Admin
    Admin almost 2 years

    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?

  • juunas
    juunas over 6 years
    Note in this case you will have to post the objects differently. So if you posted the firstClass data as JSON before, now you will have to post it as {"firstClass": {}}.
  • lucky
    lucky over 6 years
    @juunas, yes it has to be like that. Thanks for reminding.
  • Admin
    Admin over 6 years
    Thanks but I also tried that before. Problem is I have no control over the objects being posted, so it won't work for me. I'm trying something based on this though and we'll see.
  • mac
    mac over 5 years
    It does not conform desired restfull way, e.g. I wish to have following endpoints to work with: PATCH /user/1 {"gender": "male"} and PATCH /user/1 {"age": 33}. Yes I can have model that might have both age and name and check for nulls but how then will I perform PATCH /user/1 {"gender": null} when user decides to remove some info? Seems like solution should be somewhere around custom activations/constrain/binding
  • mac
    mac over 5 years
    Technically this should work, but there is one problem with this solution - it will break tools like Swashbuckle because they using reflection - generated documentation will have 1 endpoint with base class instead of multiple endpoints with concrete models.