mirror of https://github.com/dapr/dotnet-sdk.git
Issue 184 (#185)
* Add InvokeClient and InvokeHttpClient dapr #184 * Add tests for InvokeHttpClient dapr#184 * Update namespace of InvokeHttpClient dapr#184 * Add DI of InvokeHttpClient dapr#184 * Resolve code format issues dapr#184 * Remove InvokeEnvelope and add params to InvokeMethod dapr#184 * Update method signature to use generic types for Request/Response dapr#184 * Update parameter in data null check dapr#184
This commit is contained in:
parent
f10155a1d3
commit
5562d0d64b
|
|
@ -34,9 +34,11 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
|
||||
services.AddSingleton<DaprClientMarkerService>();
|
||||
|
||||
// StateHttpClient can be used with or without JsonSerializerOptions registered
|
||||
// StateHttpClient and InvokeHttpClient can be used with or without JsonSerializerOptions registered
|
||||
// in DI. If the user registers JsonSerializerOptions, it will be picked up by the client automatically.
|
||||
services.AddHttpClient("state").AddTypedClient<StateClient, StateHttpClient>();
|
||||
|
||||
services.AddHttpClient("invoke").AddTypedClient<InvokeClient, InvokeHttpClient>();
|
||||
}
|
||||
|
||||
private class DaprClientMarkerService
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace Dapr
|
|||
internal static class DaprUris
|
||||
{
|
||||
public const string StatePath = "/v1.0/state";
|
||||
public const string InvokePath = "/v1.0/invoke";
|
||||
|
||||
public static string DefaultHttpPort => Environment.GetEnvironmentVariable("DAPR_HTTP_PORT") ?? "3500";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Dapr
|
||||
{
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// A client for interacting with the Dapr invoke endpoints.
|
||||
/// </summary>
|
||||
public abstract class InvokeClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes the specified method on target service, Request data is serialized using the default System.Text.Json Serialization.
|
||||
/// </summary>
|
||||
/// <param name="serviceName">The name of the service to be called in the Dapr invoke request.</param>
|
||||
/// <param name="methodName">THe name of the method to be called in the Dapr invoke request.</param>
|
||||
/// <param name="data">The data to be sent within the body of the Dapr invoke request.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TRequest">The data type of the object that will be serialized.</typeparam>
|
||||
/// <returns>A <see cref="Task" />.</returns>
|
||||
public abstract Task InvokeMethodAsync<TRequest>(string serviceName, string methodName, TRequest data, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the specified method on target service, Response data is deserialized using the default System.Text.Json Serialization.
|
||||
/// </summary>
|
||||
/// <param name="serviceName">The name of the service to be called in the Dapr invoke request.</param>
|
||||
/// <param name="methodName">THe name of the method to be called in the Dapr invoke request.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TResponse">The data type that the Dapr response body will be deserialized to.</typeparam>
|
||||
/// <returns>A <see cref="Task" /> that will return a deserialized object of the reponse from the Invoke request. If the Dapr response content is null the return type will be null.</returns>
|
||||
public abstract Task<TResponse> InvokeMethodAsync<TResponse>(string serviceName, string methodName, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the specified method on target service, Request and Response data is serialized/deserialized using the default System.Text.Json Serialization.
|
||||
/// </summary>
|
||||
/// <param name="serviceName">The name of the service to be called in the Dapr invoke request.</param>
|
||||
/// <param name="methodName">THe name of the method to be called in the Dapr invoke request.</param>
|
||||
/// <param name="data">The data to be sent within the body of the Dapr invoke request.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TRequest">The data type of the object that will be serialized.</typeparam>
|
||||
/// <typeparam name="TResponse">The data type that the Dapr response body will be deserialized to.</typeparam>
|
||||
/// <returns>A <see cref="Task" /> that will return a deserialized object of the reponse from the Invoke request. If the Dapr response content is null the return type will be null.</returns>
|
||||
public abstract Task<TResponse> InvokeMethodAsync<TRequest, TResponse>(string serviceName, string methodName, TRequest data, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
namespace Dapr
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static Dapr.DaprUris;
|
||||
|
||||
/// <summary>
|
||||
/// A client for interacting with the Dapr invoke endpoints using <see cref="HttpClient" />.
|
||||
/// </summary>
|
||||
public class InvokeHttpClient : InvokeClient
|
||||
{
|
||||
private readonly HttpClient client;
|
||||
private readonly JsonSerializerOptions serializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokeHttpClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="HttpClient" />.</param>
|
||||
/// <param name="serializerOptions">The <see cref="JsonSerializerOptions" />.</param>
|
||||
public InvokeHttpClient(HttpClient client, JsonSerializerOptions serializerOptions = null)
|
||||
{
|
||||
if (client is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
this.client = client;
|
||||
this.serializerOptions = serializerOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the specified method on target service, Request and Response data is serialized/deserialized using the default System.Text.Json Serialization.
|
||||
/// </summary>
|
||||
/// <param name="serviceName">The name of the service to be called in the Dapr invoke request.</param>
|
||||
/// <param name="methodName">THe name of the method to be called in the Dapr invoke request.</param>
|
||||
/// <param name="data">The data to be sent within the body of the Dapr invoke request.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TRequest">The data type of the object that will be serialized.</typeparam>
|
||||
/// <typeparam name="TResponse">The data type that the Dapr response body will be deserialized to.</typeparam>
|
||||
/// <returns>A <see cref="Task" /> that will return a deserialized object of the reponse from the Invoke request. If the Dapr response content is null the return type will be null.</returns>
|
||||
public override async Task<TResponse> InvokeMethodAsync<TRequest, TResponse>(string serviceName, string methodName, TRequest data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(serviceName))
|
||||
{
|
||||
throw new ArgumentNullException("The value cannot be null or empty", nameof(serviceName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(methodName))
|
||||
{
|
||||
throw new ArgumentNullException("The value cannot be null or empty", nameof(methodName));
|
||||
}
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
throw new ArgumentNullException("The value cannot be null", nameof(data));
|
||||
}
|
||||
|
||||
var response = await this.MakeInvokeHttpRequest(serviceName, methodName, JsonSerializer.Serialize(data), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.Content == null || response.Content.Headers?.ContentLength == 0)
|
||||
{
|
||||
// If the invoke response is empty, then return.
|
||||
return default;
|
||||
}
|
||||
|
||||
using (var stream = await response.Content.ReadAsStreamAsync())
|
||||
{
|
||||
return await JsonSerializer.DeserializeAsync<TResponse>(stream, this.serializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the specified method on target service, Response data is deserialized using the default System.Text.Json Serialization.
|
||||
/// </summary>
|
||||
/// <param name="serviceName">The name of the service to be called in the Dapr invoke request.</param>
|
||||
/// <param name="methodName">THe name of the method to be called in the Dapr invoke request.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TResponse">The data type that the Dapr response body will be deserialized to.</typeparam>
|
||||
/// <returns>A <see cref="Task" /> that will return a deserialized object of the reponse from the Invoke request. If the Dapr response content is null the return type will be null.</returns>
|
||||
public override async Task<TResponse> InvokeMethodAsync<TResponse>(string serviceName, string methodName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(serviceName))
|
||||
{
|
||||
throw new ArgumentNullException("The value cannot be null or empty", nameof(serviceName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(methodName))
|
||||
{
|
||||
throw new ArgumentNullException("The value cannot be null or empty", nameof(methodName));
|
||||
}
|
||||
|
||||
var response = await this.MakeInvokeHttpRequest(serviceName, methodName, string.Empty, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.Content == null || response.Content.Headers?.ContentLength == 0)
|
||||
{
|
||||
// If the invoke response is empty, then return.
|
||||
return default;
|
||||
}
|
||||
|
||||
using (var stream = await response.Content.ReadAsStreamAsync())
|
||||
{
|
||||
return await JsonSerializer.DeserializeAsync<TResponse>(stream, this.serializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a method using the Dapr invoke endpoints.
|
||||
/// </summary>
|
||||
/// <param name="serviceName">The name of the service to be called in the Dapr invoke request.</param>
|
||||
/// <param name="methodName">THe name of the method to be called in the Dapr invoke request.</param>
|
||||
/// <param name="data">The data to be sent within the body of the Dapr invoke request.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
|
||||
/// <typeparam name="TRequest">The data type of the object that will be serialized.</typeparam>
|
||||
/// <returns>A <see cref="Task" />.</returns>
|
||||
public override async Task InvokeMethodAsync<TRequest>(string serviceName, string methodName, TRequest data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(serviceName))
|
||||
{
|
||||
throw new ArgumentNullException("The value cannot be null or empty", nameof(serviceName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(methodName))
|
||||
{
|
||||
throw new ArgumentNullException("The value cannot be null or empty", nameof(methodName));
|
||||
}
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
throw new ArgumentNullException("The value cannot be null", nameof(data));
|
||||
}
|
||||
|
||||
await this.MakeInvokeHttpRequest(serviceName, methodName, JsonSerializer.Serialize(data), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> MakeInvokeHttpRequest(string serviceName, string methodName, string data, CancellationToken cancellationToken)
|
||||
{
|
||||
var url = this.client.BaseAddress == null ? $"http://localhost:{DefaultHttpPort}{InvokePath}/{serviceName}/method/{methodName}" : $"{InvokePath}/{serviceName}/method/{methodName}";
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
|
||||
request.Content = new StringContent(data);
|
||||
|
||||
var response = await this.client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode && response.Content != null)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
throw new HttpRequestException($"Failed to invoke method with status code '{response.StatusCode}': {error}.");
|
||||
}
|
||||
else if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to invoke method with status code '{response.StatusCode}'.");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace Dapr.Client.Test
|
||||
{
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
public class InvokeHttpClientTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task InvokeMethodAsync_CanInvokeMethodWithReturnTypeAndData()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var invokeClient = new InvokeHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = invokeClient.InvokeMethodAsync<InvokedResponse>("test", "test");
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetInvokeUrl(3500, "test", "test"));
|
||||
|
||||
entry.RespondWithJson(new InvokedResponse() { Name = "Look, I was invoked!" });
|
||||
|
||||
var invokedResponse = await task;
|
||||
invokedResponse.Name.Should().Be("Look, I was invoked!");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeMethodAsync_CanInvokeMethodWithReturnTypeAndData_EmptyResponseReturnsNull()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var invokeClient = new InvokeHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = invokeClient.InvokeMethodAsync<InvokeRequest, InvokedResponse>("test", "test", new InvokeRequest() { RequestParameter = "Hello " });
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetInvokeUrl(3500, "test", "test"));
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
var invokedResponse = await task;
|
||||
invokedResponse.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeMethodAsync_CanInvokeMethodWithReturnTypeAndData_ThrowsExceptionForNonSuccess()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var invokeClient = new InvokeHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = invokeClient.InvokeMethodAsync<InvokeRequest, InvokedResponse>("test", "test", new InvokeRequest() { RequestParameter = "Hello " });
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetInvokeUrl(3500, "test", "test"));
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.NotAcceptable));
|
||||
|
||||
await FluentActions.Awaiting(async () => await task).Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeMethodAsync_CanInvokeMethodWithReturnTypeNoData()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var invokeClient = new InvokeHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = invokeClient.InvokeMethodAsync<InvokedResponse>("test", "test");
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetInvokeUrl(3500, "test", "test"));
|
||||
|
||||
entry.RespondWithJson(new InvokedResponse() { Name = "Look, I was invoked!" });
|
||||
|
||||
var invokedResponse = await task;
|
||||
invokedResponse.Name.Should().Be("Look, I was invoked!");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeMethodAsync_CanInvokeMethodWithReturnTypeNoData_ThrowsExceptionNonSuccess()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var invokeClient = new InvokeHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = invokeClient.InvokeMethodAsync<InvokedResponse>("test", "test");
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetInvokeUrl(3500, "test", "test"));
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.NotAcceptable));
|
||||
|
||||
await FluentActions.Awaiting(async () => await task).Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task InvokeMethodAsync_CanInvokeMethodWithNoReturnTypeAndData()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var invokeClient = new InvokeHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = invokeClient.InvokeMethodAsync<InvokeRequest>("test", "test", new InvokeRequest() { RequestParameter = "Hello " });
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetInvokeUrl(3500, "test", "test"));
|
||||
|
||||
entry.RespondWithJson(new InvokedResponse() { Name = "Look, I was invoked!" });
|
||||
|
||||
FluentActions.Awaiting(async () => await task).Should().NotThrow();
|
||||
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task InvokeMethodAsync_CanInvokeMethodWithNoReturnTypeAndData_ThrowsErrorNonSuccess()
|
||||
{
|
||||
var httpClient = new TestHttpClient();
|
||||
var invokeClient = new InvokeHttpClient(httpClient, new JsonSerializerOptions());
|
||||
|
||||
var task = invokeClient.InvokeMethodAsync<InvokeRequest>("test", "test", new InvokeRequest() { RequestParameter = "Hello " });
|
||||
|
||||
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
|
||||
entry.Request.RequestUri.ToString().Should().Be(GetInvokeUrl(3500, "test", "test"));
|
||||
|
||||
entry.Respond(new HttpResponseMessage(HttpStatusCode.NotAcceptable));
|
||||
|
||||
FluentActions.Awaiting(async () => await task).Should().ThrowAsync<HttpRequestException>();
|
||||
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
private static string GetInvokeUrl(int port, string serviceName, string methodName)
|
||||
{
|
||||
return $"http://localhost:{port}/v1.0/invoke/{serviceName}/method/{methodName}";
|
||||
}
|
||||
|
||||
private class InvokeRequest
|
||||
{
|
||||
public string RequestParameter { get; set; }
|
||||
}
|
||||
|
||||
private class InvokedResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue