Skip to content

Commit

Permalink
Feature: allow developers to inject the MethodInfo as a Property (#1367)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris Pulman <chris.pulman@yahoo.com>
Co-authored-by: Glenn <5834289+glennawatson@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 12, 2023
1 parent 77f084f commit b06ef7c
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 24 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ services
* [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)
* [MethodInfo of the method on the Refit client interface that was invoked](#methodinfo-of-the-method-on-the-refit-client-interface-that-was-invoked)
* [Multipart uploads](#multipart-uploads)
* [Retrieving the response](#retrieving-the-response)
* [Using generic interfaces](#using-generic-interfaces)
Expand Down Expand Up @@ -858,6 +859,54 @@ class RequestPropertyHandler : DelegatingHandler

Note: in .NET 5 `HttpRequestMessage.Properties` has been marked `Obsolete` and Refit will instead populate the value into the new `HttpRequestMessage.Options`.

#### MethodInfo of the method on the Refit client interface that was invoked

There may be times when you want access to the `MethodInfo` of the method on the Refit client interface that was invoked. An example is where you
want to decorate the method with a custom attribute in order to control some aspect of behavior in a `DelegatingHandler`:

```csharp
public interface ISomeAPI
{
[SomeCustomAttribute("SomeValue")]
[Get("/{id}")]
Task<ApiResponse<SomeClass>> GetById(int id);
}
```
To make the `MethodInfo` available you need to opt-in via the `RefitSettings` like so:

```csharp
services.AddRefitClient<ISomeAPI>(provider => new RefitSettings
{
InjectMethodInfoAsProperty = true
})
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
```

You can access the `MethodInfo` for use in a handler and then get the custom attributes:

```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 MethodInfo
MethodInfo methodInfo;
request.Options.TryGetValue(HttpRequestMessageOptions.MethodInfoKey, out methodInfo);

//get the custom attributes
var customAttributes = methodInfo.CustomAttributes;

//insert your logic here
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
```

Note: for .NET Core 3.1 and lower this will be available via `HttpRequestMessage.Properties[HttpRequestMessageOptions.MethodInfo]`.

### Multipart uploads

Methods decorated with `Multipart` attribute will be submitted with multipart content type.
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 NET6_0_OR_GREATER
Assert.Equal(3, message.Options.Count());
Assert.Equal(2, message.Options.Count());
Assert.Equal(someProperty, ((IDictionary<string, object>)message.Options)["SomeProperty"]);
#endif
#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal(3, message.Properties.Count);
Assert.Equal(2, message.Properties.Count);
Assert.Equal(someProperty, message.Properties["SomeProperty"]);
#pragma warning restore CS0618 // Type or member is obsolete
},
Expand Down
54 changes: 41 additions & 13 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 @@ -2142,23 +2142,51 @@ public void InterfaceTypeShouldBeInProperties()
}

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

MethodInfo methodInfo;
#if NET6_0_OR_GREATER
Assert.NotEmpty(output.Options);
Assert.True(output.Options.TryGetValue(new HttpRequestOptionsKey<RestMethodInfo>(HttpRequestMessageOptions.RestMethodInfo), out var restMethodInfo));
#else
output.Options.TryGetValue(HttpRequestMessageOptions.MethodInfoKey, out methodInfo);
Assert.NotNull(methodInfo);
Assert.Equal(nameof(IContainAandB.Ping), methodInfo.Name);
Assert.Equal(typeof(IAmInterfaceA), methodInfo.DeclaringType);
#endif

#pragma warning disable CS0618 // Type or member is obsolete
Assert.NotEmpty(output.Properties);
Assert.True(output.Properties.TryGetValue(HttpRequestMessageOptions.RestMethodInfo, out var restMethodInfoObj));
Assert.IsType<RestMethodInfo>(restMethodInfoObj);
var restMethodInfo = restMethodInfoObj as RestMethodInfo;
methodInfo = (MethodInfo)(output.Properties[HttpRequestMessageOptions.MethodInfo]);
Assert.NotNull(methodInfo);
Assert.Equal(nameof(IContainAandB.Ping), methodInfo.Name);
Assert.Equal(typeof(IAmInterfaceA), methodInfo.DeclaringType);
#pragma warning restore CS0618 // Type or member is obsolete
}

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

MethodInfo methodInfo;
#if NET6_0_OR_GREATER
Assert.NotEmpty(output.Options);
output.Options.TryGetValue(HttpRequestMessageOptions.MethodInfoKey, out methodInfo);
Assert.Null(methodInfo);
#endif
Assert.Equal(nameof(IContainAandB.Ping), restMethodInfo.Name);

#pragma warning disable CS0618 // Type or member is obsolete
Assert.NotEmpty(output.Properties);
Assert.False(output.Properties.ContainsKey(HttpRequestMessageOptions.MethodInfo));
#pragma warning restore CS0618 // Type or member is obsolete
}

[Fact]
Expand Down Expand Up @@ -2194,12 +2222,12 @@ public void DynamicRequestPropertiesWithDuplicateKeyShouldOverwritePreviousPrope


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

#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal(3, output.Properties.Count);
Assert.Equal(2, output.Properties.Count);
Assert.Equal(someOtherProperty, output.Properties["SomeProperty"]);
#pragma warning restore CS0618 // Type or member is obsolete
}
Expand Down
23 changes: 20 additions & 3 deletions Refit/HttpRequestMessageProperties.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Refit
using System.Reflection;

