Rich error model (#432)

* add InvokeMethodRawAsync and InvokeMethodWithResponseAsync
Co-authored-by: Ryan Nowak <nowakra@gmail.com>
This commit is contained in:
vinayada1 2020-10-30 13:59:33 -07:00 committed by GitHub
parent 0ccddf73ff
commit 4c926c9469
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 965 additions and 32 deletions

View File

@ -8,5 +8,6 @@ namespace Dapr.Client
internal class Constants
{
public const string ContentTypeApplicationJson = "application/json";
public const string ContentTypeApplicationGrpc = "application/grpc";
}
}

View File

@ -17,6 +17,7 @@
<PackageReference Include="Google.Protobuf" Version="3.13.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.32.0" />
<PackageReference Include="Grpc.Tools" Version="2.32.0" PrivateAssets="All" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Protos\" />

View File

@ -134,6 +134,38 @@ namespace Dapr.Client
Dapr.Client.Http.HTTPExtension httpExtension = default,
CancellationToken cancellationToken = default);
/// <summary>
/// Invokes a method on a Dapr app.
/// </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="data">Data to pass to the method</param>
/// <param name="httpExtension">Additional fields that may be needed if the receiving app is listening on HTTP.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
/// <returns>A <see cref="Task{InvokeResponse}" /> that will return the value when the operation has completed.</returns>
public abstract Task<InvocationResponse<TResponse>> InvokeMethodWithResponseAsync<TRequest, TResponse>(
string appId,
string methodName,
TRequest data,
Dapr.Client.Http.HTTPExtension httpExtension = default,
CancellationToken cancellationToken = default);
/// <summary>
/// Invokes a method on a Dapr app.
/// </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="data">Byte array to pass to the method</param>
/// <param name="httpExtension">Additional fields that may be needed if the receiving app is listening on HTTP.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
/// <returns>A <see cref="ValueTask{T}" /> that will return the value when the operation has completed.</returns>
public abstract Task<InvocationResponse<byte[]>> InvokeMethodRawAsync(
string appId,
string methodName,
byte[] data,
Dapr.Client.Http.HTTPExtension httpExtension = default,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the current value associated with the <paramref name="key" /> from the Dapr state store.
/// </summary>

View File

@ -8,6 +8,7 @@ namespace Dapr.Client
using System;
using System.Text.Json;
using Grpc.Net.Client;
using Autogenerated = Autogen.Grpc.v1;
/// <summary>
/// Builder for building <see cref="DaprClient"/>
@ -80,7 +81,8 @@ namespace Dapr.Client
}
var channel = GrpcChannel.ForAddress(this.daprEndpoint, this.gRPCChannelOptions ?? new GrpcChannelOptions());
return new DaprClientGrpc(channel, this.jsonSerializerOptions);
var client = new Autogenerated.Dapr.DaprClient(channel);
return new DaprClientGrpc(client, this.jsonSerializerOptions);
}
/// <summary>

View File

