reactiveui / refit

The automatic type-safe REST library for .NET Core, Xamarin and .NET. Heavily inspired by Square's Retrofit library, Refit turns your REST API into a live interface.
https://reactiveui.github.io/refit/
MIT License
8.67k stars 745 forks source link

Complex object list in Query params object doesn't get put into query string format properly #1106

Open gdodd1977 opened 3 years ago

gdodd1977 commented 3 years ago

I've got the following QueryParams object:

`public class GetLocationsQueryParams { public GetLocationsQueryParams() { LocationDeviceData = new List(); }

    public BasicAddress Address { get; set; }

    public int Distance { get; set; }

    public DistanceType DistanceType { get; set; }

    public List<LocationDeviceData> LocationDeviceData { get; set; }

    public int Limit { get; set; }
}`

LocationDeviceData is as follows:

public class LocationDeviceData { public string ServiceProvider { get; set; } public string VendorDeviceId { get; set; } public string VendorRepairId { get; set; } }

When I pass this into a Get call, the query string generated is as follows:

repair/locations?Address.AddressType=None&Address.Line1=1407 Fleet St.&Address.Line2=&Address.Region=MD&Address.City=Baltimore&Address.Country=US&Address.PostalCode=21231&Distance=30&DistanceType=Miles&LocationDeviceData=LocationDeviceData&Limit=20&ProgramId=a4e24585-a63c-4597-be12-a6cfdafa15ee&FulfillmentOption=WalkInRepair

Is refit unable to parse query string objects like this into a proper query string?

HosseinSalmanian commented 2 years ago

@clairernovotny can anyone follow this issue? we have the same issue.

clairernovotny commented 2 years ago

I'm not sure what you would expect to see here for this? Query strings are ultimately key/value pairs, not rich complex data. Those are better done in a body with a format like json, xml, or yaml.

gdodd12 commented 2 years ago

You can't pass a body to a GET.

clairernovotny commented 2 years ago

The question remains, what does the query expect?

HosseinSalmanian commented 2 years ago

@clairernovotny complex type can serialize to query string with dot as property separator and serialization format for collections (and collection of complex types)can be done with square bracket and index.

    public class ListOption
    {
        public List<FilterModel> Filter { get; set; } = new List<FilterModel>();
        public List<SortModel> Sort { get; set; } = new List<SortModel>();
        public int Page { get; set; }
        public int Limit { get; set; }
    }

    public class FilterModel
    {
        public FilterModel(string property, string value)
        {
            Property = property;
            Value = value;
        }
        public string Property { get;}
        public string Value { get;}
    }

    public class SortModel
    {
        public SortModel(string property,string direction)
        {
            Property = property;
            Direction = direction;
        }
        public string Property { get;  }
        public string Direction { get;}
    }

query string for "ListOption" model can be as like below. Page=25&Limit=0&Sort[0].Direction=asc&Sort[0].Property=entryDate&Sort[1].Direction=desc&Sort[1].Property=Age

HosseinSalmanian commented 2 years ago

we need to bind our model from query in .netCore API https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-6.0#dictionaries

Model Binding in ASP.NET Core
Learn how model binding in ASP.NET Core works and how to customize its behavior.
clairernovotny commented 2 years ago

Something like Sort[1] is a very specific representation of a property; it's not something that can be generalized between REST backends/clients so if you need that, I'd recommend using a custom formatter.

HosseinSalmanian commented 2 years ago

Can you provide me with an example or any documentation of a custom formatter?

grexican commented 2 years ago

@clairernovotny I'm not sure I follow your last answer re: Sort[1]. @HosseinSalmanian is asking for Refit to model bind for a .NET Core API. .NET binding has used square bracket notation for arrays and dictionaries since its inception. If I understand you correctly, you're saying Sort[1] is a non-standard format? What I don't understand is how can a .NET library not support the default .NET format and say it's nonstandard and won't be implemented?

Regardless, I'm in the same boat and I'd like to implement it. I have a third-party API that takes in a filter dictionary in the querystring in the following format: ?filter[email]=hello@world.com&filter[company]=github

My data type is a Params class with a Dictionary<string, object> filters { get; set; } That's fine and all, except I have no clue how to format the dictionary with bracket notation instead of dot notation.

Looking at overriding DefaultUrlParameterFormatter doesn't seem to help, I don't think. From what I can tell it doesn't distinguish between the key and the value, and by the time this gets called, it's already on the string key of my dictionary and not on the dictionary itself.

Soooooo yea... documentation or an example of a custom formatter would be helpful :) FWIW I looked at the test cases that override/implement DefaultUrlParameterFormatter and it wasn't helpful for the above case.

clairernovotny commented 2 years ago

Refit supports REST API's all up and does not make .NET-specific assumptions about formatting. It is bad idea to put a platform/language-specific feature in a URL/query as leaks implementation details into a public surface area.

That said, it should be possible for a custom formatter implementation to generate what's needed and if that extension point is missing, then we can take a PR to add it.

grexican commented 2 years ago

I'm not suggesting to put platform-specific assumptions in. But I am suggesting that, if a .NET platform supposes to support all APIs it should at least support the APIs of said platform.

I'd hate to see something hardcoded, but I'd also expect to see some formatting options built in to support an API built on the same platform.

Apologies if my explanation is confusing.

I think it could be solved by making the UrlParamterFormatter aware of if it's formatting the key or the value. Right now it says "I have something to format... how do I format it?" And instead of should say "I have a key to format? I'll do it like this; I have a value to format? I'll do it like that."

Having not yet contributed to Refit, I'm not sure if I've understood the architecture. If what I'm saying makes sense, @clairernovotny, then I'd be happy to try my hand at a PR for it. My gut says it would be some kind of breaking change, though, as I expect the underlying interface would need to change, or be broken out into two parts (Key formatter vs Value formatter).

kruzzze commented 2 years ago

@grexican @HosseinSalmanian Hi, I have the same issue, could you share your solution here, please? If it exists, of course

I think it can be useful for many refit users

grexican commented 2 years ago

@kruzzze no I never did find a solution. I ended up circumventing the issue by declaring all of the individual filters I needed and specifying the querystring param, e.g. a DTO with { Key1 { get; set; } { Key2 { get; set; } } and decorating each param with the url param "filter[Key1]" and "filter[Key2]" for example. My needs were limited to a few keys, so I could get away with it.

TimonSP commented 2 years ago

Hi there, were any customizable formatter been added? I also would like to know how such kind of Query params customization can be achieved?

karthikchintala1 commented 1 year ago

I have a similar problem. Is this issue resolved?

ducman commented 2 months ago

I read the source code and this can't be done cleanly (even with v7 UrlParameterKeyFormatter). It would have been better if it was just a UrlParameterKeyValuePairFormatter to provide a bit more flexibility but it would break a bunch of existing functionalities that they currently have.

After reading the source code, I did write a very hacky UrlParameterFormatter to get this issue to work. It's very specific/hardcoded to the api that I needed to call. It requires the URI to be unescaped and a DelegatingHandler to clean up the query string since you can't do much with the formatter.