From b4d69ffc0862153af03f75adfcbe191739ac8b57 Mon Sep 17 00:00:00 2001 From: James Tayler Date: Sat, 28 May 2022 14:55:03 +1200 Subject: [PATCH 1/2] allow developers to opt-in to injecting the MethodInfo into HttpRequestMessage.Options --- README.md | 49 ++++++++++++++++++++++++++ Refit.Tests/RequestBuilder.cs | 50 ++++++++++++++++++++++++++- Refit/HttpRequestMessageProperties.cs | 26 +++++++++++--- Refit/RefitSettings.cs | 27 ++++++++++++++- Refit/RequestBuilderImplementation.cs | 13 +++++-- 5 files changed, 156 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6b9609143..551a2d288 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) @@ -857,6 +858,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/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index b0cc116d8..3ca9f6b8e 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() { @@ -2141,6 +2141,54 @@ public void InterfaceTypeShouldBeInProperties() } + [Fact] + public void MethodInfoShouldBeInPropertiesIfInjectMethodInfoAsPropertyTrue() + { + var fixture = new RequestBuilderImplementation(new RefitSettings + { + InjectMethodInfoAsProperty = true + }); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IContainAandB.Ping)); + var output = factory(new object[] { }); + + MethodInfo methodInfo; +#if NET5_0_OR_GREATER + Assert.NotEmpty(output.Options); + 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); + 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 NET5_0_OR_GREATER + Assert.NotEmpty(output.Options); + output.Options.TryGetValue(HttpRequestMessageOptions.MethodInfoKey, out methodInfo); + Assert.Null(methodInfo); +#endif + +#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] public void DynamicRequestPropertiesWithDefaultKeysShouldBeInProperties() { diff --git a/Refit/HttpRequestMessageProperties.cs b/Refit/HttpRequestMessageProperties.cs index 801f990bb..a2c445f1c 100644 --- a/Refit/HttpRequestMessageProperties.cs +++ b/Refit/HttpRequestMessageProperties.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Reflection; namespace Refit { @@ -15,5 +11,25 @@ public static class HttpRequestMessageOptions /// Returns the of the top-level interface where the method was called from /// public static string InterfaceType { get; } = "Refit.InterfaceType"; + +#if NET5_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 NET5_0_OR_GREATER + /// + /// A typed key to access the of the method that was called + /// + public static System.Net.Http.HttpRequestOptionsKey MethodInfoKey { get; } = new(MethodInfo); +#endif } } diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index 3d6835838..2aabae344 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; } /// @@ -88,6 +102,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 bc7f5334d..223ea6279 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -733,11 +733,20 @@ 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 NET5_0_OR_GREATER - ret.Options.Set(new HttpRequestOptionsKey(HttpRequestMessageOptions.InterfaceType), TargetType); + ret.Options.Set(HttpRequestMessageOptions.InterfaceTypeKey, TargetType); + if (settings.InjectMethodInfoAsProperty) + { + ret.Options.Set(HttpRequestMessageOptions.MethodInfoKey, restMethod.MethodInfo); + } #else ret.Properties[HttpRequestMessageOptions.InterfaceType] = TargetType; + if (settings.InjectMethodInfoAsProperty) + { + ret.Properties[HttpRequestMessageOptions.MethodInfo] = restMethod.MethodInfo; + } + #endif ; From a2f0f524e2e0fe31eab12ee6cb43cd6fbbc3a4f1 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Wed, 12 Apr 2023 09:10:05 +0100 Subject: [PATCH 2/2] Fix tests --- Refit.Tests/MultipartTests.cs | 4 ++-- Refit.Tests/RequestBuilder.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 730c102c1..5eb0f741e 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -2222,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 }