feat: optimize `CachedRequestBuilder` (#1716)

Co-authored-by: Chris Pulman <chris.pulman@yahoo.com>
This commit is contained in:
Tim M 2024-06-25 19:26:43 +01:00 коммит произвёл GitHub
Родитель 107d71697d
Коммит b320e4eb4e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
2 изменённых файлов: 234 добавлений и 26 удалений

Просмотреть файл

@ -0,0 +1,148 @@
using System.Net;
using System.Net.Http;
using System.Reflection;
using RichardSzalay.MockHttp;
using Xunit;
namespace Refit.Tests;
public interface IGeneralRequests
{
[Post("/foo")]
Task Empty();
[Post("/foo")]
Task SingleParameter(string id);
[Post("/foo")]
Task MultiParameter(string id, string name);
[Post("/foo")]
Task SingleGenericMultiParameter<TValue>(string id, string name, TValue generic);
}
public interface IDuplicateNames
{
[Post("/foo")]
Task SingleParameter(string id);
[Post("/foo")]
Task SingleParameter(int id);
}
public class CachedRequestBuilderTests
{
[Fact]
public async Task CacheHasCorrectNumberOfElementsTest()
{
var mockHttp = new MockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };
var fixture = RestService.For<IGeneralRequests>("http://bar", settings);
// get internal dictionary to check count
var requestBuilderField = fixture.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Single(x => x.Name == "requestBuilder");
var requestBuilder = requestBuilderField.GetValue(fixture) as CachedRequestBuilderImplementation;
mockHttp
.Expect(HttpMethod.Post, "http://bar/foo")
.Respond(HttpStatusCode.OK);
await fixture.Empty();
Assert.Single(requestBuilder.MethodDictionary);
mockHttp
.Expect(HttpMethod.Post, "http://bar/foo")
.WithQueryString("id", "id")
.Respond(HttpStatusCode.OK);
await fixture.SingleParameter("id");
Assert.Equal(2, requestBuilder.MethodDictionary.Count);
mockHttp
.Expect(HttpMethod.Post, "http://bar/foo")
.WithQueryString("id", "id")
.WithQueryString("name", "name")
.Respond(HttpStatusCode.OK);
await fixture.MultiParameter("id", "name");
Assert.Equal(3, requestBuilder.MethodDictionary.Count);
mockHttp
.Expect(HttpMethod.Post, "http://bar/foo")
.WithQueryString("id", "id")
.WithQueryString("name", "name")
.WithQueryString("generic", "generic")
.Respond(HttpStatusCode.OK);
await fixture.SingleGenericMultiParameter("id", "name", "generic");
Assert.Equal(4, requestBuilder.MethodDictionary.Count);
mockHttp.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task NoDuplicateEntriesTest()
{
var mockHttp = new MockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };
var fixture = RestService.For<IGeneralRequests>("http://bar", settings);
// get internal dictionary to check count
var requestBuilderField = fixture.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Single(x => x.Name == "requestBuilder");
var requestBuilder = requestBuilderField.GetValue(fixture) as CachedRequestBuilderImplementation;
// send the same request repeatedly to ensure that multiple dictionary entries are not created
mockHttp
.Expect(HttpMethod.Post, "http://bar/foo")
.WithQueryString("id", "id")
.Respond(HttpStatusCode.OK);
await fixture.SingleParameter("id");
Assert.Single(requestBuilder.MethodDictionary);
mockHttp
.Expect(HttpMethod.Post, "http://bar/foo")
.WithQueryString("id", "id")
.Respond(HttpStatusCode.OK);
await fixture.SingleParameter("id");
Assert.Single(requestBuilder.MethodDictionary);
mockHttp
.Expect(HttpMethod.Post, "http://bar/foo")
.WithQueryString("id", "id")
.Respond(HttpStatusCode.OK);
await fixture.SingleParameter("id");
Assert.Single(requestBuilder.MethodDictionary);
mockHttp.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task SameNameDuplicateEntriesTest()
{
var mockHttp = new MockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };
var fixture = RestService.For<IDuplicateNames>("http://bar", settings);
// get internal dictionary to check count
var requestBuilderField = fixture.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Single(x => x.Name == "requestBuilder");
var requestBuilder = requestBuilderField.GetValue(fixture) as CachedRequestBuilderImplementation;
// send the two different requests with the same name
mockHttp
.Expect(HttpMethod.Post, "http://bar/foo")
.WithQueryString("id", "id")
.Respond(HttpStatusCode.OK);
await fixture.SingleParameter("id");
Assert.Single(requestBuilder.MethodDictionary);
mockHttp
.Expect(HttpMethod.Post, "http://bar/foo")
.WithQueryString("id", "10")
.Respond(HttpStatusCode.OK);
await fixture.SingleParameter(10);
Assert.Equal(2, requestBuilder.MethodDictionary.Count);
mockHttp.VerifyNoOutstandingExpectation();
}
}

