Suleyman OZ
Suleyman OZ

Reputation: 170

Protobuf-net.Grpc Service Contract Inheritance

I am upgrading an application from .Net FW to .Net Core. Also upgrading WCF services to gRPC services. We have decided to use protobuf-net.Grpc.

We use multiple levels of inheritance for service contracts.

[ServiceContract]
public interface IBaseServiceContract<TDataModel> where TDataModel : DataModelObject
{
}

[ServiceContract]
public interface ICRUDLServiceContract<TDataModel> : IBaseServiceContract<TDataModel> where TDataModel : DataModelObject
{
    Task<Response<TDataModel>> Create(Request<TDataModel> request);
    Task<Response<TDataModel>> Read(Request<TDataModel> request);
    Task<VoidResponse> Update(Request<TDataModel> request);
    Task<VoidResponse> Delete(Request<TDataModel> request);
    Task<Response<DataResult<TDataModel>>> List(Request<DataSourceQuery> request);
    Task<Response<IEnumerable<TDataModel>>> ListAll();
}

A simple service contract looks like:

[ServiceContract]
public interface IProductService : ICRUDLServiceContract<Product>
{
}

In our controller tier, which uses service tier, we have generic controllers that comsume these service contracts by the base generic interface:

[ApiController]
public class CRUDLController<TDataModel, TServiceContract, TViewModel> : CRUDController<TDataModel, TServiceContract, TViewModel>
    where TDataModel : DataModelObject, new()
    where TServiceContract : ICRUDLServiceContract<TDataModel>
    where TViewModel : ViewModelObject, new()
{

    public CRUDLController(ILogger<BaseController<TDataModel, TServiceContract, TViewModel>> logger, TServiceContract dataService, IMapper mapper)
        : base(logger, dataService, mapper)
    {
    }

    [HttpGet]
    [Route("ListAll")]
    public virtual async Task<ActionResult<IEnumerable<TViewModel>>> ListAll()
    {
        try
        {
            var response = await _dataService.ListAll();
            return _mapper.Map<IEnumerable<TViewModel>>(response.Result).ToList();
        }
        catch (Exception ex)
        {
            throw CreateUserException(ex, MethodBase.GetCurrentMethod().Name);
        }
    }

    [HttpGet]
    [ActionName("List")]
    public virtual async Task<ActionResult<DataSourceResult>> List([ModelBinder(typeof(DataSourceQueryModelBinder))] DataSourceQuery query)
    {
        try
        {
            var response = await _dataService.List(query.AsRequest());
            return response.MapToDataSourceResult<TDataModel, TViewModel>(_mapper);
        }
        catch (Exception ex)
        {
            throw CreateUserException(ex, MethodBase.GetCurrentMethod().Name);
        }
    }

        ...
}

In Startup.cs of the service application we map all services as:

...
endpoints.MapGrpcService<ProductService>();
endpoints.MapGrpcService<TitleService>();
...

When I start service application I see error on console log:

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:7001/OverBase.Core.Contract.CRUDLServiceContract`1/List application/grpc -
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches:

gRPC - /OverBase.Core.Contract.CRUDLServiceContract`1/List
gRPC - /OverBase.Core.Contract.CRUDLServiceContract`1/List

I've tried removing [ServiceContract] from base interfaces then

warn: Grpc.AspNetCore.Server.Model.Internal.ServiceRouteBuilder[3]
No gRPC methods discovered for OverBase.Services.Program.ProductService.

methods from base interface have disappeared from the service.

Is there a way to use methods from base interfaces in protobuf-net.Grpc?

Upvotes: 4

Views: 2130

Answers (1)

Marc Gravell
Marc Gravell

Reputation: 1062955

This would be a great bug to report on github. The problem is that the contract binder does not unroll generics, so you end up with multiple services on the same names, with the .NET generic placeholder (but different APIs internally):

IFoo`1/Method
IFoo`1/Method

Long term, we should fix the library. It isn't easy to use the attribute to fix this, because attributes aren't per-T, but: we can write our own binder:

    // note: this could also simply recognize a few known interfaces
    class MyServiceBinder : ServiceBinder
    {
        protected override string GetDefaultName(Type contractType)
        {
            var val = base.GetDefaultName(contractType);
            if (val.EndsWith("`1") && contractType.IsGenericType)
            {   // replace IFoo`1 with IFoo`TheThing
                var args = contractType.GetGenericArguments();
                if (args.Length == 1)
                {
                    val = val.Substring(0, val.Length - 1) + args[0].Name;
                }
            }
            return val;
        }
    }

For ASP.NET, we register that with DI:

services.AddSingleton(BinderConfiguration.Create(binder: new MyServiceBinder()));

For the client, you would pass it into the client creator:

static readonly ClientFactory s_ClientFactory = ClientFactory.Create(
    BinderConfiguration.Create(binder: new MyServiceBinder()));
// ...
var calculator = http.CreateGrpcService<IWhatever>(s_ClientFactory);

The result of this is that we bind to:

IFoo`X/Method
IFoo`Y/Method

Obviously feel free to suggest different patterns instead! gRPC doesn't care much what they are.

Upvotes: 3

Related Questions