Skip to content

Observing artifact registration

Chronicle registers your artifacts for you. RegisterAll is wired to the connection lifecycle and runs whenever the connection comes up, which is the right default and stays the default.

What IEventStore.Registration adds is a way to ask afterwards how it went.

A read model that cannot be built is isolated: it is logged and skipped, so one broken read model no longer costs the rest of your read side. That is a good trade, but it turns a loud failure into a quiet one. The broken read model is simply absent afterwards, and nothing throws.

Without an outcome to read, these three situations look identical from outside:

  • every artifact registered
  • some registered, and the rest were dropped
  • registration has not run yet

Registration tells them apart.

var outcome = eventStore.Registration;
if (!outcome.HasRun)
{
// Registration has not finished yet - or has not been triggered at all.
}
if (outcome.Failure is not null)
{
// Registration ran and did not finish. Whatever registered before it stopped is still in Artifacts.
Console.WriteLine($"Registration failed: {outcome.Failure.Message}");
}
foreach (var failed in outcome.Failures)
{
Console.WriteLine($"{failed.ArtifactType.Name} did not register: {failed.Failure!.Message}");
}
MemberWhat it answers
HasRunWhether RegisterAll has finished at least once, successfully or not
ArtifactsEvery declared projection artifact, with IsRegistered and the Failure that stopped it
FailureWhat stopped RegisterAll itself, or null when it finished
IsSuccessRegistration ran, finished, and every declared artifact registered
FailuresOnly the artifacts that did not register

RegistrationOutcome.NotRun is the value before registration has finished.

There are two kinds of failure here and they are not the same. An artifact in Failures could not be built, so it was isolated and skipped and registration carried on without it — the rest of the read side is up. A Failure on the outcome itself ended the run, so registration stopped wherever it got to. A run that ends this way still reports itself rather than staying at NotRun, so waiting on it returns instead of timing out.

Registration runs on the connection lifecycle, so a consumer that has just constructed a client may simply be early. WaitForRegistration waits for it to have run, with the same timeout shape as the other Chronicle wait helpers:

using Cratis.Chronicle.Registrations;
var outcome = await eventStore.WaitForRegistration(TimeSpan.FromSeconds(10));
if (!outcome.IsSuccess)
{
throw new InvalidOperationException("The read side did not come up.");
}

It waits for registration to have run, not to have succeeded — ask the returned outcome about that. A run that failed returns here too, carrying its Failure; the timeout is for a registration that never finished, not for one that finished badly. If it has not finished within the timeout, the wait throws TaskCanceledException.

Before Chronicle can register artifacts, DefaultClientArtifactsProvider discovers their types through ICanProvideAssembliesForDiscovery. The first access to any artifact collection initializes all collections. Concurrent readers wait for that discovery to finish rather than seeing an empty or partially populated collection. Later reads reuse the completed collections; they do not scan assemblies again.

If assembly initialization or type discovery throws, that access fails with the original exception. The provider does not publish the incomplete collections, and the next access retries initialization. This is a retry on access, not an automatic background retry.

If you supply custom assembly discovery, finish that work before reading the provider’s artifact collections. Reading them again on the initializing thread throws ReentrantClientArtifactsInitialization: discovery cannot consume its own unfinished result. Avoid waiting inside discovery for another thread that reads artifacts too, because that reader waits for discovery to finish.

A complete discovery result only says which types exist. It does not mean the kernel has registered them; use the registration outcome and observer-state helpers described here for that distinction.

Registration reports projection artifacts: the fluent IProjectionFor<T> implementations and the model-bound read models. Those are the artifacts whose registration round-trips to the kernel inside RegisterAll, so the outcome is observed rather than assumed.

Reactors and reducers are deliberately not reported. Their registration only opens a duplex stream and returns before the kernel has answered, so the client has nothing to report about them that would be a fact. To know a reactor or reducer is live, wait on its observer state instead — WaitTillSubscribed or WaitTillActive — which the kernel answers.

The outcome is read-only and decides nothing. Whether a failed artifact should abort start-up, fail a spec, or be tolerated is yours to decide.