Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal sealed class ClientUriBuilderDefinition : TypeProvider
private const string AppendQueryDelimitedMethodName = "AppendQueryDelimited";
private const string AppendPathDelimitedMethodName = "AppendPathDelimited";
private const string AppendPathMethodName = "AppendPath";
private const string UpdateQueryMethodName = "UpdateQuery";

private readonly FieldProvider _uriBuilderField;
private readonly FieldProvider _pathAndQueryField;
Expand Down Expand Up @@ -100,6 +101,7 @@ protected override MethodProvider[] BuildMethods()
methods.AddRange(BuildAppendPathDelimitedMethods());
methods.AddRange(BuildAppendQueryMethods());
methods.AddRange(BuildAppendQueryDelimitedMethods());
methods.Add(BuildUpdateQueryMethod());
methods.Add(BuildToUriMethod());

return methods.ToArray();
Expand Down Expand Up @@ -334,6 +336,84 @@ private MethodProvider BuildAppendDelimitedMethod(string appendDelimitedMethodNa
return new(signature, body, this, XmlDocProvider.Empty);
}

private MethodProvider BuildUpdateQueryMethod()
{
var nameParameter = new ParameterProvider("name", $"The name.", typeof(string));
var valueParameter = new ParameterProvider("value", $"The value.", typeof(string));

var signature = new MethodSignature(
Name: UpdateQueryMethodName,
Modifiers: MethodSignatureModifiers.Public,
Parameters: [nameParameter, valueParameter],
ReturnType: null,
Description: null, ReturnDescription: null);
Comment thread
radhgupta marked this conversation as resolved.
Outdated

var stringBuilder = PathAndQueryProperty.As<StringBuilder>();
var pathLength = (ValueExpression)_pathLengthField;
Comment thread
radhgupta marked this conversation as resolved.
Outdated

var body = new MethodBodyStatement[]
{
MethodBodyStatement.Empty,
new IfElseStatement(
stringBuilder.Length().Equal(pathLength),
// No query string exists - add first parameter
new MethodBodyStatement[]
{
stringBuilder.Append(Literal('?')).Terminate(),
stringBuilder.Append(nameParameter).Terminate(),
stringBuilder.Append(Literal('=')).Terminate(),
stringBuilder.Append(valueParameter).Terminate()
},
// Query string exists - update or append parameter
new MethodBodyStatement[]
{
Declare("queryStartIndex", typeof(int), new BinaryOperatorExpression("+", pathLength, Int(1)), out var queryStartIndex),
Declare("searchPattern", typeof(string), new BinaryOperatorExpression("+", nameParameter, Literal("=")), out var searchPattern),
Declare("queryString", typeof(string), stringBuilder.Invoke("ToString", queryStartIndex, new BinaryOperatorExpression("-", stringBuilder.Length(), queryStartIndex)), out var queryString),

Declare("paramStartIndex", typeof(int), Literal(-1), out var paramStartIndex),
new IfStatement(queryString.Invoke("StartsWith", searchPattern))
{
paramStartIndex.Assign(Literal(0)).Terminate()
},
new IfStatement(paramStartIndex.Equal(Literal(-1)))
{
Declare("prefixedIndex", typeof(int), queryString.Invoke("IndexOf", new BinaryOperatorExpression("+", Literal("&"), searchPattern)), out var prefixedIndex),
new IfStatement(prefixedIndex.GreaterThanOrEqual(Literal(0)))
{
paramStartIndex.Assign(new BinaryOperatorExpression("+", prefixedIndex, Int(1))).Terminate()
}
},

new IfElseStatement(
paramStartIndex.GreaterThanOrEqual(Literal(0)),
// Parameter exists - replace its value
new MethodBodyStatement[]
{
Declare("valueStartIndex", typeof(int), new BinaryOperatorExpression("+", paramStartIndex, searchPattern.Property("Length")), out var valueStartIndex),
Declare("valueEndIndex", typeof(int), queryString.Invoke("IndexOf", Literal('&'), valueStartIndex), out var valueEndIndex),
new IfStatement(valueEndIndex.Equal(Literal(-1)))
{
valueEndIndex.Assign(queryString.Property("Length")).Terminate()
},
Declare("globalStart", typeof(int), new BinaryOperatorExpression("+", queryStartIndex, valueStartIndex), out var globalStart),
Declare("globalEnd", typeof(int), new BinaryOperatorExpression("+", queryStartIndex, valueEndIndex), out var globalEnd),
stringBuilder.Invoke("Remove", globalStart, new BinaryOperatorExpression("-", globalEnd, globalStart)).Terminate(),
stringBuilder.Invoke("Insert", globalStart, valueParameter).Terminate()
},
// Parameter doesn't exist - append new parameter
new MethodBodyStatement[]
{
new InvokeMethodExpression(null, AppendQueryMethodName, [nameParameter, valueParameter, Literal(false)]).Terminate()
}
)
}
)
};

return new(signature, body, this, XmlDocProvider.Empty);
}

