Ad-hoc Querying with `IProjections.Query()`
The IProjections.Query() method lets you run a projection ad-hoc from the .NET client — without defining a read model type or registering a permanent projection. You write a PDL declaration, send it to the server, and get back the projected read model entries as JSON strings.
Basic Usage
Section titled “Basic Usage”Inject IProjections into your service, then call Query() with a PDL declaration string:
using Cratis.Chronicle.Projections;using System.Text.Json;
public class PdlOrderQueryService(IProjections projections){ public async Task<IEnumerable<PdlOrderSummary>> GetOrderSummaries() { var result = await projections.Query(""" projection OrderSummary from OrderPlaced """);
return result.ReadModelEntries .Select(json => JsonSerializer.Deserialize<PdlOrderSummary>(json)!); }}
public record PdlOrderSummary(string OrderId);import io.cratis.chronicle.IEventStoreimport io.cratis.chronicle.projections.ProjectionQueryResult
data class PdlOrderSummary(val orderId: String = "")
class PdlOrderQueryService(private val store: IEventStore) { suspend fun getOrderSummaries(): List<PdlOrderSummary> { val result = store.projections.query( """ projection OrderSummary from OrderPlaced """ )
return when (result) { is ProjectionQueryResult.Projected -> result.instancesOf(PdlOrderSummary::class) is ProjectionQueryResult.Invalid -> emptyList() } }}import io.cratis.chronicle.EventStore;import io.cratis.chronicle.java.ProjectionsServiceJavaBridge;import io.cratis.chronicle.projections.ProjectionQueryResult;
import java.util.List;
record PdlOrderSummary(String orderId) {}
class PdlOrderQueryService { private final EventStore store;
PdlOrderQueryService(EventStore store) { this.store = store; }
List<String> getOrderSummaries() { ProjectionQueryResult result = ProjectionsServiceJavaBridge.query( store.getProjections(), "projection OrderSummary\n from OrderPlaced" );
if (result instanceof ProjectionQueryResult.Projected projected) { // Raw JSON documents — instancesOf() takes a Kotlin KClass and has no Java-callable overload. return projected.getEntries(); } return List.of(); }}Elixir does not support this workflow yet.interface PdlOrderSummary { orderId: string;}
const result = await store.projections.query(` projection OrderSummary from OrderPlaced`);
const summaries = result.readModelEntries.map(json => JSON.parse(json) as PdlOrderSummary);ReadModelEntries is a IReadOnlyList<string> where each element is the JSON representation of one projected read model instance.
Explicit vs. Inferred Read Model
Section titled “Explicit vs. Inferred Read Model”You can optionally specify a => ReadModelType in your declaration. When you do, the server resolves the schema from the registered read model definition. When you omit it, the schema is inferred from the event properties.
// Inferred — schema derived from OrderPlaced and OrderShipped event propertiesvar inferred = await projections.Query(""" projection Orders from OrderPlaced from OrderShipped """);
// Explicit — schema comes from the registered 'PdlOrderReadModel' typevar explicitResult = await projections.Query(""" projection Orders => PdlOrderReadModel from OrderPlaced from OrderShipped """);import io.cratis.chronicle.IEventStore
suspend fun compareInferredAndExplicit(store: IEventStore) { // Inferred — schema derived from OrderPlaced and OrderShipped event properties val inferred = store.projections.query( """ projection Orders from OrderPlaced from OrderShipped """ )
// Explicit — schema comes from the registered 'PdlOrderReadModel' type val explicitResult = store.projections.query( """ projection Orders => PdlOrderReadModel from OrderPlaced from OrderShipped """ )}import io.cratis.chronicle.EventStore;import io.cratis.chronicle.java.ProjectionsServiceJavaBridge;import io.cratis.chronicle.projections.ProjectionQueryResult;
class InferredVsExplicitOrdersQuery { void compareInferredAndExplicit(EventStore store) { // Inferred — schema derived from OrderPlaced and OrderShipped event properties ProjectionQueryResult inferred = ProjectionsServiceJavaBridge.query( store.getProjections(), "projection Orders\n from OrderPlaced\n from OrderShipped" );
// Explicit — schema comes from the registered 'PdlOrderReadModel' type ProjectionQueryResult explicitResult = ProjectionsServiceJavaBridge.query( store.getProjections(), "projection Orders => PdlOrderReadModel\n from OrderPlaced\n from OrderShipped" ); }}Elixir does not support this workflow yet.// Inferred - schema derived from OrderPlaced and OrderShipped event propertiesconst inferred = await store.projections.query(` projection Orders from OrderPlaced from OrderShipped`);
// Explicit - schema comes from the registered 'PdlOrderReadModel' typeconst explicitResult = await store.projections.query(` projection Orders => PdlOrderReadModel from OrderPlaced from OrderShipped`);When using an inferred schema with multiple from blocks, all events that contribute the same property name must use compatible types. The compiler reports an error at query time if there is a mismatch:
// This declaration will throw UnableToQueryProjection:// OrderPlaced.value is string, but OrderShipped.value is intvar result = await projections.Query(""" projection Bad from OrderPlaced // value: string from OrderShipped // value: int → incompatible types """);import io.cratis.chronicle.IEventStoreimport io.cratis.chronicle.projections.ProjectionQueryResult
suspend fun queryBad(store: IEventStore) { // This declaration comes back as ProjectionQueryResult.Invalid: // OrderPlaced.value is a string, but OrderShipped.value is an int val result = store.projections.query( """ projection Bad from OrderPlaced // value: string from OrderShipped // value: int -> incompatible types """ )
if (result is ProjectionQueryResult.Invalid) { result.errors.forEach { println(it) } }}import io.cratis.chronicle.EventStore;import io.cratis.chronicle.java.ProjectionsServiceJavaBridge;import io.cratis.chronicle.projections.ProjectionDeclarationError;import io.cratis.chronicle.projections.ProjectionQueryResult;
class BadQuery { // This declaration comes back as ProjectionQueryResult.Invalid: // OrderPlaced.value is a string, but OrderShipped.value is an int void queryBad(EventStore store) { ProjectionQueryResult result = ProjectionsServiceJavaBridge.query( store.getProjections(), "projection Bad\n" + " from OrderPlaced // value: string\n" + " from OrderShipped // value: int -> incompatible types" );
if (result instanceof ProjectionQueryResult.Invalid invalid) { for (ProjectionDeclarationError error : invalid.getErrors()) { System.out.println(error); } } }}Elixir does not support this workflow yet.// This declaration will throw UnableToQueryProjection:// OrderPlaced.value is a string, but OrderShipped.value is a numberconst result = await store.projections.query(` projection Bad from OrderPlaced // value: string from OrderShipped // value: number -> incompatible types`);Error Handling
Section titled “Error Handling”If the declaration contains syntax or type errors, Query() throws UnableToQueryProjection:
try{ var result = await projections.Query(""" projection Orders from OrderPlaced """);}catch (UnableToQueryProjection ex){ Console.WriteLine(ex.Message);}import io.cratis.chronicle.IEventStoreimport io.cratis.chronicle.projections.ProjectionQueryResult
suspend fun queryOrders(store: IEventStore) { // A declaration that fails to parse is not an exception — it comes back as a result you branch on. when (val result = store.projections.query( """ projection Orders from OrderPlaced """ )) { is ProjectionQueryResult.Projected -> result.entries.forEach(::println) is ProjectionQueryResult.Invalid -> result.errors.forEach { println(it) } }}import io.cratis.chronicle.EventStore;import io.cratis.chronicle.java.ProjectionsServiceJavaBridge;import io.cratis.chronicle.projections.ProjectionDeclarationError;import io.cratis.chronicle.projections.ProjectionQueryResult;
class ErrorHandlingOrdersQuery { // A declaration that fails to parse is not an exception — it comes back as a result you branch on. void queryOrders(EventStore store) { ProjectionQueryResult result = ProjectionsServiceJavaBridge.query( store.getProjections(), "projection Orders\n from OrderPlaced" );
if (result instanceof ProjectionQueryResult.Projected projected) { for (String entry : projected.getEntries()) { System.out.println(entry); } } else if (result instanceof ProjectionQueryResult.Invalid invalid) { for (ProjectionDeclarationError error : invalid.getErrors()) { System.out.println(error); } } }}Elixir does not support this workflow yet.import { UnableToQueryProjection } from '@cratis/chronicle';
try { const result = await store.projections.query(` projection Orders from OrderPlaced `);} catch (error) { if (error instanceof UnableToQueryProjection) { console.log(error.message); }}Targeting a Different Event Sequence
Section titled “Targeting a Different Event Sequence”By default Query() reads from the event-log sequence. Pass a different sequence identifier as the second argument:
var result = await projections.Query( """ projection InboxMessages from MessageReceived """, eventSequenceId: "inbox");import io.cratis.chronicle.IEventStoreimport io.cratis.chronicle.eventSequences.EventSequenceId
suspend fun getInboxMessages(store: IEventStore) = store.projections.query( """ projection InboxMessages from MessageReceived """, EventSequenceId("inbox") )import io.cratis.chronicle.EventStore;import io.cratis.chronicle.java.ProjectionsServiceJavaBridge;import io.cratis.chronicle.projections.ProjectionQueryResult;
class InboxMessagesQuery { ProjectionQueryResult getInboxMessages(EventStore store) { return ProjectionsServiceJavaBridge.query( store.getProjections(), "projection InboxMessages\n from MessageReceived", "inbox" ); }}Elixir does not support this workflow yet.const result = await store.projections.query( ` projection InboxMessages from MessageReceived `, 'inbox');Practical Use Cases
Section titled “Practical Use Cases”Query() is well-suited for situations where you want projection results without the overhead of defining and registering a permanent read model:
- Back-office tooling — maintenance screens for technical staff that need one-off views into the event log.
- Diagnostic dashboards — quick summaries during incident investigation.
- Development utilities — exploring what data an event sequence contains while building a feature.
- Integration tests — asserting projected state in test scenarios without registering a projection.
Limitations
Section titled “Limitations”| Concern | Detail |
|---|---|
| Event volume | The server reads up to an internal maximum (currently 1 000 events). Sequences with more matching events will return incomplete results. |
| No persistence | Results are not stored. Every call replays the relevant portion of the event log. |
| No registration | A projection without => ReadModelType can never be saved as a permanent projection. |
| No change notifications | Results are a point-in-time snapshot; there is no observable / reactive variant of this API. |