Open TFTomSun opened 4 years ago
Ok i created more tests to figure out what is actually working and what not. The API seems to have a severe problems with generic data, even if I use json serialization. Some things are only supported between native - native, but not between core/grpc web/ - native. Actually I need the possibility to transfer generic data between GRPC web and Native Server. Is there any possibility to achieve that? Here's the test code and the test results:
[ServiceContract]
public interface IService<T>
{
IAsyncEnumerable<T> Relay(IAsyncEnumerable<T> data, CancellationToken cancel);
IAsyncEnumerable<GenericJsonData<T>> RelayWrapped(IAsyncEnumerable<GenericJsonData<T>> data, CancellationToken cancel);
IAsyncEnumerable<T> Stream(T[] testData, CancellationToken cancel);
}
public class Service<T>: IService<T>
{
public async IAsyncEnumerable<GenericJsonData<T>> RelayWrapped(IAsyncEnumerable<GenericJsonData<T>> data, [EnumeratorCancellation] CancellationToken cancel)
{
await foreach (var element in data)
{
if (cancel.IsCancellationRequested)
{
yield break;
}
yield return element;
}
}
public async IAsyncEnumerable<T> Relay(IAsyncEnumerable<T> data, [EnumeratorCancellation] CancellationToken cancel)
{
await foreach (var element in data)
{
if (cancel.IsCancellationRequested)
{
yield break;
}
yield return element;
}
}
public async IAsyncEnumerable<T> Stream(T[] testData, [EnumeratorCancellation] CancellationToken cancel)
{
for (int i = 0; i < testData.Length; ++i)
{
yield return await Task.FromResult(testData[i]);
}
}
}
//[DataContract]
public class JsonData
{
//[DataMember(Order = 1)]
public int Value { get; set; }
}
public class GenericJsonData<T>
{
public T Value { get; set; }
}
public enum ClientMode
{
Native,
Core,
Web
}
public enum TestMethod
{
Relay,
RelayWrapped,
Stream
}
[Pairwise]
[Test]
public async Task Issue_X_ReferenceType(
[Values(ClientMode.Native,ClientMode.Core,ClientMode.Web)] ClientMode mode,
[Values(TestMethod.Relay)] TestMethod testMethod)
{
// serialization
var jsonFactory = Api.Global.JsonClientFactory();
await this.DoTest(jsonFactory, mode, testMethod, x => new JsonData { Value = x }, x => x.Value);
}
[Pairwise]
[Test]
public async Task Issue_X_Generic_ReferenceType(
[Values(ClientMode.Native, ClientMode.Core, ClientMode.Web)] ClientMode mode,
[Values(TestMethod.Relay)] TestMethod testMethod)
{
// serialization
var jsonFactory = Api.Global.JsonClientFactory();
await this.DoTest(jsonFactory, mode, testMethod, x => new GenericJsonData<JsonData> { Value = new JsonData { Value = x } }, x => x.Value.Value);
}
[Pairwise]
[Test]
public async Task Issue_X_Generic_ValueType(
[Values(ClientMode.Native, ClientMode.Core, ClientMode.Web)] ClientMode mode,
[Values(TestMethod.Relay)] TestMethod testMethod)
{
// serialization
var jsonFactory = Api.Global.JsonClientFactory();
await this.DoTest(jsonFactory, mode, testMethod, x => new GenericJsonData<int> { Value = x }, x => x.Value);
}
[Pairwise]
[Test]
public async Task Issue_X_ValueType(
[Values(ClientMode.Native, ClientMode.Core, ClientMode.Web)] ClientMode mode,
[Values(TestMethod.Relay)] TestMethod testMethod)
{
// serialization
var jsonFactory = Api.Global.JsonClientFactory();
await this.DoTest(jsonFactory, mode, testMethod, x => x, x => x);
}
private async Task DoTest<T>(ClientFactory factory,
ClientMode mode, TestMethod testMethod,
Func<int, T> convertToT, Func<T, int> convertToInt)
{
var serviceClient = this.CreateClient<T>(mode, factory);
// server
var server = new Server
{
Ports = { new ServerPort("localhost", 10042, ServerCredentials.Insecure) }
};
try
{
server.Services.AddCodeFirst(new Service<T>(), factory);
server.Start();
// client
var cancelSource = new CancellationTokenSource();
var input = new[] { 1, 2, 3 };
T[] output;
switch (testMethod)
{
default:
throw new NotImplementedException();
case TestMethod.RelayWrapped:
{
var wrappedInput = input.Select(convertToT).Select(
x => new GenericJsonData<T> { Value = x });
var response = serviceClient.RelayWrapped(
wrappedInput.ToAsyncEnumerable(), cancelSource.Token);
output = (await response.ToArrayAsync()).Select(x => x.Value).ToArray();
break;
}
case TestMethod.Relay:
{
var response = serviceClient.Relay(
input.Select(convertToT).ToAsyncEnumerable(), cancelSource.Token);
output = await response.ToArrayAsync();
break;
}
case TestMethod.Stream:
{
var streamResponse = serviceClient.Stream(input.Select(convertToT).ToArray(), cancelSource.Token);
output = await streamResponse.ToArrayAsync();
break;
}
}
CollectionAssert.AreEqual(new[] { 1, 2, 3 }, output.Select(convertToInt));
}
finally
{
await server.KillAsync();
}
}
private IService<T> CreateClient<T>(ClientMode mode, ClientFactory factory)
{
GrpcClientFactory.AllowUnencryptedHttp2 = true;
var address = $"http://localhost:10042";
if (mode == ClientMode.Native)
{
var channel = new Channel("localhost",10042, ChannelCredentials.Insecure);
var service = channel.CreateGrpcService<IService<T>>(factory);
return service;
}
else
{
var client = mode == ClientMode.Web ? new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler())) : new HttpClient();
var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions { HttpClient = client, });
return channel.CreateGrpcService<IService<T>>(factory);
}
}
Actually I would expect no issues and no differences in the behavior of the API, if I take over the serialization / deserialization of the parameters / return values. So I wonder what's going on there. Why does the API care about the data structures?
I found another issue: Using generic methods, break the whole service contract, e.g.:
[ServiceContract]
public interface IService<T>
{
IAsyncEnumerable<TLocal> RelayGenericFunction<TLocal>(IAsyncEnumerable<TLocal> data, CancellationToken cancel);
....
}
Error output, even if another method of the contract is invoked:
Message:
System.TypeLoadException : Signature of the body and declaration in a method implementation do not match. Type: 'ProtoBuf.Grpc.Internal.Proxies.ClientBase.IService`1_Proxy_0'. Assembly: 'ProtoBuf.Grpc.Internal.Proxies, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'.
Stack Trace:
TypeBuilder.CreateTypeNoLock()
TypeBuilder.CreateType()
ProxyEmitter.EmitFactory[TService](BinderConfiguration binderConfig) line 295
ProxyEmitter.CreateFactory[TService](BinderConfiguration binderConfig) line 123
ConfiguredClientFactory.SlowCreateClient[TService](CallInvoker channel) line 64
ConfiguredClientFactory.CreateClient[TService](CallInvoker channel) line 75
GrpcIssueTests.CreateClient[T](ClientMode mode, ClientFactory factory) line 247
GrpcIssueTests.DoTest[T](ClientFactory factory, ClientMode mode, TestMethod testMethod, Func`2 convertToT, Func`2 convertToInt) line 172
GrpcIssueTests.Issue_115_Generic_ReferenceType(ClientMode mode, TestMethod testMethod) line 142
GenericAdapter`1.GetResult()
AsyncToSyncAdapter.Await(Func`1 invoke)
TestMethodCommand.RunTestMethod(TestExecutionContext context)
TestMethodCommand.Execute(TestExecutionContext context)
SimpleWorkItem.PerformWork()
Oof; yeah, I haven't even considered generic service methods - since that's not really a "thing" in gRPC. I'd need to think about how that is supportable, if at all. I suspect the answer is either "nope" or "well, we can kinda make it work if you're using protobuf-net.Grpc at both ends, but don't try interop"
I see, hm ... I would simply like to use the duplex communication. But I need generic support at least on the type level. Unfortunately that's the one that is broken if you don't use native - native connections. I can take care about the serialization / deserialization on my own. whats the core type which you wouldnt serialize? byte[]? Maybe I just create services with byte[] parameters and return values and create a wrapper around these kind of core services. Can I set dynamically a name for the service when I register it via AddCodeFirst(...) ?
Yes, you can do anything you like re naming by specifying a BinderConfiguration
at both client and server. If you're using asp.net dependency injection, you can register a singleton configuration and it will get used. Among other things, this allows you to control how names are obtained (the binder), and how serialization works (the marshaller/marshaller factory).
However! It may also be possible for me to add a simpler, more direct API if you want to do everything manually. I'm not at a PC currently, but if you have a scenario in mind that you can show, I can probably tweak it to illustrate how to configure it this way.
Note the names can also be specified on the [Service] or [ServiceContract]. I wonder if we should also make it possible to specify a marshaller via attributes, and then: nothing extra to configure?
My goal was to provide a GRPC based binding between collections. Thats just my first requirement, in the end I want to sync the ViewModel states between 2 different apps (legacy and modern). I want to make it generic, to be reusable. The implementation is already done, but I just tested it for a native client with native server, before. Then I discovered that generics are not really supported by the net core client / web rpc client.
There's an old expression tree based communication framework which I think about to reactive. It requires just a duplex way of sending / receiving bytes, which shouldn't be a problem with the GRPC Api. However, I wish I wouldn't have to use it, because it will increase the complexity on my side and the final communication wouldn't have anything to do with GRPC anymore. On the other hand, it bypasses the problem to implement a full featured proxy generation, because it doesn't require a proxy at all. The invocations are serialized as lambda expressions, based on the service interface. So currently I try to figure out, whats the best way to go. I don't know, if its possible, but did you try to use the castle core framework for proxy generation? I didn't dig so deep into your code, but I seems like you are doing everything by yourself on the IL level.
I read somewhere, that I need to use GRPC web for blazor webassembly. But when I configure my client like that, the communication to the native serrver doesn't work anymore. Is there anything that I need to do on the server side as well? Are async streams supported between those 2 endpoints?