private MethodProvider BuildToUriMethod()
{
var signature = new MethodSignature(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class RestClientProvider : TypeProvider
{
private const string RepeatabilityRequestIdHeader = "Repeatability-Request-ID";
private const string RepeatabilityFirstSentHeader = "Repeatability-First-Sent";
private const string MaxPageSizeParameterName = "maxpagesize";

private static readonly Dictionary<string, ParameterProvider> _knownSpecialHeaderParams = new(StringComparer.OrdinalIgnoreCase)
{
Expand Down Expand Up @@ -222,7 +223,7 @@ private MethodBodyStatements BuildMessage(

if (reinjectedParamsMap.Count > 0)
{
statements.AddRange(AppendQueryParameters(uri, operation, reinjectedParamsMap));
statements.AddRange(AppendQueryParameters(uri, operation, reinjectedParamsMap, isNextLinkRequest: true));
}
}
else
Expand Down Expand Up @@ -417,7 +418,7 @@ private IEnumerable<MethodBodyStatement> AppendHeaderParameters(HttpRequestApi r
return statements;
}

private static List<MethodBodyStatement> AppendQueryParameters(ScopedApi uri, InputOperation operation, Dictionary<string, ParameterProvider> paramMap)
private static List<MethodBodyStatement> AppendQueryParameters(ScopedApi uri, InputOperation operation, Dictionary<string, ParameterProvider> paramMap, bool isNextLinkRequest = false)
{
List<MethodBodyStatement> statements = new(operation.Parameters.Count);

Expand All @@ -426,7 +427,7 @@ private static List<MethodBodyStatement> AppendQueryParameters(ScopedApi uri, In
if (inputParameter is not InputQueryParameter inputQueryParameter)
continue;

var queryStatement = BuildQueryParameterStatement(uri, inputQueryParameter, paramMap, operation);
var queryStatement = BuildQueryParameterStatement(uri, inputQueryParameter, paramMap, operation, isNextLinkRequest);
if (queryStatement != null)
{
statements.Add(queryStatement);
Expand All @@ -440,15 +441,23 @@ private static List<MethodBodyStatement> AppendQueryParameters(ScopedApi uri, In
ScopedApi uri,
InputQueryParameter inputQueryParameter,
Dictionary<string, ParameterProvider> paramMap,
InputOperation operation)
InputOperation operation,
bool isNextLinkRequest = false)
{
GetParamInfo(paramMap, operation, inputQueryParameter, out var paramType, out var serializationFormat, out var valueExpression);
if (valueExpression == null)
{
return null;
}

var statement = BuildAppendQueryStatement(uri, inputQueryParameter, paramType, valueExpression, serializationFormat);
// Determine if we should update existing parameters or always append
bool shouldUpdateExisting = isNextLinkRequest &&
ShouldSkipReinjectedParameter(inputQueryParameter.SerializedName) &&
paramType?.IsCollection != true;

MethodBodyStatement statement = shouldUpdateExisting
? BuildUpdateQueryStatement(uri, inputQueryParameter, paramType, valueExpression, serializationFormat)
: BuildAppendQueryStatement(uri, inputQueryParameter, paramType, valueExpression, serializationFormat);

// Apply null check if needed
if (!inputQueryParameter.IsRequired || paramType?.IsNullable == true ||
Expand All @@ -460,20 +469,39 @@ private static List<MethodBodyStatement> AppendQueryParameters(ScopedApi uri, In
return statement;
}

private static ValueExpression GetQueryParameterStringExpression(
CSharpType? paramType,
ValueExpression valueExpression,
SerializationFormat? serializationFormat)
{
return paramType?.Equals(typeof(string)) == true
? valueExpression
: GetParameterValueExpression(valueExpression, serializationFormat);
}

private static MethodBodyStatement BuildUpdateQueryStatement(
ScopedApi uri,
InputQueryParameter inputQueryParameter,
CSharpType? paramType,
ValueExpression valueExpression,
SerializationFormat? serializationFormat)
{
var toStringExpression = GetQueryParameterStringExpression(paramType, valueExpression, serializationFormat);
var parameterName = inputQueryParameter.SerializedName;

return uri.UpdateQuery(Literal(parameterName), toStringExpression).Terminate();
}

private static MethodBodyStatement BuildAppendQueryStatement(
ScopedApi uri,
InputQueryParameter inputQueryParameter,
CSharpType? paramType,
ValueExpression valueExpression,
SerializationFormat? serializationFormat)
{
// Handle non-collection parameters
if (paramType?.IsCollection != true)
{
var toStringExpression = paramType?.Equals(typeof(string)) == true
? valueExpression
: GetParameterValueExpression(valueExpression, serializationFormat);

var toStringExpression = GetQueryParameterStringExpression(paramType, valueExpression, serializationFormat);
return uri.AppendQuery(Literal(inputQueryParameter.SerializedName), toStringExpression, true).Terminate();
}

Expand Down Expand Up @@ -802,6 +830,12 @@ private static bool TryGetSpecialHeaderParam(InputParameter inputParameter, [Not
return false;
}

private static bool ShouldSkipReinjectedParameter(string parameterName)
{
return parameterName.Equals(MaxPageSizeParameterName, StringComparison.OrdinalIgnoreCase);
// In the future, we can extend this to check multiple parameters
}

private static List<int> GetSuccessStatusCodes(InputOperation operation)
{
HashSet<int> statusCodes = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public static InvokeMethodExpression AppendQueryDelimited(this ScopedApi uriBuil
? uriBuilder.Invoke("AppendQueryDelimited", [name, value, Literal(delimiter), format, PositionalReference("escape", Literal(shouldEscape))])
: uriBuilder.Invoke("AppendQueryDelimited", [name, value, Literal(delimiter), PositionalReference("escape", Literal(shouldEscape))]);

public static InvokeMethodExpression UpdateQuery(this ScopedApi uriBuilder, ValueExpression name, ValueExpression value)
=> uriBuilder.Invoke("UpdateQuery", name, value);

public static ScopedApi<Uri> ToUri(this ScopedApi uriBuilder)
=> uriBuilder.Invoke("ToUri").As<Uri>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,5 +481,23 @@ public void AppendQueryDelimited(string endpoint, IEnumerable<string> value, str

Assert.AreEqual(expected, builder.ToUri().ToString());
}

[TestCase("http://localhost", "param1", "value1", "http://localhost/?param1=value1")]
[TestCase("http://localhost?existing=old", "param1", "value1", "http://localhost/?existing=old&param1=value1")]
[TestCase("http://localhost?param1=old", "param1", "new", "http://localhost/?param1=new")]
[TestCase("http://localhost?param1=old&param2=value2", "param1", "new", "http://localhost/?param1=new&param2=value2")]
[TestCase("http://localhost?param2=value2&param1=old", "param1", "new", "http://localhost/?param2=value2&param1=new")]
[TestCase("http://localhost?param2=value2&param1=old&param3=value3", "param1", "new", "http://localhost/?param2=value2&param1=new&param3=value3")]
[TestCase("http://localhost?fooparam1=value2&param1=old", "param1", "new", "http://localhost/?fooparam1=value2&param1=new")]
[TestCase("http://localhost?param1prefix=value2&param1=old", "param1", "new", "http://localhost/?param1prefix=value2&param1=new")]
public void UpdateQuery(string endpoint, string name, string value, string expected)
Comment thread
radhgupta marked this conversation as resolved.
{
var builder = new SampleTypeSpec.ClientUriBuilder();
builder.Reset(new Uri(endpoint));

builder.UpdateQuery(name, value);

Assert.AreEqual(expected, builder.ToUri().ToString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public partial class TestClient
uri.AppendQuery("p1", p1, true);
if ((maxPageSize != null))
{
uri.AppendQuery("maxPageSize", global::Sample.TypeFormatters.ConvertToString(maxPageSize), true);
uri.UpdateQuery("maxPageSize", global::Sample.TypeFormatters.ConvertToString(maxPageSize));
}
global::System.ClientModel.Primitives.PipelineMessage message = Pipeline.CreateMessage(uri.ToUri(), "GET", PipelineMessageClassifier200);
global::System.ClientModel.Primitives.PipelineRequest request = message.Request;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,53 @@ public void AppendQueryDelimited<T>(string name, IEnumerable<T> value, string de
AppendQuery(name, string.Join(delimiter, stringValues), escape);
}

public void UpdateQuery(string name, string value)
{
if (PathAndQuery.Length == _pathLength)
Comment thread
radhgupta marked this conversation as resolved.
{
PathAndQuery.Append('?');
Comment thread
radhgupta marked this conversation as resolved.
Outdated
PathAndQuery.Append(name);
PathAndQuery.Append('=');
PathAndQuery.Append(value);
}
else
{
int queryStartIndex = _pathLength + 1;
string searchPattern = name + "=";
string queryString = PathAndQuery.ToString(queryStartIndex, PathAndQuery.Length - queryStartIndex);
int paramStartIndex = -1;
if (queryString.StartsWith(searchPattern))
{
paramStartIndex = 0;
}
if (paramStartIndex == -1)
{
int prefixedIndex = queryString.IndexOf("&" + searchPattern);
if (prefixedIndex >= 0)
{
paramStartIndex = prefixedIndex + 1;
}
}
if (paramStartIndex >= 0)
{
int valueStartIndex = paramStartIndex + searchPattern.Length;
int valueEndIndex = queryString.IndexOf('&', valueStartIndex);
if (valueEndIndex == -1)
{
valueEndIndex = queryString.Length;
}
int globalStart = queryStartIndex + valueStartIndex;
int globalEnd = queryStartIndex + valueEndIndex;
PathAndQuery.Remove(globalStart, globalEnd - globalStart);
PathAndQuery.Insert(globalStart, value);
}
else
{
AppendQuery(name, value, false);
}
}
}

public Uri ToUri()
{
UriBuilder.Path = PathAndQuery.ToString(0, _pathLength);
Expand Down
Loading