MagicOnion icon indicating copy to clipboard operation
MagicOnion copied to clipboard

Service is unimplemented

Open ComptonAlvaro opened this issue 1 year ago • 12 comments

I am trying to make it work the example to can call the SumAsync().

I have an Asp project using minimal API. This is the code:

using DemoGrpc.Service.Grpc.AspCoreHost;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using ProtoBuf.Grpc.Server;
using System.Security.Cryptography.X509Certificates;

using MagicOnion.Server;
using DemoGrpc.Service.Server.MagicOnion;
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddMagicOnion();

var app = builder.Build();

app.MapMagicOnionService();

app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");

app.Run();

I have a library for the common code for client and server. It has only class, an interface.

using MagicOnion;

namespace DemoGrpc.Service.MagicOnion.Comun
{
    // Defines .NET interface as a Server/Client IDL.
    // The interface is shared between server and client.
    public interface IMyFirstService : IService<IMyFirstService>
    {
        // The return type must be `UnaryResult<T>`.
        UnaryResult<int> SumAsync(int x, int y);
    }
}

I have another library for the server, that implements the interface in the common library:

using MagicOnion.Server;
using MagicOnion;
using DemoGrpc.Service.MagicOnion.Comun;

namespace DemoGrpc.Service.MagicOnion.Server
{
    // Implements RPC service in the server project.
    // The implementation class must inherit `ServiceBase<IMyFirstService>` and `IMyFirstService`
    public class MyFirstServiceServer : ServiceBase<IMyFirstService>, IMyFirstService
    {
        // `UnaryResult<T>` allows the method to be treated as `async` method.
        public async UnaryResult<int> SumAsync(int x, int y)
        {
            Console.WriteLine($"Received:{x}, {y}");
            return x + y;
        }
    }
}

I have a library for the client:

using MagicOnion.Client; using Grpc.Net.Client; using DemoGrpc.Service.MagicOnion.Comun;

namespace DemoGrpc.Service.MagicOnion.Client
{
    public class MyFirstServiceClient
    {
        public async Task<int> SumAsync(int param1, int param2)
        {
            // Connect to the server using gRPC channel.
            var channel = GrpcChannel.ForAddress("http://localhost:5223");

            // NOTE: If your project targets non-.NET Standard 2.1, use `Grpc.Core.Channel` class instead.
            // var channel = new Channel("localhost", 5001, new SslCredentials());

            // Create a proxy to call the server transparently.
            var client = MagicOnionClient.Create<IMyFirstService>(channel);

            // Call the server-side method using the proxy.
            int result = await client.SumAsync(param1, param2);

            return result;
        }
    }
}

And I have a WPF application that uses the client library:

private async void btnSumarConMagicOnion_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                MyFirstServiceClient miCliente = new MyFirstServiceClient();

                int miResultado = await miCliente.SumAsync(1, 2);

                MessageBox.Show($"El resultado de la suma es: {miResultado}");
            }
            catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

When I call the SumAsync() method in the WPF application, in the server log I see a message that says that the interface is unimplemented:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5223
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7223
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\desarrollo\GTS.Cmms.DemoGrpc.Service.Grpc.AspCoreHost\GTS.Cmms.DemoGrpc.Service.Grpc.AspCoreHost\
info: Grpc.AspNetCore.Server.Internal.ServerCallHandlerFactory[1]
      Service 'IMyFirstService' is unimplemented.

But really the interface is implemented, because I can compily it and I can run the Asp application.

Thanks.

ComptonAlvaro avatar Sep 14 '22 10:09 ComptonAlvaro

I have realized that if I put the class that implements the interface server in the Asp pryect with the same namespace it works. But I would like to know if there is some way to can implement the interface in a external library, this could make me more flexibility to can host the server in differents ways, not only Asp, altohugh I know the best way it is in Asp.

But it is also to learn how to do it, if it is possible, because I use to prefer to separate concepts and dont couple the implementation to the Asp project.

Thanks.

ComptonAlvaro avatar Sep 14 '22 10:09 ComptonAlvaro

I have found the problem.

I have to decorate the class with [MessagePackObject] and the properties with [Key(0)], [Key(1)], [Key(3)]...

The I have a quiestion.

If I develop an application following DDD and the gRPC service use a repository to get that from a repository that returns classes from the domain, i can't send this classes to the client if I don't decorate the domain classes as I comment above.

However, in DDD, should decorate the domain classes, so I guess the only option it would be to create gRPC classes to map from the domain classes to this gRPC classes, that are decorate and I can send to the client.

Is this correct? Or there is some way to can send the domain classes directly but without the needed to decorate them or modify them?

Thanks.

ComptonAlvaro avatar Sep 14 '22 14:09 ComptonAlvaro

I am not familiar with DDD, but I would expect that it would be undesirable for implementation details to appear in the interface with external services.

For example, if the repository implementation is one that uses an ORM, is the row object a domain object? Do you send it as is to the client?

