E-Commerce Products Example
A complete example of using Orleans.Search for a product catalog with category filtering and price range queries.
Define the Grain Interface
IProductGrain.cs
public interface IProductGrain : IGrainWithStringKey
{
Task SetDetailsAsync(string name, string category, decimal price, bool inStock);
Task<ProductInfo> GetInfoAsync();
Task UpdateStockAsync(bool inStock);
Task UpdatePriceAsync(decimal price);
}
public record ProductInfo(string Name, string Category, decimal Price, bool InStock);Define the Searchable State
ProductState.cs
using TGHarker.Orleans.Search;
[Searchable(typeof(IProductGrain))]
[GenerateSerializer]
public class ProductState
{
[Queryable]
[FullTextSearchable(Weight = 2.0)]
[Id(0)]
public string Name { get; set; } = string.Empty;
[Queryable(Indexed = true)]
[Id(1)]
public string Category { get; set; } = string.Empty;
[Queryable]
[Id(2)]
public decimal Price { get; set; }
[Queryable(Indexed = true)]
[Id(3)]
public bool InStock { get; set; } = true;
[FullTextSearchable(Weight = 1.0)]
[Id(4)]
public string Description { get; set; } = string.Empty;
[Id(5)]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Id(6)]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}Implement the Grain
ProductGrain.cs
public class ProductGrain : Grain, IProductGrain
{
private readonly IPersistentState<ProductState> _state;
public ProductGrain(
[PersistentState("product", "ProductStorage")] IPersistentState<ProductState> state)
{
_state = state;
}
public async Task SetDetailsAsync(string name, string category, decimal price, bool inStock)
{
_state.State.Name = name;
_state.State.Category = category;
_state.State.Price = price;
_state.State.InStock = inStock;
_state.State.UpdatedAt = DateTime.UtcNow;
await _state.WriteStateAsync();
}
public Task<ProductInfo> GetInfoAsync() => Task.FromResult(new ProductInfo(
_state.State.Name,
_state.State.Category,
_state.State.Price,
_state.State.InStock
));
public async Task UpdateStockAsync(bool inStock)
{
_state.State.InStock = inStock;
_state.State.UpdatedAt = DateTime.UtcNow;
await _state.WriteStateAsync();
}
public async Task UpdatePriceAsync(decimal price)
{
_state.State.Price = price;
_state.State.UpdatedAt = DateTime.UtcNow;
await _state.WriteStateAsync();
}
}Product Catalog Service
ProductCatalogService.cs
public class ProductCatalogService
{
private readonly IClusterClient _client;
public ProductCatalogService(IClusterClient client)
{
_client = client;
}
// Search products by name
public async Task<List<IProductGrain>> SearchByNameAsync(string searchTerm)
{
return await _client.Search<IProductGrain>()
.Where(p => p.Name.Contains(searchTerm))
.ToListAsync();
}
// Get products by category
public async Task<List<IProductGrain>> GetByCategoryAsync(string category)
{
return await _client.Search<IProductGrain>()
.Where(p => p.Category == category)
.ToListAsync();
}
// Get products in a price range
public async Task<List<IProductGrain>> GetByPriceRangeAsync(decimal minPrice, decimal maxPrice)
{
return await _client.Search<IProductGrain>()
.Where(p => p.Price >= minPrice && p.Price <= maxPrice)
.ToListAsync();
}
// Get in-stock products
public async Task<List<IProductGrain>> GetInStockAsync()
{
return await _client.Search<IProductGrain>()
.Where(p => p.InStock == true)
.ToListAsync();
}
// Get out-of-stock products
public async Task<List<IProductGrain>> GetOutOfStockAsync()
{
return await _client.Search<IProductGrain>()
.Where(p => p.InStock == false)
.ToListAsync();
}
// Combined filter: category + in-stock + price range
public async Task<List<IProductGrain>> FilterProductsAsync(
string? category = null,
decimal? minPrice = null,
decimal? maxPrice = null,
bool? inStock = null)
{
// Start with all products
var query = _client.Search<IProductGrain>();
// Build the filter dynamically
if (!string.IsNullOrEmpty(category) && minPrice.HasValue && maxPrice.HasValue && inStock.HasValue)
{
return await query
.Where(p => p.Category == category &&
p.Price >= minPrice.Value &&
p.Price <= maxPrice.Value &&
p.InStock == inStock.Value)
.ToListAsync();
}
if (!string.IsNullOrEmpty(category) && inStock.HasValue)
{
return await query
.Where(p => p.Category == category && p.InStock == inStock.Value)
.ToListAsync();
}
if (!string.IsNullOrEmpty(category))
{
return await query
.Where(p => p.Category == category)
.ToListAsync();
}
if (inStock.HasValue)
{
return await query
.Where(p => p.InStock == inStock.Value)
.ToListAsync();
}
// Return all (use with caution in production)
return await query
.Where(p => p.InStock == true || p.InStock == false)
.ToListAsync();
}
// Count products by category
public async Task<int> CountByCategoryAsync(string category)
{
return await _client.Search<IProductGrain>()
.Where(p => p.Category == category)
.CountAsync();
}
// Check if category has in-stock products
public async Task<bool> HasInStockProductsAsync(string category)
{
return await _client.Search<IProductGrain>()
.Where(p => p.Category == category && p.InStock == true)
.AnyAsync();
}
// Find products under a price
public async Task<List<IProductGrain>> GetUnderPriceAsync(decimal maxPrice)
{
return await _client.Search<IProductGrain>()
.Where(p => p.Price < maxPrice && p.InStock == true)
.ToListAsync();
}
}API Endpoints
Program.cs (endpoints)
app.MapGet("/api/products", async (
string? search,
string? category,
decimal? minPrice,
decimal? maxPrice,
bool? inStock,
ProductCatalogService catalogService) =>
{
List<IProductGrain> products;
if (!string.IsNullOrEmpty(search))
{
products = await catalogService.SearchByNameAsync(search);
}
else
{
products = await catalogService.FilterProductsAsync(category, minPrice, maxPrice, inStock);
}
var results = new List<object>();
foreach (var product in products)
{
var info = await product.GetInfoAsync();
results.Add(new
{
id = product.GetPrimaryKeyString(),
name = info.Name,
category = info.Category,
price = info.Price,
inStock = info.InStock
});
}
return Results.Ok(results);
});
app.MapGet("/api/products/categories/{category}/count", async (
string category,
ProductCatalogService catalogService) =>
{
var count = await catalogService.CountByCategoryAsync(category);
return Results.Ok(new { category, count });
});Usage Examples
// Create products
var laptop = client.GetGrain<IProductGrain>("laptop-1");
await laptop.SetDetailsAsync("MacBook Pro 16", "electronics", 2499.99m, true);
var phone = client.GetGrain<IProductGrain>("phone-1");
await phone.SetDetailsAsync("iPhone 15 Pro", "electronics", 999.99m, true);
var shirt = client.GetGrain<IProductGrain>("shirt-1");
await shirt.SetDetailsAsync("Cotton T-Shirt", "clothing", 29.99m, true);
// Search by name
var laptops = await catalogService.SearchByNameAsync("MacBook");
// Returns: laptop-1
// Get by category
var electronics = await catalogService.GetByCategoryAsync("electronics");
// Returns: laptop-1, phone-1
// Price range query
var affordable = await catalogService.GetByPriceRangeAsync(0, 500);
// Returns: shirt-1
// Combined filters
var inStockElectronics = await catalogService.FilterProductsAsync(
category: "electronics",
inStock: true
);
// Returns: laptop-1, phone-1
// Budget electronics
var budgetElectronics = await catalogService.FilterProductsAsync(
category: "electronics",
maxPrice: 1500,
inStock: true
);
// Returns: phone-1Next Steps
- Order Tracking - Order management example
- API Reference - Full API documentation