Query APISearch Client

Search Client

Orleans.Search adds the Search<TGrain>() extension method to Orleans’s IClusterClient, enabling you to query grains by their state properties.

Getting the Client

Inject IClusterClient from the dependency injection container:

public class MyService
{
    private readonly IClusterClient _client;
 
    public MyService(IClusterClient client)
    {
        _client = client;
    }
}

Or resolve it directly:

var client = serviceProvider.GetRequiredService<IClusterClient>();

The Search Method

The Search<TGrain>() method starts a fluent query for a specific grain type:

// Start a query for IUserGrain
var query = client.Search<IUserGrain>();
 
// Chain with Where and execution methods
var users = await client.Search<IUserGrain>()
    .Where(u => u.IsActive == true)
    .ToListAsync();

Query Flow

client.Search<TGrain>()     // Start query
    .Where(predicate)        // Add filter(s)
    .ToListAsync()           // Execute and return results

Complete Example

public class ProductService
{
    private readonly IClusterClient _client;
 
    public ProductService(IClusterClient client)
    {
        _client = client;
    }
 
    // Find a single product
    public async Task<IProductGrain?> FindByNameAsync(string name)
    {
        return await _client.Search<IProductGrain>()
            .Where(p => p.Name == name)
            .FirstOrDefaultAsync();
    }
 
    // Find products in a category
    public async Task<List<IProductGrain>> GetByCategoryAsync(string category)
    {
        return await _client.Search<IProductGrain>()
            .Where(p => p.Category == category)
            .ToListAsync();
    }
 
    // Find products in price range
    public async Task<List<IProductGrain>> GetInPriceRangeAsync(decimal min, decimal max)
    {
        return await _client.Search<IProductGrain>()
            .Where(p => p.Price >= min && p.Price <= max)
            .ToListAsync();
    }
 
    // Count in-stock products
    public async Task<int> CountInStockAsync()
    {
        return await _client.Search<IProductGrain>()
            .Where(p => p.InStock == true)
            .CountAsync();
    }
 
    // Check if any products exist
    public async Task<bool> AnyCategoryAsync(string category)
    {
        return await _client.Search<IProductGrain>()
            .Where(p => p.Category == category)
            .AnyAsync();
    }
}

Standard Grain Operations

Since Search is an extension method on IClusterClient, you use the same client for both standard grain operations and search:

public class UserService
{
    private readonly IClusterClient _client;
 
    public UserService(IClusterClient client)
    {
        _client = client;
    }
 
    // Standard Orleans: Get grain by ID
    public IUserGrain GetUser(string userId)
    {
        return _client.GetGrain<IUserGrain>(userId);
    }
 
    // Orleans.Search: Find grains by state
    public async Task<IUserGrain?> FindByEmailAsync(string email)
    {
        return await _client.Search<IUserGrain>()
            .Where(u => u.Email == email)
            .FirstOrDefaultAsync();
    }
}

Type Safety

The source generator creates type-safe Where extensions for each searchable grain type. Only [Queryable] properties can be used in predicates:

[Searchable(typeof(IUserGrain))]
public class UserState
{
    [Queryable]
    public string Email { get; set; }
 
    // Not queryable
    public string InternalNotes { get; set; }
}
 
// Works - Email is queryable
var users = await client.Search<IUserGrain>()
    .Where(u => u.Email == "test@example.com")
    .ToListAsync();
 
// Compile error - InternalNotes is not queryable
// var users = await client.Search<IUserGrain>()
//     .Where(u => u.InternalNotes == "something")
//     .ToListAsync();

Next Steps