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>();
You have three problems with your serialization code:
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.
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
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.