Skip to content

Getting a Single Instance

The IReadModels API provides a straightforward way to retrieve the current state of a read model by replaying events from the event log. This ensures you always get strongly consistent data reflecting the exact current state.

When you request a read model instance using GetInstanceById, Chronicle:

  1. Identifies whether the read model is produced by a projection or reducer
  2. Retrieves all relevant events from the event log for the specified key
  3. Applies the projection or reducer logic to these events in memory
  4. Returns the resulting read model instance

This on-demand computation ensures strong consistency - you get the most up-to-date state without waiting for eventual consistency.

The generic method provides type safety:

public class AccountService
{
readonly IEventStore _eventStore;
public AccountService(IEventStore eventStore)
{
_eventStore = eventStore;
}
public async Task<AccountInfo?> GetAccountInfo(Guid accountId)
{
return await _eventStore.ReadModels.GetInstanceById<AccountInfo>(accountId);
}
}

For scenarios where the type is determined at runtime:

public async Task<object> GetReadModelInstance(Type readModelType, ReadModelKey key)
{
return await _eventStore.ReadModels.GetInstanceById(readModelType, key);
}

For read models defined using projections (either model-bound or declarative):

// Model-bound projection
public record OrderSummary(
[Key] Guid OrderId,
[SetFrom<OrderCreated>(nameof(OrderCreated.CustomerId))] Guid CustomerId,
[SetFrom<OrderCreated>(nameof(OrderCreated.TotalAmount))]
[AddFrom<PaymentReceived>(nameof(PaymentReceived.Amount))] decimal TotalPaid);
// Retrieve instance
public async Task<OrderSummary?> GetOrderSummary(Guid orderId)
{
return await _eventStore.ReadModels.GetInstanceById<OrderSummary>(orderId);
}

For read models defined using reducers:

public record ShoppingCart(Guid Id, List<CartItem> Items, decimal Total);
public class ShoppingCartReducer : IReducerFor<ShoppingCart>
{
public ShoppingCart OnCartCreated(CartCreated @event, ShoppingCart? current, EventContext context) =>
(current ?? new ShoppingCart(Guid.Empty, [], 0m)) with { Id = @event.CartId };
public ShoppingCart OnItemAdded(ItemAdded @event, ShoppingCart? current, EventContext context)
{
current ??= new ShoppingCart(Guid.Empty, [], 0m);
return current with
{
Items = [.. current.Items, new CartItem(@event.ProductId, @event.Quantity, @event.Price)],
Total = current.Total + (@event.Quantity * @event.Price)
};
}
}
// Retrieve instance
public async Task<ShoppingCart?> GetCart(Guid cartId)
{
return await _eventStore.ReadModels.GetInstanceById<ShoppingCart>(cartId);
}

Read models that haven’t received any events will return a default or initial state:

public async Task<AccountInfo> GetOrCreateDefaultAccount(Guid accountId)
{
var account = await _eventStore.ReadModels.GetInstanceById<AccountInfo>(accountId);
// For new accounts with no events, you'll get default values
if (account.Name == string.Empty)
{
// This is a new account that hasn't been initialized
return new AccountInfo(accountId, "New Account", 0m);
}
return account;
}

The performance of GetInstanceById depends on the number of events that need to be replayed:

  • Short histories (dozens of events): Fast, typically under 100ms
  • Medium histories (hundreds of events): Moderate, typically under 500ms
  • Long histories (thousands of events): Slower, may take seconds

For read models with very long event histories that are accessed frequently, consider using materialized projections with database storage instead of on-demand computation.

For read models accessed frequently within a short time window:

public class CachedAccountService
{
readonly IEventStore _eventStore;
readonly IMemoryCache _cache;
public async Task<AccountInfo?> GetAccountInfo(Guid accountId)
{
var cacheKey = $"account:{accountId}";
if (_cache.TryGetValue<AccountInfo>(cacheKey, out var cached))
{
return cached;
}
var account = await _eventStore.ReadModels.GetInstanceById<AccountInfo>(accountId);
_cache.Set(cacheKey, account, TimeSpan.FromMinutes(5));
return account;
}
}

Important: Only cache read models when you can accept the risk of serving slightly stale data. Always invalidate the cache when new events are appended that affect the read model.

When you need the absolute latest state:

public async Task<DashboardData> GetCurrentDashboard(Guid userId)
{
var profile = await _eventStore.ReadModels.GetInstanceById<UserProfile>(userId);
var stats = await _eventStore.ReadModels.GetInstanceById<UserStatistics>(userId);
return new DashboardData(profile, stats);
}

When accuracy is critical and eventual consistency is unacceptable:

public async Task<bool> CanWithdraw(Guid accountId, decimal amount)
{
var account = await _eventStore.ReadModels.GetInstanceById<Account>(accountId);
return account.Balance >= amount;
}

When you need to validate commands against current state:

public async Task<Result> ProcessOrder(PlaceOrderCommand command)
{
var inventory = await _eventStore.ReadModels.GetInstanceById<ProductInventory>(command.ProductId);
if (inventory.AvailableQuantity < command.Quantity)
{
return Result.Failure("Insufficient inventory");
}
// Process the order
await _eventStore.EventLog.Append(command.OrderId, new OrderPlaced(/* ... */));
return Result.Success();
}

When you append events and immediately need the updated state:

public async Task<Account> DepositMoney(Guid accountId, decimal amount)
{
// Append the event
await _eventStore.EventLog.Append(accountId, new MoneyDeposited(amount));
// Get the updated state immediately
return await _eventStore.ReadModels.GetInstanceById<Account>(accountId);
}
  • On-demand retrieval (GetInstanceById): Use when strong consistency is required and the read model isn’t accessed frequently
  • Materialized projections: Use for frequently accessed read models or those with long event histories
  • Projection watchers: Use for real-time updates in user interfaces
  • Infrequent access + need for accuracy: Perfect for GetInstanceById
  • Frequent access + long event history: Consider materialized projections
  • Frequent access + short event history: GetInstanceById with caching may be appropriate

Always use the generic method when the type is known at compile time:

// Good - type safe
var account = await _eventStore.ReadModels.GetInstanceById<Account>(accountId);
// Avoid unless type is truly unknown at compile time
var account = (Account)await _eventStore.ReadModels.GetInstanceById(typeof(Account), accountId);