Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom query key formatters #1570

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
140 changes: 134 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ services
* [Where does this work?](#where-does-this-work)
* [Breaking changes in 6.x](#breaking-changes-in-6x)
* [API Attributes](#api-attributes)
* [Dynamic Querystring Parameters](#dynamic-querystring-parameters)
* [Collections as Querystring parameters](#collections-as-querystring-parameters)
* [Unescape Querystring parameters](#unescape-querystring-parameters)
* [Querystrings](#querystrings)
* [Dynamic Querystring Parameters](#dynamic-querystring-parameters)
* [Collections as Querystring parameters](#collections-as-querystring-parameters)
* [Unescape Querystring parameters](#unescape-querystring-parameters)
* [Custom Querystring Parameter formatting](#custom-querystring-parameter-formatting)
* [Body content](#body-content)
* [Buffering and the Content-Length header](#buffering-and-the-content-length-header)
* [JSON content](#json-content)
Expand Down Expand Up @@ -175,7 +177,9 @@ Search("admin/products");
>>> "/search/admin/products"
```

### Dynamic Querystring Parameters
### Querystrings

#### Dynamic Querystring Parameters

If you specify an `object` as a query parameter, all public properties which are not null are used as query parameters.
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.
Expand Down Expand Up @@ -229,7 +233,7 @@ Task<Tweet> PostTweet([Query]TweetParams params);

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

### Collections as Querystring parameters
#### Collections as Querystring parameters

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

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

### Unescape Querystring parameters
#### Unescape Querystring parameters

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

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

#### Custom Querystring parameter formatting

**Formatting Keys**

To customize the format of query keys, you have two main options:

1. **Using the `AliasAs` Attribute**:

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.

```csharp
public class MyQueryParams
{
[AliasAs("order")]
public string SortOrder { get; set; }

public int Limit { get; set; }
}

[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId, [Query] MyQueryParams params);

params.SortOrder = "desc";
params.Limit = 10;

GroupList(1, params);
```

This will generate the following request:

```
/group/1/users?order=desc&Limit=10
```

2. **Using the `RefitSettings.UrlParameterKeyFormatter` Property**:

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.

The following example uses the built-in `CamelCaseUrlParameterKeyFormatter`:

```csharp
public class MyQueryParams
{
public string SortOrder { get; set; }

[AliasAs("queryLimit")]
public int Limit { get; set; }
}

[Get("/group/users")]
Task<List<User>> GroupList([Query] MyQueryParams params);

params.SortOrder = "desc";
params.Limit = 10;
```

The request will look like:

```
/group/users?sortOrder=desc&queryLimit=10
```

**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.

#### Formatting URL Parameter Values with the `UrlParameterFormatter`

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.

**Using `UrlParameterFormatter`**:

Assign a custom formatter that implements the `IUrlParameterFormatter` interface to the `UrlParameterFormatter` property.

```csharp
public class CustomDateUrlParameterFormatter : IUrlParameterFormatter
{
public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
{
if (value is DateTime dt)
{
return dt.ToString("yyyyMMdd");
}

return value?.ToString();
}
}

var settings = new RefitSettings
{
UrlParameterFormatter = new CustomDateUrlParameterFormatter()
};
```

In this example, a custom formatter is created for date values. Whenever a `DateTime` parameter is encountered, it formats the date as `yyyyMMdd`.

**Formatting Dictionary Keys**:

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.

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:

```csharp
public class CustomDictionaryKeyFormatter : IUrlParameterFormatter
{
public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
{
// Handle dictionary keys
if (attributeProvider is PropertyInfo prop && prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
// Custom formatting logic for dictionary keys
return value?.ToString().ToUpperInvariant();
}

return value?.ToString();
}
}

var settings = new RefitSettings
{
UrlParameterFormatter = new CustomDictionaryKeyFormatter()
};
```

In the above example, the dictionary keys will be converted to uppercase.

### Body content

One of the parameters in your method can be used as the body, by using the
Expand Down
2 changes: 1 addition & 1 deletion Refit.Newtonsoft.Json/Refit.Newtonsoft.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<Product>Refit Serializer for Newtonsoft.Json ($(TargetFramework))</Product>
<Description>Refit Serializers for Newtonsoft.Json</Description>
<TargetFrameworks>net462;netstandard2.0;net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net462;netstandard2.0;net6.0;net8.0</TargetFrameworks>
<GenerateDocumentationFile Condition=" '$(Configuration)' == 'Release' ">true</GenerateDocumentationFile>
<RootNamespace>Refit</RootNamespace>
<Nullable>enable</Nullable>
Expand Down
42 changes: 42 additions & 0 deletions Refit.Tests/CamelCaseUrlParameterKeyFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Xunit;

namespace Refit.Tests;

public class CamelCaselTestsRequest
{
public string alreadyCamelCased { get; set; }
public string NOTCAMELCased { get; set; }
}

public class CamelCaseUrlParameterKeyFormatterTests
{
[Fact]
public void Format_EmptyKey_ReturnsEmptyKey()
{
var urlParameterKeyFormatter = new CamelCaseUrlParameterKeyFormatter();

var output = urlParameterKeyFormatter.Format(string.Empty);
Assert.Equal(string.Empty, output);
}

[Fact]
public void FormatKey_Returns_ExpectedValue()
{
var urlParameterKeyFormatter = new CamelCaseUrlParameterKeyFormatter();

var refitSettings = new RefitSettings { UrlParameterKeyFormatter = urlParameterKeyFormatter };
var fixture = new RequestBuilderImplementation<IDummyHttpApi>(refitSettings);
var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary));

var complexQuery = new CamelCaselTestsRequest
{
alreadyCamelCased = "value1",
NOTCAMELCased = "value2"
};

var output = factory([complexQuery]);
var uri = new Uri(new Uri("http://api"), output.RequestUri);

Assert.Equal("/foo?alreadyCamelCased=value1&notcamelCased=value2", uri.PathAndQuery);
}
}
30 changes: 30 additions & 0 deletions Refit.Tests/RefitSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Xunit;

namespace Refit.Tests;

public class RefitSettingsTests
{
[Fact]
public void Can_CreateRefitSettings_WithoutException()
{
var contentSerializer = new NewtonsoftJsonContentSerializer();
var urlParameterFormatter = new DefaultUrlParameterFormatter();
var urlParameterKeyFormatter = new CamelCaseUrlParameterKeyFormatter();
var formUrlEncodedParameterFormatter = new DefaultFormUrlEncodedParameterFormatter();

var exception = Record.Exception(() => new RefitSettings());
Assert.Null(exception);

exception = Record.Exception(() => new RefitSettings(contentSerializer));
Assert.Null(exception);

exception = Record.Exception(() => new RefitSettings(contentSerializer, urlParameterFormatter));
Assert.Null(exception);

exception = Record.Exception(() => new RefitSettings(contentSerializer, urlParameterFormatter, formUrlEncodedParameterFormatter));
Assert.Null(exception);

exception = Record.Exception(() => new RefitSettings(contentSerializer, urlParameterFormatter, formUrlEncodedParameterFormatter, urlParameterKeyFormatter));
Assert.Null(exception);
}
}