Skip to content

Commit

Permalink
Make the RestMethodInfo available in the request options
Browse files Browse the repository at this point in the history
While having the interface type available is nice, it might not be enough if using reflection on the called method is desired. Providing the `RestMethodInfo` opens new possibilities to introspect the called method.

Currently, to workaround this missing information, I'm using a `methodName` parameter decorated with a `[Property]` attribute and a default value to make it possible to introspect the return type of the called method.

At the interface method:

```csharp
Task<ApiResponse<User>> GetUserAsync(string id, [Property] string methodName = nameof(GetUserAsync));
```

Inside the HTTP message handler:

```csharp
request.Options.TryGetValue(new HttpRequestOptionsKey<Type>(HttpRequestMessageOptions.InterfaceType), out var refitInterfaceType)
request.Options.TryGetValue(new HttpRequestOptionsKey<string>("methodName"), out var methodName)
var methodReturnType = refitInterfaceType.GetMethod(methodName).ReturnType;
```

With the new `RestMethodInfo`, it becomes possible to access the method without having to pollute the interface definition:

```csharp
request.Options.TryGetValue(HttpRequestMessageOptions.RestMethodInfoKey, out var restMethodInfo);
var methodReturnType = restMethodInfo.MethodInfo.ReturnType;
```

Also, the new `HttpRequestMessageOptions.InterfaceTypeKey` and  `HttpRequestMessageOptions.RestMethodInfoKey` (available on .NET 5 onwards) make it easier to access the request options.
  • Loading branch information
