To define a message class, write its constructor in C# syntax:
CreateStuffCommand(string name);
StuffCreated(string name);
Whitespace characters are mostly not significant, so you can write a single message definition on several lines:
Foo(
int a,
int b,
int c
);
Semicolons are optional at the end of a line.
The ProtoBuf members and their tags will be deduced from the constructor (see below for details). Member names will be PascalCased.
As a rule of thumb, when in doubt about how to write something, try the C# syntax.
C# syntax is used for comments:
// This is a line comment
/* This is an inline comment */
You can override the default namespace with a namespace
clause:
namespace Foo.Bar;
SomeMessage(int a);
Note that this clause is not followed by braces, and takes effect for the whole file - you can't define multiple namespaces in the same file.
Write a using
directive to import a namespace:
using SomeOtherLibrary;
The following namespaces are imported by default:
System
ProtoBuf
Abc.Zebus
Abc.Zebus.Routing
if a routable message is definedSystem.Collections.Generic
ifList<T>
is usedSystem.ComponentModel
if the[Description]
attribute is used
If the message name ends with Command
, it will implement ICommand
, otherwise it will implement IEvent
. This can be overridden with the following syntax:
DoStuff(int id) : ICommand;
Additional base types can be specified using the same syntax.
Add a !
suffix to a message type in order to implement IMessage
instead of either ICommand
or IEvent
. These types can be used as inner message types, or as reply messages.
Error!(int errorCode, string message);
ErrorsDetected(int entityId, Error[] errors);
ProtoBuf tags are assigned implicitly by default, in increasing order.
Warning
It is dangerous to add/remove/move message members without taking their tags into consideration. Tags define the wire format of the message.
When removing a member, replace it with a discard (_
) to preserve the tags of the following ones and to document the fact that a member has been removed.
A [ProtoReserved]
attribute will also be added for each range of discarded parameters.
Tags can be redefined using the [N]
syntax, where N
is the desired tag number:
Foo(int a, [4] int b, int c);
Which is equivalent to:
Foo(int a, _, _, int b, int c);
Implicit tag numbering resumes after an explicitly defined tag. The tags for the previous examples will be:
a
: 1b
: 4c
: 5
Alternatively, you can also use the full [ProtoMember(N)]
syntax:
Foo(int a, [ProtoMember(4)] int b, int c);
This will yield the same result as above.
You can set the IsRequired
parameter in the ProtoMember
attribute to false
by appending a ?
character to the member name:
Foo(int a?);
Note that this is different from:
Foo(int? a);
Which uses a nullable int
, although both examples will have IsRequired = false
, since the default value depends on the member type (repeated and nullable members will not be required).
Default parameter values can be specified:
Foo(int a = 42);
Default values are only used in the constructor. They have no influence over what is transmitted on the wire, nor on the deserialized values in case a member is missing.
Attributes can be specified for messages, message members and for enums:
[Transient]
Foo(int a, [Obsolete] int b)
Note that the [Obsolete]
attribute will go on the member property by default, not on the constructor parameter.
You can customize the targets of attributes using the same syntax as in C#:
Foo(int a, [param: Obsolete] int b)
The [Obsolete]
attribute will now be applied to the constructor parameter only, not to the associated member property.
The supported attribute targets are:
Target | Usable on | Default | Will apply on |
---|---|---|---|
type |
Messages and enums | Yes | Message class or enum |
param |
Message members | No | Constructor parameter |
property |
Message members | Yes | Member property |
field |
Enum fields | Yes | Enum field |
Only the param
target will change the default behavior. The other targets are available for consistency.
Use attributes to define routable messages:
[Routable]
Foo([RoutingPosition(1)] int id);
The Abc.Zebus.Routing
namespace will be imported automatically.
Messages can be generic and have constraints:
EntityUpdated<TEntity>(int entityId) where TEntity : IEntity;
Messages and enums are public
by default, except in a #pragma internal
scope. Accessibility can be set explicitly using the public
or internal
keywords:
internal Foo(int a);
public Bar(int b);
internal enum Color {
Red,
Green,
Blue
}
Messages can be prefixed with the following keywords: public
, internal
, sealed
, abstract
.
Use the #pragma
directive to enable or disable flags on a specific scope. The scope starts at the directive.
Foo(int id);
#pragma mutable
Bar(int id);
#pragma !mutable
Baz(int id);
The Bar
message is marked as mutable.
The available options are:
#pragma mutable
- specifies if messages are to be generated as mutable (public setters on properties) instead of read-only (private setters).#pragma proto
- declares messages for export to a.proto
file.#pragma internal
- sets the default accessibility tointernal
.#pragma public
- sets the default accessibility topublic
(default, same effect as#pragma !internal
).#pragma nullable
- enables support for nullable reference types.
A message type can inherit from another message type. The base type parameters will then be included in the constructor, unless the base type is marked as mutable.
Foo(int fooId) : Bar;
Bar(int barId) : Baz;
Baz(int bazId);
The constructor of Foo
will be Foo(int bazId, int barId, int fooId)
, and the constructor of Bar
will be Bar(int bazId, int barId)
.
A message can be emitted as a nested class using the following syntax:
Foo.Bar.Baz(int id);
The Baz
message class will be nested in Bar
, which will itself be nested in Foo
. All containing types need to be classes.
Enums can be declared just as in C#:
enum Color {
Red,
Green,
Blue = 42
};
ChangeColorCommand(int id, Color color);