Skip to content

Commit 3768a98

Browse files
authored
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>
1 parent ddce8a2 commit 3768a98

File tree

7 files changed

+238
-13
lines changed

7 files changed

+238
-13
lines changed

src/Dapr.Client/DaprClient.cs

+34-6
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,20 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN
306306
return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName);
307307
}
308308

309+
/// <summary>
310+
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
311+
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
312+
/// with the <c>POST</c> HTTP method.
313+
/// </summary>
314+
/// <param name="appId">The Dapr application id to invoke the method on.</param>
315+
/// <param name="methodName">The name of the method to invoke.</param>
316+
/// <param name="queryStringParameters">A collection of key/value pairs to populate the query string from.</param>
317+
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
318+
public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, IReadOnlyCollection<KeyValuePair<string,string>> queryStringParameters)
319+
{
320+
return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, queryStringParameters);
321+
}
322+
309323
/// <summary>
310324
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
311325
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
@@ -317,6 +331,19 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN
317331
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
318332
public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName);
319333

334+
/// <summary>
335+
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
336+
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
337+
/// with the HTTP method specified by <paramref name="httpMethod" />.
338+
/// </summary>
339+
/// <param name="httpMethod">The <see cref="HttpMethod" /> to use for the invocation request.</param>
340+
/// <param name="appId">The Dapr application id to invoke the method on.</param>
341+
/// <param name="methodName">The name of the method to invoke.</param>
342+
/// <param name="queryStringParameters">A collection of key/value pairs to populate the query string from.</param>
343+
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
344+
public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId,
345+
string methodName, IReadOnlyCollection<KeyValuePair<string, string>> queryStringParameters);
346+
320347
/// <summary>
321348
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
322349
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
@@ -329,9 +356,9 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN
329356
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
330357
public HttpRequestMessage CreateInvokeMethodRequest<TRequest>(string appId, string methodName, TRequest data)
331358
{
332-
return CreateInvokeMethodRequest<TRequest>(HttpMethod.Post, appId, methodName, data);
359+
return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, new List<KeyValuePair<string, string>>(), data);
333360
}
334-
361+
335362
/// <summary>
336363
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
337364
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
@@ -343,9 +370,10 @@ public HttpRequestMessage CreateInvokeMethodRequest<TRequest>(string appId, stri
343370
/// <param name="appId">The Dapr application id to invoke the method on.</param>
344371
/// <param name="methodName">The name of the method to invoke.</param>
345372
/// <param name="data">The data that will be JSON serialized and provided as the request body.</param>
373+
/// <param name="queryStringParameters">A collection of key/value pairs to populate the query string from.</param>
346374
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
347-
public abstract HttpRequestMessage CreateInvokeMethodRequest<TRequest>(HttpMethod httpMethod, string appId, string methodName, TRequest data);
348-
375+
public abstract HttpRequestMessage CreateInvokeMethodRequest<TRequest>(HttpMethod httpMethod, string appId, string methodName, IReadOnlyCollection<KeyValuePair<string,string>> queryStringParameters, TRequest data);
376+
349377
/// <summary>
350378
/// Perform health-check of Dapr sidecar. Return 'true' if sidecar is healthy. Otherwise 'false'.
351379
/// CheckHealthAsync handle <see cref="HttpRequestException"/> and will return 'false' if error will occur on transport level
@@ -526,7 +554,7 @@ public Task InvokeMethodAsync<TRequest>(
526554
TRequest data,
527555
CancellationToken cancellationToken = default)
528556
{
529-
var request = CreateInvokeMethodRequest<TRequest>(httpMethod, appId, methodName, data);
557+
var request = CreateInvokeMethodRequest<TRequest>(httpMethod, appId, methodName, new List<KeyValuePair<string, string>>(), data);
530558
return InvokeMethodAsync(request, cancellationToken);
531559
}
532560

@@ -620,7 +648,7 @@ public Task<TResponse> InvokeMethodAsync<TRequest, TResponse>(
620648
TRequest data,
621649
CancellationToken cancellationToken = default)
622650
{
623-
var request = CreateInvokeMethodRequest<TRequest>(httpMethod, appId, methodName, data);
651+
var request = CreateInvokeMethodRequest<TRequest>(httpMethod, appId, methodName, new List<KeyValuePair<string, string>>(), data);
624652
return InvokeMethodAsync<TResponse>(request, cancellationToken);
625653
}
626654

src/Dapr.Client/DaprClientGrpc.cs

+43-3
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,32 @@ public override async Task<BindingResponse> InvokeBindingAsync(BindingRequest re
345345

346346
#region InvokeMethod Apis
347347

348+
/// <summary>
349+
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
350+
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
351+
/// with the HTTP method specified by <paramref name="httpMethod" />.
352+
/// </summary>
353+
/// <param name="httpMethod">The <see cref="HttpMethod" /> to use for the invocation request.</param>
354+
/// <param name="appId">The Dapr application id to invoke the method on.</param>
355+
/// <param name="methodName">The name of the method to invoke.</param>
356+
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
348357
public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName)
358+
{
359+
return CreateInvokeMethodRequest(httpMethod, appId, methodName, new List<KeyValuePair<string, string>>());
360+
}
361+
362+
/// <summary>
363+
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
364+
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
365+
/// with the HTTP method specified by <paramref name="httpMethod" />.
366+
/// </summary>
367+
/// <param name="httpMethod">The <see cref="HttpMethod" /> to use for the invocation request.</param>
368+
/// <param name="appId">The Dapr application id to invoke the method on.</param>
369+
/// <param name="methodName">The name of the method to invoke.</param>
370+
/// <param name="queryStringParameters">A collection of key/value pairs to populate the query string from.</param>
371+
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
372+
public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName,
373+
IReadOnlyCollection<KeyValuePair<string, string>> queryStringParameters)
349374
{
350375
ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod));
351376
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
@@ -356,7 +381,8 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth
356381
//
357382
// This approach avoids some common pitfalls that could lead to undesired encoding.
358383
var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}";
359-
var request = new HttpRequestMessage(httpMethod, new Uri(this.httpEndpoint, path));
384+
var requestUri = new Uri(this.httpEndpoint, path).AddQueryParameters(queryStringParameters);
385+
var request = new HttpRequestMessage(httpMethod, requestUri);
360386