@ -8,14 +8,16 @@ namespace Dapr.Client
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Dapr.Client.Http;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Grpc.Net.Client;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Autogenerated = Autogen.Grpc.v1;
/// <summary>
@ -23,8 +25,14 @@ namespace Dapr.Client
/// </summary>
internal class DaprClientGrpc : DaprClient
{
private readonly Autogenerated.Dapr.DaprClient client;
private static readonly string DaprErrorInfoHttpCodeMetadata = "http.code";
private static readonly string DaprErrorInfoHttpErrorMetadata = "http.error_message";
private static readonly string GrpcStatusDetails = "grpc-status-details-bin";
private static readonly string GrpcErrorInfoDetail = "google.rpc.ErrorInfo";
private static readonly string DaprHttpStatusHeader = "dapr-http-status";
private readonly JsonSerializerOptions jsonSerializerOptions;
private readonly Autogenerated.Dapr.DaprClient client;
// property exposed for testing purposes
internal Autogenerated.Dapr.DaprClient Client => client;
@ -32,15 +40,10 @@ namespace Dapr.Client
// property exposed for testing purposes
internal JsonSerializerOptions JsonSerializerOptions => jsonSerializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="DaprClientGrpc"/> class.
/// </summary>
/// <param name="channel">gRPC channel to create gRPC clients.</param>
/// <param name="jsonSerializerOptions">Json serialization options.</param>
internal DaprClientGrpc(GrpcChannel channel, JsonSerializerOptions jsonSerializerOptions = null)
internal DaprClientGrpc(Autogenerated.Dapr.DaprClient inner, JsonSerializerOptions jsonSerializerOptions)
{
this.client = inner;
this.jsonSerializerOptions = jsonSerializerOptions;
this.client = new Autogenerated.Dapr.DaprClient(channel);
}
#region Publish Apis
@ -157,7 +160,8 @@ namespace Dapr.Client
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName));
_ = await this.MakeInvokeRequestAsync(appId, methodName, null, httpExtension, cancellationToken);
var (request, callOptions) = this.MakeInvokeRequestAsync(appId, methodName, null, httpExtension, cancellationToken);
await client.InvokeServiceAsync(request, callOptions);
}
public override async Task InvokeMethodAsync<TRequest>(
@ -176,7 +180,10 @@ namespace Dapr.Client
serializedData = TypeConverters.ToAny(data, this.jsonSerializerOptions);
}
_ = await this.MakeInvokeRequestAsync(appId, methodName, serializedData, httpExtension, cancellationToken);
Autogenerated.InvokeServiceRequest request;
CallOptions callOptions;
(request, callOptions) = this.MakeInvokeRequestAsync(appId, methodName, serializedData, httpExtension, cancellationToken);
await client.InvokeServiceAsync(request, callOptions);
}
public override async ValueTask<TResponse> InvokeMethodAsync<TResponse>(
@ -188,7 +195,10 @@ namespace Dapr.Client
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName));
var response = await this.MakeInvokeRequestAsync(appId, methodName, null, httpExtension, cancellationToken);
Autogenerated.InvokeServiceRequest request;
CallOptions callOptions;
(request, callOptions) = this.MakeInvokeRequestAsync(appId, methodName, null, httpExtension, cancellationToken);
var response = await client.InvokeServiceAsync(request, callOptions);
return response.Data.Value.IsEmpty ? default : TypeConverters.FromAny<TResponse>(response.Data, this.jsonSerializerOptions);
}
@ -202,14 +212,61 @@ namespace Dapr.Client
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName));
Any serializedData = null;
if (data != null)
var request = new InvocationRequest<TRequest>
{
serializedData = TypeConverters.ToAny(data, this.jsonSerializerOptions);
}
AppId = appId,
MethodName = methodName,
Body = data,
HttpExtension = httpExtension,
};
var response = await this.MakeInvokeRequestAsync(appId, methodName, serializedData, httpExtension, cancellationToken);
return response.Data.Value.IsEmpty ? default : TypeConverters.FromAny<TResponse>(response.Data, this.jsonSerializerOptions);
var invokeResponse = await this.MakeInvokeRequestAsyncWithResponse<TRequest, TResponse>(request, false, cancellationToken);
return invokeResponse.Body;
}
public override async Task<InvocationResponse<TResponse>> InvokeMethodWithResponseAsync<TRequest, TResponse>(
string appId,
string methodName,
TRequest data,
Dapr.Client.Http.HTTPExtension httpExtension = default,
CancellationToken cancellationToken = default)
{
ArgumentVerifier.ThrowIfNull(appId, nameof(appId));
ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName));
var request = new InvocationRequest<TRequest>
{
AppId = appId,
MethodName = methodName,
Body = data,
HttpExtension = httpExtension,
};
var invokeResponse = await this.MakeInvokeRequestAsyncWithResponse<TRequest, TResponse>(request, false, cancellationToken);
return invokeResponse;
}
public override async Task<InvocationResponse<byte[]>> InvokeMethodRawAsync(
string appId,
string methodName,
byte[] data,
HTTPExtension httpExtension = default,
CancellationToken cancellationToken = default)
{
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName));
var request = new InvocationRequest<byte[]>
{
AppId = appId,
MethodName = methodName,
Body = data,
HttpExtension = httpExtension,
};
var invokeResponse = await this.MakeInvokeRequestAsyncWithResponse<byte[], byte[]>(request, true, cancellationToken);
return invokeResponse;
}
public override async ValueTask<IReadOnlyList<BulkStateItem>> GetBulkStateAsync(string storeName, IReadOnlyList<string> keys, int? parallelism, CancellationToken cancellationToken = default)
@ -240,7 +297,7 @@ namespace Dapr.Client
return bulkResponse;
}
private async Task<Autogenerated.InvokeResponse> MakeInvokeRequestAsync(
private (Autogenerated.InvokeServiceRequest, CallOptions) MakeInvokeRequestAsync(
string appId,
string methodName,
Any data,
@ -294,10 +351,122 @@ namespace Dapr.Client
Message = invokeRequest,
};
return await this.MakeGrpcCallHandleError(
options => client.InvokeServiceAsync(request, options),
headers,
cancellationToken);
var callOptions = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken);
// add token for dapr api token based authentication
var daprApiToken = Environment.GetEnvironmentVariable("DAPR_API_TOKEN");
if (daprApiToken != null)
{
callOptions.Headers.Add("dapr-api-token", daprApiToken);
}
return (request, callOptions);
}
private async Task<InvocationResponse<TResponse>> MakeInvokeRequestAsyncWithResponse<TRequest, TResponse>(
InvocationRequest<TRequest> request,
bool useRaw,
CancellationToken cancellationToken = default)
{
Any serializedData = null;
if (request.Body != null)
{
if (useRaw)
{
// User has passed in raw bytes
var requestBytes = (byte[])(object)(request.Body);
serializedData = new Any { Value = ByteString.CopyFrom(requestBytes), TypeUrl = typeof(byte[]).FullName };
}
else
{
serializedData = TypeConverters.ToAny(request.Body, this.jsonSerializerOptions);
}
}
try
{
var invokeResponse = new InvocationResponse<TResponse>();
Autogenerated.InvokeServiceRequest invokeRequest;
CallOptions callOptions;
(invokeRequest, callOptions) = this.MakeInvokeRequestAsync(request.AppId, request.MethodName, serializedData, request.HttpExtension, cancellationToken);
var grpcCall = client.InvokeServiceAsync(invokeRequest, callOptions);
var response = await grpcCall.ResponseAsync;
var responseHeaders = await grpcCall.ResponseHeadersAsync;
var trailers = grpcCall.GetTrailers();
var grpcStatus = grpcCall.GetStatus();
var headers = grpcCall.ResponseHeadersAsync.Result.ToDictionary(kv => kv.Key, kv => kv.ValueBytes);
if (useRaw)
{
// User wants to receive raw bytes
var responseBytes = new byte[response.Data.Value.Length];
response.Data.Value?.CopyTo(responseBytes, 0);
invokeResponse.Body = (TResponse)(response.Data.Value.IsEmpty ? default : (object)(responseBytes));
}
else
{
invokeResponse.Body = response.Data.Value.IsEmpty ? default : TypeConverters.FromAny<TResponse>(response.Data, this.jsonSerializerOptions);
}
invokeResponse.Headers = headers;
invokeResponse.Trailers = grpcCall.GetTrailers().ToDictionary(kv => kv.Key, kv => kv.ValueBytes);
if (headers.TryGetValue(DaprHttpStatusHeader, out var httpStatus))
{
invokeResponse.HttpStatusCode = (HttpStatusCode)System.Enum.Parse(typeof(HttpStatusCode), Encoding.UTF8.GetString(httpStatus, 0, httpStatus.Length));
invokeResponse.ContentType = Constants.ContentTypeApplicationJson;
}
else
{
// Response is grpc
invokeResponse.GrpcStatusInfo = new GrpcStatusInfo(grpcStatus.StatusCode, grpcStatus.Detail);
invokeResponse.ContentType = Constants.ContentTypeApplicationGrpc;
}
return invokeResponse;
}
catch (RpcException ex)
{
var invokeErrorResponse = new InvocationResponse<byte[]>();
var entry = ex.Trailers.Get(GrpcStatusDetails);
if (entry != null)
{
var status = Google.Rpc.Status.Parser.ParseFrom(entry.ValueBytes);
foreach (var detail in status.Details)
{
if (Google.Protobuf.WellKnownTypes.Any.GetTypeName(detail.TypeUrl) == GrpcErrorInfoDetail)
{
var rpcError = detail.Unpack<Google.Rpc.ErrorInfo>();
var grpcStatusCode = (StatusCode)status.Code;
string innerHttpErrorCode = null;
string innerHttpErrorMessage = null;
rpcError.Metadata.TryGetValue(DaprErrorInfoHttpCodeMetadata, out innerHttpErrorCode);
rpcError.Metadata.TryGetValue(DaprErrorInfoHttpErrorMetadata, out innerHttpErrorMessage);
if(innerHttpErrorCode != null || innerHttpErrorMessage != null)
{
// Response returned by Http server
invokeErrorResponse.HttpStatusCode = (HttpStatusCode)(Convert.ToInt32(innerHttpErrorCode));
invokeErrorResponse.Body = Encoding.UTF8.GetBytes(innerHttpErrorMessage);
}
else
{
// Response returned by gRPC server
invokeErrorResponse.GrpcStatusInfo = new GrpcStatusInfo(grpcStatusCode, status.Message);
}
break;
}
}
}
throw new InvocationException($"Exception while invoking {request.MethodName} on appId:{request.AppId}", ex, invokeErrorResponse);
}
}
private bool IsResponseFromHttpCallee(Dictionary<string,byte[]> headers)
{
return headers.ContainsKey(DaprHttpStatusHeader);
}
#endregion

