Search code examples
c#grpcprotobuf-netgrpc-dotnetprotobuf-net.grpc

specified method is not supported error on calling grpc service using protobuf-net library


My team uses Google grpc communication for micro service communication. I came across protobuf-net that is fast, reduces code complexity and no .proto file to be defined. I wanted to give a try using protobuf-net to see if we gain considerable performance improvement. However, I am getting error "specified method is not supported". I think I am not able to mark the entity correctly. I can use @marc-gravel help to understand the problem. Here are the details of my dotnet code

[ProtoContract]
public class ProtoBufInput
{
    [ProtoMember(1)]
    public string Id { get; set; }

    [ProtoMember(2)]
    public Building BuildingObj { get; set; }

    [ProtoMember(3)]
    public byte[] Payload { get; set; }

    public ProtoBufInput(string id, Building buildingObj, byte[] payload)
    {
        BuildingObj = buildingObj;
        Id = id;
        Payload = payload;
    }
}

[ProtoContract]
public class ProtoBufResult
{
    [ProtoMember(1)]
    public int RandomNumber { get; set; }

    [ProtoMember(2)]
    public bool RandomBool { get; set; }

    [ProtoMember(3)]
    public IList<string> ErrorMessages { get; set; }

    [ProtoMember(5)]
    public Building BuildingObj { get; set; }

    [ProtoMember(6)]
    public string RandomString { get; set; }

    public ProtoBufResult()
    {
        RandomNumber = 0;
        RandomBool = false;
    }
}

[ProtoContract]
public class Building : Component<BuildingMetadata>
{
    [ProtoMember(1)]
    public string Id { get; set; }

    [ProtoMember(2)]
    public string tag { get; set; }
} 

[ProtoContract]
public class BuildingMetadata : ComponentMetadata
{
    [ProtoMember(1)]
    public BuildingType Type { get; set; }

    [ProtoMember(2)]
    public bool IsAttached { get; set; }

    public override object Clone()
    {
        var baseClone = base.Clone() as ComponentMetadata;
        return new BuildingMetadata()
        {
            Model = baseClone.Model,
            PropertyMetadata = baseClone.PropertyMetadata,
        };
    }
}

[ProtoContract]
public enum BuildingType
{
}

[ProtoContract]
public class ComponentMetadata : ICloneable
{
    [ProtoMember(1)]
    public string Model { get; set; }

    [ProtoMember(2)]
    public IDictionary<string, PropertyMetadata> PropertyMetadata { get; set; } = new Dictionary<string, PropertyMetadata>();

    public virtual object Clone()
    {
        return new ComponentMetadata()
        {
            Model = Model,
            PropertyMetadata = PropertyMetadata.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone() as PropertyMetadata),
        };
    }
}

[ProtoContract]
public class PropertyMetadata : ICloneable
{
    [ProtoMember(1)]
    [JsonProperty("Value")]
    public JToken Value { get; set; }

    [ProtoMember(2)]
    [JsonProperty("Version")]
    public int Version { get; set; }

    [ProtoMember(3)]
    [JsonProperty("backVersion")]
    public int BackVersion { get; set; }

    [ProtoMember(4)]
    [JsonProperty("backCode")]
    public int BackCode { get; set; }

    [ProtoMember(5)]
    [JsonProperty("Description")]
    public string Description { get; set; }

    [ProtoMember(6)]
    [JsonProperty("createTime")]
    public string CreateTime { get; set; }

    public object Clone()
    {
        return new PropertyMetadata()
        {
            CreateTime = CreateTime ?? DateTime.UtcNow.ToString("o"),
        };
    }
}

[ProtoContract]
[ProtoInclude(1, typeof(Component<ComponentMetadata>))]
public class Component<TMetadataType> : ComponentBase, IComponent where TMetadataType : ComponentMetadata, new()
{
    [ProtoMember(1)]
    public TMetadataType Metadata { get; set; } = new TMetadataType();

    public string Model => Metadata.Model;

    public IEnumerable<(string, IComponent)> ListComponents() => Components.Select(x => (x.Key, x.Value as IComponent));

    public IEnumerable<(string, JToken)> ListProperties() => Properties.Select(x => (x.Key, x.Value));

    public ComponentMetadata GetMetadata() => Metadata;

    public bool TryGetComponent(string name, out IComponent component)
    {
        component = null;
        if (!Components.TryGetValue(name, out var innerComponent))
        {
            return false;
        }

        component = innerComponent as IComponent;
        return true;
    }

