Added overload to support SDK supplying query string on invoked URL (#1310)

* Refactored extensions and their tests into separate directories

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added overload to method invocation to allow query string parameters to be passed in via the SDK instead of being uncermoniously added to the end of the produced HttpRequestMessage URI

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added unit tests to support implementation

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Marking HttpExtensions as internal to prevent external usage and updating to work against Uri instead of HttpRequestMessage.

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Updated unit tests to match new extension purpose

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Resolved an ambiguous method invocation wherein it was taking the query string and passing it as the payload for a request. Removed the offending method and reworked the remaining configurations so there's no API impact.

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

---------

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
This commit is contained in:
Whit Waldo 2024-07-03 12:47:54 -05:00 committed by GitHub
parent ddce8a2972
commit 3768a983b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 238 additions and 13 deletions

View File

@ -306,6 +306,20 @@ namespace Dapr.Client
return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName);
}
/// <summary>
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
/// with the <c>POST</c> HTTP method.
/// </summary>
/// <param name="appId">The Dapr application id to invoke the method on.</param>
/// <param name="methodName">The name of the method to invoke.</param>
/// <param name="queryStringParameters">A collection of key/value pairs to populate the query string from.</param>
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, IReadOnlyCollection<KeyValuePair<string,string>> queryStringParameters)
{
return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, queryStringParameters);
}
/// <summary>
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
@ -317,6 +331,19 @@ namespace Dapr.Client
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName);
/// <summary>
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
/// with the HTTP method specified by <paramref name="httpMethod" />.
/// </summary>
/// <param name="httpMethod">The <see cref="HttpMethod" /> to use for the invocation request.</param>
/// <param name="appId">The Dapr application id to invoke the method on.</param>
/// <param name="methodName">The name of the method to invoke.</param>
/// <param name="queryStringParameters">A collection of key/value pairs to populate the query string from.</param>
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId,
string methodName, IReadOnlyCollection<KeyValuePair<string, string>> queryStringParameters);
/// <summary>
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
@ -329,9 +356,9 @@ namespace Dapr.Client
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
public HttpRequestMessage CreateInvokeMethodRequest<TRequest>(string appId, string methodName, TRequest data)
{
return CreateInvokeMethodRequest<TRequest>(HttpMethod.Post, appId, methodName, data);
return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, new List<KeyValuePair<string, string>>(), data);
}
/// <summary>
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
@ -343,9 +370,10 @@ namespace Dapr.Client
/// <param name="appId">The Dapr application id to invoke the method on.</param>
/// <param name="methodName">The name of the method to invoke.</param>
/// <param name="data">The data that will be JSON serialized and provided as the request body.</param>
/// <param name="queryStringParameters">A collection of key/value pairs to populate the query string from.</param>
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
public abstract HttpRequestMessage CreateInvokeMethodRequest<TRequest>(HttpMethod httpMethod, string appId, string methodName, TRequest data);
public abstract HttpRequestMessage CreateInvokeMethodRequest<TRequest>(HttpMethod httpMethod, string appId, string methodName, IReadOnlyCollection<KeyValuePair<string,string>> queryStringParameters, TRequest data);
/// <summary>
/// Perform health-check of Dapr sidecar. Return 'true' if sidecar is healthy. Otherwise 'false'.
/// CheckHealthAsync handle <see cref="HttpRequestException"/> and will return 'false' if error will occur on transport level
@ -526,7 +554,7 @@ namespace Dapr.Client
TRequest data,
CancellationToken cancellationToken = default)
{
var request = CreateInvokeMethodRequest<TRequest>(httpMethod, appId, methodName, data);
var request = CreateInvokeMethodRequest<TRequest>(httpMethod, appId, methodName, new List<KeyValuePair<string, string>>(), data);
return InvokeMethodAsync(request, cancellationToken);
}
@ -620,7 +648,7 @@ namespace Dapr.Client
TRequest data,
CancellationToken cancellationToken = default)
{
var request = CreateInvokeMethodRequest<TRequest>(httpMethod, appId, methodName, data);
var request = CreateInvokeMethodRequest<TRequest>(httpMethod, appId, methodName, new List<KeyValuePair<string, string>>(), data);
return InvokeMethodAsync<TResponse>(request, cancellationToken);
}

View File

