mirror of https://github.com/dapr/dotnet-sdk.git
Rich error model (#432)
* add InvokeMethodRawAsync and InvokeMethodWithResponseAsync Co-authored-by: Ryan Nowak <nowakra@gmail.com>
This commit is contained in:
parent
0ccddf73ff
commit
4c926c9469
|
@ -8,5 +8,6 @@ namespace Dapr.Client
|
|||
internal class Constants
|
||||
{
|
||||
public const string ContentTypeApplicationJson = "application/json";
|
||||
public const string ContentTypeApplicationGrpc = "application/grpc";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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\" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
|
|
Loading…
Reference in New Issue