    public bool TryGetProperty(string name, out JToken property) => Properties.TryGetValue(name, out property);
}

[ProtoContract]
public class ComponentBase
{
    [ProtoMember(1)]
    public IDictionary<string, JToken> Properties { get; set; } = new Dictionary<string, JToken>();

    [ProtoMember(2)]
    public IDictionary<string, InnerComponent> Components { get; set; } = new Dictionary<string, InnerComponent>();
}

[ProtoContract]
public class InnerComponent : Component<ComponentMetadata>
{
    [ProtoMember(1)]
    [JsonIgnore]
    public string tag { get; set; }
}

Now coming to the service class and its implementation, I have something like this

[ServiceContract]
public interface IProtoBufService
{
    [OperationContract]
    public Task<ProtoBufResult> ProcessPb(ProtoBufInput input, CallContext context = default);
}

public class ProtoBufService : IProtoBufService
{
    public Task<ProtoBufResult> ProcessPb(ProtoBufInput protoBufInput, CallContext context)
    {
       ...
    }
}

Rest of the configuration in start up file is correct like adding

serviceCollection.AddCodeFirstGrpc();
builder.MapGrpcService<Services.V2.ProtoBufService>();

Solution

  • You have three problems with your serialization code:

    1. As noted by Marc Gravell, Protobuf-net does not know how to serialize Json.NET's JToken objects.

      Since JToken objects are intended to represent free-form JSON, the easiest way to serialize them with Protobuf-net is to serialize surrogate string properties instead that represent the raw JSON value:

      [ProtoContract]
      public class PropertyMetadata : ICloneable
      {
          [ProtoMember(1)]
          string SerializedValue { get => Value?.ToString(Formatting.None); set => Value = (value == null ? null : JToken.Parse(value)); } // FIXED
      

      and

      public class ComponentBase
      {
          [ProtoMember(1)]
          string SerializedProperties { get => Properties == null ? null : JsonConvert.SerializeObject(Properties); set => Properties = (value == null ? null : JsonConvert.DeserializeObject<Dictionary<string, JToken>>(value)); }
      

      Note I am serializing the entire IDictionary<string, JToken> Properties object as a single JSON object.

    2. When serializing an inheritance hierarchy, Protobuf-net requires that every base class TBase be informed of the existence of all immediate derived classes TDerived. This can be done via attributes by adding

      [ProtoContract]
      [ProtoInclude(N, typeof(TDerived))] 
      public class TBase { }
      

      to the base class. Note that the numbers N must be unique and not overlap with any ProtoMemberAttribute.Tag values so it is wise to start them from a large number such as 1000:

      [ProtoContract]
      [ProtoInclude(1001, typeof(BuildingMetadata))] 
      public class ComponentMetadata : ICloneable
      
      [ProtoContract]
      [ProtoInclude(1002, typeof(Building))] 
      [ProtoInclude(1001, typeof(InnerComponent))] 
      public class Component<TMetadataType> : ComponentBase, IComponent where TMetadataType : ComponentMetadata, new()
      
      [ProtoContract]
      [ProtoInclude(1002, typeof(Component<BuildingMetadata>))] 
      [ProtoInclude(1001, typeof(Component<ComponentMetadata>))] 
      public class ComponentBase
      
    3. In your demo fiddle, your class Component<TMetadataType> has a get-only property Model which you are serializing:

      [ProtoMember(2)]
      public string Model => Metadata.Model;
      

      With the other two problems fixed, for some reason this property causes the serializer to throw the following exception:

      System.InvalidOperationException: Unable to wrap ComponentBase/ComponentBase: Unable to bind serializer: It was not possible to prepare a serializer for: ComponentBase (ProtoBuf.Internal.Serializers.InheritanceTypeSerializer`2[ComponentBase,ComponentBase])
      

      This can be resolved by either removing Model from serialization, or adding a private dummy setter like so:

      [ProtoMember(2)]
      public string Model { get => Metadata.Model; private set { } } // Private set required for serialization
      

    Complete modified classes here:

    [ProtoContract]
    public class Building : Component<BuildingMetadata>
    {
        [ProtoMember(1)]
        public string Id { get; set; }
    
        [ProtoMember(2)]
        public string tag { get; set; }
    }
    
    [ProtoContract]
    public class InnerComponent : Component<ComponentMetadata>
    {
        [ProtoMember(1)]
        [JsonIgnore]
        public string tag { get; set; }
    }
    
    [ProtoContract]
    public class BuildingMetadata : ComponentMetadata
    {
        [ProtoMember(1)]
        public BuildingType Type { get; set; }
    
        [ProtoMember(2)]
        public bool IsAttached { get; set; }
    
        public override object Clone()
        {
            var baseClone = base.Clone() as ComponentMetadata;
            return new BuildingMetadata()
            {
                Model = baseClone.Model,
                PropertyMetadata = baseClone.PropertyMetadata,
            };
        }
    }
    
    [ProtoContract]
    public enum BuildingType
    {
    }
    
    [ProtoContract]
    [ProtoInclude(1001, typeof(BuildingMetadata))] 
    public class ComponentMetadata : ICloneable
    {
        [ProtoMember(1)]
        public string Model { get; set; }
    
        [ProtoMember(2)]
        public IDictionary<string, PropertyMetadata> PropertyMetadata { get; set; } = new Dictionary<string, PropertyMetadata>();
    
        public virtual object Clone()
        {
            return new ComponentMetadata()
            {
                Model = Model,
                PropertyMetadata = PropertyMetadata.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone() as PropertyMetadata),
            };
        }
    }
    
    [ProtoContract]
    public class PropertyMetadata : ICloneable
    {
        [ProtoMember(1)]
        string SerializedValue { get => Value?.ToString(Formatting.None); set => Value = (value == null ? null : JToken.Parse(value)); } // FIXED
    
        [JsonProperty("Value")]
        public JToken Value { get; set; }
    
        [ProtoMember(2)]
        [JsonProperty("Version")]
        public int Version { get; set; }
    
        [ProtoMember(3)]
        [JsonProperty("backVersion")]
        public int BackVersion { get; set; }
    
        [ProtoMember(4)]
        [JsonProperty("backCode")]
        public int BackCode { get; set; }
    
        [ProtoMember(5)]
        [JsonProperty("Description")]
        public string Description { get; set; }
    
        [ProtoMember(6)]
        [JsonProperty("createTime")]
        public string CreateTime { get; set; }
    
        public object Clone()
        {
            return new PropertyMetadata()
            {
                CreateTime = CreateTime ?? DateTime.UtcNow.ToString("o"),
            };
        }
    }
    
    [ProtoContract]
    public interface IComponent
    {
        ComponentMetadata GetMetadata();
    
        IEnumerable<(string name, IComponent component)> ListComponents();
    
        IEnumerable<(string name, JToken property)> ListProperties();
    
        bool TryGetProperty(string name, out JToken property);
    
        bool TryGetComponent(string name, out IComponent component);
    }
    
    [ProtoContract]
    [ProtoInclude(1002, typeof(Building))] 
    [ProtoInclude(1001, typeof(InnerComponent))] 
    public class Component<TMetadataType> : ComponentBase, IComponent where TMetadataType : ComponentMetadata, new()
    {
        [ProtoMember(1)]
        public TMetadataType Metadata { get; set; } = new TMetadataType();
    
        [ProtoMember(2)]
        public string Model { get => Metadata.Model; private set { } } // Private set required for serialization
    
        public IEnumerable<(string, IComponent)> ListComponents() => Components.Select(x => (x.Key, x.Value as IComponent));
    
        public IEnumerable<(string, JToken)> ListProperties() => Properties.Select(x => (x.Key, x.Value));
    
        public ComponentMetadata GetMetadata() => Metadata;
    
        public bool TryGetComponent(string name, out IComponent component)
        {
            component = null;
            if (!Components.TryGetValue(name, out var innerComponent))
            {
                return false;
            }
    
            component = innerComponent as IComponent;
            return true;
        }
    
        public bool TryGetProperty(string name, out JToken property) => Properties.TryGetValue(name, out property);
    }
    
    [ProtoContract]
    [ProtoInclude(1002, typeof(Component<BuildingMetadata>))] 
    [ProtoInclude(1001, typeof(Component<ComponentMetadata>))] 
    public class ComponentBase
    {
        [ProtoMember(1)]
        string SerializedProperties { get => Properties == null ? null : JsonConvert.SerializeObject(Properties); set => Properties = (value == null ? null : JsonConvert.DeserializeObject<Dictionary<string, JToken>>(value)); }
    
        public IDictionary<string, JToken> Properties { get; set; } = new Dictionary<string, JToken>();
    
        [ProtoMember(2)]
        public IDictionary<string, InnerComponent> Components { get; set; } = new Dictionary<string, InnerComponent>();
    }
    

    Fixed working fiddle here.