361387
request.Options.Set(new HttpRequestOptionsKey<string>(AppIdKey), appId);
362388
request.Options.Set(new HttpRequestOptionsKey<string>(MethodNameKey), methodName);
@@ -369,13 +395,27 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth
369395
return request;
370396
}
371397

372-
public override HttpRequestMessage CreateInvokeMethodRequest<TRequest>(HttpMethod httpMethod, string appId, string methodName, TRequest data)
398+
/// <summary>
399+
/// Creates an <see cref="HttpRequestMessage" /> that can be used to perform service invocation for the
400+
/// application identified by <paramref name="appId" /> and invokes the method specified by <paramref name="methodName" />
401+
/// with the HTTP method specified by <paramref name="httpMethod" /> and a JSON serialized request body specified by
402+
/// <paramref name="data" />.
403+
/// </summary>
404+
/// <typeparam name="TRequest">The type of the data that will be JSON serialized and provided as the request body.</typeparam>
405+
/// <param name="httpMethod">The <see cref="HttpMethod" /> to use for the invocation request.</param>
406+
/// <param name="appId">The Dapr application id to invoke the method on.</param>
407+
/// <param name="methodName">The name of the method to invoke.</param>
408+
/// <param name="data">The data that will be JSON serialized and provided as the request body.</param>
409+
/// <param name="queryStringParameters">A collection of key/value pairs to populate the query string from.</param>
410+
/// <returns>An <see cref="HttpRequestMessage" /> for use with <c>SendInvokeMethodRequestAsync</c>.</returns>
411+
public override HttpRequestMessage CreateInvokeMethodRequest<TRequest>(HttpMethod httpMethod, string appId, string methodName,
412+
IReadOnlyCollection<KeyValuePair<string, string>> queryStringParameters, TRequest data)
373413
{
374414
ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod));
375415
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
376416
ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName));
377417

378-
var request = CreateInvokeMethodRequest(httpMethod, appId, methodName);
418+
var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters);
379419
request.Content = JsonContent.Create<TRequest>(data, options: this.JsonSerializerOptions);
380420
return request;
381421
}

src/Dapr.Client/EnumExtensions.cs renamed to src/Dapr.Client/Extensions/EnumExtensions.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// limitations under the License.
1212
// ------------------------------------------------------------------------
1313

