Skip to content

Commit 1b45219

Browse files
feat: custom query key formatters (#1570)
* feature: Introduce support for custom URL query key formatters - Implements a key formatter for `camelCase` * docs: Adds querystrings examples * removes redundant code from `CamelCaseUrlParameterKeyFormatter.cs` * fix: restores binary-compability * Update after merge * chore: remove useless piece of code * feat(tests): CamelCaseUrlParameterKeyFormatter tests * feat(Tests): RefitSettings tests --------- Co-authored-by: Chris Pulman <chris.pulman@yahoo.com>
1 parent d85edef commit 1b45219

8 files changed

+3089
-4095
lines changed

README.md

+134-6
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ services
3939
* [Where does this work?](#where-does-this-work)
4040
* [Breaking changes in 6.x](#breaking-changes-in-6x)
4141
* [API Attributes](#api-attributes)
42-
* [Dynamic Querystring Parameters](#dynamic-querystring-parameters)
43-
* [Collections as Querystring parameters](#collections-as-querystring-parameters)
44-
* [Unescape Querystring parameters](#unescape-querystring-parameters)
42+
* [Querystrings](#querystrings)
43+
* [Dynamic Querystring Parameters](#dynamic-querystring-parameters)
44+
* [Collections as Querystring parameters](#collections-as-querystring-parameters)
45+
* [Unescape Querystring parameters](#unescape-querystring-parameters)
46+
* [Custom Querystring Parameter formatting](#custom-querystring-parameter-formatting)
4547
* [Body content](#body-content)
4648
* [Buffering and the Content-Length header](#buffering-and-the-content-length-header)
4749
* [JSON content](#json-content)
@@ -175,7 +177,9 @@ Search("admin/products");
175177
>>> "/search/admin/products"
176178
```
177179

178-
### Dynamic Querystring Parameters
180+
### Querystrings
181+
182+
#### Dynamic Querystring Parameters
179183

180184
If you specify an `object` as a query parameter, all public properties which are not null are used as query parameters.
181185
This previously only applied to GET requests, but has now been expanded to all HTTP request methods, partly thanks to Twitter's hybrid API that insists on non-GET requests with querystring parameters.
@@ -229,7 +233,7 @@ Task<Tweet> PostTweet([Query]TweetParams params);
229233

230234
Where `TweetParams` is a POCO, and properties will also support `[AliasAs]` attributes.
231235

232-
### Collections as Querystring parameters
236+
#### Collections as Querystring parameters
233237

234238
Use the `Query` attribute to specify format in which collections should be formatted in query string
235239

@@ -256,7 +260,7 @@ var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
256260
});
257261
```
258262

259-
### Unescape Querystring parameters
263+
#### Unescape Querystring parameters
260264

261265
Use the `QueryUriFormat` attribute to specify if the query parameters should be url escaped
262266

@@ -269,6 +273,130 @@ Query("Select+Id,Name+From+Account")
269273
>>> "/query?q=Select+Id,Name+From+Account"
270274
```
271275

276+
#### Custom Querystring parameter formatting
277+
278+
**Formatting Keys**
279+
280+
To customize the format of query keys, you have two main options:
281+
282+
1. **Using the `AliasAs` Attribute**:
283+
284+
You can use the `AliasAs` attribute to specify a custom key name for a property. This attribute will always take precedence over any key formatter you specify.
285+
286+
```csharp
287+
public class MyQueryParams
288+
{
289+
[AliasAs("order")]
290+
public string SortOrder { get; set; }
291+
292+
public int Limit { get; set; }
293+
}
294+
295+
[Get("/group/{id}/users")]
296+
Task<List<User>> GroupList([AliasAs("id")] int groupId, [Query] MyQueryParams params);
297+
298+
params.SortOrder = "desc";
299+
params.Limit = 10;
300+
301+
GroupList(1, params);
302+
```
303+
304+
This will generate the following request:
305+
306+
```
307+
/group/1/users?order=desc&Limit=10
308+
```
309+
310+
2. **Using the `RefitSettings.UrlParameterKeyFormatter` Property**:
311+
312+
By default, Refit uses the property name as the query key without any additional formatting. If you want to apply a custom format across all your query keys, you can use the `UrlParameterKeyFormatter` property. Remember that if a property has an `AliasAs` attribute, it will be used regardless of the formatter.
313+
314+
The following example uses the built-in `CamelCaseUrlParameterKeyFormatter`:
315+
316+
```csharp
317+
public class MyQueryParams
318+
{
319+
public string SortOrder { get; set; }
320+
321+
[AliasAs("queryLimit")]
322+
public int Limit { get; set; }
323+
}
324+
325+
[Get("/group/users")]
326+
Task<List<User>> GroupList([Query] MyQueryParams params);
327+
328+
params.SortOrder = "desc";
329+
params.Limit = 10;
330+
```
331+
332+
The request will look like:
333+
334+
```
335+
/group/users?sortOrder=desc&queryLimit=10
336+
```
337+
338+
**Note**: The `AliasAs` attribute always takes the top priority. If both the attribute and a custom key formatter are present, the `AliasAs` attribute's value will be used.
339+
340+
#### Formatting URL Parameter Values with the `UrlParameterFormatter`
341+
342+
In Refit, the `UrlParameterFormatter` property within `RefitSettings` allows you to customize how parameter values are formatted in the URL. This can be particularly useful when you need to format dates, numbers, or other types in a specific manner that aligns with your API's expectations.
343+
344+
**Using `UrlParameterFormatter`**:
345+
346+
Assign a custom formatter that implements the `IUrlParameterFormatter` interface to the `UrlParameterFormatter` property.
347+
348+
```csharp
349+
public class CustomDateUrlParameterFormatter : IUrlParameterFormatter
350+
{
351+
public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
352+
{
353+
if (value is DateTime dt)
354+
{
355+
return dt.ToString("yyyyMMdd");
356+
}
357+
358+
return value?.ToString();
359+
}
360+
}
361+
362+
var settings = new RefitSettings
363+
{
364+
UrlParameterFormatter = new CustomDateUrlParameterFormatter()
365+
};
366+
```
367+
368+
In this example, a custom formatter is created for date values. Whenever a `DateTime` parameter is encountered, it formats the date as `yyyyMMdd`.
369+
370+
**Formatting Dictionary Keys**:
371+
372+
When dealing with dictionaries, it's important to note that keys are treated as values. If you need custom formatting for dictionary keys, you should use the `UrlParameterFormatter` as well.
373+
374+
For instance, if you have a dictionary parameter and you want to format its keys in a specific way, you can handle that in the custom formatter:
375+
376+
```csharp
377+
public class CustomDictionaryKeyFormatter : IUrlParameterFormatter
378+
{
379+
public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
380+
{
381+
// Handle dictionary keys
382+
if (attributeProvider is PropertyInfo prop && prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
383+
{
384+
// Custom formatting logic for dictionary keys
385+
return value?.ToString().ToUpperInvariant();
386+
}
387+
388+
return value?.ToString();
389+
}
390+
}
391+
392+
var settings = new RefitSettings
393+
{
394+
UrlParameterFormatter = new CustomDictionaryKeyFormatter()
395+
};
396+
```
397+
398+
In the above example, the dictionary keys will be converted to uppercase.
399+
272400
### Body content
273401

274402
One of the parameters in your method can be used as the body, by using the

Refit.Newtonsoft.Json/Refit.Newtonsoft.Json.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<Product>Refit Serializer for Newtonsoft.Json ($(TargetFramework))</Product>
55
<Description>Refit Serializers for Newtonsoft.Json</Description>
6-
<TargetFrameworks>net462;netstandard2.0;net6.0;net7.0;net8.0</TargetFrameworks>
6+
<TargetFrameworks>net462;netstandard2.0;net6.0;net8.0</TargetFrameworks>
77
<GenerateDocumentationFile Condition=" '$(Configuration)' == 'Release' ">true</GenerateDocumentationFile>
88
<RootNamespace>Refit</RootNamespace>
99
<Nullable>enable</Nullable>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Xunit;
2+
3+
namespace Refit.Tests;
4+
5+
public class CamelCaselTestsRequest
6+
{
7+
public string alreadyCamelCased { get; set; }
8+
public string NOTCAMELCased { get; set; }
9+
}
10+
11+
public class CamelCaseUrlParameterKeyFormatterTests
12+
{
13+
[Fact]
14+
public void Format_EmptyKey_ReturnsEmptyKey()
15+
{
16+
var urlParameterKeyFormatter = new CamelCaseUrlParameterKeyFormatter();
17+
18+
var output = urlParameterKeyFormatter.Format(string.Empty);
19+
Assert.Equal(string.Empty, output);
20+
}
21+
22+
[Fact]
23+
public void FormatKey_Returns_ExpectedValue()
24+
{
25+
var urlParameterKeyFormatter = new CamelCaseUrlParameterKeyFormatter();
26+
27+
var refitSettings = new RefitSettings { UrlParameterKeyFormatter = urlParameterKeyFormatter };
28+
var fixture = new RequestBuilderImplementation<IDummyHttpApi>(refitSettings);
29+
var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary));
30+
31+
var complexQuery = new CamelCaselTestsRequest
32+
{
33+
alreadyCamelCased = "value1",
34+
NOTCAMELCased = "value2"
35+
};
36+
37+
var output = factory([complexQuery]);
38+
var uri = new Uri(new Uri("http://api"), output.RequestUri);
39+
40+
Assert.Equal("/foo?alreadyCamelCased=value1&notcamelCased=value2", uri.PathAndQuery);
41+
}
42+
}

Refit.Tests/RefitSettings.cs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Xunit;
2+
3+
namespace Refit.Tests;
4+
5+
public class RefitSettingsTests
6+
{
7+
[Fact]
8+
public void Can_CreateRefitSettings_WithoutException()
9+
{
10+
var contentSerializer = new NewtonsoftJsonContentSerializer();
11+
var urlParameterFormatter = new DefaultUrlParameterFormatter();
12+
var urlParameterKeyFormatter = new CamelCaseUrlParameterKeyFormatter();
13+
var formUrlEncodedParameterFormatter = new DefaultFormUrlEncodedParameterFormatter();
14+
15+
var exception = Record.Exception(() => new RefitSettings());
16+
Assert.Null(exception);
17+
18+
exception = Record.Exception(() => new RefitSettings(contentSerializer));
19+
Assert.Null(exception);
20+
21+
exception = Record.Exception(() => new RefitSettings(contentSerializer, urlParameterFormatter));
22+
Assert.Null(exception);
23+
24+
exception = Record.Exception(() => new RefitSettings(contentSerializer, urlParameterFormatter, formUrlEncodedParameterFormatter));
25+
Assert.Null(exception);
26+
27+
exception = Record.Exception(() => new RefitSettings(contentSerializer, urlParameterFormatter, formUrlEncodedParameterFormatter, urlParameterKeyFormatter));
28+
Assert.Null(exception);
29+
}
30+
}

0 commit comments

Comments
 (0)