53

I am trying to use model binding from query parameters to an object for searching.

My search object is

[DataContract]
public class Criteria 
{
  [DataMember(Name = "first_name")]
  public string FirstName { get; set; }
}

My controller has the following action

[Route("users")]
public class UserController : Controller 
{
  [HttpGet("search")]
  public IActionResult Search([FromQuery] Criteria criteria)
  {
    ...
  }
}

When I call the endpoint as follows .../users/search?first_name=dave the criteria property on the controller action is null. However, I can call the endpoint not as snake case .../users/search?firstName=dave and the criteria property contains the property value. In this case Model Binding has worked but not when I use snake_case.

How can I use snake_case with Model Binding?

1

9 Answers 9

82

You need to add [FromQuery] attribute to the model properties individually

public class Criteria
{
  [FromQuery(Name = "first_name")]
  public string FirstName { get; set; }
}
Sign up to request clarification or add additional context in comments.

6 Comments

What if I want to use data contracts with members with the custom names for GET and POST endpoints, so in the first case it will be FromQuery, and FromBody for POST request?
I can't get this to work but it is marked as an answer.
I just want to add that I had an issue which other people could encounter too: I named my parameter 'model'. My object had a property which was also named 'model'. The databinding wouldn't work untill I renamed my parameter.
FromQuery sits in Microsoft.AspNetCore.Mvc package, it is a bit sad to add this dependency to the project where you only store your POCO.
@sander 's solution worked for me - thank you I spend well over an hour trying to figure this one out. Lesson learned to check all parameter names.
|
46

Solution for .net core 2.1, 2.2, 3.0 and 3.1

Or without attributes you can do something like this which is cleaner I think (of course if the model properties are same as query parameters).

Meanwhile I use it in .net core 2.1, 2.2 and 3.0 preview & 3.1.

public async Task<IActionResult> Get([FromQuery]ReportQueryModel queryModel) 
{ 

}

11 Comments

This is the correct answer for NetCore 2.1 and above, the accepted answer is an anachronism.
Either this should be the accepted answer or the OP which happens to be the one who's given the accepted answer should update their answer.
I think the problem is that this answer doesn't address the specifics of question. The OP specifically requires a different name for the property, and this doesn't support that, as stated by the person that gave this answer.
I'm using .net core 2.2 and this method doesn't work for me. However, the accepted answer does.
Make sure you're using Properties, in the Request model, and not fields. That was my missed mistake
|
14

For anyone that got here from search engine like me:

To make it work on asp.net core 3.1+

public async Task<IActionResult> Get([FromQuery] RequestDto request);

public class RequestDto
{
  [FromQuery(Name = "otherName")]
  public string Name { get; set; }
}

Will read json property otherName into RequestDto.Name so basically you have to use FromQuery in 2 places. Above answers are IMHO too complicated for such a simple thing already provided in asp.net framework.

2 Comments

It will also works with [JsonPropertyName("otherName")] on your properties. Because of that I´m assuming it will also work with Newtonsoft.JSON annotations (but didn´t try). So you can just specify [FromQuery] in your method and could still use the same object in a Body when needed (without additional annotations). No need to be this specific about it.
@Max - hi Max. No it doesn't work for Newtonsoft [JsonPropery(Name = "otherName")]
3

In my case, I had an issue where my parameter name was option and in my class I also had the property called option so it was collapsing.

public class Content
{
    
    public string Option { get; set; }
    
    public int Page { get; set; }
}

public async Task<IActionResult> SendContent([FromQuery] Content option)

changed the parameter to something else:

public async Task<IActionResult> SendContent([FromQuery] Content contentOptions)

1 Comment

Thats the correct answer that should be accepted here, the [FromQuery] looks for a name, if the name is duplicated from an actual object it will assume you want to use , separator instead of named separator from query. So I add optionQuery as a name instead of just options
2

In .NET 6, I needed to add [FromQuery] to the endpoint's parameter, as well as add [FromQuery(Name="")] to each of the model's properties.

Rather than add the MVC dependency to my model project, I turned the service model into an interface and made a new model in the WebAPI project, which can use the annotations and implement the interface, so I don't need to create/map 2 models.

Comments

1

According to @Carl Thomas answer, here is the easier and the strongly typed way to have snake case FromQuery name:

CustomFromQuery

public class CustomFromQueryAttribute : FromQueryAttribute
{
    public CustomFromQuery(string name)
    {
        Name = name.ToSnakeCase();
    }
}

StringExtensions

public static class ObjectExtensions
{
  public static string ToSnakeCase(this string o) => Regex.Replace(o, @"(\w)([A-Z])", "$1_$2").ToLower();
}

Usage

public class Criteria
{
   [CustomFromQuery(nameof(FirstName))]
   public string FirstName { get; set; }
}

Comments

1

I don't know what is going on in .NET 8 or if it is just my setup but I can't get [FromQuery] to work like @emery-noel said in his answer or any other answer for that matter.

Example, this will not work:

[HttpGet]
public async Task<IActionResult> Agreements([FromQuery] ClientDocumentsParamsDTO parameters) { ...etc... }

[AsParameters] doesn't work either. My only option is to either do something ugly like this:

[HttpGet]
public async Task<IActionResult> Agreements(
    [FromQuery(Name = "organizationid")] int OrganizationId,
    [FromQuery(Name = "account-class-id")] int? AccountClassId,
    [FromQuery(Name = "account-preference-id")] int? AccountPreferenceId,
    [FromQuery(Name = "account-category-id")] int? AccountCategoryId,
    [FromQuery(Name = "account-type-id")] int? AccountTypeId
    )
{ 
 ... etc...

or something like this I'm guessing, which I haven't tried:

[HttpGet]
public async Task<IActionResult> Agreements(){
  string fullname1 = Request.QueryString["fullname"];
}

I'm guessing it has something to do with THIS issue but I don't know.

1 Comment

I also was not able to get it to work in .NET 8 for a while, finally come up with this approach: stackoverflow.com/a/78970956/12888162
1

I found one solution to get it to work and do not look too ugly:

public static async Task<IResult> GetAttachments([AsParameters] PagedRequest request, IAttachmentService service)
{
    try
    {
        var attachments = await service.GetAttachmentsAsync(request);

        return TypedResults.Ok(attachments);
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message);
    }
}

My PagedRequest looks like this:

public sealed class PagedRequest
{
    [FromQuery(Name = "page")]
    public int Page { get; set; } = 1;

    [FromQuery(Name = "pageSize")]
    public int PageSize { get; set; } = 10;

    [FromQuery(Name = "search")]
    public string? Search { get; set; }

    [FromQuery(Name = "searchBy")]
    public string? SearchBy { get; set; }

    [FromQuery(Name = "sortBy")]
    public string? SortBy { get; set; }

    [FromQuery(Name = "sortDirection")]
    public string? SortDirection { get; set; }
}

Endpoint will look like this:

enter image description here

Comments

0

If the public async Task<IActionResult> Get([FromQuery] RequestDto request);

not work for anyone, you can try [FromRoute]

public async Task<IActionResult> Get([FromRoute] RequestDto request);.

In your dto you must keep the [FromQuery]

public class RequestDto
{
  [FromQuery(Name = "otherName")]
  public string Name { get; set; }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.