Reputation: 2680
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:
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.
Upvotes: 3
Views: 502
Reputation: 2680
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.
Upvotes: 2