Route Templates
Controller-based queries use standard ASP.NET Core routing to define URL patterns and bind parameters from the URL path.
Basic Route Configuration
Section titled “Basic Route Configuration”Use the [Route] attribute on your controller class to define the base route:
[Route("api/accounts")]public class Accounts : Controller{ readonly IMongoCollection<DebitAccount> _collection;
public Accounts(IMongoCollection<DebitAccount> collection) => _collection = collection;
[HttpGet] public IEnumerable<DebitAccount> GetAll() { /* ... */ }
[HttpGet("{id}")] public DebitAccount GetById(AccountId id) { /* ... */ }}Route Parameters
Section titled “Route Parameters”Single Parameter
Section titled “Single Parameter”Route parameters are defined with curly braces in the route template:
[Route("api/accounts")]public class Accounts : Controller{ [HttpGet("{id}")] public DebitAccount GetById(AccountId id) { return _collection.Find(a => a.Id == id).FirstOrDefault(); }
[HttpGet("{id}/balance")] public decimal GetBalance(AccountId id) { var account = _collection.Find(a => a.Id == id).FirstOrDefault(); return account?.Balance ?? 0; }}Multiple Parameters
Section titled “Multiple Parameters”Routes can include multiple parameters:
[HttpGet("owner/{ownerId}/account/{accountId}")]public DebitAccount GetAccountByOwner(CustomerId ownerId, AccountId accountId){ return _collection.Find(a => a.Owner == ownerId && a.Id == accountId).FirstOrDefault();}
[HttpGet("date/{year}/{month}")]public IEnumerable<DebitAccount> GetAccountsByDate(int year, int month){ // Implementation for date-based filtering return _collection.Find(_ => true).ToList();}Named Routes
Section titled “Named Routes”You can name routes for URL generation:
[HttpGet("{id}", Name = "GetAccount")]public DebitAccount GetById(AccountId id){ return _collection.Find(a => a.Id == id).FirstOrDefault();}Route Constraints
Section titled “Route Constraints”Add constraints to route parameters to improve matching:
[Route("api/accounts")]public class Accounts : Controller{ // Only match numeric IDs [HttpGet("{id:int}")] public DebitAccount GetByNumericId(int id) { /* ... */ }
// Only match GUID format [HttpGet("{id:guid}")] public DebitAccount GetByGuidId(Guid id) { /* ... */ }
// Minimum length constraint [HttpGet("name/{name:minlength(3)}")] public IEnumerable<DebitAccount> GetByName(string name) { /* ... */ }
// Range constraint [HttpGet("page/{pageNumber:int:min(1)}")] public IEnumerable<DebitAccount> GetPage(int pageNumber) { /* ... */ }}Optional Parameters
Section titled “Optional Parameters”Make route parameters optional with a question mark:
[HttpGet("owner/{ownerId}/category/{category?}")]public IEnumerable<DebitAccount> GetByOwnerAndCategory(CustomerId ownerId, string? category = null){ if (string.IsNullOrEmpty(category)) { return _collection.Find(a => a.Owner == ownerId).ToList(); }
// Filter by category if provided return _collection.Find(a => a.Owner == ownerId /* && category filter */).ToList();}Action-Specific Routes
Section titled “Action-Specific Routes”Override the controller route for specific actions:
[Route("api/accounts")]public class Accounts : Controller{ [HttpGet] public IEnumerable<DebitAccount> GetAll() { /* ... */ }
[HttpGet("search")] public IEnumerable<DebitAccount> Search([FromQuery] string term) { /* ... */ }
[HttpGet("by-owner/{ownerId}")] public IEnumerable<DebitAccount> GetByOwner(CustomerId ownerId) { /* ... */ }
// Complete override of the base route [HttpGet("~/api/special/accounts/summary")] public AccountSummary GetSummary() { /* ... */ }}Complex Route Patterns
Section titled “Complex Route Patterns”Hierarchical Resources
Section titled “Hierarchical Resources”Model parent-child relationships in your routes:
[Route("api/customers/{customerId}/accounts")]public class CustomerAccounts : Controller{ [HttpGet] public IEnumerable<DebitAccount> GetAccountsByCustomer(CustomerId customerId) { return _collection.Find(a => a.Owner == customerId).ToList(); }
[HttpGet("{accountId}")] public DebitAccount GetCustomerAccount(CustomerId customerId, AccountId accountId) { return _collection.Find(a => a.Owner == customerId && a.Id == accountId).FirstOrDefault(); }
[HttpGet("{accountId}/transactions")] public IEnumerable<Transaction> GetAccountTransactions(CustomerId customerId, AccountId accountId) { // Implementation for getting transactions return new List<Transaction>(); }}Multiple Route Templates
Section titled “Multiple Route Templates”An action can have multiple route templates:
[Route("api/accounts")]public class Accounts : Controller{ [HttpGet("search")] [HttpGet("find")] // Alternative route public IEnumerable<DebitAccount> Search([FromQuery] string term) { var filter = Builders<DebitAccount>.Filter.Regex( a => a.Name, new BsonRegularExpression(term, "i"));
return _collection.Find(filter).ToList(); }}Route Values and Concepts
Section titled “Route Values and Concepts”When using Cratis concepts (value objects), the route binding works seamlessly:
// The AccountId concept is automatically bound from the route parameter[HttpGet("{id}")]public DebitAccount GetAccount(AccountId id){ return _collection.Find(a => a.Id == id).FirstOrDefault();}
// Multiple concept parameters[HttpGet("owner/{ownerId}/account/{accountId}")]public decimal GetAccountBalanceForOwner(CustomerId ownerId, AccountId accountId){ var account = _collection.Find(a => a.Owner == ownerId && a.Id == accountId).FirstOrDefault(); return account?.Balance ?? 0;}Route Tokens
Section titled “Route Tokens”Use route tokens for common patterns:
// Using [controller] token[Route("api/[controller]")]public class Accounts : Controller{ // Matches: /api/accounts
[HttpGet("[action]")] public IEnumerable<DebitAccount> GetAll() { /* ... */ } // Matches: /api/accounts/GetAll}Query String vs Route Parameters
Section titled “Query String vs Route Parameters”Choose between route parameters and query strings based on the data’s role:
Route Parameters (part of the resource identity)
Section titled “Route Parameters (part of the resource identity)”// Account ID is part of the resource identity[HttpGet("{id}")]public DebitAccount GetAccount(AccountId id) { /* ... */ }
// Owner ID identifies a specific subset[HttpGet("owner/{ownerId}")]public IEnumerable<DebitAccount> GetByOwner(CustomerId ownerId) { /* ... */ }Query String Parameters (filtering/options)
Section titled “Query String Parameters (filtering/options)”// Filtering options[HttpGet("search")]public IEnumerable<DebitAccount> Search( [FromQuery] string? name = null, [FromQuery] decimal? minBalance = null, [FromQuery] bool includeInactive = false){ // Apply filters based on query parameters return _collection.Find(_ => true).ToList();}Best Practices
Section titled “Best Practices”- Use meaningful route patterns - Routes should be intuitive and RESTful
- Keep routes simple - Avoid overly complex route templates
- Use constraints - Add route constraints to improve matching accuracy
- Be consistent - Use consistent naming and structure across your API
- Consider hierarchy - Use hierarchical routes for parent-child relationships
- Route parameters for identity - Use route parameters for resource identifiers
- Query strings for filtering - Use query strings for optional filters and options
Example: Complete RESTful Route Structure
Section titled “Example: Complete RESTful Route Structure”[Route("api/accounts")]public class Accounts : Controller{ // GET /api/accounts [HttpGet] public IEnumerable<DebitAccount> GetAll() { /* ... */ }
// GET /api/accounts/{id} [HttpGet("{id}")] public DebitAccount GetById(AccountId id) { /* ... */ }
// GET /api/accounts/search?name=john&minBalance=100 [HttpGet("search")] public IEnumerable<DebitAccount> Search( [FromQuery] string? name = null, [FromQuery] decimal? minBalance = null) { /* ... */ }
// GET /api/accounts/owner/{ownerId} [HttpGet("owner/{ownerId}")] public IEnumerable<DebitAccount> GetByOwner(CustomerId ownerId) { /* ... */ }
// GET /api/accounts/{id}/balance [HttpGet("{id}/balance")] public decimal GetBalance(AccountId id) { /* ... */ }}