diff --git a/README.md b/README.md index 82c2fd2fe..dba94d18b 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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> GetById(int id); +} +``` +To make the `MethodInfo` available you need to opt-in via the `RefitSettings` like so: + +```csharp +services.AddRefitClient(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 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. diff --git a/Refit.Tests/MultipartTests.cs b/Refit.Tests/MultipartTests.cs index 95c87a36d..9c155e10a 100644 --- a/Refit.Tests/MultipartTests.cs +++ b/Refit.Tests/MultipartTests.cs @@ -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)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 }, diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index 5505c19d7..5eb0f741e 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -511,7 +511,7 @@ public void ParameterMappingSmokeTest() Assert.Empty(fixture.QueryParameterMap); Assert.Null(fixture.BodyParameterInfo); } - + [Fact] public void ParameterMappingWithTheSameIdInAFewPlaces() { @@ -2142,23 +2142,51 @@ public void InterfaceTypeShouldBeInProperties() } [Fact] - public void RestMethodInfoShouldBeInProperties() + public void MethodInfoShouldBeInPropertiesIfInjectMethodInfoAsPropertyTrue() { - var someProperty = new object(); - var fixture = new RequestBuilderImplementation(); + var fixture = new RequestBuilderImplementation(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(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(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(); + 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] @@ -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)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 } diff --git a/Refit/HttpRequestMessageProperties.cs b/Refit/HttpRequestMessageProperties.cs index e35a2ac7a..21d3b1bf0 100644 --- a/Refit/HttpRequestMessageProperties.cs +++ b/Refit/HttpRequestMessageProperties.cs @@ -1,4 +1,6 @@ -namespace Refit +using System.Reflection; + +namespace Refit { /// /// Contains Refit-defined properties on the HttpRequestMessage.Properties/Options @@ -10,9 +12,24 @@ public static class HttpRequestMessageOptions /// public static string InterfaceType { get; } = "Refit.InterfaceType"; +#if NET6_0_OR_GREATER + /// + /// A typed key to access the of the top-level interface where the method was called from + /// on the . + /// + public static System.Net.Http.HttpRequestOptionsKey InterfaceTypeKey { get; } = new(InterfaceType); +#endif + + /// + /// Returns the of the method that was called + /// + public static string MethodInfo { get; } = "Refit.MethodInfo"; + +#if NET6_0_OR_GREATER /// - /// Returns the of the top-level interface + /// A typed key to access the of the method that was called /// - public static string RestMethodInfo { get; } = "Refit.RestMethodInfo"; + public static System.Net.Http.HttpRequestOptionsKey MethodInfoKey { get; } = new(MethodInfo); +#endif } } diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index 686bd77f8..0ada3d865 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -26,21 +26,35 @@ public RefitSettings() ExceptionFactory = new DefaultApiExceptionFactory(this).CreateAsync; } + +#if NET5_0_OR_GREATER /// /// Creates a new instance with the specified parameters /// /// The instance to use /// The instance to use (defaults to ) /// The instance to use (defaults to ) + /// Controls injecting the of the method on the Refit client interface that was invoked into the HttpRequestMessage.Options (defaults to false) +#else + /// + /// Creates a new instance with the specified parameters + /// + /// The instance to use + /// The instance to use (defaults to ) + /// The instance to use (defaults to ) + /// Controls injecting the of the method on the Refit client interface that was invoked into the HttpRequestMessage.Properties (defaults to false) +#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; } /// @@ -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) /// public bool Buffered { get; set; } = false; + +#if NET5_0_OR_GREATER + /// + /// Controls injecting the of the method on the Refit client interface that was invoked into the HttpRequestMessage.Options (defaults to false) + /// +#else + /// + /// Controls injecting the of the method on the Refit client interface that was invoked into the HttpRequestMessage.Properties (defaults to false) + /// +#endif + public bool InjectMethodInfoAsProperty { get; set; } = false; } /// diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index a528aecd9..5085917ba 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -733,14 +733,23 @@ Func 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(HttpRequestMessageOptions.InterfaceType), TargetType); - ret.Options.Set(new HttpRequestOptionsKey(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