namespace Refit
{
/// <summary>
/// Contains Refit-defined properties on the HttpRequestMessage.Properties/Options
Expand All @@ -10,9 +12,24 @@ public static class HttpRequestMessageOptions
/// </summary>
public static string InterfaceType { get; } = "Refit.InterfaceType";

#if NET6_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="System.Reflection.MethodInfo"/> of the method that was called
/// </summary>
public static string MethodInfo { get; } = "Refit.MethodInfo";

#if NET6_0_OR_GREATER
/// <summary>
/// Returns the <see cref="Refit.RestMethodInfo"/> of the top-level interface
/// A typed key to access the <see cref="System.Reflection.MethodInfo"/> of the method that was called
/// </summary>
public static string RestMethodInfo { get; } = "Refit.RestMethodInfo";
public static System.Net.Http.HttpRequestOptionsKey<MethodInfo> MethodInfoKey { get; } = new(MethodInfo);
#endif
}
}
27 changes: 26 additions & 1 deletion Refit/RefitSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,35 @@ public RefitSettings()
ExceptionFactory = new DefaultApiExceptionFactory(this).CreateAsync;
}


#if NET5_0_OR_GREATER
/// <summary>
/// Creates a new <see cref="RefitSettings"/> instance with the specified parameters
/// </summary>
/// <param name="contentSerializer">The <see cref="IHttpContentSerializer"/> instance to use</param>
/// <param name="urlParameterFormatter">The <see cref="IUrlParameterFormatter"/> instance to use (defaults to <see cref="DefaultUrlParameterFormatter"/>)</param>
/// <param name="formUrlEncodedParameterFormatter">The <see cref="IFormUrlEncodedParameterFormatter"/> instance to use (defaults to <see cref="DefaultFormUrlEncodedParameterFormatter"/>)</param>
/// <param name="injectMethodInfoAsProperty">Controls injecting the <see cref="MethodInfo"/> of the method on the Refit client interface that was invoked into the HttpRequestMessage.Options (defaults to false)</param>
#else
/// <summary>
/// Creates a new <see cref="RefitSettings"/> instance with the specified parameters
/// </summary>
/// <param name="contentSerializer">The <see cref="IHttpContentSerializer"/> instance to use</param>
/// <param name="urlParameterFormatter">The <see cref="IUrlParameterFormatter"/> instance to use (defaults to <see cref="DefaultUrlParameterFormatter"/>)</param>
/// <param name="formUrlEncodedParameterFormatter">The <see cref="IFormUrlEncodedParameterFormatter"/> instance to use (defaults to <see cref="DefaultFormUrlEncodedParameterFormatter"/>)</param>
/// <param name="injectMethodInfoAsProperty">Controls injecting the <see cref="MethodInfo"/> of the method on the Refit client interface that was invoked into the HttpRequestMessage.Properties (defaults to false)</param>
#endif
public RefitSettings(
IHttpContentSerializer contentSerializer,
IUrlParameterFormatter? urlParameterFormatter = null,
IFormUrlEncodedParameterFormatter? formUrlEncodedParameterFormatter = null)
IFormUrlEncodedParameterFormatter? formUrlEncodedParameterFormatter = null,
bool injectMethodInfoAsProperty = false)
{
ContentSerializer = contentSerializer ?? throw new ArgumentNullException(nameof(contentSerializer), "The content serializer can't be null");
UrlParameterFormatter = urlParameterFormatter ?? new DefaultUrlParameterFormatter();
FormUrlEncodedParameterFormatter = formUrlEncodedParameterFormatter ?? new DefaultFormUrlEncodedParameterFormatter();
ExceptionFactory = new DefaultApiExceptionFactory(this).CreateAsync;
InjectMethodInfoAsProperty = injectMethodInfoAsProperty;
}

/// <summary>
Expand Down Expand Up @@ -93,6 +107,17 @@ public RefitSettings()
/// Sets the default behavior when sending a request's body content. (defaults to false, request body is not streamed to the server)
/// </summary>
public bool Buffered { get; set; } = false;

#if NET5_0_OR_GREATER
/// <summary>
/// Controls injecting the <see cref="MethodInfo"/> of the method on the Refit client interface that was invoked into the HttpRequestMessage.Options (defaults to false)
/// </summary>
#else
/// <summary>
/// Controls injecting the <see cref="MethodInfo"/> of the method on the Refit client interface that was invoked into the HttpRequestMessage.Properties (defaults to false)
/// </summary>
#endif
public bool InjectMethodInfoAsProperty { get; set; } = false;
}

/// <summary>
Expand Down
19 changes: 14 additions & 5 deletions Refit/RequestBuilderImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -733,14 +733,23 @@ void AddMultipartItem(MultipartFormDataContent multiPartContent, string fileName
#endif
}
// Always add the top-level type of the interface to the properties
// Always add the top-level type of the interface to the options/properties and include the MethodInfo if the developer has opted-in to that behavior
#if NET6_0_OR_GREATER
ret.Options.Set(new HttpRequestOptionsKey<Type>(HttpRequestMessageOptions.InterfaceType), TargetType);
ret.Options.Set(new HttpRequestOptionsKey<RestMethodInfo>(HttpRequestMessageOptions.RestMethodInfo), restMethod);
ret.Options.Set(HttpRequestMessageOptions.InterfaceTypeKey, TargetType);
if (settings.InjectMethodInfoAsProperty)
{
ret.Options.Set(HttpRequestMessageOptions.MethodInfoKey, restMethod.MethodInfo);
}
#else
ret.Properties[HttpRequestMessageOptions.InterfaceType] = TargetType;
ret.Properties[HttpRequestMessageOptions.RestMethodInfo] = restMethod;
#endif
if (settings.InjectMethodInfoAsProperty)
{
ret.Properties[HttpRequestMessageOptions.MethodInfo] = restMethod.MethodInfo;
}
#endif
;
// NB: The URI methods in .NET are dumb. Also, we do this
// UriBuilder business so that we preserve any hardcoded query
Expand Down

0 comments on commit b06ef7c

Please sign in to comment.