Skip to content

Commit fa3a57b

Browse files
feat: calculate path substitutions in RestMethodInfo (#1897)
Co-authored-by: Chris Pulman <[email protected]>
1 parent 6654c95 commit fa3a57b

File tree

3 files changed

+235
-160
lines changed

3 files changed

+235
-160
lines changed

Refit.Tests/RestService.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,25 @@ await fixture.GetFooBars(
450450
mockHttp.VerifyNoOutstandingExpectation();
451451
}
452452

453+
[Fact]
454+
public async Task GetWithLongPathBoundObject()
455+
{
456+
var mockHttp = new MockHttpMessageHandler();
457+
var longPathString = string.Concat(Enumerable.Repeat("barNone", 1000));
458+
mockHttp
459+
.Expect(HttpMethod.Get, $"http://foo/foos/12345/bar/{longPathString}")
460+
.WithExactQueryString("")
461+
.Respond("application/json", "Ok");
462+
463+
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };
464+
var fixture = RestService.For<IApiBindPathToObject>("http://foo", settings);
465+
466+
await fixture.GetFooBars(
467+
new PathBoundObject() { SomeProperty = 12345, SomeProperty2 = longPathString }
468+
);
469+
mockHttp.VerifyNoOutstandingExpectation();
470+
}
471+
453472
[Fact]
454473
public async Task GetWithPathBoundObjectDifferentCasing()
455474
{

Refit/RequestBuilderImplementation.cs

Lines changed: 92 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using System.Collections;
22
using System.Collections.Concurrent;
3+
using System.Diagnostics;
34
using System.Net.Http;
45
using System.Reflection;
56
using System.Text;
6-
using System.Text.RegularExpressions;
77
using System.Web;
88

99
namespace Refit
@@ -14,6 +14,7 @@ class RequestBuilderImplementation<TApi>(RefitSettings? refitSettings = null)
1414

1515
partial class RequestBuilderImplementation : IRequestBuilder
1616
{
17+
private const int StackallocThreshold = 512;
1718
static readonly QueryAttribute DefaultQueryAttribute = new ();
1819
static readonly Uri BaseUri = new ("http://api");
1920
readonly Dictionary<string, List<RestMethodInfoInternal>> interfaceHttpMethods;
@@ -645,8 +646,6 @@ bool paramsContainsCancellationToken
645646
ret.Content = multiPartContent;
646647
}
647648

648-
var urlTarget =
649-
(basePath == "/" ? string.Empty : basePath) + restMethod.RelativePath;
650649
var queryParamsToAdd = new List<KeyValuePair<string, string?>>();
651650
var headersToAdd = restMethod.Headers.Count > 0 ?
652651
new Dictionary<string, string?>(restMethod.Headers)
@@ -662,14 +661,10 @@ bool paramsContainsCancellationToken
662661
if (restMethod.ParameterMap.TryGetValue(i, out var parameterMapValue))
663662
{
664663
parameterInfo = parameterMapValue;
665-
if (parameterInfo.IsObjectPropertyParameter)
664+
if (!parameterInfo.IsObjectPropertyParameter)
666665
{
667-
urlTarget = AddObjectParametersToUrl(parameterInfo, param, urlTarget);
668-
//don't continue here as we want it to fall through so any parameters on this object not bound here get passed as query parameters
669-
}
670-
else
671-
{
672-
urlTarget = AddValueParameterToUrl(restMethod, parameterMapValue, param, i, urlTarget);
666+
// mark parameter mapped if not an object
667+
// we want objects to fall through so any parameters on this object not bound here get passed as query parameters
673668
isParameterMappedToRequest = true;
674669
}
675670
}
@@ -758,6 +753,8 @@ bool paramsContainsCancellationToken
758753
// NB: The URI methods in .NET are dumb. Also, we do this
759754
// UriBuilder business so that we preserve any hardcoded query
760755
// parameters as well as add the parameterized ones.
756+
var urlTarget = BuildRelativePath(basePath, restMethod, paramList);
757+
761758
var uri = new UriBuilder(new Uri(BaseUri, urlTarget));
762759
ParseExistingQueryString(uri, queryParamsToAdd);
763760

@@ -778,72 +775,102 @@ bool paramsContainsCancellationToken
778775
};
779776
}
780777

781-
string AddObjectParametersToUrl(RestMethodParameterInfo parameterInfo, object param, string urlTarget)
778+
string BuildRelativePath(string basePath, RestMethodInfoInternal restMethod, object[] paramList)
782779
{
783-
foreach (var propertyInfo in parameterInfo.ParameterProperties)
780+
basePath = basePath == "/" ? string.Empty : basePath;
781+
var pathFragments = restMethod.FragmentPath;
782+
if (pathFragments.Count == 0)
784783
{
785-
var propertyObject = propertyInfo.PropertyInfo.GetValue(param);
786-
urlTarget = Regex.Replace(
787-
urlTarget,
788-
"{" + propertyInfo.Name + "}",
789-
Uri.EscapeDataString(
790-
settings.UrlParameterFormatter.Format(
791-
propertyObject,
792-
propertyInfo.PropertyInfo,
793-
propertyInfo.PropertyInfo.PropertyType
794-
) ?? string.Empty
795-
),
796-
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
797-
);
784+
return basePath;
785+
}
786+
if (string.IsNullOrEmpty(basePath) && pathFragments.Count == 1)
787+
{
788+
Debug.Assert(pathFragments[0].IsConstant);
789+
return pathFragments[0].Value!;
798790
}
799791

800-
return urlTarget;
792+
#pragma warning disable CA2000
793+
var vsb = new ValueStringBuilder(stackalloc char[StackallocThreshold]);
794+
#pragma warning restore CA2000
795+
vsb.Append(basePath);
796+
797+
foreach (var fragment in pathFragments)
798+
{
799+
AppendPathFragmentValue(ref vsb, restMethod, paramList, fragment);
800+
}
801+
802+
return vsb.ToString();
801803
}
802804

