Use protobuf packaging for the Any type (#358)

* Use protobuf packaging for the Any type

This pull request implements two converters which help with the type conversion between arbitrary types and the protobuf `Any` type. The C# protobuf library provides a mechanism to pack or unpack protobuf messages to `Any`. It provides the methods `Any.Pack` and `Any.Unpack` to serialize/deserialize messages based on `Google.Protobuf.IMessage`. For types that are not based on `Google.Protobuf.IMessage`, the existing JSON serialization/deserialization will be used.

I've also cleaned the existing codebase a little bit.

Fixes #268

* Fix suggested changes
This commit is contained in:
Christian Kaps 2020-08-03 19:15:23 +02:00 committed by GitHub
parent ff0ead390a
commit fdf17b7dbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 288 additions and 219 deletions

View File

@ -11,13 +11,11 @@ namespace Dapr.Client
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Dapr.Client.Autogen.Grpc.v1;
using Dapr.Client.Http;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Grpc.Net.Client;
using Autogenerated = Dapr.Client.Autogen.Grpc.v1;
using Autogenerated = Autogen.Grpc.v1;
/// <summary>
/// A client for interacting with the Dapr endpoints.
@ -64,14 +62,11 @@ namespace Dapr.Client
if (content != null)
{
envelope.Data = ConvertToByteStringAsync(content, this.jsonSerializerOptions);
envelope.Data = TypeConverters.ToJsonByteString(content, this.jsonSerializerOptions);
}
await this.MakeGrpcCallHandleError(
(options) =>
{
return client.PublishEventAsync(envelope, options);
},
options => client.PublishEventAsync(envelope, options),
cancellationToken);
}
#endregion
@ -103,32 +98,32 @@ namespace Dapr.Client
ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name));
ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation));
InvokeBindingResponse response = await MakeInvokeBindingRequestAsync(name, operation, data, metadata, cancellationToken);
Autogenerated.InvokeBindingResponse response = await MakeInvokeBindingRequestAsync(name, operation, data, metadata, cancellationToken);
return ConvertFromInvokeBindingResponse<TResponse>(response, this.jsonSerializerOptions);
}
private static T ConvertFromInvokeBindingResponse<T>(InvokeBindingResponse response, JsonSerializerOptions options = null)
private static T ConvertFromInvokeBindingResponse<T>(Autogenerated.InvokeBindingResponse response, JsonSerializerOptions options = null)
{
var responseData = response.Data.ToStringUtf8();
return JsonSerializer.Deserialize<T>(responseData, options);
}
private async Task<InvokeBindingResponse> MakeInvokeBindingRequestAsync<TContent>(
private async Task<Autogenerated.InvokeBindingResponse> MakeInvokeBindingRequestAsync<TContent>(
string name,
string operation,
TContent data,
Dictionary<string, string> metadata = default,
CancellationToken cancellationToken = default)
{
{
var envelope = new Autogenerated.InvokeBindingRequest()
{
Name = name,
Operation = operation
};
if (data != null)
{
envelope.Data = ConvertToByteStringAsync(data, this.jsonSerializerOptions);
envelope.Data = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions);
}
if (metadata != null)
@ -137,10 +132,7 @@ namespace Dapr.Client
}
return await this.MakeGrpcCallHandleError(
(options) =>
{
return client.InvokeBindingAsync(envelope, options);
},
options => client.InvokeBindingAsync(envelope, options),
cancellationToken);
}
#endregion
@ -149,7 +141,7 @@ namespace Dapr.Client
public override async Task InvokeMethodAsync(
string appId,
string methodName,
Http.HTTPExtension httpExtension = default,
HTTPExtension httpExtension = default,
CancellationToken cancellationToken = default)
{
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
@ -162,7 +154,7 @@ namespace Dapr.Client
string appId,
string methodName,
TRequest data,
Http.HTTPExtension httpExtension = default,
HTTPExtension httpExtension = default,
CancellationToken cancellationToken = default)
{
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
@ -171,7 +163,7 @@ namespace Dapr.Client
Any serializedData = null;
if (data != null)
{
serializedData = ConvertToAnyAsync(data, this.jsonSerializerOptions);
serializedData = TypeConverters.ToAny(data, this.jsonSerializerOptions);
}
_ = await this.MakeInvokeRequestAsync(appId, methodName, serializedData, httpExtension, cancellationToken);
@ -180,26 +172,21 @@ namespace Dapr.Client
public override async ValueTask<TResponse> InvokeMethodAsync<TResponse>(
string appId,
string methodName,
Http.HTTPExtension httpExtension = default,
HTTPExtension httpExtension = default,
CancellationToken cancellationToken = default)
{
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName));
var response = await this.MakeInvokeRequestAsync(appId, methodName, null, httpExtension, cancellationToken);
if (response.Data.Value.IsEmpty)
{
return default;
}
return ConvertFromInvokeResponse<TResponse>(response, this.jsonSerializerOptions);
return response.Data.Value.IsEmpty ? default : TypeConverters.FromAny<TResponse>(response.Data, this.jsonSerializerOptions);
}
public override async ValueTask<TResponse> InvokeMethodAsync<TRequest, TResponse>(
string appId,
string methodName,
TRequest data,
Http.HTTPExtension httpExtension = default,
HTTPExtension httpExtension = default,
CancellationToken cancellationToken = default)
{
ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
@ -208,23 +195,18 @@ namespace Dapr.Client
Any serializedData = null;
if (data != null)
{
serializedData = ConvertToAnyAsync(data, this.jsonSerializerOptions);
serializedData = TypeConverters.ToAny(data, this.jsonSerializerOptions);
}
var response = await this.MakeInvokeRequestAsync(appId, methodName, serializedData, httpExtension, cancellationToken);
if (response.Data.Value.IsEmpty)
{
return default;
}
return ConvertFromInvokeResponse<TResponse>(response, this.jsonSerializerOptions);
return response.Data.Value.IsEmpty ? default : TypeConverters.FromAny<TResponse>(response.Data, this.jsonSerializerOptions);
}
private async Task<InvokeResponse> MakeInvokeRequestAsync(
private async Task<Autogenerated.InvokeResponse> MakeInvokeRequestAsync(
string appId,
string methodName,
Any data,
Http.HTTPExtension httpExtension,
HTTPExtension httpExtension,
CancellationToken cancellationToken = default)
{
var protoHTTPExtension = new Autogenerated.HTTPExtension();
@ -236,9 +218,9 @@ namespace Dapr.Client
if (httpExtension.QueryString != null)
{
foreach (var kv in httpExtension.QueryString)
foreach (var (key, value) in httpExtension.QueryString)
{
protoHTTPExtension.Querystring.Add(kv.Key, kv.Value);
protoHTTPExtension.Querystring.Add(key, value);
}
}
@ -250,7 +232,7 @@ namespace Dapr.Client
contentType = Constants.ContentTypeApplicationJson;
}
var invokeRequest = new InvokeRequest()
var invokeRequest = new Autogenerated.InvokeRequest()
{
Method = methodName,
Data = data,
@ -266,10 +248,7 @@ namespace Dapr.Client
};
return await this.MakeGrpcCallHandleError(
(options) =>
{
return client.InvokeServiceAsync(request, options);
},
options => client.InvokeServiceAsync(request, options),
cancellationToken);
}
#endregion
@ -281,7 +260,7 @@ namespace Dapr.Client
ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName));
ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key));
var getStateEnvelope = new GetStateRequest()
var getStateEnvelope = new Autogenerated.GetStateRequest()
{
StoreName = storeName,
Key = key,
@ -293,10 +272,7 @@ namespace Dapr.Client
}
var response = await this.MakeGrpcCallHandleError(
(options) =>
{
return client.GetStateAsync(getStateEnvelope, options);
},
options => client.GetStateAsync(getStateEnvelope, options),
cancellationToken);
if (response.Data.IsEmpty)
@ -314,7 +290,7 @@ namespace Dapr.Client
ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName));
ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key));
var getStateEnvelope = new GetStateRequest()
var getStateEnvelope = new Autogenerated.GetStateRequest()
{
StoreName = storeName,
Key = key,
@ -326,15 +302,12 @@ namespace Dapr.Client
}
var response = await this.MakeGrpcCallHandleError(
(options) =>
{
return client.GetStateAsync(getStateEnvelope, options);
},
options => client.GetStateAsync(getStateEnvelope, options),
cancellationToken);
if (response.Data.IsEmpty)
{
return (default(TValue), response.Etag);
return (default, response.Etag);
}
var responseData = response.Data.ToStringUtf8();
@ -354,7 +327,7 @@ namespace Dapr.Client
ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName));
ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key));
await this.MakeSaveStateCallAsync<TValue>(
await this.MakeSaveStateCallAsync(
storeName,
key,
value,
@ -379,16 +352,17 @@ namespace Dapr.Client
try
{
await this.MakeSaveStateCallAsync<TValue>(storeName, key, value, etag, stateOptions, metadata, cancellationToken);
await this.MakeSaveStateCallAsync(storeName, key, value, etag, stateOptions, metadata, cancellationToken);
return true;
}
catch (RpcException)
{ }
{
}
return false;
}
internal async ValueTask MakeSaveStateCallAsync<TValue>(
private async ValueTask MakeSaveStateCallAsync<TValue>(
string storeName,
string key,
TValue value,
@ -425,16 +399,13 @@ namespace Dapr.Client
if (value != null)
{
stateItem.Value = ConvertToByteStringAsync(value, this.jsonSerializerOptions);
stateItem.Value = TypeConverters.ToJsonByteString(value, this.jsonSerializerOptions);
}
saveStateEnvelope.States.Add(stateItem);
await this.MakeGrpcCallHandleError(
(options) =>
{
return client.SaveStateAsync(saveStateEnvelope, options);
},
options => client.SaveStateAsync(saveStateEnvelope, options),
cancellationToken);
}
@ -448,7 +419,7 @@ namespace Dapr.Client
ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName));
ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key));
await this.MakeDeleteStateCallsync(
await this.MakeDeleteStateCallAsync(
storeName,
key,
etag: null,
@ -460,7 +431,7 @@ namespace Dapr.Client
public override async ValueTask<bool> TryDeleteStateAsync(
string storeName,
string key,
string etag = default,
string etag,
StateOptions stateOptions = default,
CancellationToken cancellationToken = default)
{
@ -469,7 +440,7 @@ namespace Dapr.Client
try
{
await this.MakeDeleteStateCallsync(storeName, key, etag, stateOptions, cancellationToken);
await this.MakeDeleteStateCallAsync(storeName, key, etag, stateOptions, cancellationToken);
return true;
}
catch (Exception)
@ -479,14 +450,14 @@ namespace Dapr.Client
return false;
}
private async ValueTask MakeDeleteStateCallsync(
private async ValueTask MakeDeleteStateCallAsync(
string storeName,
string key,
string etag = default,
StateOptions stateOptions = default,
CancellationToken cancellationToken = default)
{
var deleteStateEnvelope = new DeleteStateRequest()
var deleteStateEnvelope = new Autogenerated.DeleteStateRequest()
{
StoreName = storeName,
Key = key,
@ -503,10 +474,7 @@ namespace Dapr.Client
}
await this.MakeGrpcCallHandleError(
(options) =>
{
return client.DeleteStateAsync(deleteStateEnvelope, options);
},
options => client.DeleteStateAsync(deleteStateEnvelope, options),
cancellationToken);
}
#endregion
@ -534,10 +502,7 @@ namespace Dapr.Client
}
var response = await this.MakeGrpcCallHandleError(
(options) =>
{
return client.GetSecretAsync(envelope, options);
},
options => client.GetSecretAsync(envelope, options),
cancellationToken);
return response.Data.ToDictionary(kv => kv.Key, kv => kv.Value);
@ -558,7 +523,7 @@ namespace Dapr.Client
{
var callOptions = new CallOptions(cancellationToken: cancellationToken);
// Common Exception Handling logic can be added here for all calls.
// Common Exception Handling logic can be added here for all calls.
return await callFunc.Invoke(callOptions);
}
@ -576,60 +541,32 @@ namespace Dapr.Client
stateRequestOptions.Concurrency = GetStateConcurrencyForConcurrencyMode(stateOptions.Concurrency.Value);
}
if (stateOptions.RetryOptions != null)
if (stateOptions.RetryOptions == null)
{
var retryPolicy = new Autogenerated.StateRetryPolicy();
if (stateOptions.RetryOptions.RetryMode != null)
{
retryPolicy.Pattern = GetRetryPatternForRetryMode(stateOptions.RetryOptions.RetryMode.Value);
}
if (stateOptions.RetryOptions.RetryInterval != null)
{
retryPolicy.Interval = Duration.FromTimeSpan(stateOptions.RetryOptions.RetryInterval.Value);
}
if (stateOptions.RetryOptions.RetryThreshold != null)
{
retryPolicy.Threshold = stateOptions.RetryOptions.RetryThreshold.Value;
}
stateRequestOptions.RetryPolicy = retryPolicy;
return stateRequestOptions;
}
var retryPolicy = new Autogenerated.StateRetryPolicy();
if (stateOptions.RetryOptions.RetryMode != null)
{
retryPolicy.Pattern = GetRetryPatternForRetryMode(stateOptions.RetryOptions.RetryMode.Value);
}
if (stateOptions.RetryOptions.RetryInterval != null)
{
retryPolicy.Interval = Duration.FromTimeSpan(stateOptions.RetryOptions.RetryInterval.Value);
}
if (stateOptions.RetryOptions.RetryThreshold != null)
{
retryPolicy.Threshold = stateOptions.RetryOptions.RetryThreshold.Value;
}
stateRequestOptions.RetryPolicy = retryPolicy;
return stateRequestOptions;
}
private static Any ConvertToAnyAsync<T>(T data, JsonSerializerOptions options = null)
{
var any = new Any();
if (data != null)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(data, options);
any.Value = ByteString.CopyFrom(bytes);
}
return any;
}
private static ByteString ConvertToByteStringAsync<T>(T data, JsonSerializerOptions options = null)
{
if (data != null)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(data, options);
return ByteString.CopyFrom(bytes);
}
return ByteString.Empty;
}
private static T ConvertFromInvokeResponse<T>(InvokeResponse response, JsonSerializerOptions options = null)
{
var responseData = response.Data.Value.ToStringUtf8();
return JsonSerializer.Deserialize<T>(responseData, options);
}
private static Autogenerated.HTTPExtension.Types.Verb ConvertHTTPVerb(HTTPVerb verb)
{
return verb switch
@ -648,49 +585,33 @@ namespace Dapr.Client
private static Autogenerated.StateOptions.Types.StateConsistency GetStateConsistencyForConsistencyMode(ConsistencyMode consistencyMode)
{
if (consistencyMode.Equals(ConsistencyMode.Eventual))
return consistencyMode switch
{
return Autogenerated.StateOptions.Types.StateConsistency.ConsistencyEventual;
}
if (consistencyMode.Equals(ConsistencyMode.Strong))
{
return Autogenerated.StateOptions.Types.StateConsistency.ConsistencyStrong;
}
throw new ArgumentException($"{consistencyMode} Consistency Mode is not supported.");
ConsistencyMode.Eventual => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyEventual,
ConsistencyMode.Strong => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyStrong,
_ => throw new ArgumentException($"{consistencyMode} Consistency Mode is not supported.")
};
}
private static Autogenerated.StateOptions.Types.StateConcurrency GetStateConcurrencyForConcurrencyMode(ConcurrencyMode concurrencyMode)
{
if (concurrencyMode.Equals(ConcurrencyMode.FirstWrite))
return concurrencyMode switch
{
return Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyFirstWrite;
}
if (concurrencyMode.Equals(ConcurrencyMode.LastWrite))
{
return Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyLastWrite;
}
throw new ArgumentException($"{concurrencyMode} Concurrency Mode is not supported.");
ConcurrencyMode.FirstWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyFirstWrite,
ConcurrencyMode.LastWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyLastWrite,
_ => throw new ArgumentException($"{concurrencyMode} Concurrency Mode is not supported.")
};
}
private static Autogenerated.StateRetryPolicy.Types.RetryPattern GetRetryPatternForRetryMode(RetryMode retryMode)
{
if (retryMode.Equals(RetryMode.Exponential))
return retryMode switch
{
return Autogenerated.StateRetryPolicy.Types.RetryPattern.RetryExponential;
}
if (retryMode.Equals(RetryMode.Linear))
{
return Autogenerated.StateRetryPolicy.Types.RetryPattern.RetryLinear;
}
throw new ArgumentException($"{retryMode} Retry Mode is not supported.");
RetryMode.Exponential => Autogenerated.StateRetryPolicy.Types.RetryPattern.RetryExponential,
RetryMode.Linear => Autogenerated.StateRetryPolicy.Types.RetryPattern.RetryLinear,
_ => throw new ArgumentException($"{retryMode} Retry Mode is not supported.")
};
}
#endregion Helper Methods
}
}

