Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 33 additions & 0 deletions src/Refit.Reflection/CachedAttributeProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
// ReactiveUI and Contributors licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Reflection;

namespace Refit;

/// <summary>Wraps an <see cref="ICustomAttributeProvider"/> and materializes its <see cref="QueryAttribute"/> lookup once
/// so the URL parameter formatter, which re-reads the attribute for every formatted value, never re-materializes it from
/// metadata.</summary>
/// <param name="inner">The underlying provider (a parameter, property or type) to read attributes from.</param>
/// <remarks>Only the <c>(typeof(QueryAttribute), inherit: true)</c> lookup the default formatter performs is cached; every
/// other query delegates to <paramref name="inner"/>, so a custom formatter that reads other attributes sees exactly the
/// same values it would from the original provider. The cached array holds immutable metadata attributes, so sharing it
/// across format calls is behaviourally identical to re-reading it.</remarks>
internal sealed class CachedAttributeProvider(ICustomAttributeProvider inner) : ICustomAttributeProvider
{
/// <summary>The materialized <see cref="QueryAttribute"/> lookup, cached on first read.</summary>
private object[]? _queryAttributes;

/// <inheritdoc/>
public object[] GetCustomAttributes(bool inherit) => inner.GetCustomAttributes(inherit);

/// <inheritdoc/>
public object[] GetCustomAttributes(Type attributeType, bool inherit) =>
inherit && attributeType == typeof(QueryAttribute)
? _queryAttributes ??= inner.GetCustomAttributes(typeof(QueryAttribute), true)
: inner.GetCustomAttributes(attributeType, inherit);

/// <inheritdoc/>
public bool IsDefined(Type attributeType, bool inherit) => inner.IsDefined(attributeType, inherit);
}
14 changes: 14 additions & 0 deletions src/Refit.Reflection/IQueryValueSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
// ReactiveUI and Contributors licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Refit;

/// <summary>Receives formatted enumerable query values so they can be appended in place without an intermediate sequence
/// or iterator state machine.</summary>
internal interface IQueryValueSink
{
/// <summary>Appends one formatted value.</summary>
/// <param name="value">The formatted value, or <see langword="null"/>.</param>
void Add(string? value);
}
6 changes: 3 additions & 3 deletions src/Refit.Reflection/MethodTableKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ public MethodTableKey(string methodName, Type[] parameters, Type[] genericArgume
}

/// <summary>Gets the methods name.</summary>
private string MethodName { get; }
internal string MethodName { get; }

/// <summary>Gets the Array containing the methods parameters.</summary>
private Type[] Parameters { get; }
internal Type[] Parameters { get; }

/// <summary>Gets the Array containing the methods generic arguments.</summary>
private Type[] GenericArguments { get; }
internal Type[] GenericArguments { get; }

/// <inheritdoc/>
public override int GetHashCode()
Expand Down
25 changes: 25 additions & 0 deletions src/Refit.Reflection/ParameterAttributeSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
// ReactiveUI and Contributors licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Refit;

/// <summary>The request-shaping attributes read from a single method parameter in one metadata pass, so the request
/// builder's construction classifies each parameter without re-enumerating its attribute records per lookup.</summary>
/// <param name="Query">The parameter's <see cref="QueryAttribute"/>, or null.</param>
/// <param name="Header">The parameter's <see cref="HeaderAttribute"/>, or null.</param>
/// <param name="HeaderCollection">The parameter's <see cref="HeaderCollectionAttribute"/>, or null.</param>
/// <param name="Property">The parameter's <see cref="PropertyAttribute"/>, or null.</param>
/// <param name="Authorize">The parameter's <see cref="AuthorizeAttribute"/>, or null.</param>
/// <param name="Body">The parameter's <see cref="BodyAttribute"/>, or null.</param>
/// <param name="Url">The parameter's <see cref="UrlAttribute"/>, or null.</param>
/// <param name="FormObject">The parameter's <see cref="FormObjectAttribute"/>, or null.</param>
internal readonly record struct ParameterAttributeSet(
QueryAttribute? Query,
HeaderAttribute? Header,
HeaderCollectionAttribute? HeaderCollection,
PropertyAttribute? Property,
AuthorizeAttribute? Authorize,
BodyAttribute? Body,
UrlAttribute? Url,
FormObjectAttribute? FormObject);
14 changes: 14 additions & 0 deletions src/Refit.Reflection/QueryMapEntrySink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
// ReactiveUI and Contributors licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Refit;

