Skip to content

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.

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;
}

View C# snippet source on GitHub

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
{
}

View C# snippet source on GitHub

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:

MessageLegacy fieldDisabling companionField number
ReactorDefinitionIsReplayable (4)IsNotReplayable7
WebhookDefinitionIsReplayable (5)IsNotReplayable7
WebhookDefinitionIsActive (6)IsInactive8

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.

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);
}

View C# snippet source on GitHub

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; }
}

View C# snippet source on GitHub

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);
}
}

View C# snippet source on GitHub

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);
}

View C# snippet source on GitHub

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();
}

View C# snippet source on GitHub

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);
}
}

View C# snippet source on GitHub