Просмотреть файл

@ -20,10 +20,10 @@ namespace Refit
}
readonly IRequestBuilder innerBuilder;
readonly ConcurrentDictionary<
string,
internal readonly ConcurrentDictionary<
MethodTableKey,
Func<HttpClient, object[], object?>
> methodDictionary = new();
> MethodDictionary = new();
public Func<HttpClient, object[], object?> BuildRestResultFuncForMethod(
string methodName,
@ -31,13 +31,22 @@ namespace Refit
Type[]? genericArgumentTypes = null
)
{
var cacheKey = GetCacheKey(
var cacheKey = new MethodTableKey(
methodName,
parameterTypes ?? Array.Empty<Type>(),
genericArgumentTypes ?? Array.Empty<Type>()
);
var func = methodDictionary.GetOrAdd(
cacheKey,
if (MethodDictionary.TryGetValue(cacheKey, out var methodFunc))
{
return methodFunc;
}
// use GetOrAdd with cloned array method table key. This prevents the array from being modified, breaking the dictionary.
var func = MethodDictionary.GetOrAdd(
new MethodTableKey(methodName,
parameterTypes?.ToArray() ?? Array.Empty<Type>(),
genericArgumentTypes?.ToArray() ?? Array.Empty<Type>()),
_ =>
innerBuilder.BuildRestResultFuncForMethod(
methodName,
@ -48,37 +57,88 @@ namespace Refit
return func;
}
}
static string GetCacheKey(
string methodName,
Type[] parameterTypes,
Type[] genericArgumentTypes
)
/// <summary>
/// Represents a method composed of its name, generic arguments and parameters.
/// </summary>
internal readonly struct MethodTableKey : IEquatable<MethodTableKey>
{
/// <summary>
/// Constructs an instance of <see cref="MethodTableKey"/>.
/// </summary>
/// <param name="methodName">Represents the methods name.</param>
/// <param name="parameters">Array containing the methods parameters.</param>
/// <param name="genericArguments">Array containing the methods generic arguments.</param>
public MethodTableKey (string methodName, Type[] parameters, Type[] genericArguments)
{
var genericDefinition = GetGenericString(genericArgumentTypes);
var argumentString = GetArgumentString(parameterTypes);
return $"{methodName}{genericDefinition}({argumentString})";
MethodName = methodName;
Parameters = parameters;
GenericArguments = genericArguments;
}
static string GetArgumentString(Type[] parameterTypes)
/// <summary>
/// The methods name.
/// </summary>
string MethodName { get; }
/// <summary>
/// Array containing the methods parameters.
/// </summary>
Type[] Parameters { get; }
/// <summary>
/// Array containing the methods generic arguments.
/// </summary>
Type[] GenericArguments { get; }
public override int GetHashCode()
{
if (parameterTypes == null || parameterTypes.Length == 0)
unchecked
{
return "";
var hashCode = MethodName.GetHashCode();
foreach (var argument in Parameters)
{
hashCode = (hashCode * 397) ^ argument.GetHashCode();
}
foreach (var genericArgument in GenericArguments)
{
hashCode = (hashCode * 397) ^ genericArgument.GetHashCode();
}
return hashCode;
}
}
public bool Equals(MethodTableKey other)
{
if (Parameters.Length != other.Parameters.Length
|| GenericArguments.Length != other.GenericArguments.Length
|| MethodName != other.MethodName)
{
return false;
}
return string.Join(", ", parameterTypes.Select(t => t.FullName));
}
static string GetGenericString(Type[] genericArgumentTypes)
{
if (genericArgumentTypes == null || genericArgumentTypes.Length == 0)
for (var i = 0; i < Parameters.Length; i++)
{
return "";
if (Parameters[i] != other.Parameters[i])
{
return false;
}
}
return "<" + string.Join(", ", genericArgumentTypes.Select(t => t.FullName)) + ">";
for (var i = 0; i < GenericArguments.Length; i++)
{
if (GenericArguments[i] != other.GenericArguments[i])
{
return false;
}
}
return true;
}
public override bool Equals(object? obj) => obj is MethodTableKey other && Equals(other);
}
}