@ -345,7 +345,32 @@ namespace Dapr.Client
#region InvokeMethod Apis
/// <summary>
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
/// with the HTTP method specified by <paramref name="httpMethod" />.
/// </summary>
/// <param name="httpMethod">The <see cref="HttpMethod" /> to use for the invocation request.</param>
/// <param name="appId">The Dapr application id to invoke the method on.</param>
/// <param name="methodName">The name of the method to invoke.</param>
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName)
{
return CreateInvokeMethodRequest(httpMethod, appId, methodName, new List<KeyValuePair<string, string>>());
}
/// <summary>
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
/// with the HTTP method specified by <paramref name="httpMethod" />.
/// </summary>
/// <param name="httpMethod">The <see cref="HttpMethod" /> to use for the invocation request.</param>
/// <param name="appId">The Dapr application id to invoke the method on.</param>
/// <param name="methodName">The name of the method to invoke.</param>
/// <param name="queryStringParameters">A collection of key/value pairs to populate the query string from.</param>
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName,
IReadOnlyCollection<KeyValuePair<string, string>> queryStringParameters)
{
ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod));
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
@ -356,7 +381,8 @@ namespace Dapr.Client
//
// This approach avoids some common pitfalls that could lead to undesired encoding.
var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}";
var request = new HttpRequestMessage(httpMethod, new Uri(this.httpEndpoint, path));
var requestUri = new Uri(this.httpEndpoint, path).AddQueryParameters(queryStringParameters);
var request = new HttpRequestMessage(httpMethod, requestUri);
request.Options.Set(new HttpRequestOptionsKey<string>(AppIdKey), appId);
request.Options.Set(new HttpRequestOptionsKey<string>(MethodNameKey), methodName);
@ -369,13 +395,27 @@ namespace Dapr.Client
return request;
}
public override HttpRequestMessage CreateInvokeMethodRequest<TRequest>(HttpMethod httpMethod, string appId, string methodName, TRequest data)
/// <summary>
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
/// with the HTTP method specified by <paramref name="httpMethod" /> and a JSON serialized request body specified by
/// <paramref name="data" />.
/// </summary>
/// <typeparam name="TRequest">The type of the data that will be JSON serialized and provided as the request body.</typeparam>
/// <param name="httpMethod">The <see cref="HttpMethod" /> to use for the invocation request.</param>
/// <param name="appId">The Dapr application id to invoke the method on.</param>
/// <param name="methodName">The name of the method to invoke.</param>
/// <param name="data">The data that will be JSON serialized and provided as the request body.</param>
/// <param name="queryStringParameters">A collection of key/value pairs to populate the query string from.</param>
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
public override HttpRequestMessage CreateInvokeMethodRequest<TRequest>(HttpMethod httpMethod, string appId, string methodName,
IReadOnlyCollection<KeyValuePair<string, string>> queryStringParameters, TRequest data)
{
ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod));
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName));
var request = CreateInvokeMethodRequest(httpMethod, appId, methodName);
var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters);
request.Content = JsonContent.Create<TRequest>(data, options: this.JsonSerializerOptions);
return request;
}

View File

@ -11,6 +11,7 @@
// limitations under the License.
// ------------------------------------------------------------------------
#nullable enable
using System;
using System.Reflection;
using System.Runtime.Serialization;
@ -27,12 +28,14 @@ namespace Dapr.Client
/// <returns></returns>
public static string GetValueFromEnumMember<T>(this T value) where T : Enum
{
ArgumentNullException.ThrowIfNull(value, nameof(value));
var memberInfo = typeof(T).GetMember(value.ToString(), BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
if (memberInfo.Length <= 0)
return value.ToString();
var attributes = memberInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false);
return attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString();
return (attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString()) ?? value.ToString();
}
}
}

View File

@ -0,0 +1,51 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
#nullable enable
using System;
using System.Collections.Generic;
using System.Text;
namespace Dapr.Client
{
/// <summary>
/// Provides extensions specific to HTTP types.
/// </summary>
internal static class HttpExtensions
{
/// <summary>
/// Appends key/value pairs to the query string on an HttpRequestMessage.
/// </summary>
/// <param name="uri">The uri to append the query string parameters to.</param>
/// <param name="queryStringParameters">The key/value pairs to populate the query string with.</param>
public static Uri AddQueryParameters(this Uri? uri,
IReadOnlyCollection<KeyValuePair<string, string>>? queryStringParameters)
{
ArgumentNullException.ThrowIfNull(uri, nameof(uri));
if (queryStringParameters is null)
return uri;
var uriBuilder = new UriBuilder(uri);
var qsBuilder = new StringBuilder(uriBuilder.Query);
foreach (var kvParam in queryStringParameters)
{
if (qsBuilder.Length > 0)
qsBuilder.Append('&');
qsBuilder.Append($"{Uri.EscapeDataString(kvParam.Key)}={Uri.EscapeDataString(kvParam.Value)}");
}
uriBuilder.Query = qsBuilder.ToString();
return uriBuilder.Uri;
}
}
}

