Contracts
The Contracts project contains all gRPC definitions for data and services, representing the complete protocol of the Kernel. Contracts are defined as C# types: data is represented using classes, and services as interfaces.
We use protobuf-net.Grpc to enable a code-first approach to defining protobuf contracts.
The implementation of these contracts resides in the Kernel. For more details, see the services documentation.
Attributes
Section titled “Attributes”protobuf-net.Grpc supports defining contracts using annotations from System.ServiceModel
as well as those from Protobuf and Protobuf.Grpc. We use the latter.
For data types, decorate the class with [ProtoContract] and each property with [ProtoMember(<id>)], where id is a sequential number
indicating the field’s position in the contract.
Example:
using ProtoBuf;using ProtoBuf.Grpc;
[ProtoContract]public class ContributingKernelAppendRequest{ [ProtoMember(1)] public string EventStore { get; set; } = string.Empty;
[ProtoMember(2)] public string Namespace { get; set; } = string.Empty;
[ProtoMember(3)] public string EventSequenceId { get; set; } = string.Empty;}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Always add new properties at the end, without reordering existing ones.
For service contracts, use the [Service] and [Operation] attributes.
Example:
using ProtoBuf.Grpc;using ProtoBuf.Grpc.Configuration;
[Service]public interface IContributingKernelEventSequences{ /// <summary> /// Append an event to an event sequence. /// </summary> /// <param name="request">The <see cref="ContributingKernelAppendRequest"/>.</param> /// <param name="context">gRPC call context.</param> /// <returns>The response.</returns> [Operation] Task<ContributingKernelAppendResponse> Append(ContributingKernelAppendRequest request, CallContext context = default);}
public class ContributingKernelAppendResponse{}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Presence-aware booleans
Section titled “Presence-aware booleans”Proto3 omits scalar false values. A legacy C# contract initialized to true with
DefaultValue(true) reads that omission as true; in the other direction,
protobuf-net omits true and an ordinary proto3 client reads it as false.
For client-written inputs, keep the legacy fields and their true defaults.
Add inverted, default-false companions as plain bool properties with
[ProtoMember], without DefaultValue:
| Message | Legacy field | Disabling companion | Field number |
|---|---|---|---|
ReactorDefinition | IsReplayable (4) | IsNotReplayable | 7 |
WebhookDefinition | IsReplayable (5) | IsNotReplayable | 7 |
WebhookDefinition | IsActive (6) | IsInactive | 8 |
Write both representations consistently: IsReplayable = value and
IsNotReplayable = !value; likewise, IsActive = value and IsInactive = !value.
The kernel resolves replayability as IsReplayable && !IsNotReplayable and
activity as IsActive && !IsInactive. A disabling companion wins a conflict.
An absent companion is false, so older .NET clients retain their legacy
behavior. Proto3 clients can disable replay or activity by sending a true
companion even when their encoder omits the legacy false scalar.
For kernel-written booleans, use IsRequired = true on the existing
[ProtoMember] so protobuf-net writes both true and false. For example,
ObserverInformation.IsReplayable uses [ProtoMember(10, IsRequired = true)]
and retains DefaultValue(true) and its initializer. It needs no companion:
field 10 carries the verdict directly, just as CommandResult.IsAuthorized
and QueryResult.IsAuthorized do. This does not make a proto3 field required;
it controls protobuf-net’s writer.
Use this pattern for future contract booleans: choose default-false input
semantics where possible; when preserving a legacy default-true input, add an
inverted companion. For kernel-written verdicts that clients must receive,
force emission with IsRequired rather than adding another field. Plain bools
also preserve the Contracts package’s existing protobuf-net dependency floor.
Using the input companions requires regenerated bindings and a kernel that
understands them. Older kernels ignore the new fields; older proto3 clients
keep their omitted-field limitations. Public .NET APIs remain positive.
Webhook query responses use WebhookDetailsResponse, whose ordinary boolean
fields are unaffected.
Streaming APIs
Section titled “Streaming APIs”Some APIs require streaming data from client to server, server to client, or both. We use the System.Reactive type IObservable<> for streaming, as it is intuitive,
consistent with our implementation, and provides expressive power through the fluent interfaces of System.Reactive.
Example:
using ProtoBuf;using ProtoBuf.Grpc;using ProtoBuf.Grpc.Configuration;
[ProtoContract]public class ContributingKernelReactorMessage{ [ProtoMember(1)] public string Content { get; set; } = string.Empty;}
[ProtoContract]public class ContributingKernelEventsToObserve{ [ProtoMember(1)] public string EventType { get; set; } = string.Empty;}
[Service]public interface IContributingKernelReactors{ [Operation] IObservable<ContributingKernelEventsToObserve> Observe(IObservable<ContributingKernelReactorMessage> messages, CallContext context = default);}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.SerializableDateTimeOffset
Section titled “SerializableDateTimeOffset”Protobuf does not natively support DateTimeOffset, so we provide SerializableDateTimeOffset in the Contracts.Primitives namespace.
This type serializes a DateTimeOffset as an ISO 8601 string (round-trip format "O", e.g. "2024-01-15T12:30:00.0000000+02:00"), which is protobuf-compatible.
SerializableDateTimeOffset provides implicit conversion operators to and from DateTimeOffset, making it transparent to use in your code.
Always use SerializableDateTimeOffset instead of DateTimeOffset in contract definitions.
Example:
using ProtoBuf;using Cratis.Chronicle.Contracts.Primitives;
[ProtoContract]public class ContributingKernelUser{ [ProtoMember(1)] public string Username { get; set; } = string.Empty;
[ProtoMember(2)] public SerializableDateTimeOffset CreatedAt { get; set; } = new();
[ProtoMember(3)] public SerializableDateTimeOffset? LastModifiedAt { get; set; }}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.The implicit conversions allow natural usage:
public static class ContributingKernelDateTimeOffsetUsage{ public static void Run() { var user = new ContributingKernelUser { Username = "john", CreatedAt = DateTimeOffset.UtcNow // Implicit conversion from DateTimeOffset };
DateTimeOffset created = user.CreatedAt; // Implicit conversion to DateTimeOffset Console.WriteLine(created); }}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.When you need a contract property that can hold one of several different types (a discriminated union), use the OneOf<T0, T1, ...> generic types
from the Contracts.Primitives namespace. These support 2, 3, or 4 type parameters.
Each OneOf instance stores the value in one of its Value0, Value1, etc. properties, with the others set to null. The Value property
returns whichever value is set, or throws NoValueSetForOneOf if none are set.
Example with a service returning either a result or an error:
using ProtoBuf;using ProtoBuf.Grpc;using ProtoBuf.Grpc.Configuration;using Cratis.Chronicle.Contracts.Primitives;
[ProtoContract]public class ContributingKernelGetJobRequest{ [ProtoMember(1)] public string JobId { get; set; } = string.Empty;}
[ProtoContract]public class ContributingKernelJob{ [ProtoMember(1)] public string Id { get; set; } = string.Empty;}
[ProtoContract]public class ContributingKernelJobError{ [ProtoMember(1)] public string Message { get; set; } = string.Empty;}
[Service]public interface IContributingKernelJobs{ [Operation] Task<OneOf<ContributingKernelJob, ContributingKernelJobError>> GetJob(ContributingKernelGetJobRequest request, CallContext context = default);}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Example with a message containing different content types:
using ProtoBuf;using Cratis.Chronicle.Contracts.Primitives;
[ProtoContract]public class ContributingKernelRegisterReactor{ [ProtoMember(1)] public string ReactorId { get; set; } = string.Empty;}
[ProtoContract]public class ContributingKernelReactorResult{ [ProtoMember(1)] public bool Success { get; set; }}
[ProtoContract]public class ContributingKernelReactorOneOfMessage{ [ProtoMember(1)] public OneOf<ContributingKernelRegisterReactor, ContributingKernelReactorResult> Content { get; set; } = new();}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Usage on the client side:
public static class ContributingKernelOneOfUsage{ public static async Task Run(IContributingKernelJobs jobsService, ContributingKernelGetJobRequest request) { var result = await jobsService.GetJob(request);
if (result.Value0 is not null) { // Handle Job var job = result.Value0; Console.WriteLine($"Job: {job.Id}"); } else if (result.Value1 is not null) { // Handle JobError var error = result.Value1; Console.WriteLine($"Error: {error.Message}"); }
// Or use the Value property to get whichever is set object value = result.Value; Console.WriteLine(value); }}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.TypeScript does not support this workflow yet.