2

I'm currently migrating a bunch of JSON Apis to gRPC. Once I call one of my service methods, I'm getting a "Specified method is not supported" exception with no further details. I suspect it might be something related to the nested generics, but I can't tell from which one therefore I have detailed them down below. Any help or guidance is greatly appreciated.

All operations have a common format defined as follows:

/// <summary>
/// A common class for results returned by the services. No data is returned here.
/// </summary>
[ProtoContract]
public class GrpcServiceResult
{
    [ProtoMember(1)]
    public bool Success { get; set; }

    [ProtoMember(2)]
    public string Message { get; set; } = null!;

    public GrpcServiceResult() { }

    public GrpcServiceResult(bool success, string message = "")
    {
        Success = success;
        Message = message;
    }
}

I originally had the class below inherit from the GrpcServiceResult above but I removed it since I suspected I might need to add ProtoInclude to it above, but the T is unknown therefore I can't do that:

/// <summary>
/// A common class for results returned by the services.
/// </summary>
/// <typeparam name="T">The <see cref="Data"/> type.</typeparam>
[ProtoContract]
public class GrpcServiceResult<T> where T : class
{
    [ProtoMember(1)]
    public bool Success { get; set; }

    [ProtoMember(2)]
    public string Message { get; set; } = null!;

    [ProtoMember(3)]
    public T Data { get; set; } = null!;

    public GrpcServiceResult() { }

    public GrpcServiceResult(bool success, T result = null!, string message = "")
    {
        Success = success;
        Message = message;
        Data = result;
    }
}

In addition, some of the services return data that are paginated; thus, I defined another generic type to encapsulate the data:

[ProtoContract]
public class PagedResult<T> where T : class
{
    [ProtoMember(1)]
    public int CurrentPage { get; set; }

    [ProtoMember(2)]
    public int PageCount { get; set; }

    [ProtoMember(3)]
    public int PageSize { get; set; }

    [ProtoMember(4)]
    public int RowCount { get; set; }

    [ProtoIgnore]
    public int FirstRowOnPage
    {
        get { return (CurrentPage - 1) * PageSize + 1; }
    }

    [ProtoIgnore]
    public int LastRowOnPage
    {
        get { return Math.Min(CurrentPage * PageSize, RowCount); }
    }

    [ProtoMember(5)]
    public IList<T> Results { get; set; }

    public PagedResult()
    {
        Results = new List<T>();
    }
}

Finally, I have the following service with the following operation:

[ServiceContract]
public interface IDataReadService
{
    // ...

    /// <summary>
    /// Get the data entry item structure.
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    [OperationContract]
    Task<GrpcServiceResult<PagedResult<SystemItemsResponseContract>>> GetSystemItems(SystemItemsRequestContract contract);
}

Regarding the request and response, they are defined as follows:

[ProtoContract]
public class SystemItemsRequestContract
{
    [ProtoMember(1)]
    public string SearchPattern { get; set; } = null!;

    [ProtoMember(2)]
    public int PartnerId { get; set; }

    [ProtoMember(3)]
    public int SearchField { get; set; }

    [ProtoMember(4)]
    public int Page { get; set; }

    [ProtoMember(5)]
    public int Count { get; set; }
}

And for the response (some fields with immutable types are removed for brevity):

[ProtoContract]
public class SystemItemsResponseContract : ItemInfoSyncResponseContract
{
    [ProtoMember(1)]
    public int Id { get; set; }

    // I have built a custom surrogate for this one, and even if I remove it, I'm getting the same error
    [ProtoMember(2)]
    public DateTimeOffset? InsertedOn { get; set; }

    //...

    [ProtoMember(7)]
    public ESellingType SoldAs { get; set; }

    //...

    // List of the same class
    [ProtoMember(10)]
    public List<SystemItemsResponseContract> Packs { get; set; }
}

[ProtoContract]
[ProtoInclude(1000, typeof(SystemItemsResponseContract))]
public class ItemInfoSyncResponseContract
{
    //...

    [ProtoMember(2)]
    public List<ItemBarcode> Barcodes { get; set; } = null!;


    //...

    [ProtoMember(6)]
    public List<string> Images { get; set; } = null!;

    //... other properties are solely string, double, double?, int

}


[ProtoContract]
public class ItemBarcode
{
    [ProtoMember(1)]
    public string Barcode { get; set; } = null!;

    [ProtoMember(2)]
    public string Check { get; set; } = null!;
}

And this is the custom surrogate for the DateTimeOffset:

[ProtoContract]
public class DateTimeOffsetSurrogate
{
    [ProtoMember(1)]
    public long DateTimeTicks { get; set; }
    [ProtoMember(2)]
    public short OffsetMinutes { get; set; }

