diff --git a/README.md b/README.md index 0b07f0d0f..82db4cb66 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ Refit currently supports the following platforms and any .NET Standard 2.0 targe * UWP * Xamarin.Android * Xamarin.Mac -* Xamarin.iOS -* Desktop .NET 4.6.1 +* Xamarin.iOS +* Desktop .NET 4.6.1 * .NET Core * Uno Platform @@ -65,7 +65,7 @@ You can also specify query parameters in the URL: A request URL can be updated dynamically using replacement blocks and parameters on the method. A replacement block is an alphanumeric string -surrounded by { and }. +surrounded by { and }. If the name of your parameter doesn't match the name in the URL path, use the `AliasAs` attribute. @@ -227,18 +227,18 @@ type of the parameter: * If the type is `Stream`, the content will be streamed via `StreamContent` * If the type is `string`, the string will be used directly as the content unless `[Body(BodySerializationMethod.Json)]` is set which will send it as a `StringContent` -* If the parameter has the attribute `[Body(BodySerializationMethod.UrlEncoded)]`, +* If the parameter has the attribute `[Body(BodySerializationMethod.UrlEncoded)]`, the content will be URL-encoded (see [form posts](#form-posts) below) -* For all other types, the object will be serialized using the content serializer specified in +* For all other types, the object will be serialized using the content serializer specified in RefitSettings (JSON is the default). #### Buffering and the `Content-Length` header By default, Refit streams the body content without buffering it. This means you can -stream a file from disk, for example, without incurring the overhead of loading -the whole file into memory. The downside of this is that no `Content-Length` header +stream a file from disk, for example, without incurring the overhead of loading +the whole file into memory. The downside of this is that no `Content-Length` header is set _on the request_. If your API needs you to send a `Content-Length` header with -the request, you can disable this streaming behavior by setting the `buffered` argument +the request, you can disable this streaming behavior by setting the `buffered` argument of the `[Body]` attribute to `true`: ```csharp @@ -258,8 +258,8 @@ var settings = new RefitSettings(new SystemTextJsonContentSerializer()); If instead you're using the default settings, which use the `Newtonsoft.Json` APIs, you can customize their behavior by setting the `Newtonsoft.Json.JsonConvert.DefaultSettings` property: ```csharp -JsonConvert.DefaultSettings = - () => new JsonSerializerSettings() { +JsonConvert.DefaultSettings = + () => new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver(), Converters = {new StringEnumConverter()} }; @@ -269,16 +269,16 @@ await PostSomeStuff(new { Day = DayOfWeek.Saturday }); ``` As these are global settings they will affect your entire application. It -might be beneficial to isolate the settings for calls to a particular API. -When creating a Refit generated live interface, you may optionally pass a -`RefitSettings` that will allow you to specify what serializer settings you +might be beneficial to isolate the settings for calls to a particular API. +When creating a Refit generated live interface, you may optionally pass a +`RefitSettings` that will allow you to specify what serializer settings you would like. This allows you to have different serializer settings for separate APIs: ```csharp var gitHubApi = RestService.For("https://api.github.com", new RefitSettings { - ContentSerializer = new NewtonsoftJsonContentSerializer( + ContentSerializer = new NewtonsoftJsonContentSerializer( new JsonSerializerSettings { ContractResolver = new SnakeCasePropertyNamesContractResolver() } @@ -286,28 +286,28 @@ var gitHubApi = RestService.For("https://api.github.com", var otherApi = RestService.For("https://api.example.com", new RefitSettings { - ContentSerializer = new NewtonsoftJsonContentSerializer( + ContentSerializer = new NewtonsoftJsonContentSerializer( new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() } )}); ``` -Property serialization/deserialization can be customised using Json.NET's +Property serialization/deserialization can be customised using Json.NET's JsonProperty attribute: -```csharp -public class Foo +```csharp +public class Foo { // Works like [AliasAs("b")] would in form posts (see below) - [JsonProperty(PropertyName="b")] + [JsonProperty(PropertyName="b")] public string Bar { get; set; } -} +} ``` #### XML Content -XML requests and responses are serialized/deserialized using _System.Xml.Serialization.XmlSerializer_. +XML requests and responses are serialized/deserialized using _System.Xml.Serialization.XmlSerializer_. By default, Refit will use JSON content serialization, to use XML content configure the ContentSerializer to use the `XmlContentSerializer`: ```csharp @@ -362,9 +362,9 @@ public interface IMeasurementProtocolApi } var data = new Dictionary { - {"v", 1}, - {"tid", "UA-1234-5"}, - {"cid", new Guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c")}, + {"v", 1}, + {"tid", "UA-1234-5"}, + {"cid", new Guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c")}, {"t", "event"}, }; @@ -372,8 +372,8 @@ var data = new Dictionary { await api.Collect(data); ``` -Or you can just pass any object and all _public, readable_ properties will -be serialized as form fields in the request. This approach allows you to alias +Or you can just pass any object and all _public, readable_ properties will +be serialized as form fields in the request. This approach allows you to alias property names using `[AliasAs("whatever")]` which can help if the API has cryptic field names: @@ -388,30 +388,30 @@ public class Measurement { // Properties can be read-only and [AliasAs] isn't required public int v { get { return 1; } } - + [AliasAs("tid")] public string WebPropertyId { get; set; } [AliasAs("cid")] public Guid ClientId { get; set; } - [AliasAs("t")] + [AliasAs("t")] public string Type { get; set; } public object IgnoreMe { private get; set; } } -var measurement = new Measurement { - WebPropertyId = "UA-1234-5", - ClientId = new Guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c"), - Type = "event" -}; +var measurement = new Measurement { + WebPropertyId = "UA-1234-5", + ClientId = new Guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c"), + Type = "event" +}; // Serialized as: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event await api.Collect(measurement); -``` +``` -If you have a type that has `[JsonProperty(PropertyName)]` attributes setting property aliases, Refit will use those too (`[AliasAs]` will take precedence where you have both). +If you have a type that has `[JsonProperty(PropertyName)]` attributes setting property aliases, Refit will use those too (`[AliasAs]` will take precedence where you have both). This means that the following type will serialize as `one=value1&two=value2`: ```csharp @@ -434,7 +434,7 @@ public class SomeObject #### Static headers -You can set one or more static request headers for a request applying a `Headers` +You can set one or more static request headers for a request applying a `Headers` attribute to the method: ```csharp @@ -443,7 +443,7 @@ attribute to the method: Task GetUser(string user); ``` -Static headers can also be added to _every request in the API_ by applying the +Static headers can also be added to _every request in the API_ by applying the `Headers` attribute to the interface: ```csharp @@ -452,7 +452,7 @@ public interface IGitHubApi { [Get("/users/{user}")] Task GetUser(string user); - + [Post("/users/new")] Task CreateUser([Body] User user); } @@ -468,7 +468,7 @@ with a dynamic value to a request by applying a `Header` attribute to a paramete Task GetUser(string user, [Header("Authorization")] string authorization); // Will add the header "Authorization: token OAUTH-TOKEN" to the request -var user = await GetUser("octocat", "token OAUTH-TOKEN"); +var user = await GetUser("octocat", "token OAUTH-TOKEN"); ``` #### Dynamic authorization header with scheme @@ -482,12 +482,12 @@ you can add a dynamic value to a request by applying an `Authorize` attribute to Task GetUser(string user, [Authorize("Bearer")] string token); // Will add the header "Authorization: Bearer OAUTH-TOKEN}" to the request -var user = await GetUser("octocat", "OAUTH-TOKEN"); +var user = await GetUser("octocat", "OAUTH-TOKEN"); ``` #### Authorization (Dynamic Headers redux) -Another way to encapsulate these kinds of token usage, a custom `HttpClientHandler` can be inserted instead. +Another way to encapsulate these kinds of token usage, a custom `HttpClientHandler` can be inserted instead. There are two classes for doing this: one is `AuthenticatedHttpClientHandler`, which takes a `Func>` parameter, where a signature can be generated without knowing about the request. The other is `AuthenticatedParameterizedHttpClientHandler`, which takes a `Func>` parameter, where the signature requires information about the request (see earlier notes about Twitter's API) @@ -554,14 +554,14 @@ This class is used like so (example uses the [ADAL](http://msdn.microsoft.com/en class LoginViewModel { AuthenticationContext context = new AuthenticationContext(...); - + private async Task GetToken() { // The AcquireTokenAsync call will prompt with a UI if necessary // Or otherwise silently use a refresh token to return - // a valid access token + // a valid access token var token = await context.AcquireTokenAsync("http://my.service.uri/app", "clientId", new Uri("callback://complete")); - + return token; } @@ -583,13 +583,13 @@ interface IMyRestService } ``` -In the above example, any time a method that requires authentication is called, the `AuthenticatedHttpClientHandler` will try to get a fresh access token. It's up to the app to provide one, checking the expiration time of an existing access token and obtaining a new one if needed. +In the above example, any time a method that requires authentication is called, the `AuthenticatedHttpClientHandler` will try to get a fresh access token. It's up to the app to provide one, checking the expiration time of an existing access token and obtaining a new one if needed. #### Redefining headers -Unlike Retrofit, where headers do not overwrite each other and are all added to -the request regardless of how many times the same header is defined, Refit takes -a similar approach to the approach ASP.NET MVC takes with action filters — +Unlike Retrofit, where headers do not overwrite each other and are all added to +the request regardless of how many times the same header is defined, Refit takes +a similar approach to the approach ASP.NET MVC takes with action filters — **redefining a header will replace it**, in the following order of precedence: * `Headers` attribute on the interface _(lowest priority)_ @@ -602,11 +602,11 @@ public interface IGitHubApi { [Get("/users/list")] Task GetUsers(); - + [Get("/users/{user}")] [Headers("X-Emoji: :smile_cat:")] Task GetUser(string user); - + [Post("/users/new")] [Headers("X-Emoji: :metal:")] Task CreateUser([Body] User user, [Header("X-Emoji")] string emoji); @@ -641,8 +641,8 @@ var user = await api.PostTheThing(3); #### Removing headers -Headers defined on an interface or method can be removed by redefining -a static header without a value (i.e. without `: `) or passing `null` for +Headers defined on an interface or method can be removed by redefining +a static header without a value (i.e. without `: `) or passing `null` for a dynamic header. _Empty strings will be included as empty headers._ ```csharp @@ -652,11 +652,11 @@ public interface IGitHubApi [Get("/users/list")] [Headers("X-Emoji")] // Remove the X-Emoji header Task GetUsers(); - + [Get("/users/{user}")] [Headers("X-Emoji:")] // Redefine the X-Emoji header as empty Task GetUser(string user); - + [Post("/users/new")] Task CreateUser([Body] User user, [Header("X-Emoji")] string emoji); } @@ -664,14 +664,60 @@ public interface IGitHubApi // No X-Emoji header var users = await GetUsers(); -// X-Emoji: +// X-Emoji: var user = await GetUser("octocat"); // No X-Emoji header -await CreateUser(user, null); +await CreateUser(user, null); -// X-Emoji: -await CreateUser(user, ""); +// X-Emoji: +await CreateUser(user, ""); +``` + +### Passing state into custom HttpClient middleware + +If there is runtime state that you need to pass to a `DelegatingHandler` you can add a property with a dynamic value to the underlying `HttpRequestMessage.Properties` +by applying a `Property` attribute to a parameter: + +```csharp +public interface IGitHubApi +{ + [Post("/users/new")] + Task CreateUser([Body] User user, [Property("SomeKey")] string someValue); + + [Post("/users/new")] + Task CreateUser([Body] User user, [Property] string someOtherKey); +} +``` + +The attribute constructor optionally takes a string which becomes the key in the `HttpRequestMessage.Properties` dictionary. +If no key is explicitly defined then the name of the parameter becomes the key. +If a key is defined multiple times the value in `HttpRequestMessage.Properties` will be overwritten. +The parameter itself can be any `object`. Properties can be accessed inside a `DelegatingHandler` as follows: + +```csharp +class RequestPropertyHandler : DelegatingHandler +{ + public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {} + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // See if the request has a the property + if(request.Properties.ContainsKey("SomeKey") + { + var someProperty = request.Properties["SomeKey"]; + //do stuff + } + + if(request.Properties.ContainsKey("someOtherKey") + { + var someOtherProperty = request.Properties["someOtherKey"]; + //do stuff + } + + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } +} ``` ### Multipart uploads @@ -805,10 +851,10 @@ public interface IReallyExcitingCrudApi where T : class Which can be used like this: ```csharp -// The "/users" part here is kind of important if you want it to work for more +// The "/users" part here is kind of important if you want it to work for more // than one type (unless you have a different domain for each type) -var api = RestService.For>("http://api.example.com/users"); -``` +var api = RestService.For>("http://api.example.com/users"); +``` ### Interface inheritance When multiple services that need to be kept separate share a number of APIs, it is possible to leverage interface inheritance to avoid having to define the same Refit methods multiple times in different services: @@ -875,7 +921,7 @@ Here `IAmInterfaceC.Foo` would use the header attribute inherited from `IAmInter ### Using HttpClientFactory -Refit has first class support for the ASP.Net Core 2.1 HttpClientFactory. Add a reference to `Refit.HttpClientFactory` and call +Refit has first class support for the ASP.Net Core 2.1 HttpClientFactory. Add a reference to `Refit.HttpClientFactory` and call the provided extension method in your `ConfigureServices` method to configure your Refit interface: ```csharp @@ -886,9 +932,9 @@ services.AddRefitClient() // .SetHandlerLifetime(TimeSpan.FromMinutes(2)); ``` -Optionally, a `RefitSettings` object can be included: +Optionally, a `RefitSettings` object can be included: ```csharp -var settings = new RefitSettings(); +var settings = new RefitSettings(); // Configure refit settings here services.AddRefitClient(settings) @@ -944,7 +990,7 @@ catch (ApiException exception) // ... ``` -You can also override default exceptions behavior by providing custom exception factory in `RefitSettings`. For example, you can suppress all exceptions with the following: +You can also override default exceptions behavior by providing custom exception factory in `RefitSettings`. For example, you can suppress all exceptions with the following: ```csharp var nullTask = Task.FromResult(null); diff --git a/Refit.Tests/MultipartTests.cs b/Refit.Tests/MultipartTests.cs index f7854ba6b..444326d0e 100644 --- a/Refit.Tests/MultipartTests.cs +++ b/Refit.Tests/MultipartTests.cs @@ -45,6 +45,10 @@ public interface IRunscopeApi [Post("/")] Task UploadString([AliasAs("SomeStringAlias")]string someString); + [Multipart] + [Post("/")] + Task UploadStringWithHeaderAndRequestProperty([Header("Authorization")] string authorization, [Property("SomeProperty")] string someProperty, [AliasAs("SomeStringAlias")]string someString); + [Multipart] [Post("/")] Task UploadFileInfo(IEnumerable fileInfos, FileInfo anotherFile); @@ -290,6 +294,46 @@ public async Task MultipartUploadShouldWorkWithString() var result = await fixture.UploadString(text); } + [Fact] + public async Task MultipartUploadShouldWorkWithHeaderAndRequestProperty() + { + const string text = "This is random text"; + const string someHeader = "someHeader"; + const string someProperty = "someProperty"; + + var handler = new MockHttpMessageHandler + { + RequestAsserts = async message => + { + Assert.Equal(someHeader, message.Headers.Authorization.ToString()); + Assert.Single(message.Properties); + Assert.Equal(someProperty, message.Properties["SomeProperty"]); + }, + Asserts = async content => + { + var parts = content.ToList(); + + Assert.Single(parts); + + Assert.Equal("SomeStringAlias", parts[0].Headers.ContentDisposition.Name); + Assert.Null(parts[0].Headers.ContentDisposition.FileName); + Assert.Equal("text/plain", parts[0].Headers.ContentType.MediaType); + Assert.Equal("utf-8", parts[0].Headers.ContentType.CharSet); + var str = await parts[0].ReadAsStringAsync(); + Assert.Equal(text, str); + } + }; + + var settings = new RefitSettings() + { + HttpMessageHandlerFactory = () => handler + }; + + + var fixture = RestService.For(BaseAddress, settings); + var result = await fixture.UploadStringWithHeaderAndRequestProperty(someHeader,someProperty, text); + } + [Fact] public async Task MultipartUploadShouldWorkWithStreamPart() { diff --git a/Refit.Tests/RefitStubs.Net46.cs b/Refit.Tests/RefitStubs.Net46.cs index c98aa3bfe..896de6bde 100644 --- a/Refit.Tests/RefitStubs.Net46.cs +++ b/Refit.Tests/RefitStubs.Net46.cs @@ -2635,6 +2635,14 @@ Task IRunscopeApi.UploadString(string someString) return (Task)func(Client, arguments); } + /// + Task IRunscopeApi.UploadStringWithHeaderAndRequestProperty(string authorization, string someProperty, string someString) + { + var arguments = new object[] { authorization, someProperty, someString }; + var func = requestBuilder.BuildRestResultFuncForMethod("UploadStringWithHeaderAndRequestProperty", new Type[] { typeof(string), typeof(string), typeof(string) }); + return (Task)func(Client, arguments); + } + /// Task IRunscopeApi.UploadFileInfo(IEnumerable fileInfos, FileInfo anotherFile) { diff --git a/Refit.Tests/RefitStubs.Net5.cs b/Refit.Tests/RefitStubs.Net5.cs index c98aa3bfe..896de6bde 100644 --- a/Refit.Tests/RefitStubs.Net5.cs +++ b/Refit.Tests/RefitStubs.Net5.cs @@ -2635,6 +2635,14 @@ Task IRunscopeApi.UploadString(string someString) return (Task)func(Client, arguments); } + /// + Task IRunscopeApi.UploadStringWithHeaderAndRequestProperty(string authorization, string someProperty, string someString) + { + var arguments = new object[] { authorization, someProperty, someString }; + var func = requestBuilder.BuildRestResultFuncForMethod("UploadStringWithHeaderAndRequestProperty", new Type[] { typeof(string), typeof(string), typeof(string) }); + return (Task)func(Client, arguments); + } + /// Task IRunscopeApi.UploadFileInfo(IEnumerable fileInfos, FileInfo anotherFile) { diff --git a/Refit.Tests/RefitStubs.NetCore2.cs b/Refit.Tests/RefitStubs.NetCore2.cs index c98aa3bfe..896de6bde 100644 --- a/Refit.Tests/RefitStubs.NetCore2.cs +++ b/Refit.Tests/RefitStubs.NetCore2.cs @@ -2635,6 +2635,14 @@ Task IRunscopeApi.UploadString(string someString) return (Task)func(Client, arguments); } + /// + Task IRunscopeApi.UploadStringWithHeaderAndRequestProperty(string authorization, string someProperty, string someString) + { + var arguments = new object[] { authorization, someProperty, someString }; + var func = requestBuilder.BuildRestResultFuncForMethod("UploadStringWithHeaderAndRequestProperty", new Type[] { typeof(string), typeof(string), typeof(string) }); + return (Task)func(Client, arguments); + } + /// Task IRunscopeApi.UploadFileInfo(IEnumerable fileInfos, FileInfo anotherFile) { diff --git a/Refit.Tests/RefitStubs.NetCore3.cs b/Refit.Tests/RefitStubs.NetCore3.cs index c98aa3bfe..896de6bde 100644 --- a/Refit.Tests/RefitStubs.NetCore3.cs +++ b/Refit.Tests/RefitStubs.NetCore3.cs @@ -2635,6 +2635,14 @@ Task IRunscopeApi.UploadString(string someString) return (Task)func(Client, arguments); } + /// + Task IRunscopeApi.UploadStringWithHeaderAndRequestProperty(string authorization, string someProperty, string someString) + { + var arguments = new object[] { authorization, someProperty, someString }; + var func = requestBuilder.BuildRestResultFuncForMethod("UploadStringWithHeaderAndRequestProperty", new Type[] { typeof(string), typeof(string), typeof(string) }); + return (Task)func(Client, arguments); + } + /// Task IRunscopeApi.UploadFileInfo(IEnumerable fileInfos, FileInfo anotherFile) { diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index b39ade307..ad04b95d7 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -61,7 +61,16 @@ public interface IRestMethodInfoTests Task FetchSomeStuffWithDynamicHeader(int id, [Header("Authorization")] string authorization); [Get("/foo")] - Task FetchSomeStuffWithDynamicHeaderQueryParamAndArrayQueryParam([Header("Authorization")] string authorization, int id, [Query(CollectionFormat.Multi)] string[] someArray); + Task FetchSomeStuffWithDynamicHeaderQueryParamAndArrayQueryParam([Header("Authorization")] string authorization, int id, [Query(CollectionFormat.Multi)] string[] someArray, [Property("SomeProperty")] object someValue); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someValue); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey(int id, [Property("SomeProperty")] object someValue1, [Property("SomeProperty")] object someValue2); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestPropertyWithoutKey(int id, [Property] object someValue, [Property("")] object someOtherValue); [Post("/foo/{id}")] Task OhYeahValueTypes(int id, [Body] int whatever); @@ -540,6 +549,7 @@ public void DynamicHeadersShouldWork() Assert.Equal("id", fixture.ParameterMap[0].Name); Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.RequestPropertyParameterMap); Assert.Null(fixture.BodyParameterInfo); Assert.Equal("Authorization", fixture.HeaderParameterMap[1]); @@ -548,6 +558,50 @@ public void DynamicHeadersShouldWork() Assert.Equal(2, fixture.Headers.Count); } + [Fact] + public void DynamicRequestPropertiesShouldWork() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicRequestProperty))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Equal("SomeProperty", fixture.RequestPropertyParameterMap[1]); + } + + [Fact] + public void DynamicRequestPropertiesWithoutKeysShouldDefaultKeyToParameterName() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicRequestPropertyWithoutKey))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Equal("someValue", fixture.RequestPropertyParameterMap[1]); + Assert.Equal("someOtherValue", fixture.RequestPropertyParameterMap[2]); + } + + [Fact] + public void DynamicRequestPropertiesWithDuplicateKeysDontBlowUp() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == nameof(IRestMethodInfoTests.FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey))); + Assert.Equal("id", fixture.ParameterMap[0].Name); + Assert.Equal(ParameterType.Normal, fixture.ParameterMap[0].Type); + Assert.Empty(fixture.QueryParameterMap); + Assert.Empty(fixture.HeaderParameterMap); + Assert.Null(fixture.BodyParameterInfo); + + Assert.Equal("SomeProperty", fixture.RequestPropertyParameterMap[1]); + Assert.Equal("SomeProperty", fixture.RequestPropertyParameterMap[2]); + } + [Fact] public void ValueTypesDontBlowUpBuffered() { @@ -668,6 +722,7 @@ public void ParameterMappingWithHeaderQueryParamAndQueryArrayParam() Assert.Equal("GET", fixture.HttpMethod.Method); Assert.Equal(2, fixture.QueryParameterMap.Count); Assert.Single(fixture.HeaderParameterMap); + Assert.Single(fixture.RequestPropertyParameterMap); } } @@ -721,6 +776,15 @@ public interface IDummyHttpApi [Post("/foo/bar/{id}")] Task PostSomeStuffWithCustomHeader(int id, [Body] object body, [Header("X-Emoji")] string emoji); + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestProperty(int id, [Property("SomeProperty")] object someProperty); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey(int id, [Property("SomeProperty")] object someValue1, [Property("SomeProperty")] object someValue2); + + [Get("/foo/bar/{id}")] + Task FetchSomeStuffWithDynamicRequestPropertyWithoutKey(int id, [Property] object someValue, [Property("")] object someOtherValue); + [Get("/string")] Task FetchSomeStuffWithoutFullPath(); @@ -1434,6 +1498,45 @@ public void AddCustomHeadersToRequestHeadersOnly() Assert.False(output.Content.Headers.Contains("X-Emoji"), "Content headers include X-Emoji header"); } + [Fact] + public void DynamicRequestPropertiesShouldBeInProperties() + { + var someProperty = new object(); + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicRequestProperty)); + var output = factory(new object[] { 6, someProperty }); + + Assert.NotEmpty(output.Properties); + Assert.Equal(someProperty, output.Properties["SomeProperty"]); + } + + [Fact] + public void DynamicRequestPropertiesWithDefaultKeysShouldBeInProperties() + { + var someProperty = new object(); + var someOtherProperty = new object(); + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicRequestPropertyWithoutKey)); + var output = factory(new object[] { 6, someProperty, someOtherProperty }); + + Assert.NotEmpty(output.Properties); + Assert.Equal(someProperty, output.Properties["someValue"]); + Assert.Equal(someOtherProperty, output.Properties["someOtherValue"]); + } + + [Fact] + public void DynamicRequestPropertiesWithDuplicateKeyShouldOverwritePreviousProperty() + { + var someProperty = new object(); + var someOtherProperty = new object(); + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.FetchSomeStuffWithDynamicRequestPropertyWithDuplicateKey)); + var output = factory(new object[] { 6, someProperty, someOtherProperty }); + + Assert.Single(output.Properties); + Assert.Equal(someOtherProperty, output.Properties["SomeProperty"]); + } + [Fact] public void HttpClientShouldPrefixedAbsolutePathToTheRequestUri() { @@ -2094,7 +2197,7 @@ public static Func RunRequest(this IRequestBui { } - + return testHttpMessageHandler; }; } diff --git a/Refit/Attributes.cs b/Refit/Attributes.cs index c8123a35a..73817f100 100644 --- a/Refit/Attributes.cs +++ b/Refit/Attributes.cs @@ -127,7 +127,7 @@ public enum BodySerializationMethod UrlEncoded, /// - /// Encodes everything using the ContentSerializer in RefitSettings + /// Encodes everything using the ContentSerializer in RefitSettings /// Serialized } @@ -206,6 +206,27 @@ public HeaderAttribute(string header) public string Header { get; } } + /// + /// Used to store the value in HttpRequestMessage.Properties for further processing in a custom DelegatingHandler. + /// If a string is supplied to the constructor then it will be used as the key in the HttpRequestMessage.Properties dictionary. + /// If no key is specified then the key will be defaulted to the name of the parameter. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public class PropertyAttribute : Attribute + { + public PropertyAttribute() { } + + public PropertyAttribute(string key) + { + Key = key; + } + + /// + /// Specifies the key under which to store the value on the HttpRequestMessage.Properties dictionary. + /// + public string Key { get; } + } + [AttributeUsage(AttributeTargets.Parameter)] public class AuthorizeAttribute : Attribute { @@ -255,7 +276,7 @@ public QueryAttribute(CollectionFormat collectionFormat) public string Delimiter { get; protected set; } = "."; /// - /// Used to customize the name of the encoded value. + /// Used to customize the name of the encoded value. /// /// /// Gets combined with in the format var name = $"{Prefix}{Delimiter}{originalFieldName}" diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index a115d7302..75a7bf9fd 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -435,7 +435,7 @@ Func> BuildRequestFactoryForMethod(RestMethod return async paramList => { - // make sure we strip out any cancelation tokens + // make sure we strip out any cancellation tokens if (paramsContainsCancellationToken) { paramList = paramList.Where(o => o == null || o.GetType() != typeof(CancellationToken)).ToArray(); @@ -457,6 +457,8 @@ Func> BuildRequestFactoryForMethod(RestMethod var urlTarget = (basePath == "/" ? string.Empty : basePath) + restMethod.RelativePath; var queryParamsToAdd = new List>(); var headersToAdd = new Dictionary(restMethod.Headers); + var propertiesToAdd = new Dictionary(); + RestMethodParameterInfo parameterInfo = null; for (var i = 0; i < paramList.Length; i++) @@ -587,6 +589,13 @@ Func> BuildRequestFactoryForMethod(RestMethod headersToAdd["Authorization"] = $"{restMethod.AuthorizeParameterInfo.Item1} {param}"; } + //if property, add to populate into HttpRequestMessage.Properties + if (restMethod.RequestPropertyParameterMap.ContainsKey(i)) + { + propertiesToAdd[restMethod.RequestPropertyParameterMap[i]] = param; + isParameterMappedToRequest = true; + } + // ignore nulls and already processed parameters if (isParameterMappedToRequest || param == null) continue; @@ -666,6 +675,11 @@ Func> BuildRequestFactoryForMethod(RestMethod } } + foreach (var property in propertiesToAdd) + { + ret.Properties[property.Key] = property.Value; + } + // NB: The URI methods in .NET are dumb. Also, we do this // UriBuilder business so that we preserve any hardcoded query // parameters as well as add the parameterized ones. diff --git a/Refit/RestMethodInfo.cs b/Refit/RestMethodInfo.cs index 9e2753908..1c79d0c2e 100644 --- a/Refit/RestMethodInfo.cs +++ b/Refit/RestMethodInfo.cs @@ -24,6 +24,7 @@ public class RestMethodInfo public ParameterInfo CancellationToken { get; set; } public Dictionary Headers { get; set; } public Dictionary HeaderParameterMap { get; set; } + public Dictionary RequestPropertyParameterMap { get; set; } public Tuple BodyParameterInfo { get; set; } public Tuple AuthorizeParameterInfo { get; set; } public Dictionary QueryParameterMap { get; set; } @@ -75,6 +76,7 @@ public RestMethodInfo(Type targetInterface, MethodInfo methodInfo, RefitSettings Headers = ParseHeaders(methodInfo); HeaderParameterMap = BuildHeaderParameterMap(parameterList); + RequestPropertyParameterMap = BuildRequestPropertyMap(parameterList); // get names for multipart attachments AttachmentNameMap = new Dictionary>(); @@ -82,7 +84,7 @@ public RestMethodInfo(Type targetInterface, MethodInfo methodInfo, RefitSettings { for (var i = 0; i < parameterList.Count; i++) { - if (ParameterMap.ContainsKey(i) || HeaderParameterMap.ContainsKey(i)) + if (ParameterMap.ContainsKey(i) || HeaderParameterMap.ContainsKey(i) || RequestPropertyParameterMap.ContainsKey(i)) { continue; } @@ -100,11 +102,12 @@ public RestMethodInfo(Type targetInterface, MethodInfo methodInfo, RefitSettings { if (ParameterMap.ContainsKey(i) || HeaderParameterMap.ContainsKey(i) || + RequestPropertyParameterMap.ContainsKey(i) || (BodyParameterInfo != null && BodyParameterInfo.Item3 == i) || (AuthorizeParameterInfo != null && AuthorizeParameterInfo.Item2 == i)) { continue; - } + } QueryParameterMap.Add(i, GetUrlNameForParameter(parameterList[i])); } @@ -123,6 +126,28 @@ public RestMethodInfo(Type targetInterface, MethodInfo methodInfo, RefitSettings || ReturnResultType == typeof(IApiResponse)); } + private Dictionary BuildRequestPropertyMap(List parameterList) + { + var requestPropertyMap = new Dictionary(); + + for (var i = 0; i < parameterList.Count; i++) + { + var param = parameterList[i]; + var requestProperty = param.GetCustomAttributes(true) + .OfType() + .FirstOrDefault(); + + if (requestProperty != null) + { + var propertyKey = !string.IsNullOrEmpty(requestProperty.Key) ? requestProperty.Key : param.Name; + requestPropertyMap[i] = propertyKey; + } + + } + + return requestPropertyMap; + } + private PropertyInfo[] GetParameterProperties(ParameterInfo parameter) { return parameter.ParameterType