This document covers all query options supported by the PanoramicData.OData.Client library.
- Basic Queries
- Fluent Query Execution
- Filtering ($filter)
- Selecting Fields ($select)
- Expanding Related Entities ($expand)
- Ordering Results ($orderby)
- Paging ($skip, $top)
- Counting Results ($count)
- Searching ($search)
- Aggregations ($apply)
- Computed Properties ($compute)
- Derived Types (Type Casting)
- Helper Methods
Create a query using the fluent query builder:
using PanoramicData.OData.Client;
var client = new ODataClient(new ODataClientOptions
{
BaseUrl = "https://services.odata.org/V4/OData/OData.svc/"
});
// Create a query builder for Products
var query = client.For<Product>("Products");
// Execute the query
var response = await client.GetAsync(query);
// Access results
foreach (var product in response.Value)
{
Console.WriteLine($"{product.Id}: {product.Name}");
}Execute queries directly from the query builder for streamlined code:
// Get all matching entities
var products = await client.For<Product>("Products")
.Filter("Price gt 100")
.OrderBy("Name")
.GetAsync(cancellationToken);
// Get all pages automatically (follows @odata.nextLink)
var allProducts = await client.For<Product>("Products")
.Filter("Rating gt 3")
.GetAllAsync(cancellationToken);
// Get first or default (returns null if no match)
var cheapest = await client.For<Product>("Products")
.OrderBy("Price")
.GetFirstOrDefaultAsync(cancellationToken);
// Get single entity (throws if not exactly one)
var unique = await client.For<Product>("Products")
.Filter("Name eq 'SpecialWidget'")
.GetSingleAsync(cancellationToken);
// Get single or default (returns null if none, throws if multiple)
var maybeOne = await client.For<Product>("Products")
.Filter("ID eq 123")
.GetSingleOrDefaultAsync(cancellationToken);
// Get count only
var count = await client.For<Product>("Products")
.Filter("Price gt 50")
.GetCountAsync(cancellationToken);These methods are equivalent to creating a query and passing it to the corresponding ODataClient method, but provide a more fluent, chainable API.
// Simple comparison
var query = client.For<Product>("Products")
.Filter("Price gt 100");
// Multiple conditions
var query = client.For<Product>("Products")
.Filter("Price gt 100 and Rating ge 4");
// String functions
var query = client.For<Product>("Products")
.Filter("contains(Name, 'Widget')");
var query = client.For<Product>("Products")
.Filter("startswith(Name, 'Super')");
var query = client.For<Product>("Products")
.Filter("endswith(Name, 'Pro')");
// Case-insensitive search
var query = client.For<Product>("Products")
.Filter("contains(tolower(Name), 'widget')");// Simple comparison
var query = client.For<Product>("Products")
.Filter(p => p.Price > 100);
// Multiple conditions
var query = client.For<Product>("Products")
.Filter(p => p.Price > 100 && p.Rating >= 4);
// String operations
var query = client.For<Product>("Products")
.Filter(p => p.Name.Contains("Widget"));
var query = client.For<Product>("Products")
.Filter(p => p.Name.StartsWith("Super"));
// Collection contains (IN clause)
var categories = new[] { "Electronics", "Tools" };
var query = client.For<Product>("Products")
.Filter(p => categories.Contains(p.Category));
// Generates: Category in ('Electronics','Tools')
// Any/All on collections
var query = client.For<Person>("People")
.Filter(p => p.Emails.Any(e => e.Contains("@company.com")));
// Generates: Emails/any(e: contains(e,'@company.com'))// Multiple Filter() calls are combined with AND
var query = client.For<Product>("Products")
.Filter("Price gt 50")
.Filter("Rating ge 3")
.Filter("InStock eq true");
// Generates: $filter=(Price gt 50) and (Rating ge 3) and (InStock eq true)// Select specific fields (reduces payload)
var query = client.For<Product>("Products")
.Select("Id,Name,Price");
// Using expression
var query = client.For<Product>("Products")
.Select(p => new { p.Id, p.Name, p.Price });// Expand navigation properties
var query = client.For<Product>("Products")
.Expand("Category");
// Multiple expansions
var query = client.For<Product>("Products")
.Expand("Category,Supplier");
// Using expression
var query = client.For<Product>("Products")
.Expand(p => p.Category);
// Nested expansion (raw string)
var query = client.For<Order>("Orders")
.Expand("Customer($expand=Address)");// Ascending order
var query = client.For<Product>("Products")
.OrderBy("Name");
// Descending order
var query = client.For<Product>("Products")
.OrderBy("Price desc");
// Multiple ordering
var query = client.For<Product>("Products")
.OrderBy("Category,Price desc");
// Using expression
var query = client.For<Product>("Products")
.OrderBy(p => p.Name);
var query = client.For<Product>("Products")
.OrderBy(p => p.Price, descending: true);
// Using key-value pairs
var orderBys = new Dictionary<string, bool>
{
{ "Category", false }, // ascending
{ "Price", true } // descending
};
var query = client.For<Product>("Products")
.OrderBy(orderBys);// Get first 10 items
var query = client.For<Product>("Products")
.Top(10);
// Skip first 20, take next 10
var query = client.For<Product>("Products")
.Skip(20)
.Top(10);When the server returns more results than fit in one response, it includes @odata.nextLink:
// Get single page
var response = await client.GetAsync(query);
// response.NextLink contains URL for next page if more results exist
// Get all pages automatically
var allProducts = await client.GetAllAsync(query, cancellationToken);// Include count in response
var query = client.For<Product>("Products")
.Top(10)
.Count();
var response = await client.GetAsync(query);
Console.WriteLine($"Total matching: {response.Count}");
Console.WriteLine($"Returned in this page: {response.Value.Count}");// Get only the count, no entities
var count = await client.GetCountAsync<Product>();
// With filter
var query = client.For<Product>("Products")
.Filter("Price gt 100");
var count = await client.GetCountAsync(query);// Full-text search (server must support it)
var query = client.For<Product>("Products")
.Search("widget blue");// Group by and aggregate
var query = client.For<Product>("Products")
.Apply("groupby((Category),aggregate(Price with average as AvgPrice))");
// Filter then aggregate
var query = client.For<Product>("Products")
.Apply("filter(Rating ge 4)/groupby((Category),aggregate($count as Count))");OData V4.01 feature for computed properties:
var query = client.For<OrderLine>("OrderLines")
.Compute("Price mul Quantity as Total")
.Select("ProductName,Price,Quantity,Total");Query derived types in an inheritance hierarchy:
// Get only employees (derived from Person)
var query = client.For<Person>("People")
.OfType<Employee>("Microsoft.OData.Service.Models.Employee");
// Using simple type name
var employeeQuery = client.For<Person>("People")
.OfType<Employee>();
// Cast without changing result type
var query = client.For<Person>("People")
.Cast("Microsoft.OData.Service.Models.Employee")
.Filter("EmployeeId ne null");var query = client.For<Product>("Products")
.Filter("Name eq 'Widget'");
var product = await client.GetFirstOrDefaultAsync(query);
// Returns null if no matchvar query = client.For<Product>("Products")
.Filter("Name eq 'UniqueWidget'");
var product = await client.GetSingleAsync(query);
// Throws if zero or more than one result
var product = await client.GetSingleOrDefaultAsync(query);
// Throws only if more than one result// Get single entity by key
var product = await client.GetByKeyAsync<Product, int>(123);
// With additional query options
var query = client.For<Product>("Products")
.Expand("Category");
var product = await client.GetByKeyAsync<Product, int>(123, query);// Add headers to specific query
var query = client.For<Product>("Products")
.WithHeader("Prefer", "return=representation")
.WithHeader("OData-MaxVersion", "4.01");// Get raw JSON when you need full control
var json = await client.GetRawAsync("Products?$filter=Price gt 100");
// Returns JsonDocument for manual parsing