View File

@ -0,0 +1,36 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
namespace Dapr.Client
{
using Dapr.Client.Http;
using System;
using System.Net;
/// <summary>
/// This class is only needed if the app you are calling app is listening on gRPC.
/// It contains propertes that represent status info that may be populated for an gRPC response.
/// </summary>
public class GrpcStatusInfo
{
/// <summary>
/// The constructor.
/// </summary>
public GrpcStatusInfo(Grpc.Core.StatusCode statusCode, string message)
{
this.GrpcStatusCode = statusCode;
this.GrpcErrorMessage = message;
}
/// <summary>
/// The gRPC Status Code
/// </summary>
public Grpc.Core.StatusCode GrpcStatusCode { get; }
/// <summary>
/// The gRPC Error Message
/// </summary>
public string GrpcErrorMessage { get; }
}
}

View File

@ -0,0 +1,30 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
namespace Dapr.Client
{
using Dapr.Client.Http;
using System;
using System.Net;
/// <summary>
/// This class represents the exception thrown when Service Invocation via Dapr encounters an error
/// </summary>
public class InvocationException : Exception
{
/// <summary>
/// The constructor.
/// </summary>
public InvocationException(string message, Exception innerException, InvocationResponse<byte[]> response)
: base(message, innerException)
{
this.Response = response;
}
/// <summary>
/// The Response
/// </summary>
public InvocationResponse<byte[]> Response { get; }
}
}

View File