803-
string AddValueParameterToUrl(RestMethodInfoInternal restMethod, RestMethodParameterInfo parameterMapValue,
804-
object param, int i, string urlTarget)
805+
void AppendPathFragmentValue(ref ValueStringBuilder vsb, RestMethodInfoInternal restMethod, object[] paramList,
806+
ParameterFragment fragment)
805807
{
806-
string pattern;
807-
string replacement;
808-
if (parameterMapValue.Type == ParameterType.RoundTripping)
808+
if (fragment.IsConstant)
809809
{
810-
pattern = $@"{{\*\*{parameterMapValue.Name}}}";
811-
var paramValue = (string)param;
812-
replacement = string.Join(
813-
"/",
814-
paramValue
815-
.Split('/')
816-
.Select(
817-
s =>
818-
Uri.EscapeDataString(
819-
settings.UrlParameterFormatter.Format(
820-
s,
821-
restMethod.ParameterInfoArray[i],
822-
restMethod.ParameterInfoArray[i].ParameterType
823-
) ?? string.Empty
824-
)
825-
)
826-
);
810+
vsb.Append(fragment.Value!);
811+
return;
827812
}
828-
else
813+
814+
var contains = restMethod.ParameterMap.TryGetValue(fragment.ArgumentIndex, out var parameterMapValue);
815+
if (!contains || parameterMapValue is null)
816+
throw new InvalidOperationException($"{restMethod.ParameterMap} should contain parameter.");
817+
818+
if (fragment.IsObjectProperty)
829819
{
830-
pattern = "{" + parameterMapValue.Name + "}";
831-
replacement = Uri.EscapeDataString(
832-
settings.UrlParameterFormatter.Format(
833-
param,
834-
restMethod.ParameterInfoArray[i],
835-
restMethod.ParameterInfoArray[i].ParameterType
836-
) ?? string.Empty
837-
);
820+
var param = paramList[fragment.ArgumentIndex];
821+
var property = parameterMapValue.ParameterProperties[fragment.PropertyIndex];
822+
var propertyObject = property.PropertyInfo.GetValue(param);
823+
824+
vsb.Append(Uri.EscapeDataString(settings.UrlParameterFormatter.Format(
825+
propertyObject,
826+
property.PropertyInfo,
827+
property.PropertyInfo.PropertyType
828+
) ?? string.Empty));
829+
return;
838830
}
839831

840-
urlTarget = Regex.Replace(
841-
urlTarget,
842-
pattern,
843-
replacement,
844-
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant
845-
);
846-
return urlTarget;
832+
if (fragment.IsDynamicRoute)
833+
{
834+
var param = paramList[fragment.ArgumentIndex];
835+
836+
if (parameterMapValue.Type == ParameterType.Normal)
837+
{
838+
vsb.Append(Uri.EscapeDataString(
839+
settings.UrlParameterFormatter.Format(
840+
param,
841+
restMethod.ParameterInfoArray[fragment.ArgumentIndex],
842+
restMethod.ParameterInfoArray[fragment.ArgumentIndex].ParameterType
843+
) ?? string.Empty
844+
));
845+
return;
846+
}
847+
848+
// If round tripping, split string up, format each segment and append to vsb.
849+
Debug.Assert(parameterMapValue.Type == ParameterType.RoundTripping);
850+
var paramValue = (string)param;
851+
var split = paramValue.Split('/');
852+
853+
var firstSection = true;
854+
foreach (var section in split)
855+
{
856+
if(!firstSection)
857+
vsb.Append('/');
858+
859+
vsb.Append(
860+
Uri.EscapeDataString(
861+
settings.UrlParameterFormatter.Format(
862+
section,
863+
restMethod.ParameterInfoArray[fragment.ArgumentIndex],
864+
restMethod.ParameterInfoArray[fragment.ArgumentIndex].ParameterType
865+
) ?? string.Empty
866+
));
867+
firstSection = false;
868+
}
869+
870+
return;
871+
}
872+
873+
throw new ArgumentException($"{nameof(ParameterFragment)} is in an invalid form.");
847874
}
848875

849876
void AddBodyToRequest(RestMethodInfoInternal restMethod, object param, HttpRequestMessage ret)
@@ -1168,7 +1195,7 @@ static string CreateQueryString(List<KeyValuePair<string, string?>> queryParamsT
11681195
{
11691196
// Suppress warning as ValueStringBuilder.ToString calls Dispose()
11701197
#pragma warning disable CA2000
1171-
var vsb = new ValueStringBuilder(stackalloc char[512]);
1198+
var vsb = new ValueStringBuilder(stackalloc char[StackallocThreshold]);
11721199
#pragma warning restore CA2000
11731200
var firstQuery = true;
11741201
foreach (var queryParam in queryParamsToAdd)

0 commit comments

Comments
 (0)