    public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset value)
    {
        return new DateTimeOffsetSurrogate
        {
            DateTimeTicks = value.Ticks,
            OffsetMinutes = (short)value.Offset.TotalMinutes
        };
    }

    public static implicit operator DateTimeOffset(DateTimeOffsetSurrogate value)
    {
        return new DateTimeOffset(value.DateTimeTicks, TimeSpan.FromMinutes(value.OffsetMinutes));
    }
}

Sorry for the lengthy blocks of code but I'm currently not finding the issue. I'm running on these versions: Protobuf-net versions

As for the call stack of the error:

   at ProtoBuf.Grpc.Internal.Proxies.ClientBase.IDataReadService_Proxy_1.IDataReadService.GetSystemItems(SystemItemsRequestContract )

(No further information, the error appears to be thrown on the service call itself on the client side - not the server side).


Update If I remove the parameter from the GetSystemItems function and use the following JSON surrogate to serialize the response (I'm communicating between 2 dotnet microservices), it will work:

[ProtoContract]
public class JsonSerializationSurrogate<T> where T : class
{
    [ProtoMember(1)]
    public byte[] ObjectData { get; set; } = null!;

    [return: MaybeNull]
    public static implicit operator T(JsonSerializationSurrogate<T> surrogate)
    {
        if (surrogate == null || surrogate.ObjectData == null) return null;

        var encoding = new UTF8Encoding();
        var json = encoding.GetString(surrogate.ObjectData);
        return JsonSerializer.Deserialize<T>(json);
    }

    [return: MaybeNull]
    public static implicit operator JsonSerializationSurrogate<T>(T obj)
    {
        if (obj == null) return null;

        var encoding = new UTF8Encoding(); 
        var bytes = encoding.GetBytes(JsonSerializer.Serialize(obj));
        return new JsonSerializationSurrogate<T> { ObjectData = bytes };
    }
}

Even if it does work for the response, I'd like to know what can be done to improve this since I'm sure this surrogate kind of defeats the purpose of gRPC.

Ziad Akiki
  • 2,601
  • 2
  • 26
  • 41
  • What is the call stack for the exception? What line does it occur on? – Ron Beyer Jan 15 '23 at 15:04
  • @RonBeyer added. Thank you for mentioning that. – Ziad Akiki Jan 15 '23 at 15:08
  • I tried to reproduce your problem in a standalone fiddle here: https://dotnetfiddle.net/WjHiRP. I found two problems. 1) You must use a surrogate for `DateTimeOffset` as shown in [this answer](https://stackoverflow.com/a/7046868) by Marc Gravell to [Can I serialize arbitrary types with protobuf-net?](https://stackoverflow.com/q/7046506). 2) You must have a parameterless constructor for `GrpcServiceResult`. It can be private. See https://dotnetfiddle.net/5TFK95. Does that fix your problem? If not, any chance you could fork my fiddle and reproduce it standalone? – dbc Jan 15 '23 at 21:12
  • Regarding pt 1, yes I do have it and I mentioned in the comment. Regarding point 2, will give it a go now – Ziad Akiki Jan 15 '23 at 21:16
  • @ZiadAkiki - I saw the comment but, since you didn't include it in your question, that was the first failure I encountered trying to reproduce your problem. – dbc Jan 15 '23 at 21:18
  • @dbc you're right will add that shortly. Regarding the parameterless constructor suggestion, it did not work. Editing my question to include the surrogate. – Ziad Akiki Jan 15 '23 at 21:41
  • @dbc added the missing code. – Ziad Akiki Jan 15 '23 at 21:46

1 Answers1

2

The issue turned out from neither of those but rather from the way I initialized my client. Since I needed to add the surrogate, I created a new RuntimeTypeModel and a ClientFactory and passed those to the GRPC clients:

var runtimeTypeModel = RuntimeTypeModel.Create();
runtimeTypeModel.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate));

var binderConfig = BinderConfiguration.Create(new List<MarshallerFactory> {
      ProtoBufMarshallerFactory.Create(runtimeTypeModel.Compile())
});
clientFactory = ClientFactory.Create(binderConfig);
services.AddSingleton(clientFactory);

var dataEntryChannel = GrpcChannel.ForAddress(GlobalSettings.Services.DataEntryEndpoint);
services.AddSingleton(_ => dataEntryChannel.CreateGrpcService<IDataReadService>(clientFactory));

I reused the default RuntimeTypeModel as follows:

RuntimeTypeModel.Default.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate));

var dataEntryChannel = GrpcChannel.ForAddress(GlobalSettings.Services.DataEntryEndpoint);
services.AddSingleton(_ => dataEntryChannel.CreateGrpcService<IDataReadService>());

You might ask why I did the first approach in the first place. I ran into issues previously that I though were from the RuntimeTypeModel not being passed on the GrpcChannel since some of my surrogates were not detected. Somehow it now works.

Ziad Akiki
  • 2,601
  • 2
  • 26
  • 41