@ -0,0 +1,49 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
namespace Dapr.Client
{
using Dapr.Client;
using Dapr.Client.Http;
/// <summary>
/// Represents an Invoke Request used for service invocation
/// </summary>
/// <typeparam name="TRequest">Data type of the request.</typeparam>
public sealed class InvocationRequest<TRequest>
{
/// <summary>
/// Initializes a new instance of the <see cref="InvocationRequest{TRequest}"/> class.
/// </summary>
public InvocationRequest()
{
}
/// <summary>
/// Gets or sets the app identifier.
/// </summary>
public string AppId { get; set; }
/// <summary>
/// Gets or sets the method name to invoke.
/// </summary>
public string MethodName { get; set; }
/// <summary>
/// Gets or sets the request body.
/// </summary>
public TRequest Body { get; set; }
/// <summary>
/// Gets or sets the content type.
/// </summary>
public string ContentType { get; set; }
/// <summary>
/// Gets or sets the HTTP extension info.
/// </summary>
public HTTPExtension HttpExtension { get; set; }
}
}

View File

@ -0,0 +1,56 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
namespace Dapr.Client
{
using System.Net;
using System.Collections.Generic;
using Dapr.Client;
using Dapr.Client.Http;
/// <summary>
/// Represents a response returned by service invocation
/// </summary>
/// <typeparam name="TResponse">Data type of the response.</typeparam>
public sealed class InvocationResponse<TResponse>
{
/// <summary>
/// Initializes a new instance of the <see cref="InvocationResponse{TResponse}"/> class.
/// </summary>
public InvocationResponse()
{
}
/// <summary>
/// Gets or sets the response body.
/// </summary>
public TResponse Body { get; set; }
/// <summary>
/// Gets or sets the content type.
/// </summary>
public string ContentType { get; set; }
/// <summary>
/// Gets or sets the headers.
/// </summary>
public IDictionary<string, byte[]> Headers { get; set; }
/// <summary>
/// Gets or sets the trailers.
/// </summary>
public IDictionary<string, byte[]> Trailers { get; set; }
/// <summary>
/// Gets or sets the HTTP status info.
/// </summary>
public HttpStatusCode? HttpStatusCode { get; set; }
/// <summary>
/// Gets or sets the gRPC status info.
/// </summary>
public GrpcStatusInfo GrpcStatusInfo { get; set; }
}
}

View File

