Skip to content

Commit

Permalink
Added overload to support SDK supplying query string on invoked URL (#…
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
WhitWaldo authored Jul 3, 2024
1 parent ddce8a2 commit 3768a98
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 13 deletions.
40 changes: 34 additions & 6 deletions src/Dapr.Client/DaprClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,20 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN
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" />
Expand All @@ -317,6 +331,19 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN
/// <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" />
Expand All @@ -329,9 +356,9 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN
/// <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" />
Expand All @@ -343,9 +370,10 @@ public HttpRequestMessage CreateInvokeMethodRequest<TRequest>(string appId, stri
/// <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
Expand Down Expand Up @@ -526,7 +554,7 @@ public Task InvokeMethodAsync<TRequest>(
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);
}

Expand Down Expand Up @@ -620,7 +648,7 @@ public Task<TResponse> InvokeMethodAsync<TRequest, TResponse>(
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);
}

Expand Down
46 changes: 43 additions & 3 deletions src/Dapr.Client/DaprClientGrpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,32 @@ public override async Task<BindingResponse> InvokeBindingAsync(BindingRequest re

#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));
Expand All @@ -356,7 +381,8 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth
//
// 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);
Expand All @@ -369,13 +395,27 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// limitations under the License.
// ------------------------------------------------------------------------

#nullable enable
using System;
using System.Reflection;
using System.Runtime.Serialization;
Expand All @@ -27,12 +28,14 @@ internal static class EnumExtensions
/// <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();
}
}
}
51 changes: 51 additions & 0 deletions src/Dapr.Client/Extensions/HttpExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
40 changes: 40 additions & 0 deletions test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,18 @@ public async Task CreateInvokeMethodRequest_TransformsUrlCorrectly(string method
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()
{
Expand Down Expand Up @@ -617,6 +629,34 @@ public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContent()
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()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Runtime.Serialization;
using Xunit;

namespace Dapr.Client.Test
namespace Dapr.Client.Test.Extensions
{
public class EnumExtensionTest
{
Expand Down Expand Up @@ -29,9 +29,9 @@ public void GetValueFromEnumMember_BlueResolvesAsExpected()

public enum TestEnum
{
[EnumMember(Value="red")]
[EnumMember(Value = "red")]
Red,
[EnumMember(Value="YELLOW")]
[EnumMember(Value = "YELLOW")]
Yellow,
Blue
}
Expand Down
Loading

0 comments on commit 3768a98

Please sign in to comment.