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.
Overview
Section titled “Overview”When you request a read model instance using GetInstanceById, Chronicle:
- Identifies whether the read model is produced by a projection or reducer
- Retrieves all relevant events from the event log for the specified key
- Applies the projection or reducer logic to these events in memory
- 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.
Basic Usage
Section titled “Basic Usage”Using Generic Method
Section titled “Using Generic Method”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); }}Using Non-Generic Method
Section titled “Using Non-Generic Method”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);}Working with Different Read Model Types
Section titled “Working with Different Read Model Types”Projection-Based Read Models
Section titled “Projection-Based Read Models”For read models defined using projections (either model-bound or declarative):
// Model-bound projectionpublic 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 instancepublic async Task<OrderSummary?> GetOrderSummary(Guid orderId){ return await _eventStore.ReadModels.GetInstanceById<OrderSummary>(orderId);}Reducer-Based Read Models
Section titled “Reducer-Based Read Models”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 instancepublic async Task<ShoppingCart?> GetCart(Guid cartId){ return await _eventStore.ReadModels.GetInstanceById<ShoppingCart>(cartId);}Handling Null Results
Section titled “Handling Null Results”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;}Performance Considerations
Section titled “Performance Considerations”Event History Length
Section titled “Event History Length”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.
Caching Strategies
Section titled “Caching Strategies”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.
Use Cases
Section titled “Use Cases”Real-Time Dashboards
Section titled “Real-Time Dashboards”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);}Financial Transactions
Section titled “Financial Transactions”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;}Command Validation
Section titled “Command Validation”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();}Read-After-Write Consistency
Section titled “Read-After-Write Consistency”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);}Best Practices
Section titled “Best Practices”Choose the Right Approach
Section titled “Choose the Right Approach”- 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
Consider Access Patterns
Section titled “Consider Access Patterns”- Infrequent access + need for accuracy: Perfect for
GetInstanceById - Frequent access + long event history: Consider materialized projections
- Frequent access + short event history:
GetInstanceByIdwith caching may be appropriate
Type Safety
Section titled “Type Safety”Always use the generic method when the type is known at compile time:
// Good - type safevar account = await _eventStore.ReadModels.GetInstanceById<Account>(accountId);
// Avoid unless type is truly unknown at compile timevar account = (Account)await _eventStore.ReadModels.GetInstanceById(typeof(Account), accountId);Related Topics
Section titled “Related Topics”- Getting a Collection of Instances - Learn how to retrieve all instances of a read model
- Getting Snapshots - Learn how to retrieve historical state snapshots
- Watching Read Models - Real-time notifications for read model changes
- Projections - Learn more about defining projections
- Reducers - Learn more about defining reducers