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:
James Eastham 2019-12-13 18:05:17 +00:00 committed by Aman Bhardwaj
parent f10155a1d3
commit 5562d0d64b
5 changed files with 377 additions and 1 deletions

View File

@ -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

View File

@ -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";
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}
}