/// <summary>Appends formatted enumerable values as <see cref="QueryMapEntry"/> items under a fixed key.</summary>
/// <param name="Entries">The list receiving the entries.</param>
/// <param name="Key">The query key applied to every appended value.</param>
internal readonly record struct QueryMapEntrySink(List<QueryMapEntry> Entries, string Key) : IQueryValueSink
{
/// <inheritdoc/>
public void Add(string? value) => Entries.Add(new(Key, value));
}
14 changes: 14 additions & 0 deletions src/Refit.Reflection/QueryParameterEntrySink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
// ReactiveUI and Contributors licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Refit;

/// <summary>Appends formatted enumerable values as <see cref="QueryParameterEntry"/> items under a fixed key.</summary>
/// <param name="Entries">The list receiving the entries.</param>
/// <param name="Key">The query key applied to every appended value.</param>
internal readonly record struct QueryParameterEntrySink(List<QueryParameterEntry> Entries, string Key) : IQueryValueSink
{
/// <inheritdoc/>
public void Add(string? value) => Entries.Add(new(Key, value));
}
19 changes: 19 additions & 0 deletions src/Refit.Reflection/QueryPropertyMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
// ReactiveUI and Contributors licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Reflection;

namespace Refit;

/// <summary>One readable public property of a query object, together with the attribute-derived facts the reflection
/// request builder would otherwise re-read from metadata on every request.</summary>
/// <param name="Property">The readable public instance property.</param>
/// <param name="IsIgnored">Whether the property carries a serialization-ignore attribute and is always skipped.</param>
/// <param name="QueryAttribute">The property's <see cref="Refit.QueryAttribute"/>, or <see langword="null"/> when absent.</param>
/// <param name="AliasAttribute">The property's <see cref="AliasAsAttribute"/>, or <see langword="null"/> when absent.</param>
internal readonly record struct QueryPropertyMetadata(
PropertyInfo Property,
bool IsIgnored,
QueryAttribute? QueryAttribute,
AliasAsAttribute? AliasAttribute);
2 changes: 2 additions & 0 deletions src/Refit.Reflection/Refit.Reflection.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

<ItemGroup>
<InternalsVisibleTo Include="Refit.Tests"/>
<InternalsVisibleTo Include="Refit.Reflection.Tests"/>
<InternalsVisibleTo Include="Refit.Reflection.Benchmarks"/>
<ProjectReference Include="..\Refit\Refit.csproj" PrivateAssets="Analyzers"/>
</ItemGroup>

Expand Down
54 changes: 27 additions & 27 deletions src/Refit.Reflection/RequestBuilderImplementation.Execution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ namespace Refit;
/// <summary>Reflection-based request builder that turns Refit interface calls into HTTP requests.</summary>
internal partial class RequestBuilderImplementation
{
/// <summary>Gets the cancellation token declared by the interface method, if any.</summary>
/// <param name="restMethod">The rest method being invoked.</param>
/// <param name="paramList">The argument values for the call.</param>
/// <returns>The method's cancellation token, or <see cref="CancellationToken.None"/>.</returns>
internal static CancellationToken GetMethodCancellationToken(
RestMethodInfoInternal restMethod,
object[] paramList) =>
restMethod.CancellationToken is not null
? GetCancellationToken(paramList)
: CancellationToken.None;

/// <summary>Determines whether the request body should be buffered before sending.</summary>
/// <param name="restMethod">The rest method being invoked.</param>
/// <param name="request">The built request message.</param>
/// <returns><see langword="true"/> if the body should be buffered; otherwise <see langword="false"/>.</returns>
internal static bool IsBodyBuffered(
RestMethodInfoInternal restMethod,
HttpRequestMessage request) =>
(restMethod.BodyParameterInfo?.Item2 ?? false) && request.Content is not null;

/// <summary>Builds and streams the response for a method returning an asynchronous sequence.</summary>
/// <typeparam name="T">The element type yielded to the caller.</typeparam>
/// <param name="client">The HTTP client to send with.</param>
Expand Down Expand Up @@ -53,31 +73,11 @@ internal partial class RequestBuilderImplementation
}
}

/// <summary>Gets the cancellation token declared by the interface method, if any.</summary>
/// <param name="restMethod">The rest method being invoked.</param>
/// <param name="paramList">The argument values for the call.</param>
/// <returns>The method's cancellation token, or <see cref="CancellationToken.None"/>.</returns>
private static CancellationToken GetMethodCancellationToken(
RestMethodInfoInternal restMethod,
object[] paramList) =>
restMethod.CancellationToken is not null
? GetCancellationToken(paramList)
: CancellationToken.None;

