Skip to content

Commit

Permalink
Added overload to method invocation to allow query string parameters …
Browse files Browse the repository at this point in the history
…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 <[email protected]>
  • Loading branch information
WhitWaldo committed Jun 19, 2024
1 parent 3c537d8 commit 15e44a2
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 2 deletions.
42 changes: 42 additions & 0 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 Down Expand Up @@ -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.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
52 changes: 52 additions & 0 deletions src/Dapr.Client/Extensions/HttpExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// ------------------------------------------------------------------------
// 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.Net.Http;
using System.Text;

namespace Dapr.Client
{
/// <summary>
/// Provides extensions specific to HTTP types.
/// </summary>
public static class HttpExtensions
{
/// <summary>
/// Appends key/value pairs to the query string on an HttpRequestMessage.
/// </summary>
/// <param name="message">The message to append the query string parameters to.</param>
/// <param name="queryStringParameters">The key/value pairs to populate the query string with.</param>
public static void AddQueryParameters(this HttpRequestMessage? message,
IReadOnlyCollection<KeyValuePair<string, string>>? queryStringParameters)
{
ArgumentNullException.ThrowIfNull(message, nameof(message));
if (queryStringParameters is null || message.RequestUri is null)
return;

var uriBuilder = new UriBuilder(message.RequestUri);
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();
message.RequestUri = uriBuilder.Uri;
}
}
}
63 changes: 63 additions & 0 deletions test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs
Original file line number Diff line number Diff line change
@@ -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);
httpRq.AddQueryParameters(null);
Assert.Equal(uri, httpRq.RequestUri.AbsoluteUri);
}

[Fact]
public void AddQueryParameters_ReturnsOriginalQueryStringWithNullParameters()
{
const string uri = "https://localhost/mypath?a=0&b=1";
var httpRq = new HttpRequestMessage(HttpMethod.Get, uri);
httpRq.AddQueryParameters(null);
Assert.Equal(uri, httpRq.RequestUri.AbsoluteUri);
}

[Fact]
public void AddQueryParameters_BuildsQueryString()
{
var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath?a=0");
httpRq.AddQueryParameters(new List<KeyValuePair<string,string>>
{
new("test", "value")
});
Assert.Equal("https://localhost/mypath?a=0&test=value", httpRq.RequestUri.AbsoluteUri);
}

[Fact]
public void AddQueryParameters_BuildQueryStringWithDuplicateKeys()
{
var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath");
httpRq.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", httpRq.RequestUri.AbsoluteUri);
}

[Fact]
public void AddQueryParameters_EscapeSpacesInValues()
{
var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath");
httpRq.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", httpRq.RequestUri.AbsoluteUri);
}
}
}

0 comments on commit 15e44a2

Please sign in to comment.