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

Feature: allow developers to inject the MethodInfo as a Property #1367

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
50 changes: 39 additions & 11 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
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 @@ 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 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