Include the appcallback.proto into the Dapr.Client package (#349)

* Include the appcallback.proto into the Dapr.Client package

To implement service invocation for a gRPC API, a user must implement the AppCallback service on the callee site. Currently this must be done by integrating the `appcallpack.proto` file as also the depending `common.proto` file into the gRPC service application. The `Dapr.Client` package contains already the same `common.proto` file to generate the client classes. This results in a CS0433 error, because the `Dapr.Client` package and the generated AppCallback service will contain a `Dapr.Client.Autogen.Grpc.v1` namespace with the exact same classes.

This pull requests integrates the `appcallpack.proto` into the client package. With this fix the user does not need to integrate the proto files by itself.

See: https://gitter.im/Dapr/community?at=5f14b7e98a9a0a08cbab5d53

* Remove specific names
This commit is contained in:
Christian Kaps 2020-07-25 18:20:34 +02:00 committed by GitHub
parent fcd14b278c
commit 5b246c8665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 326 additions and 44 deletions

View File

@ -4,8 +4,9 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\dapr\proto\common\v1\common.proto" ProtoRoot="Protos" GrpcServices="Client" />
<Protobuf Include="Protos\dapr\proto\common\v1\common.proto" ProtoRoot="Protos" GrpcServices="Client,Server" />
<Protobuf Include="Protos\dapr\proto\dapr\v1\dapr.proto" ProtoRoot="Protos" GrpcServices="Client" />
<Protobuf Include="Protos\dapr\proto\dapr\v1\appcallback.proto" ProtoRoot="Protos" GrpcServices="Server" />
</ItemGroup>
<!-- Additional Nuget package properties. -->

View File

@ -0,0 +1,131 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
syntax = "proto3";
package dapr.proto.runtime.v1;
import "google/protobuf/empty.proto";
import "dapr/proto/common/v1/common.proto";
option csharp_namespace = "Dapr.AppCallback.Autogen.Grpc.v1";
option java_outer_classname = "DaprAppCallbackProtos";
option java_package = "io.dapr.v1";
option go_package = "github.com/dapr/dapr/pkg/proto/runtime/v1;runtime";
// AppCallback V1 allows user application to interact with Dapr runtime.
// User application needs to implement AppCallback service if it needs to
// receive message from dapr runtime.
service AppCallback {
// Invokes service method with InvokeRequest.
rpc OnInvoke (common.v1.InvokeRequest) returns (common.v1.InvokeResponse) {}
// Lists all topics subscribed by this app.
rpc ListTopicSubscriptions(google.protobuf.Empty) returns (ListTopicSubscriptionsResponse) {}
// Subscribes events from Pubsub
rpc OnTopicEvent(TopicEventRequest) returns (google.protobuf.Empty) {}
// Lists all input bindings subscribed by this app.
rpc ListInputBindings(google.protobuf.Empty) returns (ListInputBindingsResponse) {}
// Listens events from the input bindings
//
// User application can save the states or send the events to the output
// bindings optionally by returning BindingEventResponse.
rpc OnBindingEvent(BindingEventRequest) returns (BindingEventResponse) {}
}
// TopicEventRequest message is compatiable with CloudEvent spec v1.0
// https://github.com/cloudevents/spec/blob/v1.0/spec.md
message TopicEventRequest {
// id identifies the event. Producers MUST ensure that source + id
// is unique for each distinct event. If a duplicate event is re-sent
// (e.g. due to a network error) it MAY have the same id.
string id = 1;
// source identifies the context in which an event happened.
// Often this will include information such as the type of the
// event source, the organization publishing the event or the process
// that produced the event. The exact syntax and semantics behind
// the data encoded in the URI is defined by the event producer.
string source = 2;
// The type of event related to the originating occurrence.
string type = 3;
// The version of the CloudEvents specification.
string spec_version = 4;
// The content type of data value.
string data_content_type = 5;
// The content of the event.
bytes data = 7;
// The pubsub topic which publisher sent to.
string topic = 6;
}
// BindingEventRequest represents input bindings event.
message BindingEventRequest {
// Requried. The name of the input binding component.
string name = 1;
// Required. The payload that the input bindings sent
bytes data = 2;
// The metadata set by the input binging components.
map<string,string> metadata = 3;
}
// BindingEventResponse includes operations to save state or
// send data to output bindings optionally.
message BindingEventResponse {
// The name of state store where states are saved.
string store_name = 1;
// The state key values which will be stored in store_name.
repeated common.v1.StateItem states = 2;
// BindingEventConcurrency is the kind of concurrency
enum BindingEventConcurrency {
// SEQUENTIAL sends data to output bindings specified in "to" sequentially.
SEQUENTIAL = 0;
// PARALLEL sends data to output bindings specified in "to" in parallel.
PARALLEL = 1;
}
// The list of output bindings.
repeated string to = 3;
// The content which will be sent to "to" output bindings.
bytes data = 4;
// The concurrency of output bindings to send data to
// "to" output bindings list. The default is SEQUENTIAL.
BindingEventConcurrency concurrency = 5;
}
// ListTopicSubscriptionsResponse is the message including the list of the subscribing topics.
message ListTopicSubscriptionsResponse {
// The list of topics.
repeated TopicSubscription subscriptions = 1;
}
// TopicSubscription represents topic and metadata.
message TopicSubscription {
// Required. The name of topic which will be subscribed
string topic = 1;
// The optional properties used for this topic's subscribtion e.g. session id
map<string,string> metadata = 2;
}
// ListInputBindingsResponse is the message including the list of input bindings.
message ListInputBindingsResponse {
// The list of input bindings.
repeated string bindings = 1;
}

View File

@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.9.0" />
<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="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="xunit" Version="2.4.1" />
@ -16,6 +17,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="..\Shared\AppCallbackClient.cs" />
<Compile Include="..\Shared\TestHttpClient.cs" />
<Compile Include="..\Shared\GrpcUtils.cs" />
<Compile Include="..\Shared\ProtobufUtils.cs" />

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.AppCallback.Autogen.Grpc.v1;
using Dapr.Client.Http;
using FluentAssertions;
using Grpc.Core;
@ -37,9 +38,9 @@ namespace Dapr.Client.Test
QueryString = queryString
};
var task = daprClient.InvokeMethodAsync<InvokedResponse>("app1", "mymethod", httpExtension);
var task = daprClient.InvokeMethodAsync<Response>("app1", "mymethod", httpExtension);
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
@ -64,9 +65,9 @@ namespace Dapr.Client.Test
.Build();
// httpExtension not specified
var task = daprClient.InvokeMethodAsync<InvokedResponse>("app1", "mymethod");
var task = daprClient.InvokeMethodAsync<Response>("app1", "mymethod");
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
@ -87,9 +88,9 @@ namespace Dapr.Client.Test
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.Build();
var task = daprClient.InvokeMethodAsync<InvokedResponse>("test", "test");
var task = daprClient.InvokeMethodAsync<Response>("test", "test");
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
envelope.Id.Should().Be("test");
@ -97,7 +98,7 @@ namespace Dapr.Client.Test
envelope.Message.ContentType.Should().Be(Constants.ContentTypeApplicationJson);
// Create Response & Respond
var data = new InvokedResponse() { Name = "Look, I was invoked!" };
var data = new Response() { Name = "Look, I was invoked!" };
SendResponse(data, entry);
// Validate Response
@ -114,9 +115,9 @@ namespace Dapr.Client.Test
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.Build();
var task = daprClient.InvokeMethodAsync<InvokeRequest, InvokedResponse>("test", "test", new InvokeRequest() { RequestParameter = "Hello " });
var task = daprClient.InvokeMethodAsync<Request, Response>("test", "test", new Request() { RequestParameter = "Hello " });
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
envelope.Id.Should().Be("test");
@ -124,11 +125,11 @@ namespace Dapr.Client.Test
envelope.Message.ContentType.Should().Be(Constants.ContentTypeApplicationJson);
var json = envelope.Message.Data.Value.ToStringUtf8();
var typeFromRequest = JsonSerializer.Deserialize<InvokeRequest>(json);
var typeFromRequest = JsonSerializer.Deserialize<Request>(json);
typeFromRequest.RequestParameter.Should().Be("Hello ");
// Create Response & Respond
SendResponse<InvokedResponse>(null, entry);
SendResponse<Response>(null, entry);
// Validate Response.
var invokedResponse = await task;
@ -145,9 +146,9 @@ namespace Dapr.Client.Test
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.Build();
var task = daprClient.InvokeMethodAsync<InvokeRequest, InvokedResponse>("test", "test", new InvokeRequest() { RequestParameter = "Hello " });
var task = daprClient.InvokeMethodAsync<Request, Response>("test", "test", new Request() { RequestParameter = "Hello " });
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
envelope.Id.Should().Be("test");
@ -155,7 +156,7 @@ namespace Dapr.Client.Test
envelope.Message.ContentType.Should().Be(Constants.ContentTypeApplicationJson);
var json = envelope.Message.Data.Value.ToStringUtf8();
var typeFromRequest = JsonSerializer.Deserialize<InvokeRequest>(json);
var typeFromRequest = JsonSerializer.Deserialize<Request>(json);
typeFromRequest.RequestParameter.Should().Be("Hello ");
// Create Response & Respond
@ -175,9 +176,9 @@ namespace Dapr.Client.Test
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.Build();
var task = daprClient.InvokeMethodAsync<InvokedResponse>("test", "test");
var task = daprClient.InvokeMethodAsync<Response>("test", "test");
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
envelope.Id.Should().Be("test");
@ -185,7 +186,7 @@ namespace Dapr.Client.Test
envelope.Message.ContentType.Should().Be(Constants.ContentTypeApplicationJson);
// Create Response & Respond
var data = new InvokedResponse() { Name = "Look, I was invoked!" };
var data = new Response() { Name = "Look, I was invoked!" };
SendResponse(data, entry);
// Validate Response
@ -202,9 +203,9 @@ namespace Dapr.Client.Test
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.Build();
var task = daprClient.InvokeMethodAsync<InvokedResponse>("test", "test");
var task = daprClient.InvokeMethodAsync<Response>("test", "test");
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
envelope.Id.Should().Be("test");
@ -228,10 +229,10 @@ namespace Dapr.Client.Test
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.Build();
InvokeRequest invokeRequest = new InvokeRequest() { RequestParameter = "Hello " };
var task = daprClient.InvokeMethodAsync<InvokeRequest>("test", "test", invokeRequest);
Request request = new Request() { RequestParameter = "Hello " };
var task = daprClient.InvokeMethodAsync<Request>("test", "test", request);
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
envelope.Id.Should().Be("test");
@ -239,11 +240,11 @@ namespace Dapr.Client.Test
envelope.Message.ContentType.Should().Be(Constants.ContentTypeApplicationJson);
var json = envelope.Message.Data.Value.ToStringUtf8();
var typeFromRequest = JsonSerializer.Deserialize<InvokeRequest>(json);
var typeFromRequest = JsonSerializer.Deserialize<Request>(json);
typeFromRequest.RequestParameter.Should().Be("Hello ");
// Create Response & Respond
var response = new InvokedResponse() { Name = "Look, I was invoked!" };
var response = new Response() { Name = "Look, I was invoked!" };
SendResponse(response, entry);
FluentActions.Awaiting(async () => await task).Should().NotThrow();
@ -258,9 +259,9 @@ namespace Dapr.Client.Test
.UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient })
.Build();
var task = daprClient.InvokeMethodAsync<InvokeRequest>("test", "test", new InvokeRequest() { RequestParameter = "Hello " });
var task = daprClient.InvokeMethodAsync<Request>("test", "test", new Request() { RequestParameter = "Hello " });
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
envelope.Id.Should().Be("test");
@ -268,7 +269,7 @@ namespace Dapr.Client.Test
envelope.Message.ContentType.Should().Be(Constants.ContentTypeApplicationJson);
var json = envelope.Message.Data.Value.ToStringUtf8();
var typeFromRequest = JsonSerializer.Deserialize<InvokeRequest>(json);
var typeFromRequest = JsonSerializer.Deserialize<Request>(json);
typeFromRequest.RequestParameter.Should().Be("Hello ");
// Create Response & Respond
@ -290,10 +291,10 @@ namespace Dapr.Client.Test
.UseJsonSerializationOptions(jsonOptions)
.Build();
var invokeRequest = new InvokeRequest() { RequestParameter = "Hello" };
var task = daprClient.InvokeMethodAsync<InvokeRequest, InvokedResponse>("test", "test", invokeRequest);
var invokeRequest = new Request() { RequestParameter = "Hello" };
var task = daprClient.InvokeMethodAsync<Request, Response>("test", "test", invokeRequest);
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
envelope.Id.Should().Be("test");
@ -315,12 +316,12 @@ namespace Dapr.Client.Test
.UseJsonSerializationOptions(jsonOptions)
.Build();
var invokeRequest = new InvokeRequest() { RequestParameter = "Hello " };
var invokedResponse = new InvokedResponse { Name = "Look, I was invoked!" };
var invokeRequest = new Request() { RequestParameter = "Hello " };
var invokedResponse = new Response { Name = "Look, I was invoked!" };
var task = daprClient.InvokeMethodAsync<InvokeRequest, InvokedResponse>("test", "test", invokeRequest);
var task = daprClient.InvokeMethodAsync<Request, Response>("test", "test", invokeRequest);
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
envelope.Id.Should().Be("test");
@ -347,8 +348,8 @@ namespace Dapr.Client.Test
.UseJsonSerializationOptions(jsonOptions)
.Build();
var invokeRequest = new InvokeRequest() { RequestParameter = "Hello " };
var invokedResponse = new InvokedResponse { Name = "Look, I was invoked!" };
var invokeRequest = new Request() { RequestParameter = "Hello " };
var invokedResponse = new Response { Name = "Look, I was invoked!" };
Dictionary<string, string> queryString = new Dictionary<string, string>();
queryString.Add("key1", "value1");
@ -358,13 +359,13 @@ namespace Dapr.Client.Test
QueryString = queryString
};
var task = daprClient.InvokeMethodAsync<InvokeRequest, InvokedResponse>("test", "test", invokeRequest, httpExtension);
var task = daprClient.InvokeMethodAsync<Request, Response>("test", "test1", invokeRequest, httpExtension);
// Get Request and validate
// Get Request and validate
httpClient.Requests.TryDequeue(out var entry).Should().BeTrue();
var envelope = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(entry.Request);
envelope.Id.Should().Be("test");
envelope.Message.Method.Should().Be("test");
envelope.Message.Method.Should().Be("test1");
envelope.Message.ContentType.Should().Be(Constants.ContentTypeApplicationJson);
envelope.Message.HttpExtension.Verb.Should().Be(Autogen.Grpc.v1.HTTPExtension.Types.Verb.Put);
envelope.Message.HttpExtension.Querystring.Count.Should().Be(1);
@ -381,6 +382,42 @@ namespace Dapr.Client.Test
response.Name.Should().Be(invokedResponse.Name);
}
[Fact]
public async Task InvokeMethodAsync_AppCallback_SayHello()
{
// 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 request = new Request() { RequestParameter = "Look, I was invoked!" };
var response = await daprClient.InvokeMethodAsync<Request, Response>("test1", "sayHello", request);
response.Name.Should().Be("Hello Look, I was invoked!");
}
[Fact]
public async Task InvokeMethodAsync_AppCallback_UnexpectedMethod()
{
// 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 request = new Request() { RequestParameter = "Look, I was invoked!" };
var response = await daprClient.InvokeMethodAsync<Request, Response>("test1", "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);
@ -392,14 +429,48 @@ namespace Dapr.Client.Test
entry.Completion.SetResult(response);
}
private class InvokeRequest
private class Request
{
public string RequestParameter { get; set; }
}
private class InvokedResponse
private class Response
{
public string Name { get; set; }
}
// Test implementation of the AppCallback.AppCallbackBase service
private class DaprAppCallbackService : AppCallback.AppCallbackBase
{
private readonly JsonSerializerOptions jsonOptions;
public DaprAppCallbackService(JsonSerializerOptions jsonOptions)
{
this.jsonOptions = jsonOptions;
}
public override Task<InvokeResponse> OnInvoke(InvokeRequest request, ServerCallContext context)
{
return request.Method switch
{
"sayHello" => SayHello(request),
_ => Task.FromResult(new InvokeResponse()
{
Data = ProtobufUtils.ConvertToAnyAsync(new Response() { Name = $"unexpected" }, this.jsonOptions)
})
};
}
private Task<InvokeResponse> SayHello(InvokeRequest request)
{
var helloRequest = ProtobufUtils.ConvertFromAnyAsync<Request>(request.Data, this.jsonOptions);
var helloResponse = new Response() { Name = $"Hello {helloRequest.RequestParameter}" };
return Task.FromResult(new InvokeResponse()
{
Data = ProtobufUtils.ConvertToAnyAsync(helloResponse, this.jsonOptions)
});
}
}
}
}

