The Intent.AspNetCore.ODataQuery module adds OData query capabilities to standard ASP.NET Core controllers, enabling clients to filter, sort, page, and expand data using OData query syntax.
Overview
OData (Open Data Protocol) is an ISO/IEC standard that defines best practices for building and consuming RESTful APIs. This module adds OData query support to your existing controllers without requiring a full OData implementation.
What Gets Generated
ODataQuerySwaggerFilter
Documents OData query parameters in Swagger:
public class ODataQuerySwaggerFilter : IOperationFilter
{
public void Apply ( OpenApiOperation operation , OperationFilterContext context )
{
var hasODataAttribute = context . MethodInfo
. GetCustomAttributes ( typeof ( EnableQueryAttribute ), true )
. Any ();
if ( ! hasODataAttribute )
return ;
operation . Parameters ??= new List < OpenApiParameter >();
// Add OData query parameters
operation . Parameters . Add ( new OpenApiParameter
{
Name = "$filter" ,
In = ParameterLocation . Query ,
Description = "Filter the results using OData syntax" ,
Schema = new OpenApiSchema { Type = "string" }
});
operation . Parameters . Add ( new OpenApiParameter
{
Name = "$orderby" ,
In = ParameterLocation . Query ,
Description = "Order the results using OData syntax" ,
Schema = new OpenApiSchema { Type = "string" }
});
// ... $top, $skip, $expand, $select
}
}
Key Features
Filtering Filter collections using OData filter syntax
Sorting Order results by one or more properties
Paging Skip and take results for pagination
Expand Include related data in responses
Module Settings
Allow Filter Option
Enable $filter query option for expression-based filtering of data
Allow OrderBy Option
Enable $orderby query option for expression-based ordering of data
Allow Expand Option
Enable $expand query option to include related data inline
Allow Select Option
Enable $select query option to change the return type projection
Max Top
Maximum value of $top that a client can request
Usage in Controllers
Apply [EnableQuery] attribute to controller actions:
[ HttpGet ]
[ EnableQuery ]
public IQueryable < ProductDto > GetAll ()
{
return _dbContext . Products
. ProjectTo < ProductDto >( _mapper . ConfigurationProvider );
}
Important: Return IQueryable<T> not Task<List<T>> to enable OData query processing.
OData Query Operations
$filter - Filtering
Filter results using logical expressions:
# Products with price greater than 100
GET /api/products?$filter=price gt 100
# Products in Electronics category
GET /api/products?$filter=category eq 'Electronics'
# Products with name containing 'Laptop'
GET /api/products?$filter=contains(name, 'Laptop')
# Complex filter with AND/OR
GET /api/products?$filter=price gt 100 and (category eq 'Electronics' or category eq 'Computers')
# Filter by date
GET /api/orders?$filter=orderDate ge 2024-01-01
Filter Operators
Operator Description Example eqEqual category eq 'Electronics'neNot equal status ne 'Deleted'gtGreater than price gt 100geGreater than or equal price ge 100ltLess than price lt 1000leLess than or equal price le 1000andLogical and price gt 100 and inStock eq trueorLogical or category eq 'A' or category eq 'B'notLogical not not(category eq 'Electronics')
Filter Functions
Function Description Example containsString contains contains(name, 'phone')startswithString starts with startswith(name, 'iPhone')endswithString ends with endswith(name, 'Pro')tolowerConvert to lowercase tolower(name) eq 'iphone'toupperConvert to uppercase toupper(category) eq 'ELECTRONICS'lengthString length length(name) gt 10yearYear from date year(orderDate) eq 2024monthMonth from date month(orderDate) eq 12dayDay from date day(orderDate) eq 25
$orderby - Sorting
Order results by one or more properties:
# Order by price ascending
GET /api/products?$orderby=price
# Order by price descending
GET /api/products?$orderby=price desc
# Order by multiple fields
GET /api/products?$orderby=category asc, price desc
# Order by calculated field
GET /api/products?$orderby=year(createdDate) desc
t o p a n d top and t o p an d skip - Paging
Implement pagination:
# Get first 10 products
GET /api/products?$top=10
# Skip first 20, take next 10 (page 3)
GET /api/products?$skip=20&$top=10
# Combined with filter and sort
GET /api/products?$filter=inStock eq true&$orderby=price&$skip=0&$top=20
Client-side pagination example:
interface PageRequest {
pageNumber : number ;
pageSize : number ;
}
async function getProducts ( page : PageRequest ) : Promise < ProductDto []> {
const skip = ( page . pageNumber - 1 ) * page . pageSize ;
const response = await fetch (
`/api/products?$skip= ${ skip } &$top= ${ page . pageSize } `
);
return await response . json ();
}
Include related entities:
# Expand single navigation property
GET /api/orders?$expand=customer
# Expand multiple properties
GET /api/orders?$expand=customer,items
# Nested expand
GET /api/orders?$expand=customer,items($expand=product)
# Expand with filter
GET /api/orders?$expand=items($filter=quantity gt 1)
$select - Property Selection
Select specific properties:
# Select specific fields
GET /api/products?$select=id,name,price
# Select with expand
GET /api/orders?$expand=customer($select=name,email)&$select=id,orderDate,totalAmount
$count - Get Total Count
Include total count in response:
# Get products with count
GET /api/products?$count=true
Response :
{
"@odata.count" : 150 ,
"value" : [ /* products */ ]
}
Advanced Scenarios
Complex Filtering
# Products that are either:
# - In Electronics category with price > 500, OR
# - In Computers category regardless of price
GET /api/products?$filter=(category eq 'Electronics' and price gt 500) or (category eq 'Computers')
# Orders placed in the last 30 days by active customers
GET /api/orders?$filter=orderDate ge 2024-01-01 and customer/isActive eq true
# Products with any tag containing 'sale'
GET /api/products?$filter=tags/any(t: contains(t, 'sale'))
Collection Filtering
# Orders that have any item with quantity > 5
GET /api/orders?$filter=items/any(i: i/quantity gt 5)
# Orders where all items are in stock
GET /api/orders?$filter=items/all(i: i/inStock eq true)
# Products with at least 3 reviews
GET /api/products?$filter=reviews/count() ge 3
Combining Operations
# Complete example: filtered, sorted, paged
GET /api/products?
$filter=category eq 'Electronics' and price gt 100
& $orderby = price desc
& $skip = 0
& $top = 20
& $expand = reviews
& $count = true
Configuration
Query Options
Configure allowed query options:
[ HttpGet ]
[ EnableQuery (
AllowedQueryOptions = AllowedQueryOptions . Filter |
AllowedQueryOptions . OrderBy |
AllowedQueryOptions . Top |
AllowedQueryOptions . Skip
)]
public IQueryable < ProductDto > GetAll ()
{
return _repository . GetAll ();
}
Max Results
Limit maximum results:
[ HttpGet ]
[ EnableQuery ( PageSize = 50 , MaxTop = 100 )]
public IQueryable < ProductDto > GetAll ()
{
return _repository . GetAll ();
}
Allowed Properties
Restrict filterable/sortable properties:
[ HttpGet ]
[ EnableQuery (
AllowedOrderByProperties = "name,price,createdDate" ,
AllowedFilterProperties = "category,inStock,price"
)]
public IQueryable < ProductDto > GetAll ()
{
return _repository . GetAll ();
}
Entity Framework Integration
OData queries are translated to SQL:
[ HttpGet ]
[ EnableQuery ]
public IQueryable < OrderDto > GetOrders ()
{
return _dbContext . Orders
. Include ( x => x . Customer )
. Include ( x => x . Items )
. ThenInclude ( x => x . Product )
. ProjectTo < OrderDto >( _mapper . ConfigurationProvider );
}
Query:
GET /api/orders?$filter=customer/name eq 'Acme Corp'&$orderby=orderDate desc&$top=10
Generated SQL:
SELECT TOP 10 *
FROM Orders o
INNER JOIN Customers c ON o . CustomerId = c . Id
WHERE c . Name = 'Acme Corp'
ORDER BY o . OrderDate DESC
Cosmos DB Integration
Works with Cosmos DB repositories:
[ HttpGet ]
[ EnableQuery ]
public IQueryable < ProductDto > GetProducts ()
{
return _cosmosRepository . Query ()
. ProjectTo < ProductDto >( _mapper . ConfigurationProvider );
}
Client Examples
TypeScript/JavaScript
class ProductService {
async getProducts ( options : {
filter ?: string ;
orderBy ?: string ;
skip ?: number ;
top ?: number ;
}) : Promise < ProductDto []> {
const params = new URLSearchParams ();
if ( options . filter ) params . append ( '$filter' , options . filter );
if ( options . orderBy ) params . append ( '$orderby' , options . orderBy );
if ( options . skip ) params . append ( '$skip' , options . skip . toString ());
if ( options . top ) params . append ( '$top' , options . top . toString ());
const response = await fetch ( `/api/products? ${ params } ` );
return await response . json ();
}
async getElectronicsUnder500 () : Promise < ProductDto []> {
return this . getProducts ({
filter: "category eq 'Electronics' and price lt 500" ,
orderBy: 'price desc' ,
top: 20
});
}
}
C# Client
public class ProductClient
{
private readonly HttpClient _httpClient ;
public async Task < List < ProductDto >> GetProductsAsync (
string ? filter = null ,
string ? orderBy = null ,
int ? skip = null ,
int ? top = null )
{
var query = new List < string >();
if ( filter != null ) query . Add ( $"$filter= { Uri . EscapeDataString ( filter )} " );
if ( orderBy != null ) query . Add ( $"$orderby= { Uri . EscapeDataString ( orderBy )} " );
if ( skip . HasValue ) query . Add ( $"$skip= { skip } " );
if ( top . HasValue ) query . Add ( $"$top= { top } " );
var url = $"/api/products? { string . Join ( "&" , query )} " ;
return await _httpClient . GetFromJsonAsync < List < ProductDto >>( url );
}
}
Best Practices
Validate and sanitize filter inputs
Limit maximum page size
Restrict allowed properties
Implement authorization checks
Document supported OData operations
Provide examples in Swagger/Scalar
Return meaningful error messages
Support both OData and standard queries
Troubleshooting
Ensure return type is IQueryable<T> not List<T>
Verify [EnableQuery] attribute is present
Check query option is allowed in settings
Review query syntax for typos
Check property names match DTO
Verify data types in filter
Ensure properties are allowed
Review operator syntax
Installation
Intent.AspNetCore.ODataQuery
Dependencies
Intent.Application.AutoMapper
Intent.Common.CSharp
Intent.Modelers.Services
Intent.Modelers.Services.CQRS
Next Steps
Controllers Create OData-enabled controllers
Entity Framework Optimize queries for EF Core
AutoMapper Project entities to DTOs