View File

@ -0,0 +1,85 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
namespace Dapr.Client
{
using System.Text.Json;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
/// <summary>
/// Some type converters.
/// </summary>
internal static class TypeConverters
{
/// <summary>
/// Converts an arbitrary type to a <see cref="System.Text.Json"/> based <see cref="ByteString"/>.
/// </summary>
/// <param name="data">The data to convert.</param>
/// <param name="options">The JSON serialization options.</param>
/// <typeparam name="T">The type of the given data.</typeparam>
/// <returns>The given data as JSON based byte string.</returns>
public static ByteString ToJsonByteString<T>(T data, JsonSerializerOptions options = null)
{
if (data == null)
{
return ByteString.Empty;
}
var bytes = JsonSerializer.SerializeToUtf8Bytes(data, options);
return ByteString.CopyFrom(bytes);
}
/// <summary>
/// Converts an arbitrary type to the <see cref="Any"/> Protocol Buffer type.
///
/// If the given type is a subtype of <see cref="IMessage"/>, then it's save to use the Protocol Buffer
/// packaging provided for the <see cref="Any"/> type with the <see cref="Any.Pack(Google.Protobuf.IMessage)"/>.
/// For all other types, we use JSON serialization based on <see cref="System.Text.Json"/>.
/// </summary>
/// <param name="data">The data to convert.</param>
/// <param name="options">The JSON serialization options.</param>
/// <typeparam name="T">The type of the given data.</typeparam>
/// <returns>The <see cref="Any"/> type representation of the given data.</returns>
public static Any ToAny<T>(T data, JsonSerializerOptions options = null)
{
if (data == null)
{
return new Any();
}
var t = typeof(T);
return typeof(IMessage).IsAssignableFrom(t)
? Any.Pack((IMessage) data)
: new Any {Value = ToJsonByteString(data, options), TypeUrl = t.FullName};
}
/// <summary>
/// Converts the Protocol Buffer <see cref="Any"/> type to an arbitrary type.
///
/// If the type to convert to is a subtype of <see cref="IMessage"/> and if the type has an empty default
/// constructor, then we use the <see cref="Any.Unpack{T}"/> method to deserialize the given <see cref="Any"/>
/// instance. For all other types, we use JSON deserialization based on <see cref="System.Text.Json"/>.
/// </summary>
/// <param name="any">The any instance to convert.</param>
/// <param name="options">The JSON serialization options.</param>
/// <typeparam name="T">The type to convert to.</typeparam>
/// <returns>An instance of T.</returns>
public static T FromAny<T>(Any any, JsonSerializerOptions options = null)
{
var t = typeof(T);
if (typeof(IMessage).IsAssignableFrom(t) && t.GetConstructor(System.Type.EmptyTypes) != null)
{
var method = any.GetType().GetMethod("Unpack");
var generic = method.MakeGenericMethod(t);
return (T) generic.Invoke(any, null);
}
return JsonSerializer.Deserialize<T>(any.Value.ToStringUtf8(), options);
}
}
}

