Core ConceptsOverview

Overview

Orleans.Search uses a decorator pattern to transparently intercept grain state writes and maintain a synchronized search index in PostgreSQL.

Architecture

Application Code
    |
    v
IClusterClient.Search<TGrain>().Where(...).ToListAsync()
    |
    v
OrleansQueryProvider (Translates LINQ to EF Core queries)
    |
    v
ISearchProvider<TGrain, TState> (Generated per grain type)
    |
    v
SearchableGrainStorage (Intercepts writes, syncs to index)
    |
    v
PostgreSqlSearchContext (EF Core DbContext)
    |
    v
PostgreSQL Database (Search index + full-text search)

Write Path

When a grain updates its state:

  1. Grain calls WriteStateAsync()
  2. SearchableGrainStorage intercepts the call
  3. Delegates to inner storage (your actual persistence layer)
  4. Asynchronously calls SearchProvider.UpsertAsync() (fire-and-forget)
  5. Search index updated in PostgreSQL

Key insight: The search sync is fire-and-forget, meaning it doesn’t block the grain write. This keeps grain performance fast while maintaining eventual consistency in the search index.

Query Path

When you search for grains:

  1. client.Search<TGrain>().Where(...).ToListAsync() is called
  2. Generated Where extension translates grain expression to entity expression
  3. OrleansQueryProvider calls SearchProvider.QueryWithFilterAsync()
  4. EF Core executes query against PostgreSQL
  5. Returns list of grain IDs
  6. IDs materialized into grain references
  7. Grain references returned to caller

Source Generation

Orleans.Search uses Roslyn source generators to create code at compile time, eliminating reflection and providing type safety.

For each [Searchable] state class, the generator creates:

GeneratedPurpose
Entity ClassEF Core entity with queryable properties (e.g., UserStateEntity)
Search ProviderImplementation of ISearchProvider<TGrain, TState> with state-to-entity mapping
Search ModelQuery model for type-safe LINQ expressions
DbContext PartialConfigures EF Core with the generated entity
DI ExtensionsRegistration method for dependency injection
Query ExtensionsWhere() methods for fluent API

All generated code is placed in a .Generated namespace.

Optimistic Concurrency

Orleans.Search uses version-based optimistic concurrency to handle stale updates:

  • Each entity has a Version property
  • Updates only succeed if the version matches
  • Stale updates are ignored (later versions win)
  • This prevents race conditions when grains update rapidly

Supported Property Types

The following property types can be marked as [Queryable]:

CategoryTypes
Primitivesstring, bool, int, long, short, byte
Numericdecimal, double, float
DateTimeDateTime, DateTimeOffset, Guid

Design Decisions

DecisionRationale
Fire-and-forget syncDon’t impact grain write performance; eventual consistency
Source generationZero runtime reflection; compile-time safety
Decorator patternNon-invasive; works with any existing storage provider
EF Core for queriesMature ORM; automatic SQL optimization
Opt-in [Queryable]Only index what you need; keeps index lean
PostgreSQL focusProduction-ready; robust full-text search

Current Limitations

  • Searchable states must use IGrainWithStringKey (string-keyed grains only)
  • OrderBy/pagination not yet supported (planned for v1.1)

Next Steps