The problem

We have the following action method:

[Route("[controller]")]
[ApiController]
[Produces("application/json")]
public class HomeController : ControllerBase
{
    public IActionResult Post(
    [FromBody] PostRequestBody body,
    [FromHeader(Name = "Accept")] string accept,
    [FromHeader(Name = "X-Correlation-Id")] string correlationId,
    [FromQuery(Name = "filter")] string filter)
    {
        // Do something
        return Ok();
    }
}

We want to consolidate all the parameters to one class, PostRequest, instead of using a bunch of different parameters.

The solution

Let's create a class that holds all our data:

public class PostRequest
{
    [FromBody]
    public PostRequestBody Body { get; set; } = null!;

    [FromHeader(Name = "Accept")]
    public string Accept { get; set; } = null!;

    [FromHeader(Name = "X-Correlation-Id")]
    public string CorrelationId { get; set; } = null!;

    [FromQuery(Name = "filter")]
    public string Filter { get; set; } = null!;
}

public class PostRequestBody
{
    public string SomeString { get; set; } = null!;
    public bool SomeBool { get; set; }
}

Looks good, right? Let's change our action method to have the following signature instead:

[Route("[controller]")]
[ApiController]
[Produces("application/json")]
public class HomeController : ControllerBase
{
    [HttpPost]
    public IActionResult Post(PostRequest req)
    {
        return new OkObjectResult(req);
    } 
}

Now, if we try to call our action method like this:

POST
Accept: application/json
X-Correlation-Id: my-correlation-id
/post?filter=one

{"someString": "Some string", "someBool": true}

We would get the following error message:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-5962a6779c924d01bddf7f5a9a8cbf87-bf0d5d719504e20b-00",
    "errors": {
        "Body": ["The Body field is required."],
        "Accept": ["The Accept field is required."],
        "filter": ["The Filter field is required."],
        "X-Correlation-Id": ["The CorrelationId field is required."]
    }
}

Out of the box, ASP.NET Core is not capable of model binding all the different sources to a single class. Let's solve that.

The first thing we will do is to create a new attribute that we will decorate our action parameter with.

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
public class FromRequestAttribute : Attribute, IBindingSourceMetadata
{
    public BindingSource BindingSource => new FromRequestModelBindingSource();
}

The attribute specifies that we should use FromRequestModelBindingSource as a BindingSource.

FromRequestModelBindingSource looks like this:

public class FromRequestModelBindingSource : BindingSource
{
    public FromRequestModelBindingSource() : base(
        "FromModelBinding",
        "FromModelBinding",
        true,
        true
    )
    {
    }

    public override bool CanAcceptDataFrom(BindingSource bindingSource)
    {
        return bindingSource == ModelBinding;
    }
}

The only thing left to do is to add the attribute to our action method like this:

[HttpPost]
public IActionResult Post([FromRequest]PostRequest req)
{
    return new OkObjectResult(req);
} 

Now when we do the same request as we did previously, we can see that we manage to bind all the different sources to one class:

{
    "body": {
        "someString": "Some string",
        "someBool": true
    },
    "accept": "application/json",
    "correlationId": "my-correlation-id",
    "filter": "one"
}

A complete example with tests can be found here.

As noted in the comments by Piotr, it's possible to achieve the same behaviour by just configuring the ApiBehaviourOptions like this:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressInferBindingSourcesForParameters = true;
});