@ -11,8 +11,8 @@ namespace Dapr
using System.Threading;
using System.Threading.Tasks;
using Dapr.Client;
using Autogenerated = Dapr.Client.Autogen.Grpc;
using Grpc.Net.Client;
using Autogenerated = Dapr.Client.Autogen.Grpc.v1;
internal class StateTestClient : DaprClientGrpc
{
@ -22,9 +22,9 @@ namespace Dapr
/// <summary>
/// Initializes a new instance of the <see cref="DaprClientGrpc"/> class.
/// </summary>
internal StateTestClient()
: base(channel)
{ }
internal StateTestClient() : base(new Autogenerated.Dapr.DaprClient(channel), null)
{
}
public override ValueTask<TValue> GetStateAsync<TValue>(string storeName, string key, ConsistencyMode? consistencyMode = default, Dictionary<string, string> metadata = default, CancellationToken cancellationToken = default)
{

View File

@ -14,6 +14,9 @@
<PackageReference Include="Grpc.Net.Client" Version="2.32.0" />
<PackageReference Include="Grpc.Tools" Version="2.32.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="moq" Version="4.14.7" />
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.123" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>

View File

@ -5,19 +5,25 @@
namespace Dapr.Client.Test
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Dapr.AppCallback.Autogen.Grpc.v1;
using Dapr.Client;
using Dapr.Client.Autogen.Grpc.v1;
using Dapr.Client.Autogen.Test.Grpc.v1;
using Dapr.AppCallback.Autogen.Grpc.v1;
using Dapr.Client.Http;
using FluentAssertions;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Grpc.Net.Client;
using Moq;
using Xunit;
using System.Linq;
public class InvokeApiTest
{
@ -195,7 +201,7 @@ namespace Dapr.Client.Test
entry.Completion.SetResult(response);
//validate response
await FluentActions.Awaiting(async () => await task).Should().ThrowAsync<RpcException>();
await FluentActions.Awaiting(async () => await task).Should().ThrowAsync<InvocationException>();
}
[Fact]
@ -413,6 +419,446 @@ namespace Dapr.Client.Test
response.Name.Should().Be(invokedResponse.Name);
}
[Fact]
public async Task InvokeMethodAsync_CanInvokeMethodWithResponse_CalleeSideGrpc()
{
var client = new MockClient();
var data = new Response() { Name = "Look, I was invoked!" };
var invokeResponse = new InvokeResponse();
invokeResponse.Data = TypeConverters.ToAny(data);
var response =
client.Call<InvokeResponse>()
.SetResponse(invokeResponse)
.Build();
client.Mock
.Setup(m => m.InvokeServiceAsync(It.IsAny<Autogen.Grpc.v1.InvokeServiceRequest>(), It.IsAny<CallOptions>()))
.Returns(response);
var body = new Request() { RequestParameter = "Hello " };
var task = client.DaprClient.InvokeMethodWithResponseAsync<Request, Response>("test", "testMethod", body);
// Validate Response
var invokedResponse = await task;
invokedResponse.Body.Name.Should().Be("Look, I was invoked!");
invokedResponse.Headers.Count.Should().Be(0);
invokedResponse.ContentType.Should().Be(Constants.ContentTypeApplicationGrpc);
invokedResponse.HttpStatusCode.Should().BeNull();
invokedResponse.GrpcStatusInfo.Should().NotBeNull();
invokedResponse.GrpcStatusInfo.GrpcStatusCode.Should().Be(Grpc.Core.StatusCode.OK);
}
[Fact]
public async Task InvokeMethodAsync_CanInvokeMethodWithResponse_CalleeSideHttp()
{
var client = new MockClient();
var data = new Response() { Name = "Look, I was invoked!" };
var invokeResponse = new InvokeResponse();
invokeResponse.Data = TypeConverters.ToAny(data);
var response =
client.Call<InvokeResponse>()
.SetResponse(invokeResponse)
.AddHeader("dapr-http-status", "200")
.Build();
client.Mock
.Setup(m => m.InvokeServiceAsync(It.IsAny<Autogen.Grpc.v1.InvokeServiceRequest>(), It.IsAny<CallOptions>()))
.Returns(response);
var body = new Request() { RequestParameter = "Hello " };
var task = client.DaprClient.InvokeMethodWithResponseAsync<Request, Response>("test", "testMethod", body);
// Validate Response
var invokedResponse = await task;
invokedResponse.Body.Name.Should().Be("Look, I was invoked!");
invokedResponse.Headers.ContainsKey("dapr-http-status").Should().BeTrue();
invokedResponse.ContentType.Should().Be(Constants.ContentTypeApplicationJson);
invokedResponse.GrpcStatusInfo.Should().BeNull();
invokedResponse.HttpStatusCode.Should().NotBeNull();
invokedResponse.HttpStatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task InvokeMethodAsync_CanInvokeMethodWithResponse_GrpcServerReturnsNonSuccessResponse()
{
var client = new MockClient();
var data = new Response() { Name = "Look, I was invoked!" };
var invokeResponse = new InvokeResponse();
invokeResponse.Data = TypeConverters.ToAny(data);
var response =
client.Call<InvokeResponse>()
.SetResponse(invokeResponse)
.Build();
var trailers = new Metadata();
const string grpcErrorInfoReason = "Insufficient permissions";
int grpcStatusCode = Convert.ToInt32(StatusCode.PermissionDenied);
const string grpcStatusMessage = "Bad permissions";
var details = new Google.Rpc.Status
{
Code = grpcStatusCode,
Message = grpcStatusMessage,
};
var errorInfo = new Google.Rpc.ErrorInfo
{
Reason = grpcErrorInfoReason,
Domain = "dapr.io",
};
details.Details.Add(Google.Protobuf.WellKnownTypes.Any.Pack(errorInfo));
var entry = new Metadata.Entry("grpc-status-details-bin", Google.Protobuf.MessageExtensions.ToByteArray(details));
trailers.Add(entry);
const string rpcExceptionMessage = "No access to app";
const StatusCode rpcStatusCode = StatusCode.PermissionDenied;
const string rpcStatusDetail = "Insufficient permissions";
var rpcException = new RpcException(new Status(rpcStatusCode, rpcStatusDetail), trailers, rpcExceptionMessage);
// Setup the mock client to throw an Rpc Exception with the expected details info
client.Mock
.Setup(m => m.InvokeServiceAsync(It.IsAny<Autogen.Grpc.v1.InvokeServiceRequest>(), It.IsAny<CallOptions>()))
.Throws(rpcException);
try
{
var body = new Request() { RequestParameter = "Hello " };
await client.DaprClient.InvokeMethodWithResponseAsync<Request, Response>("test", "testMethod", body);
Assert.False(true);
}
catch(InvocationException ex)
{
ex.Message.Should().Be("Exception while invoking testMethod on appId:test");
ex.InnerException.Message.Should().Be(rpcExceptionMessage);
ex.Response.GrpcStatusInfo.GrpcErrorMessage.Should().Be(grpcStatusMessage);
ex.Response.GrpcStatusInfo.GrpcStatusCode.Should().Be(grpcStatusCode);
ex.Response.Body.Should().BeNull();
ex.Response.HttpStatusCode.Should().BeNull();
}
}
[Fact]
public async Task InvokeMethodAsync_CanInvokeMethodWithResponse_HttpServerReturnsNonSuccessResponse()
{
var client = new MockClient();
var data = new Response() { Name = "Look, I was invoked!" };
var invokeResponse = new InvokeResponse();
invokeResponse.Data = TypeConverters.ToAny(data);
invokeResponse.ContentType = Constants.ContentTypeApplicationJson;
var response =
client.Call<InvokeResponse>()
.SetResponse(invokeResponse)
.AddHeader("dapr-http-status", "200")
.Build();
var trailers = new Metadata();
const string grpcErrorInfoReason = "Insufficient permissions";
const int grpcErrorInfoDetailHttpCode = 500;
const string grpcErrorInfoDetailHttpErrorMsg = "no permissions";
int grpcStatusCode = Convert.ToInt32(StatusCode.PermissionDenied);
const string grpcStatusMessage = "Bad permissions";
var details = new Google.Rpc.Status
{
Code = grpcStatusCode,
Message = grpcStatusMessage,
};
var errorInfo = new Google.Rpc.ErrorInfo
{
Reason = grpcErrorInfoReason,
Domain = "dapr.io",
};
errorInfo.Metadata.Add("http.code", grpcErrorInfoDetailHttpCode.ToString());
errorInfo.Metadata.Add("http.error_message", grpcErrorInfoDetailHttpErrorMsg);
details.Details.Add(Google.Protobuf.WellKnownTypes.Any.Pack(errorInfo));
var entry = new Metadata.Entry("grpc-status-details-bin", Google.Protobuf.MessageExtensions.ToByteArray(details));
trailers.Add(entry);
const string rpcExceptionMessage = "No access to app";
const StatusCode rpcStatusCode = StatusCode.PermissionDenied;
const string rpcStatusDetail = "Insufficient permissions";
var rpcException = new RpcException(new Status(rpcStatusCode, rpcStatusDetail), trailers, rpcExceptionMessage);
// Setup the mock client to throw an Rpc Exception with the expected details info
client.Mock
.Setup(m => m.InvokeServiceAsync(It.IsAny<Autogen.Grpc.v1.InvokeServiceRequest>(), It.IsAny<CallOptions>()))
.Throws(rpcException);
try
{
var body = new Request() { RequestParameter = "Hello " };
await client.DaprClient.InvokeMethodWithResponseAsync<Request, Response>("test", "testMethod", body);
Assert.False(true);
}
catch(InvocationException ex)
{
ex.Message.Should().Be("Exception while invoking testMethod on appId:test");
ex.InnerException.Message.Should().Be(rpcExceptionMessage);
ex.Response.GrpcStatusInfo.Should().BeNull();
Encoding.UTF8.GetString(ex.Response.Body).Should().Be(grpcErrorInfoDetailHttpErrorMsg);
ex.Response.HttpStatusCode.Should().Be(grpcErrorInfoDetailHttpCode);
}
}
[Fact]
public async Task InvokeMethodAsync_CanInvokeRawMethodWithResponse_CalleeSideGrpc()
{
var client = new MockClient();
var responseBody = new Response() { Name = "Look, I was invoked!" };
// var dataBytes = new byte[]{1,2,3};
var responseBytes = JsonSerializer.SerializeToUtf8Bytes(responseBody);
var invokeResponse = new InvokeResponse();
invokeResponse.Data = new Any { Value = ByteString.CopyFrom(responseBytes), TypeUrl = typeof(byte[]).FullName };
var response =
client.Call<InvokeResponse>()
.SetResponse(invokeResponse)
.Build();
var requestBody = new Request() { RequestParameter = "Hello " };
var requestBytes = JsonSerializer.SerializeToUtf8Bytes(requestBody);
client.Mock
.Setup(m => m.InvokeServiceAsync(It.IsAny<Autogen.Grpc.v1.InvokeServiceRequest>(), It.IsAny<CallOptions>()))
.Returns(response);
var task = client.DaprClient.InvokeMethodRawAsync("test", "testMethod", requestBytes);
// Validate Response
var invokedResponse = await task;
var invokedResponseBody = JsonSerializer.Deserialize<Response>(invokedResponse.Body);
invokedResponseBody.Name.Should().Be("Look, I was invoked!");
invokedResponse.HttpStatusCode.Should().BeNull();
invokedResponse.GrpcStatusInfo.Should().NotBeNull();
invokedResponse.GrpcStatusInfo.GrpcStatusCode.Should().Be(Grpc.Core.StatusCode.OK);
invokedResponse.Headers.Count.Should().Be(0);
invokedResponse.ContentType.Should().Be(Constants.ContentTypeApplicationGrpc);
var expectedRequest = new Autogen.Grpc.v1.InvokeServiceRequest
{
Id = "test",
Message = new InvokeRequest
{
Method = "testMethod",
Data = new Any { Value = ByteString.CopyFrom(requestBytes), TypeUrl = typeof(byte[]).FullName },
HttpExtension = new Autogen.Grpc.v1.HTTPExtension
{
Verb = Autogen.Grpc.v1.HTTPExtension.Types.Verb.Post,
},
ContentType = Constants.ContentTypeApplicationJson,
},
};
client.Mock.Verify(m => m.InvokeServiceAsync(It.Is<Autogen.Grpc.v1.InvokeServiceRequest>(request => request.Equals(expectedRequest)), It.IsAny<CallOptions>()));
}
[Fact]
public async Task InvokeMethodAsync_CanInvokeRawMethodWithResponse_CalleeSideHttp()
{
var client = new MockClient();
var responseBody = new Response() { Name = "Look, I was invoked!" };
var responseBytes = JsonSerializer.SerializeToUtf8Bytes(responseBody);
var invokeResponse = new InvokeResponse();
invokeResponse.Data = new Any { Value = ByteString.CopyFrom(responseBytes), TypeUrl = typeof(byte[]).FullName };
var response =
client.Call<InvokeResponse>()
.SetResponse(invokeResponse)
.AddHeader("dapr-http-status", "200")
.Build();
var requestBody = new Request() { RequestParameter = "Hello " };
var requestBytes = JsonSerializer.SerializeToUtf8Bytes(requestBody);
client.Mock
.Setup(m => m.InvokeServiceAsync(It.IsAny<Autogen.Grpc.v1.InvokeServiceRequest>(), It.IsAny<CallOptions>()))
.Returns(response);
var task = client.DaprClient.InvokeMethodRawAsync("test", "testMethod", requestBytes);
// Validate Response
var invokedResponse = await task;
var invokedResponseBody = JsonSerializer.Deserialize<Response>(invokedResponse.Body);
invokedResponseBody.Name.Should().Be("Look, I was invoked!");
invokedResponse.Headers.ContainsKey("dapr-http-status");
invokedResponse.ContentType = Constants.ContentTypeApplicationJson;
invokedResponse.GrpcStatusInfo.Should().BeNull();
invokedResponse.HttpStatusCode.Should().NotBeNull();
invokedResponse.HttpStatusCode.Should().Be(HttpStatusCode.OK);
invokedResponse.Headers.ContainsKey("dapr-http-status").Should().BeTrue();
invokedResponse.ContentType.Should().Be(Constants.ContentTypeApplicationJson);
var expectedRequest = new Autogen.Grpc.v1.InvokeServiceRequest
{
Id = "test",
Message = new InvokeRequest
{
Method = "testMethod",
Data = new Any { Value = ByteString.CopyFrom(requestBytes), TypeUrl = typeof(byte[]).FullName },
HttpExtension = new Autogen.Grpc.v1.HTTPExtension
{
Verb = Autogen.Grpc.v1.HTTPExtension.Types.Verb.Post,
},
ContentType = "application/json",
},
};
client.Mock.Verify(m => m.InvokeServiceAsync(It.Is<Autogen.Grpc.v1.InvokeServiceRequest>(request => request.Equals(expectedRequest)), It.IsAny<CallOptions>()));
}
[Fact]
public async Task InvokeMethodAsync_CanInvokeRawMethodWithResponse_GrpcServerReturnsNonSuccessResponse()
{
var client = new MockClient();
var data = new Response() { Name = "Look, I was invoked!" };
var invokeResponse = new InvokeResponse();
invokeResponse.Data = TypeConverters.ToAny(data);
var response =
client.Call<InvokeResponse>()
.SetResponse(invokeResponse)
.Build();
var trailers = new Metadata();
const string grpcErrorInfoReason = "Insufficient permissions";
int grpcStatusCode = Convert.ToInt32(StatusCode.PermissionDenied);
const string grpcStatusMessage = "Bad permissions";
var details = new Google.Rpc.Status
{
Code = grpcStatusCode,
Message = grpcStatusMessage,
};
var errorInfo = new Google.Rpc.ErrorInfo
{
Reason = grpcErrorInfoReason,
Domain = "dapr.io",
};
details.Details.Add(Google.Protobuf.WellKnownTypes.Any.Pack(errorInfo));
var entry = new Metadata.Entry("grpc-status-details-bin", Google.Protobuf.MessageExtensions.ToByteArray(details));
trailers.Add(entry);
const string rpcExceptionMessage = "No access to app";
const StatusCode rpcStatusCode = StatusCode.PermissionDenied;
const string rpcStatusDetail = "Insufficient permissions";
var rpcException = new RpcException(new Status(rpcStatusCode, rpcStatusDetail), trailers, rpcExceptionMessage);
// Setup the mock client to throw an Rpc Exception with the expected details info
client.Mock
.Setup(m => m.InvokeServiceAsync(It.IsAny<Autogen.Grpc.v1.InvokeServiceRequest>(), It.IsAny<CallOptions>()))
.Throws(rpcException);
try
{
var body = new Request() { RequestParameter = "Hello " };
var bytes = JsonSerializer.SerializeToUtf8Bytes(body);
await client.DaprClient.InvokeMethodRawAsync("test", "testMethod", bytes);
Assert.False(true);
}
catch(InvocationException ex)
{
ex.Message.Should().Be("Exception while invoking testMethod on appId:test");
ex.InnerException.Message.Should().Be(rpcExceptionMessage);
ex.Response.GrpcStatusInfo.GrpcErrorMessage.Should().Be(grpcStatusMessage);
ex.Response.GrpcStatusInfo.GrpcStatusCode.Should().Be(grpcStatusCode);
ex.Response.Body.Should().BeNull();
ex.Response.HttpStatusCode.Should().BeNull();
}
}
[Fact]
public async Task InvokeMethodAsync_CanInvokeRawMethodWithResponse_HttpServerReturnsNonSuccessResponse()
{
var client = new MockClient();
var data = new Response() { Name = "Look, I was invoked!" };
var invokeResponse = new InvokeResponse();
invokeResponse.Data = TypeConverters.ToAny(data);
var response =
client.Call<InvokeResponse>()
.SetResponse(invokeResponse)
.AddHeader("dapr-status-header", "200")
.Build();
var trailers = new Metadata();
const string grpcErrorInfoReason = "Insufficient permissions";
const int grpcErrorInfoDetailHttpCode = 500;
const string grpcErrorInfoDetailHttpErrorMsg = "no permissions";
int grpcStatusCode = Convert.ToInt32(StatusCode.PermissionDenied);
const string grpcStatusMessage = "Bad permissions";
var details = new Google.Rpc.Status
{
Code = grpcStatusCode,
Message = grpcStatusMessage,
};
var errorInfo = new Google.Rpc.ErrorInfo
{
Reason = grpcErrorInfoReason,
Domain = "dapr.io",
};
errorInfo.Metadata.Add("http.code", grpcErrorInfoDetailHttpCode.ToString());
errorInfo.Metadata.Add("http.error_message", grpcErrorInfoDetailHttpErrorMsg);
details.Details.Add(Google.Protobuf.WellKnownTypes.Any.Pack(errorInfo));
var entry = new Metadata.Entry("grpc-status-details-bin", Google.Protobuf.MessageExtensions.ToByteArray(details));
trailers.Add(entry);
const string rpcExceptionMessage = "No access to app";
const StatusCode rpcStatusCode = StatusCode.PermissionDenied;
const string rpcStatusDetail = "Insufficient permissions";
var rpcException = new RpcException(new Status(rpcStatusCode, rpcStatusDetail), trailers, rpcExceptionMessage);
// Setup the mock client to throw an Rpc Exception with the expected details info
client.Mock
.Setup(m => m.InvokeServiceAsync(It.IsAny<Autogen.Grpc.v1.InvokeServiceRequest>(), It.IsAny<CallOptions>()))
.Throws(rpcException);
try
{
var body = new Request() { RequestParameter = "Hello " };
var bytes = JsonSerializer.SerializeToUtf8Bytes(body);
await client.DaprClient.InvokeMethodRawAsync("test", "testMethod", bytes);
Assert.False(true);
}
catch(InvocationException ex)
{
ex.Message.Should().Be("Exception while invoking testMethod on appId:test");
ex.InnerException.Message.Should().Be(rpcExceptionMessage);
ex.Response.GrpcStatusInfo.Should().BeNull();
Encoding.UTF8.GetString(ex.Response.Body).Should().Be(grpcErrorInfoDetailHttpErrorMsg);
ex.Response.HttpStatusCode.Should().Be(grpcErrorInfoDetailHttpCode);
}
}
[Fact]
public async Task InvokeMethodAsync_AppCallback_SayHello()
{
@ -484,6 +930,17 @@ namespace Dapr.Client.Test
entry.Completion.SetResult(response);
}
private async void SendResponseFromHttpServer<T>(T data, TestHttpClient.Entry entry, JsonSerializerOptions options = null)
{
var dataAny = TypeConverters.ToAny(data, options);
var dataResponse = new InvokeResponse();
dataResponse.Data = dataAny;
var streamContent = await GrpcUtils.CreateResponseContent(dataResponse);
var response = GrpcUtils.CreateResponseFromHttpServer(HttpStatusCode.OK, streamContent);
entry.Completion.SetResult(response);
}
private class Request
{
public string RequestParameter { get; set; }

View File

@ -0,0 +1,85 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
namespace Dapr.Client
{
using System.Text.Json;
using System.Threading.Tasks;
using Grpc.Core;
using Moq;
public class MockClient
{
public MockClient()
{
Mock = new Mock<Autogen.Grpc.v1.Dapr.DaprClient>(MockBehavior.Strict);
DaprClient = new DaprClientGrpc(Mock.Object, new JsonSerializerOptions());
}
public Mock<Autogen.Grpc.v1.Dapr.DaprClient> Mock { get; }
public DaprClient DaprClient { get; }
public CallBuilder<TResponse> Call<TResponse>()
{
return new CallBuilder<TResponse>();
}
public void SetupMockToThrow(RpcException rpcException)
{
this.Mock
.Setup(m => m.InvokeServiceAsync(It.IsAny<Autogen.Grpc.v1.InvokeServiceRequest>(), It.IsAny<CallOptions>()))
.Throws(rpcException);
}
public class CallBuilder<TResponse>
{
private TResponse response;
private Metadata headers;
private Status status;
private Metadata trailers;
public CallBuilder()
{
headers = new Metadata();
trailers = new Metadata();
}
public AsyncUnaryCall<TResponse> Build()
{
return new AsyncUnaryCall<TResponse>(
Task.FromResult(response),
Task.FromResult(headers),
() => status,
() => trailers,
() => {});
}
public CallBuilder<TResponse> SetResponse(TResponse response)
{
this.response = response;
return this;
}
public CallBuilder<TResponse> SetStatus(Status status)
{
this.status = status;
return this;
}
public CallBuilder<TResponse> AddHeader(string key, string value)
{
this.headers.Add(key, value);
return this;
}
public CallBuilder<TResponse> AddTrailer(string key, string value)
{
this.trailers.Add(key, value);
return this;
}
}
}
}

View File

@ -11,6 +11,7 @@ namespace Dapr
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Google.Protobuf;
using Grpc.Core;
@ -75,6 +76,17 @@ namespace Dapr
return message;
}
public static HttpResponseMessage CreateResponseFromHttpServer(
HttpStatusCode statusCode,
HttpContent payload)
{
var message = CreateResponse(statusCode, payload);
// Add Dapr HTTP status header
message.Headers.Add("dapr-http-status", "200");
return message;
}
public static Task<StreamContent> CreateResponseContent<TResponse>(TResponse response) where TResponse : IMessage<TResponse>
{
return CreateResponseContentCore(new[] { response });