View File

@ -16,7 +16,6 @@
<ItemGroup>
<Compile Include="..\Shared\TestHttpClient.cs" />
<Compile Include="..\Shared\GrpcUtils.cs" />
<Compile Include="..\Shared\ProtobufUtils.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -109,7 +109,7 @@ namespace Dapr.AspNetCore.Test
private async void SendResponseWithState<T>(T state, TestHttpClient.Entry entry)
{
var stateData = ProtobufUtils.ConvertToByteStringAsync(state);
var stateData = TypeConverters.ToJsonByteString(state);
var stateResponse = new GetStateResponse();
stateResponse.Data = stateData;
stateResponse.Etag = "test";

View File

@ -8,6 +8,7 @@
<PackageReference Include="Google.Protobuf" Version="3.11.4" />
<PackageReference Include="Grpc.Core.Testing" Version="2.31.0-pre1" />
<PackageReference Include="Grpc.Net.Client" Version="2.27.0" />
<PackageReference Include="Grpc.Tools" Version="2.27.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
@ -16,11 +17,14 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\test.proto" ProtoRoot="Protos" GrpcServices="Client" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Shared\AppCallbackClient.cs" />
<Compile Include="..\Shared\TestHttpClient.cs" />
<Compile Include="..\Shared\GrpcUtils.cs" />
<Compile Include="..\Shared\ProtobufUtils.cs" />
</ItemGroup>
<ItemGroup>

View File

@ -10,6 +10,7 @@ namespace Dapr.Client.Test
using System.Text.Json;
using System.Threading.Tasks;
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;
@ -395,11 +396,35 @@ namespace Dapr.Client.Test
var request = new Request() { RequestParameter = "Look, I was invoked!" };
var response = await daprClient.InvokeMethodAsync<Request, Response>("test1", "sayHello", request);
var response = await daprClient.InvokeMethodAsync<Request, Response>("test", "SayHello", request);
response.Name.Should().Be("Hello Look, I was invoked!");
}
[Fact]
public async Task InvokeMethodAsync_AppCallback_RepeatedField()
{
// Configure Client
var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var httpClient = new AppCallbackClient(new DaprAppCallbackService(jsonOptions));
var daprClient = new DaprClientBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.UseJsonSerializationOptions(jsonOptions)
.Build();
var testRun = new TestRun();
testRun.Tests.Add(new TestCase() { Name = "test1" });
testRun.Tests.Add(new TestCase() { Name = "test2" });
testRun.Tests.Add(new TestCase() { Name = "test3" });
var response = await daprClient.InvokeMethodAsync<TestRun, TestRun>("test", "TestRun", testRun);
response.Tests.Count.Should().Be(3);
response.Tests[0].Name.Should().Be("test1");
response.Tests[1].Name.Should().Be("test2");
response.Tests[2].Name.Should().Be("test3");
}
[Fact]
public async Task InvokeMethodAsync_AppCallback_UnexpectedMethod()
{
@ -413,14 +438,14 @@ namespace Dapr.Client.Test
var request = new Request() { RequestParameter = "Look, I was invoked!" };
var response = await daprClient.InvokeMethodAsync<Request, Response>("test1", "not-existing", request);
var response = await daprClient.InvokeMethodAsync<Request, Response>("test", "not-existing", request);
response.Name.Should().Be("unexpected");
}
private async void SendResponse<T>(T data, TestHttpClient.Entry entry, JsonSerializerOptions options = null)
{
var dataAny = ProtobufUtils.ConvertToAnyAsync(data, options);
var dataAny = TypeConverters.ToAny(data, options);
var dataResponse = new InvokeResponse();
dataResponse.Data = dataAny;
@ -453,22 +478,33 @@ namespace Dapr.Client.Test
{
return request.Method switch
{
"sayHello" => SayHello(request),
"SayHello" => SayHello(request),
"TestRun" => TestRun(request),
_ => Task.FromResult(new InvokeResponse()
{
Data = ProtobufUtils.ConvertToAnyAsync(new Response() { Name = $"unexpected" }, this.jsonOptions)
Data = TypeConverters.ToAny(new Response() { Name = $"unexpected" }, this.jsonOptions)
})
};
}
private Task<InvokeResponse> SayHello(InvokeRequest request)
{
var helloRequest = ProtobufUtils.ConvertFromAnyAsync<Request>(request.Data, this.jsonOptions);
var helloRequest = TypeConverters.FromAny<Request>(request.Data, this.jsonOptions);
var helloResponse = new Response() { Name = $"Hello {helloRequest.RequestParameter}" };
return Task.FromResult(new InvokeResponse()
{
Data = ProtobufUtils.ConvertToAnyAsync(helloResponse, this.jsonOptions)
Data = TypeConverters.ToAny(helloResponse, this.jsonOptions)
});
}
private Task<InvokeResponse> TestRun(InvokeRequest request)
{
var echoRequest = TypeConverters.FromAny<TestRun>(request.Data, this.jsonOptions);
return Task.FromResult(new InvokeResponse()
{
Data = TypeConverters.ToAny(echoRequest, this.jsonOptions)
});
}
}

