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

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

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

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

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

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

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

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