14+
#nullable enable
1415
using System;
1516
using System.Reflection;
1617
using System.Runtime.Serialization;
@@ -27,12 +28,14 @@ internal static class EnumExtensions
2728
/// <returns></returns>
2829
public static string GetValueFromEnumMember<T>(this T value) where T : Enum
2930
{
31+
ArgumentNullException.ThrowIfNull(value, nameof(value));
32+
3033
var memberInfo = typeof(T).GetMember(value.ToString(), BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
3134
if (memberInfo.Length <= 0)
3235
return value.ToString();
3336

3437
var attributes = memberInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false);
35-
return attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString();
38+
return (attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString()) ?? value.ToString();
3639
}
3740
}
3841
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// ------------------------------------------------------------------------
2+
// Copyright 2024 The Dapr Authors
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ------------------------------------------------------------------------
13+
14+
#nullable enable
15+
using System;
16+
using System.Collections.Generic;
17+
using System.Text;
18+
19+
namespace Dapr.Client
20+
{
21+
/// <summary>
22+
/// Provides extensions specific to HTTP types.
23+
/// </summary>
24+
internal static class HttpExtensions
25+
{
26+
/// <summary>
27+
/// Appends key/value pairs to the query string on an HttpRequestMessage.
28+
/// </summary>
29+
/// <param name="uri">The uri to append the query string parameters to.</param>
30+
/// <param name="queryStringParameters">The key/value pairs to populate the query string with.</param>
31+
public static Uri AddQueryParameters(this Uri? uri,
32+
IReadOnlyCollection<KeyValuePair<string, string>>? queryStringParameters)
33+
{
34+
ArgumentNullException.ThrowIfNull(uri, nameof(uri));
35+
if (queryStringParameters is null)
36+
return uri;
37+
38+
var uriBuilder = new UriBuilder(uri);
39+
var qsBuilder = new StringBuilder(uriBuilder.Query);
40+
foreach (var kvParam in queryStringParameters)
41+
{
42+
if (qsBuilder.Length > 0)
43+
qsBuilder.Append('&');
44+
qsBuilder.Append($"{Uri.EscapeDataString(kvParam.Key)}={Uri.EscapeDataString(kvParam.Value)}");
45+
}
46+
47+
uriBuilder.Query = qsBuilder.ToString();
48+
return uriBuilder.Uri;
49+
}
50+
}
51+
}

test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs

+40
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,18 @@ public async Task CreateInvokeMethodRequest_TransformsUrlCorrectly(string method
518518
Assert.Equal(new Uri(expected).AbsoluteUri, request.RequestUri.AbsoluteUri);
519519
}
520520

521+
[Fact]
522+
public async Task CreateInvokeMethodRequest_AppendQueryStringValuesCorrectly()
523+
{
524+
await using var client = TestClient.CreateForDaprClient(c =>
525+
{
526+
c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions);
527+
});
528+
529+
var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "mymethod", (IReadOnlyCollection<KeyValuePair<string,string>>)new List<KeyValuePair<string, string>> { new("a", "0"), new("b", "1") });
530+
Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri);
531+
}
532+
521533
[Fact]
522534
public async Task CreateInvokeMethodRequest_WithoutApiToken_CreatesHttpRequestWithoutApiTokenHeader()
523535
{
@@ -617,6 +629,34 @@ public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContent()
617629
Assert.Equal(data.Color, actual.Color);
618630
}
619631

632+
[Fact]
633+
public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContentWithQueryString()
634+
{
635+
await using var client = TestClient.CreateForDaprClient(c =>
636+
{
637+
c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions);
638+
});
639+
640+
var data = new Widget
641+
{
642+
Color = "red",
643+
};
644+
645+
var request = client.InnerClient.CreateInvokeMethodRequest(HttpMethod.Post, "test-app", "test", new List<KeyValuePair<string, string>> { new("a", "0"), new("b", "1") }, data);
646+
647+
Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/test?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri);
648+
649+
var content = Assert.IsType<JsonContent>(request.Content);
650+
Assert.Equal(typeof(Widget), content.ObjectType);
651+
Assert.Same(data, content.Value);
652+
653+
// the best way to verify the usage of the correct settings object
654+
var actual = await content.ReadFromJsonAsync<Widget>(this.jsonSerializerOptions);
655+
Assert.Equal(data.Color, actual.Color);
656+
}
657+
658+
659+
620660
[Fact]
621661
public async Task InvokeMethodWithResponseAsync_ReturnsMessageWithoutCheckingStatus()
622662
{

test/Dapr.Client.Test/EnumExtensionTest.cs renamed to test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System.Runtime.Serialization;
22
using Xunit;
33

4-
namespace Dapr.Client.Test
4+
namespace Dapr.Client.Test.Extensions
55
{
66
public class EnumExtensionTest
77
{
@@ -29,9 +29,9 @@ public void GetValueFromEnumMember_BlueResolvesAsExpected()
2929

3030
public enum TestEnum
3131
{
32-
[EnumMember(Value="red")]
32+
[EnumMember(Value = "red")]
3333
Red,
34-
[EnumMember(Value="YELLOW")]
34+
[EnumMember(Value = "YELLOW")]
3535
Yellow,
3636
Blue
3737
}

0 commit comments

Comments
 (0)