View File

@ -0,0 +1,73 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
namespace Dapr
{
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Dapr.Client.Autogen.Grpc.v1;
using Grpc.Core;
using Grpc.Core.Testing;
using Grpc.Core.Utils;
using AppCallbackBase = AppCallback.Autogen.Grpc.v1.AppCallback.AppCallbackBase;
// This client will forward requests to the AppCallback service implementation which then responds to the request
public class AppCallbackClient : HttpClient
{
public AppCallbackClient(AppCallbackBase callbackService)
: this(new TestHttpClientHandler(callbackService))
{
}
private AppCallbackClient(TestHttpClientHandler handler)
: base(handler)
{
}
private class TestHttpClientHandler : HttpMessageHandler
{
private readonly AppCallbackBase callbackService;
public TestHttpClientHandler(AppCallbackBase callbackService)
{
this.callbackService = callbackService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage httpRequest, CancellationToken cancellationToken)
{
var metadata = new Metadata();
foreach (var (key, value) in httpRequest.Headers)
{
metadata.Add(key, string.Join(",", value.ToArray()));
}
var context = TestServerCallContext.Create(
method: httpRequest.Method.Method,
host: httpRequest.RequestUri.Host,
deadline: DateTime.UtcNow.AddHours(1),
requestHeaders: metadata,
cancellationToken: cancellationToken,
peer: "127.0.0.1",
authContext: null,
contextPropagationToken: null,
writeHeadersFunc: _ => TaskUtils.CompletedTask,
writeOptionsGetter: () => new WriteOptions(),
writeOptionsSetter: writeOptions => {});
var grpcRequest = await GrpcUtils.GetRequestFromRequestMessageAsync<InvokeServiceRequest>(httpRequest);
var grpcResponse = await this.callbackService.OnInvoke(grpcRequest.Message, context);
var streamContent = await GrpcUtils.CreateResponseContent(grpcResponse);
var httpResponse = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent);
return httpResponse;
}
}
}
}

View File

@ -5,9 +5,7 @@
namespace Dapr
{
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
@ -37,5 +35,11 @@ namespace Dapr
return ByteString.Empty;
}
public static T ConvertFromAnyAsync<T>(Any any, JsonSerializerOptions options = null)
{
var utf8String = any.Value.ToStringUtf8();
return JsonSerializer.Deserialize<T>(utf8String, options);
}
}
}