Receive complex object FromQuery in .Net 6.0 Web API

Yiyi Chen 25 Reputation points
2023-04-01T09:54:56.2466667+00:00

Hi all,

I have a solution .Net 6.0 Web API project in which I want to use an object like these as model for query string:

public class UserParam
{
  public Guid? Id { get; set; }
  public string? Email { get; set; }
  public IEnumerable<string>? Select { get; set; }
  public IEnumerable<string>? Include { get; set; }
  public IEnumerable<SortingParam>? Sorting { get; set; }
  public PagingParam? Paging { get; set; }
}

public class PagingParam
{
  public int Skip { get; }
  public int Take { get; }

  public PagingParam(int skip, int take) => (Skip, Take) = (skip, take);
}

public class SortingParam
{
  public string? SortBy { get; }
  public SortDirection? SortDirection { get; }

  public SortingParam(string sortBy, SortDirection sortDirection) =>
    (SortBy, SortDirection) = (sortBy, sortDirection);
}

public enum SortDirection
{
  [EnumString("ASC")]
  ASC,

  [EnumString("DESC")]
  DESC
}

The controller method is like this:

[HttpGet]
public Task<Result<User>> ReadUsersAsync([FromQuery] UserParam userParam)
{
  return userService.ReadUsersAsync(userParam);
}

Simple properties like Id and Email are the string[] are mapped correctly but I can't get the values of Sorting and Paging into the controller. When I check in debug they are always null.

I am using swagger to test the API that to send a request with these params

{
  Paging: {
    Skip: 0,
    Take: 3
  },
  Sorting: [
    { SortBy: "Email", SortDirection: 0 }
  ]
}

Performs the following HTTP GET:

http://localhost:5280/api/v1/user?Sorting=%7B%0A%20%20%22sortBy%22%3A%20%22Email%22%2C%0A%20%20%22sortDirection%22%3A%200%0A%7D&Paging.Skip=0&Paging.Take=3

Decoded URL:

http://localhost:5280/api/v1/user?Sorting={
  "sortBy": "Email",
  "sortDirection": 0
}&Paging.Skip=0&Paging.Take=3

How can I correctly make a request with the nested properties in UserParam.

Another question, in the past I worked with Angular and NestJS backend, in that case I used @jsonurl/jsonurl and it worked fine because I could use is in both client and server. Is there something similar for c#?

Thank you

ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,132 questions
{count} votes

2 answers