If you want to handle this issue methodically, you may need to re-map the objects, otherwise it would be better to handle it as is.

mayuki avatar Sep 15 '22 06:09 mayuki

Then perhaps in my case the best option it is remap the domain objects to DTOs that can be serialize and transfer to the client.

Here I have a doubt. When an object is serialize, it is convert first to protobuf messages? I mean, if I convert my domain class to a DTO and this DTO is serialize with MagicOnion, this DTO is convert to protobuf messages or is send directly?

Which does it offer better performance? To convert my domain class to protobuf and send it or convert my domain class to DTO and transfer it with MagicOnion? is there some performance documentation about it?

Thanks so much.

ComptonAlvaro avatar Sep 15 '22 07:09 ComptonAlvaro

Basically, MagicOnion simply serializes objects with MessagePack (instead of Protobuf) and sends/receives them as gRPC messages.

If you remap them, you need to pay the cost, but that is the responsibility of the application. In other words, performance would be better if the object is serialized as is.

mayuki avatar Sep 15 '22 07:09 mayuki

Thanks so much.

And is it planned to can set the classes and properties to serialize in an external way outside the class? This could give more flexibilty because it is not needed to modify the classes and the coupling it would be less.

For example something similiar what EF Core does to map a class, it use fluent API in an external file to map the classes, so it is not needed to modify the original classes.

Thanks so much.

ComptonAlvaro avatar Sep 15 '22 08:09 ComptonAlvaro

There are no plans to do so. However, MagicOnion is built on top of MessagePack serialization mechanism, it is possible to use the extension points provided in MessagePack.

https://github.com/neuecc/MessagePack-CSharp

For example, it is possible to implement a custom resolver, or use a dynamic resolver that does not require contracts.

mayuki avatar Sep 15 '22 08:09 mayuki

I was reading the documenation but I am not sure.

Supose I have this class:

class MyClass
{
    long Mid;
    string Name;
    string Description;
    List<MyClass2> ListOfItems;
}

With a custom reslver, could I config the way how to serialize the class without modify this class?

Which is the difference of custom resolver and dynamic resolver?

ComptonAlvaro avatar Sep 15 '22 09:09 ComptonAlvaro

Try using ContractlessStandardResolver. https://github.com/neuecc/MessagePack-CSharp#object-serialization

mayuki avatar Sep 15 '22 09:09 mayuki

It seems it is an option that I was looking for. But I have the doubt to use it.

In my server, I have this method that implements the interfaz:

public class MyFirstServiceServer : ServiceBase<IMyFirstService>, IMyFirstService
{
    public async UnaryResult<MyClass> GetMyClassAsync()
    {
        return new MyClass();
    }
}

Of course it throw an exception because I don't have decorate the class with the attributes. In the example that you give me, it shows it is possible to use a class with no decoration, the code is this:

var data = new ContractlessSample { MyProperty1 = 99, MyProperty2 = 9999 };
MessagePackSerializer.DefaultOptions = MessagePack.Resolvers.ContractlessStandardResolver.Options;

// Now serializable...
var bin2 = MessagePackSerializer.Serialize(data);

So in my case I guess I have to do:

var data = new MyClass();
MessagePackSerializer.DefaultOptions = MessagePack.Resolvers.ContractlessStandardResolver.Options;

// Now serializable...
var bin2 = MessagePackSerializer.Serialize(data);

But where I have to put this code? In my method GetMyClassAsync()? But I don't know what to do with the bin serialized data. I was thinking simething like that, but I can't return bin2. I have to return MyClass.

public class MyFirstServiceServer : ServiceBase<IMyFirstService>, IMyFirstService
{
    public async UnaryResult<MyClass> GetMyClassAsync()
    {

        var data = new MyClass();
        MessagePackSerializer.DefaultOptions = MessagePack.Resolvers.ContractlessStandardResolver.Options;
        var bin2 = MessagePackSerializer.Serialize(data);
        return bin2;
    }
}

Thanks.

ComptonAlvaro avatar Sep 15 '22 09:09 ComptonAlvaro

MessagePackSerializer.DefaultOptions is a singleton global application-wide option. It should be set at application initialization (e.g., entry point).

mayuki avatar Sep 15 '22 10:09 mayuki

Setting this: "MessagePackSerializer.DefaultOptions = MessagePack.Resolvers.ContractlessStandardResolver.Options;" in the program.cs file of the ASP Core project it works.

But when I call the method, the client gets the following error: "GTS.Cmms.DemoGrpc.Service.MagicOnion.Comun.ClaseTest2 is not registered in resolver: MessagePack.Resolvers.StandardResolver".

How should I have to register the class in the resolver?

EDIT: I could solve it setting in the client library, in the constructor, the same than in the server:

MessagePackSerializer.DefaultOptions = MessagePack.Resolvers.ContractlessStandardResolver.Options;

ComptonAlvaro avatar Sep 15 '22 11:09 ComptonAlvaro

This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 7 days.

github-actions[bot] avatar Dec 15 '22 00:12 github-actions[bot]