/// <summary>Determines whether the request body should be buffered before sending.</summary>
/// <param name="restMethod">The rest method being invoked.</param>
/// <param name="request">The built request message.</param>
/// <returns><see langword="true"/> if the body should be buffered; otherwise <see langword="false"/>.</returns>
private static bool IsBodyBuffered(
RestMethodInfoInternal restMethod,
HttpRequestMessage request) =>
(restMethod.BodyParameterInfo?.Item2 ?? false) && request.Content is not null;

/// <summary>Builds a delegate that constructs and returns the request for a <c>Task&lt;HttpRequestMessage&gt;</c> method.</summary>
/// <param name="restMethod">The rest method to build a delegate for.</param>
/// <returns>A delegate that returns a task producing the built request without sending it.</returns>
[RequiresDynamicCode("Serializing a body by runtime Type requires runtime generic method instantiation.")]
private Func<HttpClient, object[], object?> BuildRequestMessageFuncForMethod(
internal Func<HttpClient, object[], object?> BuildRequestMessageFuncForMethod(
RestMethodInfoInternal restMethod) =>
(client, paramList) => BuildRequestMessageWithoutSendingAsync(client, restMethod, paramList);

Expand All @@ -90,7 +90,7 @@ private static bool IsBodyBuffered(
/// logging, or manual dispatch. Any configured async authorization token getter runs at dispatch time and is
/// therefore not applied to a request obtained this way.</remarks>
[RequiresDynamicCode("Serializing a body by runtime Type requires runtime generic method instantiation.")]
private Task<HttpRequestMessage> BuildRequestMessageWithoutSendingAsync(
internal Task<HttpRequestMessage> BuildRequestMessageWithoutSendingAsync(
HttpClient client,
RestMethodInfoInternal restMethod,
object[] paramList)
Expand All @@ -113,7 +113,7 @@ private Task<HttpRequestMessage> BuildRequestMessageWithoutSendingAsync(
/// <param name="cancellationToken">A token to cancel the request.</param>
/// <returns>A task that completes when the request finishes.</returns>
[RequiresDynamicCode("Serializing a body by runtime Type requires runtime generic method instantiation.")]
private async Task ExecuteVoidRequestAsync(
internal async Task ExecuteVoidRequestAsync(
HttpClient client,
RestMethodInfoInternal restMethod,
object[] paramList,
Expand Down Expand Up @@ -154,7 +154,7 @@ await RequestExecutionHelpers.SendVoidAsync(
"SST2307:Generic method type parameters should be inferable from the parameters",
Justification = "Type parameter intentionally specified explicitly by callers.")]
[RequiresDynamicCode("Serializing a body by runtime Type requires runtime generic method instantiation.")]
private async Task<T?> ExecuteRequestAsync<T, TBody>(
internal async Task<T?> ExecuteRequestAsync<T, TBody>(
HttpClient client,
RestMethodInfoInternal restMethod,
object[] paramList,
Expand Down Expand Up @@ -192,7 +192,7 @@ await RequestExecutionHelpers.SendVoidAsync(
Justification = "Type parameter intentionally specified explicitly by callers.")]
[RequiresDynamicCode("Serializing a body by runtime Type requires runtime generic method instantiation.")]
[ExcludeFromCodeCoverage]
private async IAsyncEnumerable<T?> ExecuteAsyncEnumerableRequestAsync<T>(
internal async IAsyncEnumerable<T?> ExecuteAsyncEnumerableRequestAsync<T>(
HttpClient client,
RestMethodInfoInternal restMethod,
object[] paramList,
Expand All @@ -218,7 +218,7 @@ await RequestExecutionHelpers.SendVoidAsync(
"SST2307:Generic method type parameters should be inferable from the parameters",
Justification = "Type parameter intentionally specified explicitly by callers.")]
[RequiresDynamicCode("Serializing a body by runtime Type requires runtime generic method instantiation.")]
private Func<HttpClient, CancellationToken, object[], Task<T?>> BuildCancellableTaskFuncForMethod<T, TBody>(
internal Func<HttpClient, CancellationToken, object[], Task<T?>> BuildCancellableTaskFuncForMethod<T, TBody>(
RestMethodInfoInternal restMethod) =>
async (client, ct, paramList) =>
{
Expand Down Expand Up @@ -254,7 +254,7 @@ await RequestExecutionHelpers.SendVoidAsync(
"Design",
"SST2307:Generic method type parameters should be inferable from the parameters",
Justification = "Type parameter intentionally specified explicitly by callers.")]
private Task<T?> SendAndProcessResponseAsync<T, TBody>(
internal Task<T?> SendAndProcessResponseAsync<T, TBody>(
HttpClient client,
RestMethodInfoInternal restMethod,
HttpRequestMessage request,
Expand Down
Loading
Loading