View File

@ -518,6 +518,18 @@ namespace Dapr.Client.Test
Assert.Equal(new Uri(expected).AbsoluteUri, request.RequestUri.AbsoluteUri);
}
[Fact]
public async Task CreateInvokeMethodRequest_AppendQueryStringValuesCorrectly()
{
await using var client = TestClient.CreateForDaprClient(c =>
{
c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions);
});
var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "mymethod", (IReadOnlyCollection<KeyValuePair<string,string>>)new List<KeyValuePair<string, string>> { new("a", "0"), new("b", "1") });
Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri);
}
[Fact]
public async Task CreateInvokeMethodRequest_WithoutApiToken_CreatesHttpRequestWithoutApiTokenHeader()
{
@ -617,6 +629,34 @@ namespace Dapr.Client.Test
Assert.Equal(data.Color, actual.Color);
}
[Fact]
public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContentWithQueryString()
{
await using var client = TestClient.CreateForDaprClient(c =>
{
c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions);
});
var data = new Widget
{
Color = "red",
};
var request = client.InnerClient.CreateInvokeMethodRequest(HttpMethod.Post, "test-app", "test", new List<KeyValuePair<string, string>> { new("a", "0"), new("b", "1") }, data);
Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/test?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri);
var content = Assert.IsType<JsonContent>(request.Content);
Assert.Equal(typeof(Widget), content.ObjectType);
Assert.Same(data, content.Value);
// the best way to verify the usage of the correct settings object
var actual = await content.ReadFromJsonAsync<Widget>(this.jsonSerializerOptions);
Assert.Equal(data.Color, actual.Color);
}
[Fact]
public async Task InvokeMethodWithResponseAsync_ReturnsMessageWithoutCheckingStatus()
{

View File

@ -1,7 +1,7 @@
using System.Runtime.Serialization;
using Xunit;
namespace Dapr.Client.Test
namespace Dapr.Client.Test.Extensions
{
public class EnumExtensionTest
{
@ -29,9 +29,9 @@ namespace Dapr.Client.Test
public enum TestEnum
{
[EnumMember(Value="red")]
[EnumMember(Value = "red")]
Red,
[EnumMember(Value="YELLOW")]
[EnumMember(Value = "YELLOW")]
Yellow,
Blue
}

View File

@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Net.Http;
using Xunit;
namespace Dapr.Client.Test.Extensions
{
public class HttpExtensionTest
{
[Fact]
public void AddQueryParameters_ReturnsEmptyQueryStringWithNullParameters()
{
const string uri = "https://localhost/mypath";
var httpRq = new HttpRequestMessage(HttpMethod.Get, uri);
var updatedUri = httpRq.RequestUri.AddQueryParameters(null);
Assert.Equal(uri, updatedUri.AbsoluteUri);
}
[Fact]
public void AddQueryParameters_ReturnsOriginalQueryStringWithNullParameters()
{
const string uri = "https://localhost/mypath?a=0&b=1";
var httpRq = new HttpRequestMessage(HttpMethod.Get, uri);
var updatedUri = httpRq.RequestUri.AddQueryParameters(null);
Assert.Equal(uri, updatedUri.AbsoluteUri);
}
[Fact]
public void AddQueryParameters_BuildsQueryString()
{
var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath?a=0");
var updatedUri = httpRq.RequestUri.AddQueryParameters(new List<KeyValuePair<string,string>>
{
new("test", "value")
});
Assert.Equal("https://localhost/mypath?a=0&test=value", updatedUri.AbsoluteUri);
}
[Fact]
public void AddQueryParameters_BuildQueryStringWithDuplicateKeys()
{
var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath");
var updatedUri = httpRq.RequestUri.AddQueryParameters(new List<KeyValuePair<string,string>>
{
new("test", "1"),
new("test", "2"),
new("test", "3")
});
Assert.Equal("https://localhost/mypath?test=1&test=2&test=3", updatedUri.AbsoluteUri);
}
[Fact]
public void AddQueryParameters_EscapeSpacesInValues()
{
var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath");
var updatedUri = httpRq.RequestUri.AddQueryParameters(new List<KeyValuePair<string,string>>
{
new("name1", "John Doe"),
new("name2", "Jane Doe")
});
Assert.Equal("https://localhost/mypath?name1=John%20Doe&name2=Jane%20Doe", updatedUri.AbsoluteUri);
}
}
}