Sort by: Most helpful
  1. AgaveJoe 26,181 Reputation points
    2023-04-01T12:46:35.7866667+00:00

    Data passed in the URL are name/value pairs. The client should pass a JSON string as a parameter. The web api action deserializes the string parameter.

    With that being said, there are several issues with the class design. First, read only properties cannot be deserialized. Second, either remove the constructor or add a parameterless constructor. Third, define collections as an array or list not an IEnumerable<T>.

    Working example

        public class UserParam
        {
            public Guid? id { get; set; }
            public string? email { get; set; }
            public string[]? select { get; set; }
            public string[]? include { get; set; }
            public SortingParam[]? sorting { get; set; }
            public PagingParam? paging { get; set; }
        }
    
        public class PagingParam
        {
            public int skip { get; set; }
            public int take { get; set; }
        }
    
        public class SortingParam
        {
            public string? sortBy { get; set; }
            public SortDirection? sortDirection { get; set; }
        }
    
        public enum SortDirection
        {
            ASC,
            DESC
        }
    

    Action

    [HttpGet]
    public ActionResult<UserParam> ReadUsersAsync([FromQuery]string json)
    {
        UserParam userParam = JsonSerializer.Deserialize<UserParam>(json);
        return Ok(userParam);
    }
    

    Encoded URL

    https://localhost:7004/api/User?json=%7B%22id%22%3A%227626f694-8ffe-4063-94f9-dd369efa090d%22%2C%22email%22%3A%22email%40eamil.com%22%2C%22select%22%3A%5B%22Sel1%22%2C%22Sel2%22%5D%2C%22include%22%3A%5B%22col1%22%2C%22col2%22%5D%2C%22sorting%22%3A%5B%7B%22sortBy%22%3A%22col1%22%2C%22sortDirection%22%3A0%7D%5D%2C%22paging%22%3A%7B%22skip%22%3A0%2C%22take%22%3A3%7D%7D
    

  2. Zhi Lv - MSFT 32,006 Reputation points Microsoft Vendor
    2023-04-05T02:26:28.4833333+00:00

    Hi @Yiyi Chen

    Simple properties like Id and Email are the string[] are mapped correctly but I can't get the values of Sorting and Paging into the controller. When I check in debug they are always null.

    This issue is caused by the following:

    1. In the PagingParam and SortingParam class, the property is missing the setter accessor.
    2. The swagger UI does not generate the correct parameter formatting. From the url, we can see that the Sorting value is a json string. User's image the correct URL should like this: http://localhost:5280/api/v1/user?Sorting[0].SortBy=Email&Sorting[0].SortDirection=0&Paging.Skip=0&Paging.Take=3

    To solve this issue, you can modify the model as below: Add setter and Constructor.

        public class PagingParam
        {
            public int Skip { get; set; }
            public int Take { get; set; }
            public PagingParam() { }
            public PagingParam(int skip, int take) => (Skip, Take) = (skip, take);
        }
    
        public class SortingParam
        {
            public string? SortBy { get; set; }
            public SortDirection? SortDirection { get; set; }
            public SortingParam() { }
            public SortingParam(string sortBy, SortDirection sortDirection) =>
              (SortBy, SortDirection) = (sortBy, sortDirection);
        }
    

    Then, for the Swagger UI parameter incorrect issue, you can contact the Swagger UI to verify if there is a workaround to fix the incorrect parameter formatting issue for the complex object.
    Or, you can check the API using PostMan with the correct URL (like this: http://localhost:5280/api/v1/user?Sorting[0].SortBy="Email"&Sorting[0].SortDirection=0&Paging.Skip=0&Paging.Take=3), instead of using Swagger. User's image

    Finally, if you still want to Swagger with the incorrect parameter (json string), you can create a custom model binder to handle the data: get the json string first, then deserialize it. Code Like this:

        [ModelBinder(BinderType = typeof(UserParamModelBinder))]
        public class UserParam
        {
            public Guid? Id { get; set; }
            public string? Email { get; set; }
            public List<string>? Select { get; set; }
            public List<string>? Include { get; set; }
            public List<SortingParam>? Sorting { get; set; }
            public PagingParam? Paging { get; set; }
        }
    
        public class PagingParam
        {
            public int Skip { get; set; }
            public int Take { get; set; }
            public PagingParam() { }
            public PagingParam(int skip, int take) => (Skip, Take) = (skip, take);
        }
    
        public class SortingParam
        {
            public string? SortBy { get; set; }
            public SortDirection? SortDirection { get; set; }
            public SortingParam() { }
            public SortingParam(string sortBy, SortDirection sortDirection) =>
              (SortBy, SortDirection) = (sortBy, sortDirection);
        }
    
        public enum SortDirection
        { 
            ASC,
            DESC
        }
    
    
        public class UserParamModelBinder : IModelBinder
        {
            public Task BindModelAsync(ModelBindingContext bindingContext)
            {
                if (bindingContext == null)
                    throw new ArgumentNullException(nameof(bindingContext));
                   
                //get value from bindingContext
                var valueproviders = bindingContext.ValueProvider;
    
                var result = new UserParam();
    
                if (valueproviders.GetValue("Id").Length > 0)
                    result.Id = new Guid(valueproviders.GetValue("Id").FirstValue);
                if (valueproviders.GetValue("Email").Length > 0)
                    result.Email = valueproviders.GetValue("Email").FirstValue;
                if (valueproviders.GetValue("Select").Length > 0)
                    result.Select = valueproviders.GetValue("Select").ToList();
                if (valueproviders.GetValue("Include").Length > 0)
                    result.Include = valueproviders.GetValue("Include").ToList();
                if (valueproviders.GetValue("Sorting").Length>0)
                {
                    var options = new JsonSerializerOptions
                    {
                        PropertyNameCaseInsensitive = true
                    };
                    var sortlist = new List<SortingParam>();
                    foreach (var item in valueproviders.GetValue("Sorting").ToList())
                    {
                        var newsort = JsonSerializer.Deserialize<SortingParam>(item.Replace("\n", ""), options);
                        sortlist.Add(newsort);
                    }
                    result.Sorting = sortlist;
                }
                var newpaging = new PagingParam();
                if (valueproviders.GetValue("Paging.Skip").Length>0)
                    newpaging.Skip = Convert.ToInt32(valueproviders.GetValue("Paging.Skip").FirstValue);
                if (valueproviders.GetValue("Paging.Take").Length>0)
                    newpaging.Take = Convert.ToInt32(valueproviders.GetValue("Paging.Take").FirstValue);
    
                result.Paging = newpaging;
    
                bindingContext.Result = ModelBindingResult.Success(result);
                return Task.CompletedTask;
            }
        }
        public class UserParaBinderProvider : IModelBinderProvider
        {
            public IModelBinder GetBinder(ModelBinderProviderContext context)
            {
                if (context == null)
                {
                    throw new ArgumentNullException(nameof(context));
                }
    
                if (context.Metadata.ModelType == typeof(UserParam))
                {
                    return new BinderTypeModelBinder(typeof(UserParamModelBinder));
                }
    
                return null;
            }
        }
    

    Then, register the binder provider in the Program.cs:

    builder.Services.AddControllers(options =>
    {
        options.ModelBinderProviders.Insert(0, new UserParaBinderProvider());
    });
    

    The result as below: image1


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment". Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    Best regards,

    Dillion

    0 comments No comments