0xced committed Feb 15, 2022
1 parent 246ee8d commit 53ab900
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 21 deletions.
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ services
* [Removing headers](#removing-headers)
* [Passing state into DelegatingHandlers](#passing-state-into-delegatinghandlers)
* [Support for Polly and Polly.Context](#support-for-polly-and-pollycontext)
* [Target Interface type](#target-interface-type)
* [Target Interface type and method info](#target-interface-type-and-method-info)
* [Multipart uploads](#multipart-uploads)
* [Retrieving the response](#retrieving-the-response)
* [Using generic interfaces](#using-generic-interfaces)
Expand Down Expand Up @@ -809,7 +809,7 @@ Because Refit supports `HttpClientFactory` it is possible to configure Polly pol
If your policy makes use of `Polly.Context` this can be passed via Refit by adding `[Property("PollyExecutionContext")] Polly.Context context`
as behind the scenes `Polly.Context` is simply stored in `HttpRequestMessage.Properties` under the key `PollyExecutionContext` and is of type `Polly.Context`

#### Target Interface Type
#### Target Interface Type and method info

There may be times when you want to know what the target interface type is of the Refit instance. An example is where you
have a derived interface that implements a common base like this:
Expand Down Expand Up @@ -855,7 +855,33 @@ class RequestPropertyHandler : DelegatingHandler
```
[//]: # ({% endraw %})

Note: in .NET 5 `HttpRequestMessage.Properties` has been marked `Obsolete` and Refit will instead populate the value into the new `HttpRequestMessage.Options`.
The full method information (`RestMethodInfo`) is also always available in the request options. The `RestMethodInfo` contains more information about the method being called such as the full `MethodInfo` when using reflection is needed:

[//]: # ({% raw %})
```csharp
class RequestPropertyHandler : DelegatingHandler
{
public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Get the method info
if (request.Options.TryGetValue(HttpRequestMessageOptions.RestMethodInfoKey, out RestMethodInfo restMethodInfo))
{
var builder = new UriBuilder(request.RequestUri);
// Alter the Path in some way based on the method info or an attribute on it
builder.Path = $"/{restMethodInfo.MethodInfo.Name}{builder.Path}";
// Set the new Uri on the outgoing message
request.RequestUri = builder.Uri;
}

return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
```
[//]: # ({% endraw %})

Note: in .NET 5 `HttpRequestMessage.Properties` has been marked `Obsolete` and Refit will instead populate the value into the new `HttpRequestMessage.Options`. Refit provides `HttpRequestMessageOptions.InterfaceTypeKey` and `HttpRequestMessageOptions.RestMethodInfoKey` to respectively access the interface type and REST method info from the options.

### Multipart uploads

Expand Down
4 changes: 2 additions & 2 deletions Refit.Tests/MultipartTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,12 @@ public async Task MultipartUploadShouldWorkWithHeaderAndRequestProperty()
Assert.Equal(someHeader, message.Headers.Authorization.ToString());

#if NET5_0_OR_GREATER
Assert.Equal(2, message.Options.Count());
Assert.Equal(3, message.Options.Count());
Assert.Equal(someProperty, ((IDictionary<string, object>)message.Options)["SomeProperty"]);
#endif

#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal(2, message.Properties.Count);
Assert.Equal(3, message.Properties.Count);
Assert.Equal(someProperty, message.Properties["SomeProperty"]);
#pragma warning restore CS0618 // Type or member is obsolete
},
Expand Down
35 changes: 30 additions & 5 deletions Refit.Tests/RequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ public void ParameterMappingSmokeTest()
Assert.Empty(fixture.QueryParameterMap);
Assert.Null(fixture.BodyParameterInfo);
}

[Fact]
public void ParameterMappingWithTheSameIdInAFewPlaces()
{
Expand Down Expand Up @@ -2124,14 +2124,14 @@ public void DynamicRequestPropertiesShouldBeInProperties(string interfaceMethodN
[Fact]
public void InterfaceTypeShouldBeInProperties()
{
var someProperty = new object();
var fixture = new RequestBuilderImplementation<IContainAandB>();
var factory = fixture.BuildRequestFactoryForMethod(nameof(IContainAandB.Ping));
var output = factory(new object[] { });

#if NET5_0_OR_GREATER
Assert.NotEmpty(output.Options);
Assert.Equal(typeof(IContainAandB), ((IDictionary<string, object>)output.Options)[HttpRequestMessageOptions.InterfaceType]);
output.Options.TryGetValue(HttpRequestMessageOptions.InterfaceTypeKey, out var interfaceType);
Assert.Equal(typeof(IContainAandB), interfaceType);
#endif

#pragma warning disable CS0618 // Type or member is obsolete
Expand All @@ -2141,6 +2141,31 @@ public void InterfaceTypeShouldBeInProperties()

}

[Fact]
public void RestMethodInfoShouldBeInProperties()
{
var fixture = new RequestBuilderImplementation<IContainAandB>();
var factory = fixture.BuildRequestFactoryForMethod(nameof(IContainAandB.Ping));
var output = factory(new object[] { });

RestMethodInfo restMethodInfo;
#if NET5_0_OR_GREATER
Assert.NotEmpty(output.Options);
output.Options.TryGetValue(HttpRequestMessageOptions.RestMethodInfoKey, out restMethodInfo);
Assert.NotNull(restMethodInfo);
Assert.Equal(nameof(IContainAandB.Ping), restMethodInfo.Name);
Assert.Equal(typeof(IAmInterfaceA), restMethodInfo.MethodInfo.DeclaringType);
#endif

#pragma warning disable CS0618 // Type or member is obsolete
Assert.NotEmpty(output.Properties);
restMethodInfo = (RestMethodInfo)(output.Properties[HttpRequestMessageOptions.RestMethodInfo]);
Assert.NotNull(restMethodInfo);
Assert.Equal(nameof(IContainAandB.Ping), restMethodInfo.Name);
Assert.Equal(typeof(IAmInterfaceA), restMethodInfo.MethodInfo.DeclaringType);
#pragma warning restore CS0618 // Type or member is obsolete
}

[Fact]
public void DynamicRequestPropertiesWithDefaultKeysShouldBeInProperties()
{
Expand Down Expand Up @@ -2174,12 +2199,12 @@ public void DynamicRequestPropertiesWithDuplicateKeyShouldOverwritePreviousPrope


#if NET5_0_OR_GREATER
Assert.Equal(2, output.Options.Count());
Assert.Equal(3, output.Options.Count());
Assert.Equal(someOtherProperty, ((IDictionary<string, object>)output.Options)["SomeProperty"]);
#endif

#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal(2, output.Properties.Count);
Assert.Equal(3, output.Properties.Count);
Assert.Equal(someOtherProperty, output.Properties["SomeProperty"]);
#pragma warning restore CS0618 // Type or member is obsolete
}
Expand Down
28 changes: 21 additions & 7 deletions Refit/HttpRequestMessageProperties.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Refit
namespace Refit
{
/// <summary>
/// Contains Refit-defined properties on the HttpRequestMessage.Properties/Options
Expand All @@ -15,5 +9,25 @@ public static class HttpRequestMessageOptions
/// Returns the <see cref="System.Type"/> of the top-level interface where the method was called from
/// </summary>
public static string InterfaceType { get; } = "Refit.InterfaceType";

#if NET5_0_OR_GREATER
/// <summary>
/// A typed key to access the <see cref="System.Type"/> of the top-level interface where the method was called from
/// on the <see cref="System.Net.Http.HttpRequestMessage.Options"/>.
/// </summary>
public static System.Net.Http.HttpRequestOptionsKey<System.Type> InterfaceTypeKey { get; } = new(InterfaceType);
#endif

/// <summary>
/// Returns the <see cref="Refit.RestMethodInfo"/> of the method that was called
/// </summary>
public static string RestMethodInfo { get; } = "Refit.RestMethodInfo";

#if NET5_0_OR_GREATER
/// <summary>
/// A typed key to access the <see cref="Refit.RestMethodInfo"/> of the method that was called
/// </summary>
public static System.Net.Http.HttpRequestOptionsKey<RestMethodInfo> RestMethodInfoKey { get; } = new(RestMethodInfo);
#endif
}
}
8 changes: 4 additions & 4 deletions Refit/RequestBuilderImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -733,15 +733,15 @@ Func<object[], HttpRequestMessage> BuildRequestFactoryForMethod(RestMethodInfo r
#endif
}

// Always add the top-level type of the interface to the properties
// Always add the top-level type of the interface and the rest method info to the options/properties
#if NET5_0_OR_GREATER
ret.Options.Set(new HttpRequestOptionsKey<Type>(HttpRequestMessageOptions.InterfaceType), TargetType);
ret.Options.Set(HttpRequestMessageOptions.InterfaceTypeKey, TargetType);
ret.Options.Set(HttpRequestMessageOptions.RestMethodInfoKey, restMethod);
#else
ret.Properties[HttpRequestMessageOptions.InterfaceType] = TargetType;
ret.Properties[HttpRequestMessageOptions.RestMethodInfo] = restMethod;
#endif

;

// 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.
Expand Down

0 comments on commit 53ab900

Please sign in to comment.