This guide provides detailed information on how to use the REST-Parser library in your .NET applications.
- Installation
- Quick Start
- Query Syntax Reference
- Operators
- Filtering Examples
- Sorting Examples
- Pagination Examples
- Advanced Usage
- Exception Handling
- API Reference
- Best Practices
- Troubleshooting
Install-Package REST-Parserdotnet add package REST-Parser<PackageReference Include="REST-Parser" Version="1.2.5" />public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public DateTime ReleaseDate { get; set; }
public bool IsActive { get; set; }
public string? Description { get; set; }
public double? Rating { get; set; }
}using REST_Parser.DependencyResolution;
// In Program.cs or Startup.cs
builder.Services.RegisterRestParser<Product>();using REST_Parser;
using REST_Parser.Models;
public class ProductService
{
private readonly IRestToLinqParser<Product> _parser;
private readonly AppDbContext _context;
public ProductService(IRestToLinqParser<Product> parser, AppDbContext context)
{
_parser = parser;
_context = context;
}
public RestResult<Product> GetProducts(string query)
{
// Parse and execute the query
return _parser.Run(_context.Products, query);
}
}GET /api/products?category=Electronics&price[lt]=1000&$sort_by=price[ASC]&$page=1&$pagesize=20field[operator]=value
Use & to separate conditions:
field1=value1&field2[operator]=value2&field3[operator]=value3
- Sorting:
$sort_by=field[ASC|DESC] - Pagination:
$page=nand$pagesize=n
Whitespace is automatically trimmed:
field [eq] = value ✅ Valid
field[eq]=value ✅ Valid
field = value ✅ Valid (defaults to eq)
| Operator | Description | Supported Types |
|---|---|---|
eq |
Equal to (default) | All types |
ne |
Not equal to | All types |
gt |
Greater than | int, double, decimal, DateTime |
ge |
Greater than or equal | int, double, decimal, DateTime |
lt |
Less than | int, double, decimal, DateTime |
le |
Less than or equal | int, double, decimal, DateTime |
| Operator | Description | Example |
|---|---|---|
eq |
Exact match (case-sensitive) | name[eq]=iPhone |
ne |
Not equal | name[ne]=Samsung |
contains |
Contains substring (case-sensitive) | name[contains]=Pro |
- ✅
string - ✅
int/int? - ✅
double/double? - ✅
decimal/decimal? - ✅
DateTime/DateTime? - ✅
bool/bool? - ✅
Guid/Guid?
// Exact match (default operator)
"name=iPhone"
"name[eq]=iPhone"
// Not equal
"category[ne]=Electronics"
// Contains (case-sensitive)
"description[contains]=wireless"
// Multiple string conditions
"category=Electronics&brand=Apple"REST Query:
GET /api/products?name[contains]=Pro&category=Electronics// Integer
"stock[gt]=10" // Stock greater than 10
"stock[le]=100" // Stock less than or equal to 100
"id=42" // ID equals 42 (default operator)
// Decimal/Double
"price[lt]=999.99" // Price less than 999.99
"rating[ge]=4.5" // Rating greater than or equal to 4.5REST Query:
GET /api/products?price[gt]=100&price[lt]=1000&stock[gt]=0// Standard date formats
"releaseDate[gt]=2023-01-01"
"releaseDate[lt]=2024-12-31"
// Date ranges
"releaseDate[ge]=2023-01-01&releaseDate[le]=2023-12-31"REST Query:
GET /api/products?releaseDate[gt]=2023-06-01&isActive=true// Boolean values
"isActive=true"
"isActive[eq]=false"
"isDiscontinued[ne]=true"REST Query:
GET /api/products?isActive=true&isFeatured=true// GUID equality
"productId[eq]=123e4567-e89b-12d3-a456-426614174000"
"productId[ne]=123e4567-e89b-12d3-a456-426614174000"// Works with nullable types
"rating[ge]=4.0" // double?
"discount[gt]=10" // decimal?
"lastPurchaseDate[lt]=2024-01-01" // DateTime?// Ascending (default)
"$sort_by=name"
"$sort_by=name[ASC]"
// Descending
"$sort_by=price[DESC]"REST Query:
GET /api/products?$sort_by=price[DESC]// Sort by category ascending, then price descending
"$sort_by=category[ASC]&$sort_by=price[DESC]"
// Sort by rating descending, then name ascending
"$sort_by=rating[DESC]&$sort_by=name[ASC]"REST Query:
GET /api/products?category=Electronics&$sort_by=brand[ASC]&$sort_by=price[ASC]If no $sort_by is specified, results are automatically sorted by Id ascending:
// These are equivalent
""
"$sort_by=Id[ASC]"// Get page 1 with 20 items
"$page=1&$pagesize=20"
// Get page 2 with 50 items
"$page=2&$pagesize=50"REST Query:
GET /api/products?$page=2&$pagesize=25// Complex query with all features
"category=Electronics&price[lt]=1000&$sort_by=price[ASC]&$page=1&$pagesize=20"REST Query:
GET /api/products?category=Electronics&isActive=true&$sort_by=name[ASC]&$page=1&$pagesize=10- Default Page: 1 (if
$pageis specified without value) - Default Page Size: 25 (if
$pagesizeis specified without value) - Maximum Page Size: 1000 (enforced by the parser)
The RestResult<T> includes pagination information:
var result = _parser.Run(_context.Products, query);
Console.WriteLine($"Page: {result.Page}");
Console.WriteLine($"Page Size: {result.PageSize}");
Console.WriteLine($"Total Count: {result.TotalCount}");
Console.WriteLine($"Total Pages: {result.PageCount}");
// Access the data
var products = result.Data.ToList();// Parse the query without executing it
var parseResult = _parser.Parse("category=Electronics&price[lt]=1000");
// Inspect what was parsed
Console.WriteLine($"Number of filters: {parseResult.Expressions.Count}");
Console.WriteLine($"Number of sorts: {parseResult.SortOrder.Count}");
Console.WriteLine($"Page: {parseResult.Page}, PageSize: {parseResult.PageSize}");
// Apply manually with custom logic
IQueryable<Product> query = _context.Products;
// Apply your own pre-filters
query = query.Where(p => p.IsActive);
// Apply parsed expressions
foreach (var expression in parseResult.Expressions)
{
query = query.Where(expression);
}
// Apply sorting
var orderedQuery = query.OrderBy(parseResult.SortOrder[0].Expression);
// ... etc// Parse and execute in one call
var result = _parser.Run(_context.Products, "category=Electronics&price[lt]=1000");
// Data is already filtered, sorted, and paginated
var products = result.Data.ToList();// Parse the user's query
var result = _parser.Parse(userQuery);
// Start with your base query
IQueryable<Product> query = _context.Products
.Where(p => p.IsActive) // Always filter active
.Where(p => p.TenantId == tenantId); // Tenant isolation
// Apply user's filters
foreach (var expression in result.Expressions)
{
query = query.Where(expression);
}
// Continue with sorting and pagination...public async Task<RestResult<Product>> GetProductsAsync(string query)
{
var result = _parser.Run(_context.Products.AsNoTracking(), query);
// Materialize the query
result.Data = result.Data.ToList().AsQueryable();
return result;
}var result = _parser.Run(_context.Products, query);
// Project to DTOs to reduce data transfer
var data = result.Data
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
Category = p.Category
})
.ToList();// Register multiple parsers
builder.Services.RegisterRestParser<Product>();
builder.Services.RegisterRestParser<Customer>();
builder.Services.RegisterRestParser<Order>();
// Inject specific parsers
public class MultiService
{
private readonly IRestToLinqParser<Product> _productParser;
private readonly IRestToLinqParser<Customer> _customerParser;
public MultiService(
IRestToLinqParser<Product> productParser,
IRestToLinqParser<Customer> customerParser)
{
_productParser = productParser;
_customerParser = customerParser;
}
}The library throws three custom exceptions:
using REST_Parser.Exceptions;
try
{
var result = _parser.Run(_context.Products, query);
return Ok(result.Data.ToList());
}
catch (REST_InvalidFieldnameException ex)
{
// Field doesn't exist on the entity
// Example: "invalidField=value"
return BadRequest(new { error = "Invalid field", details = ex.Message });
}
catch (REST_InvalidOperatorException ex)
{
// Operator not supported for the field type
// Example: "name[gt]=test" (gt not valid for strings)
return BadRequest(new { error = "Invalid operator", details = ex.Message });
}
catch (REST_InvalidValueException ex)
{
// Value cannot be converted to the field's type
// Example: "price=notanumber"
return BadRequest(new { error = "Invalid value", details = ex.Message });
}
catch (ArgumentException ex)
{
// Security limits exceeded
// - Query too long (>2000 chars)
// - Too many conditions (>50)
// - Invalid condition format
return BadRequest(new { error = "Invalid query", details = ex.Message });
}[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IRestToLinqParser<Product> _parser;
private readonly AppDbContext _context;
public ProductsController(IRestToLinqParser<Product> parser, AppDbContext context)
{
_parser = parser;
_context = context;
}
[HttpGet]
public IActionResult Get([FromQuery] string q)
{
// Provide default if empty
if (string.IsNullOrWhiteSpace(q))
{
q = "$sort_by=Id&$page=1&$pagesize=20";
}
try
{
var result = _parser.Run(_context.Products, q);
return Ok(new
{
data = result.Data.ToList(),
pagination = new
{
page = result.Page,
pageSize = result.PageSize,
totalCount = result.TotalCount,
totalPages = result.PageCount
}
});
}
catch (REST_InvalidFieldnameException ex)
{
return BadRequest(new { error = "Invalid field name", message = ex.Message });
}
catch (REST_InvalidOperatorException ex)
{
return BadRequest(new { error = "Invalid operator", message = ex.Message });
}
catch (REST_InvalidValueException ex)
{
return BadRequest(new { error = "Invalid value", message = ex.Message });
}
catch (ArgumentException ex)
{
return BadRequest(new { error = "Invalid query format", message = ex.Message });
}
catch (Exception ex)
{
// Log the exception
return StatusCode(500, new { error = "An error occurred processing your request" });
}
}
}RestResult<T> Parse(string request)
Parses a REST query string without executing it.
- Parameters:
request- The REST query string
- Returns:
RestResult<T>with parsed expressions and settings - Throws:
ArgumentException- Query exceeds limitsREST_InvalidFieldnameException- Invalid field nameREST_InvalidOperatorException- Invalid operatorREST_InvalidValueException- Invalid value
RestResult<T> Run(IQueryable<T> source, string rest)
Parses and executes a REST query against a data source.
- Parameters:
source- The IQueryable data sourcerest- The REST query string
- Returns:
RestResult<T>with executed data and metadata - Throws: Same as
Parse()
List<Expression<Func<T, bool>>> Expressions- Filter expressionsList<SortBy<T>> SortOrder- Sort operationsIQueryable<T> Data- Query result (only populated byRun())int Page- Current page number (1-based)int PageSize- Items per pageint PageCount- Total number of pagesint TotalCount- Total items matching filters
Expression<Func<T, object>> Expression- Sort expressionbool Ascending- True for ascending, false for descending
[HttpGet]
public IActionResult Get([FromQuery] string q = "")
{
if (string.IsNullOrWhiteSpace(q))
{
q = "$sort_by=Id&$pagesize=20"; // Sensible defaults
}
// Use try-catch for exception handling
// ...
}var result = _parser.Run(_context.Products, query);
// The parser already enforces MAX_PAGE_SIZE (1000)
// But you can add your own stricter limit
const int MAX_ALLOWED_PAGE_SIZE = 100;
if (result.PageSize > MAX_ALLOWED_PAGE_SIZE)
{
return BadRequest($"Page size cannot exceed {MAX_ALLOWED_PAGE_SIZE}");
}var result = _parser.Run(_context.Products, query);
var response = new
{
data = result.Data.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
}).ToList(),
page = result.Page,
pageSize = result.PageSize,
totalCount = result.TotalCount,
totalPages = result.PageCount
};
return Ok(response);// Parse the query
var parsed = _parser.Parse(query);
// Apply tenant filter first
IQueryable<Product> data = _context.Products
.Where(p => p.TenantId == currentTenantId);
// Then apply user's filters
foreach (var expr in parsed.Expressions)
{
data = data.Where(expr);
}var result = _parser.Run(
_context.Products.AsNoTracking(),
query
);catch (REST_InvalidFieldnameException ex)
{
_logger.LogWarning(ex, "Invalid field in query: {Query}", query);
return BadRequest(new { error = ex.Message });
}// Use distributed cache for common queries
var cacheKey = $"products:{query}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
{
return JsonSerializer.Deserialize<ProductListResponse>(cached);
}
var result = _parser.Run(_context.Products, query);
await _cache.SetStringAsync(cacheKey,
JsonSerializer.Serialize(result),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});Cause: Query string is longer than 2000 characters.
Solution: Simplify your query or contact support if you need a larger limit.
Cause: More than 50 filter conditions in the query.
Solution: Reduce the number of conditions or use server-side filtering.
Cause: Field doesn't exist on the entity.
Solution: Check spelling and ensure the property exists:
public class Product
{
public int Id { get; set; } // Use: id=1
public string Name { get; set; } // Use: name=iPhone
}Cause: Using an operator not supported for that type.
Solution:
- Strings: Only
eq,ne,contains - Numbers/Dates:
eq,ne,gt,ge,lt,le - Booleans: Only
eq,ne
Cause: Value cannot be converted to the field's type.
Solution: Ensure value matches the field type:
price=999.99 // ✅ Correct for decimal
price=abc // ❌ Invalid
releaseDate=2023-01-01 // ✅ Correct for DateTime
releaseDate=notadate // ❌ InvalidCause: String comparisons are case-sensitive.
Solution:
name=iPhone // Matches "iPhone" but not "iphone"
name[contains]=pro // Matches "MacBook Pro" but not "MacBook PRO"If you need case-insensitive search, handle it on the server:
var result = _parser.Parse(query);
IQueryable<Product> data = _context.Products;
// Apply case-insensitive filter manually
data = data.Where(p => p.Name.ToLower().Contains(searchTerm.ToLower()));
// Then apply other filters
foreach (var expr in result.Expressions)
{
data = data.Where(expr);
}Possible causes:
- Filters are too restrictive
- Data doesn't exist
- Tenant/user isolation filters
Debug:
var result = _parser.Parse(query);
Console.WriteLine($"Filters: {result.Expressions.Count}");
Console.WriteLine($"Sort: {result.SortOrder.Count}");
// Test without filters
var allData = _context.Products.ToList();
Console.WriteLine($"Total records: {allData.Count}");The parser enforces the following limits to prevent abuse:
| Limit | Value | Description |
|---|---|---|
| MAX_QUERY_LENGTH | 2000 | Maximum query string length |
| MAX_CONDITIONS | 50 | Maximum number of filter conditions |
| MAX_PAGE_SIZE | 1000 | Maximum page size |
These limits are enforced automatically and will throw ArgumentException if exceeded.
Here's a complete working example:
// Entity
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public DateTime ReleaseDate { get; set; }
public bool IsActive { get; set; }
}
// Startup/Program.cs
builder.Services.AddDbContext<AppDbContext>();
builder.Services.RegisterRestParser<Product>();
// Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IRestToLinqParser<Product> _parser;
private readonly AppDbContext _context;
private readonly ILogger<ProductsController> _logger;
public ProductsController(
IRestToLinqParser<Product> parser,
AppDbContext context,
ILogger<ProductsController> logger)
{
_parser = parser;
_context = context;
_logger = logger;
}
[HttpGet]
public IActionResult Get([FromQuery] string q = "")
{
try
{
// Default query if none provided
if (string.IsNullOrWhiteSpace(q))
{
q = "$sort_by=Id&$pagesize=20";
}
// Parse and execute
var result = _parser.Run(_context.Products.AsNoTracking(), q);
// Build response
return Ok(new
{
data = result.Data.Select(p => new
{
p.Id,
p.Name,
p.Category,
p.Price,
p.Stock
}).ToList(),
pagination = new
{
page = result.Page,
pageSize = result.PageSize,
totalCount = result.TotalCount,
totalPages = result.PageCount
}
});
}
catch (REST_InvalidFieldnameException ex)
{
_logger.LogWarning(ex, "Invalid field in query: {Query}", q);
return BadRequest(new { error = "Invalid field name", message = ex.Message });
}
catch (REST_InvalidOperatorException ex)
{
_logger.LogWarning(ex, "Invalid operator in query: {Query}", q);
return BadRequest(new { error = "Invalid operator", message = ex.Message });
}
catch (REST_InvalidValueException ex)
{
_logger.LogWarning(ex, "Invalid value in query: {Query}", q);
return BadRequest(new { error = "Invalid value", message = ex.Message });
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Invalid query format: {Query}", q);
return BadRequest(new { error = "Invalid query", message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing query: {Query}", q);
return StatusCode(500, new { error = "An error occurred processing your request" });
}
}
}
// Sample Requests
// GET /api/products
// GET /api/products?category=Electronics
// GET /api/products?price[gt]=100&price[lt]=1000
// GET /api/products?name[contains]=Pro&isActive=true
// GET /api/products?category=Electronics&$sort_by=price[ASC]&$page=1&$pagesize=10- GitHub Repository: https://github.com/BigBadJock/REST-Parser
- NuGet Package: https://www.nuget.org/packages/REST-Parser/
- Report Issues: https://github.com/BigBadJock/REST-Parser/issues
Last Updated: 2025
Library Version: 1.2.5
Target Framework: .NET 10