Table of Contents

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

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

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

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

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

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

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

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

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

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

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

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

Choose between route parameters and query strings based on the data's role:

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)

// 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

  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

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