Is it possible to generate nullable members in protobuf-net?
message ProtoBuf1 {
optional Int32? databit = 1;
optional Nullable<bool> databool = 2;
}
Is it possible to generate nullable members in protobuf-net?
message ProtoBuf1 {
optional Int32? databit = 1;
optional Nullable<bool> databool = 2;
}
Yes, but it doesn't generate them by default if you are doing codegen from .proto.
If this is just C#, of course, you don't need a .proto - just:
[ProtoContract]
public class ProgoBuf1
{
[ProtoMember(1)]
public int? Foo {get;set;}
[ProtoMember(2)]
public float? Bar {get;set;}
}
If you are working from .proto, you could consider copying and editing csharp.xslt
to suit your preferred layout.
Here's my solution for nullable types when using Google's Protobuf .NET API which requires Protocol Buffers version 3. (Note that this is not using Marc Gravell's protobuf-net, so this isn't an exact answer to the question asked.)
In NullableInt32.proto
:
syntax = "proto3";
message NullableInt32 {
int32 value = 1;
}
In NullableInt32Extensions.cs
:
public static class NullableInt32Extensions
{
public static bool HasValue(this NullableInt32 source)
{
return source != null;
}
}
public partial class NullableInt32
{
public static implicit operator int? (NullableInt32 other)
{
return other == null ? (int?)null : other.Value;
}
public static implicit operator NullableInt32(int? other)
{
return other == null ? null : new NullableInt32 { Value = other.Value };
}
}
This pattern can be used for any of the Protobuf non-length delimited scalar values—double
, float
, int32
, int64
, uint32
, uint64
, sint32
, sint64
, fixed32
, fixed64
, sfixed32
, sfixed64
, and bool
.
Here's how all of this works. Say you have a Record
message that has a NullableInt32
field, and in this contrived example it's the only field.
In Record.proto
:
syntax = "proto3";
import "NullableInt32.proto";
message Record {
NullableInt32 id = 1;
}
Once this is compiled to C# with Google's protoc.exe
, you can treat the Id
property almost exactly like a Nullable<int>
.
var r = new Record();
// r.Id is null by default, but we can still call HasValue()
// because extension methods work on null references.
r.Id.HasValue(); // => false
// We can explicitly set Id to null.
r.Id = null;
// We can set Id to a primitive numeric value directly
// thanks to our implicit conversion operators.
r.Id = 1;
// We can also use NullableInt32 in any context that expects a
// Nullable<int>. The signature of the following method is
// bool Equals(int?, int?).
Nullable.Equals<int>(r.Id, 1); // => true
// We can explicitly set Id to a NullableInt32.
r.Id = new NullableInt32 { Value = 1 };
// Just like Nullable<int>, we can get or set the Value of a
// NullableInt32 directly, but only if it's not null. Otherwise,
// we'll get a NullReferenceException. Use HasValue() to avoid this.
if(r.Id.HasValue())
r.Id.Value.ToString(); // => "1"
// Setting Id to 0 is the same as setting Id to a new
// NullableInt32 since the default value of int32 is 0.
// The following expressions are equivalent.
r.Id = 0;
r.Id = new NullableInt32();
r.Id = new NullableInt32 { Value = 0 };
r.Id.Value = 0; // as long as Id is not null
Finally, let's look at how our Record
message will be transfered over the wire with different values for Id
.
var r = new Record();
// When Id is null, Record is empty since it has no other fields.
// Explicitly setting Id to null will have the same effect as
// never setting it at all.
r.Id = null;
r.ToByteArray(); // => byte[0]
// Since NullableInt32 is a Protobuf message, it's encoded as a
// length delimited type. Setting Id to 1 will yield four bytes.
// The first two indicate the type and length of the NullableInt32
// message, and the last two indicate the type and value held within.
r.Id = 1;
r.ToByteArray(); // => byte[] {
// 0x0a, // field = 1, type = 2 (length delimited)
// 0x02, // length = 2
// 0x08, // field = 1, type = 0 (varint)
// 0x01, // value = 1
// }
// When Id is set to the default int32 value of 0, only two bytes
// are needed since default values are not sent over the wire.
// These two bytes just indicate that an empty NullableInt32 exists.
r.Id = 0;
r.ToByteArray(); // => byte[] {
// 0x0a, // field = 1, type = 2 (length delimited)
// 0x00, // length = 0
// }