Skip to content

Route Templates

Controller-based queries use standard ASP.NET Core routing to define URL patterns and bind parameters from the URL path.

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 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;
}
}

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();
}

You can name routes for URL generation:

[HttpGet("{id}", Name = "GetAccount")]
public DebitAccount GetById(AccountId id)
{
return _collection.Find(a => a.Id == id).FirstOrDefault();
}

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) { /* ... */ }
}

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();
}

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() { /* ... */ }
}

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>();
}
}

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();
}
}

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;
}

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
}

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();
}
  1. Use meaningful route patterns - Routes should be intuitive and RESTful
  2. Keep routes simple - Avoid overly complex route templates
  3. Use constraints - Add route constraints to improve matching accuracy
  4. Be consistent - Use consistent naming and structure across your API
  5. Consider hierarchy - Use hierarchical routes for parent-child relationships
  6. Route parameters for identity - Use route parameters for resource identifiers
  7. Query strings for filtering - Use query strings for optional filters and options
[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) { /* ... */ }
}