Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Jul 3, 2024
44 changes: 43 additions & 1 deletion 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,7 +356,7 @@ 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, data);
}

/// <summary>
Expand All @@ -346,6 +373,21 @@ public HttpRequestMessage CreateInvokeMethodRequest<TRequest>(string appId, stri
/// <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);

/// <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 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
61 changes: 59 additions & 2 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 @@ -357,7 +382,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));

request.RequestUri = request.RequestUri.AddQueryParameters(queryStringParameters);

request.Options.Set(new HttpRequestOptionsKey<string>(AppIdKey), appId);
request.Options.Set(new HttpRequestOptionsKey<string>(MethodNameKey), methodName);

Expand All @@ -369,13 +395,44 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth
return request;
}

/// <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>
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
public override HttpRequestMessage CreateInvokeMethodRequest<TRequest>(HttpMethod httpMethod, string appId, string methodName, TRequest data)
{
return CreateInvokeMethodRequest(httpMethod, 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" />
/// 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why not NameValueCollection?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fundamentally, I was looking to ensure that a read-only collection is passed. Conceptually, they wind up operationally working the same way. I don't see why an overload couldn't be added for a NameValueCollection as well.

{
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 @@ -59,7 +59,7 @@
Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri);
Assert.Null(request.Request.Content);

await request.CompleteAsync(new HttpResponseMessage());

Check failure on line 62 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 6.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidVoidNoHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.

Check failure on line 62 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 7.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidVoidNoHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.

Check failure on line 62 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 8.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidVoidNoHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.
}

[Fact]
Expand All @@ -80,7 +80,7 @@
Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri);
Assert.Null(request.Request.Content);

await request.CompleteAsync(new HttpResponseMessage());

Check failure on line 83 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 6.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidVoidWithHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.

Check failure on line 83 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 7.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidVoidWithHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.

Check failure on line 83 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 8.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidVoidWithHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.
}

[Fact]
Expand All @@ -107,7 +107,7 @@
};

var actual = await request.CompleteWithJsonAsync(expected, jsonSerializerOptions);
Assert.Equal(expected.Color, actual.Color);

Check failure on line 110 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 6.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidResponseNoHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.

Check failure on line 110 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 7.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidResponseNoHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.

Check failure on line 110 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 8.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidResponseNoHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.
}

[Fact]
Expand All @@ -133,7 +133,7 @@
};

var actual = await request.CompleteWithJsonAsync(expected, jsonSerializerOptions);
Assert.Equal(expected.Color, actual.Color);

Check failure on line 136 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 6.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidResponseWithHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.

Check failure on line 136 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 7.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidResponseWithHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.

Check failure on line 136 in test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

View workflow job for this annotation

GitHub Actions / Test .NET 8.0

Dapr.Client.Test.DaprClientTest.InvokeMethodAsync_VoidResponseWithHttpMethod_Success: System.InvalidOperationException : The client has 1 or more incomplete requests. Use 'request.Dismiss()' if the test is uninterested in the response.
}

[Fact]
Expand Down Expand Up @@ -518,6 +518,18 @@
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 @@
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
Loading