Skip to content

Controller query route templates

MVC combines the controller’s [Route] with the action’s [HttpGet] template. Unlike model-bound [Path] queries, MVC reads values from route placeholders.

This alternative banking example uses the shared domain concepts and [AspNetResult] so its not-found response has ordinary MVC 404 semantics instead of Arc’s null-result failure. It requires an ASP.NET Core Arc host, configured authorization, and the MongoDB provider.

using System;
using System.Threading.Tasks;
using Cratis.Arc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
namespace Banking.Accounts;
public record DebitAccount(AccountId Id, AccountName Name, decimal Balance);
[Authorize(Roles = "AccountReader")]
[Route("api/accounts")]
public class AccountsController(IMongoCollection<DebitAccount> collection) : ControllerBase
{
[AspNetResult]
[HttpGet("{id:guid}", Name = "AccountById")]
public async Task<ActionResult<DebitAccount>> ById([FromRoute] Guid id)
{
AccountId accountId = id;
var account = await collection.Find(candidate => candidate.Id == accountId).FirstOrDefaultAsync();
if (account is null)
{
return NotFound();
}
return Ok(account);
}
}

GET /api/accounts/11111111-1111-1111-1111-111111111111 binds a GUID at the HTTP boundary. The action converts it to AccountId before the domain-specific query. The action returns a raw account or 404, not a QueryResult. Use a client expecting that raw contract; see without wrappers.

These are route-template fragments, not independent controller declarations:

TemplateUse
api/accounts on the controller, {id} on the actionA resource under the controller path
{id:guid}Match only GUID-shaped route values
{id:int}Match only integer-shaped route values
category/{category?}Optional final segment; give the action argument a compatible default
~/api/account-summaryOverride rather than append to the controller path
api/[controller]Substitute the MVC controller name
[action]Substitute the action name

Constraints choose which endpoint matches; they are not a replacement for business validation or authorization. A nested path such as /customers/{customerId}/accounts/{accountId} does not prove ownership. Check the authenticated caller’s access and apply both identifiers in the actual database predicate.

Use route values for resource identity and query strings for filters. Keep the predicate consistent with the declared inputs: do not accept a category/date/filter and then return an unfiltered collection.

For the simpler static-query alternative, use model-bound arguments and explicit paths. For MVC collection/DTO binding, continue with query arguments.