View File

@ -0,0 +1,16 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
syntax = "proto3";
option csharp_namespace = "Dapr.Client.Autogen.Test.Grpc.v1";
message TestRun {
repeated TestCase tests = 1;
}
message TestCase {
string name = 1;
}

View File

@ -13,7 +13,6 @@ namespace Dapr.Client.Test
using Grpc.Core;
using Grpc.Net.Client;
using Xunit;
using Google.Protobuf.WellKnownTypes;
using System;
using System.Collections.Generic;
using StateConsistency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConsistency;
@ -145,11 +144,11 @@ namespace Dapr.Client.Test
var widget = new Widget() { Size = "small", Color = "yellow", };
var task = daprClient.SaveStateAsync("testStore", "test", widget);
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var request = await GrpcUtils.GetRequestFromRequestMessageAsync<Autogenerated.SaveStateRequest>(entry.Request);
request.StoreName.Should().Be("testStore");
request.States.Count.Should().Be(1);
var state = request.States[0];
@ -192,7 +191,7 @@ namespace Dapr.Client.Test
var daprClient = new DaprClientBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.Build();
var widget = new Widget() { Size = "small", Color = "yellow", };
var task = daprClient.SaveStateAsync("testStore", "test", widget);
@ -347,7 +346,7 @@ namespace Dapr.Client.Test
// Get Request and validate
httpClient.Requests.TryDequeue(out entry).Should().BeTrue();
var request = await GrpcUtils.GetRequestFromRequestMessageAsync<Autogenerated.DeleteStateRequest>(entry.Request);
var request = await GrpcUtils.GetRequestFromRequestMessageAsync<Autogenerated.DeleteStateRequest>(entry.Request);
request.StoreName.Should().Be("testStore");
request.Key.Should().Be("test");
}
@ -584,7 +583,7 @@ namespace Dapr.Client.Test
private async void SendResponseWithState<T>(T state, TestHttpClient.Entry entry, string etag = null)
{
var stateDate = ProtobufUtils.ConvertToByteStringAsync(state);
var stateDate = TypeConverters.ToJsonByteString(state);
var stateResponse = new Autogenerated.GetStateResponse();
stateResponse.Data = stateDate;

View File

@ -0,0 +1,54 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
namespace Dapr.Client.Test
{
using Dapr.Client.Autogen.Test.Grpc.v1;
using FluentAssertions;
using Xunit;
public class TypeConvertersTest
{
[Fact]
public void AnyConversion_GRPC_Pack_Unpack()
{
var testRun = new TestRun();
testRun.Tests.Add(new TestCase() { Name = "test1" });
testRun.Tests.Add(new TestCase() { Name = "test2" });
testRun.Tests.Add(new TestCase() { Name = "test3" });
var any = TypeConverters.ToAny(testRun);
var type = TypeConverters.FromAny<TestRun>(any);
type.Should().BeEquivalentTo(testRun);
any.TypeUrl.Should().Be("type.googleapis.com/TestRun");
type.Tests.Count.Should().Be(3);
type.Tests[0].Name.Should().Be("test1");
type.Tests[1].Name.Should().Be("test2");
type.Tests[2].Name.Should().Be("test3");
}
[Fact]
public void AnyConversion_JSON_Serialization_Deserialization()
{
var response = new Response()
{
Name = "test"
};
var any = TypeConverters.ToAny(response);
var type = TypeConverters.FromAny<Response>(any);
type.Should().BeEquivalentTo(response);
any.TypeUrl.Should().Be("Dapr.Client.Test.TypeConvertersTest+Response");
type.Name.Should().Be("test");
}
private class Response
{
public string Name { get; set; }
}
}
}

View File

@ -1,45 +0,0 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
namespace Dapr
{
using System.Text.Json;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
public class ProtobufUtils
{
public static Any ConvertToAnyAsync<T>(T data, JsonSerializerOptions options = null)
{
var any = new Any();
if (data != null)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(data, options);
any.Value = ByteString.CopyFrom(bytes);
}
return any;
}
public static ByteString ConvertToByteStringAsync<T>(T data, JsonSerializerOptions options = null)
{
if (data != null)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(data, options);
return ByteString.CopyFrom(bytes);
}
return ByteString.Empty;
}
public static T ConvertFromAnyAsync<T>(Any any, JsonSerializerOptions options = null)
{
var utf8String = any.Value.ToStringUtf8();
return JsonSerializer.Deserialize<T>(utf8String, options);
}
}
}