Query Arguments
Model-bound queries can accept arguments as method parameters. Arguments are automatically bound from the HTTP request and can include route parameters, query string parameters, or complex objects.
Method Parameters
Arguments are passed as method parameters and are automatically bound from the HTTP request:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static DebitAccount GetAccountById(
AccountId id,
IMongoCollection<DebitAccount> collection)
{
return collection.Find(a => a.Id == id).FirstOrDefault();
}
public static IEnumerable<DebitAccount> SearchAccounts(
string nameFilter,
decimal? minBalance,
IMongoCollection<DebitAccount> collection,
ILogger<DebitAccount> logger)
{
logger.LogInformation("Searching accounts with filter: {Filter}", nameFilter);
var filterBuilder = Builders<DebitAccount>.Filter;
var filters = new List<FilterDefinition<DebitAccount>>();
if (!string.IsNullOrEmpty(nameFilter))
{
filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(nameFilter, "i")));
}
if (minBalance.HasValue)
{
filters.Add(filterBuilder.Gte(a => a.Balance, minBalance.Value));
}
var combinedFilter = filters.Any()
? filterBuilder.And(filters)
: filterBuilder.Empty;
return collection.Find(combinedFilter).ToList();
}
}
Argument Types
Model-bound queries support various argument types:
Primitive Types
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static IEnumerable<DebitAccount> GetAccountsByBalance(
decimal balance,
bool exactMatch,
IMongoCollection<DebitAccount> collection)
{
return exactMatch
? collection.Find(a => a.Balance == balance).ToList()
: collection.Find(a => a.Balance >= balance).ToList();
}
}
Concept Types
Using concept types (value objects) for stronger typing:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static IEnumerable<DebitAccount> GetAccountsByOwnerConcept(
CustomerId ownerId,
IMongoCollection<DebitAccount> collection)
{
return collection.Find(a => a.Owner == ownerId).ToList();
}
}
Enums
public enum AccountStatus { Active, Inactive, Suspended }
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static IEnumerable<DebitAccount> GetAccountsByStatus(
AccountStatus status,
IMongoCollection<DebitAccount> collection)
{
// Implement status filtering logic
return status switch
{
AccountStatus.Active => collection.Find(a => a.Balance > 0).ToList(),
AccountStatus.Inactive => collection.Find(a => a.Balance == 0).ToList(),
AccountStatus.Suspended => collection.Find(a => a.Balance < 0).ToList(),
_ => collection.Find(_ => false).ToList()
};
}
}
Collection Arguments
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static IEnumerable<DebitAccount> GetAccountsByIds(
IEnumerable<AccountId> ids,
IMongoCollection<DebitAccount> collection)
{
return collection.Find(a => ids.Contains(a.Id)).ToList();
}
public static IEnumerable<DebitAccount> GetAccountsByOwners(
List<CustomerId> ownerIds,
IMongoCollection<DebitAccount> collection)
{
return collection.Find(a => ownerIds.Contains(a.Owner)).ToList();
}
}
Nullable Arguments
Optional arguments should be nullable:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static IEnumerable<DebitAccount> FlexibleSearch(
string? name,
CustomerId? ownerId,
decimal? minBalance,
decimal? maxBalance,
IMongoCollection<DebitAccount> collection)
{
var filterBuilder = Builders<DebitAccount>.Filter;
var filters = new List<FilterDefinition<DebitAccount>>();
if (!string.IsNullOrEmpty(name))
filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(name, "i")));
if (ownerId.HasValue)
filters.Add(filterBuilder.Eq(a => a.Owner, ownerId.Value));
if (minBalance.HasValue)
filters.Add(filterBuilder.Gte(a => a.Balance, minBalance.Value));
if (maxBalance.HasValue)
filters.Add(filterBuilder.Lte(a => a.Balance, maxBalance.Value));
var combinedFilter = filters.Any()
? filterBuilder.And(filters)
: filterBuilder.Empty;
return collection.Find(combinedFilter).ToList();
}
}
Default Values
Provide sensible default values for optional parameters:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static IEnumerable<DebitAccount> GetPagedAccounts(
int page = 0,
int pageSize = 50,
string sortBy = "name",
bool ascending = true,
IMongoCollection<DebitAccount> collection)
{
var query = collection.Find(_ => true);
// Apply sorting
query = ascending
? query.SortBy(sortBy)
: query.SortByDescending(sortBy);
// Apply paging
return query.Skip(page * pageSize).Limit(pageSize).ToList();
}
}
Complex Query Objects
For complex search criteria, create dedicated parameter objects:
public record AccountSearchCriteria(
string? NamePattern,
CustomerId? OwnerId,
decimal? MinBalance,
decimal? MaxBalance,
DateTime? CreatedAfter,
DateTime? CreatedBefore,
bool IncludeInactive);
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static IEnumerable<DebitAccount> SearchAccounts(
AccountSearchCriteria criteria,
IMongoCollection<DebitAccount> collection)
{
var filterBuilder = Builders<DebitAccount>.Filter;
var filters = new List<FilterDefinition<DebitAccount>>();
if (!string.IsNullOrEmpty(criteria.NamePattern))
filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(criteria.NamePattern, "i")));
if (criteria.OwnerId.HasValue)
filters.Add(filterBuilder.Eq(a => a.Owner, criteria.OwnerId.Value));
if (criteria.MinBalance.HasValue)
filters.Add(filterBuilder.Gte(a => a.Balance, criteria.MinBalance.Value));
if (criteria.MaxBalance.HasValue)
filters.Add(filterBuilder.Lte(a => a.Balance, criteria.MaxBalance.Value));
// Add date filters if the model supports them
// if (criteria.CreatedAfter.HasValue)
// filters.Add(filterBuilder.Gte(a => a.CreatedDate, criteria.CreatedAfter.Value));
if (!criteria.IncludeInactive)
filters.Add(filterBuilder.Gt(a => a.Balance, 0));
var combinedFilter = filters.Any()
? filterBuilder.And(filters)
: filterBuilder.Empty;
return collection.Find(combinedFilter).ToList();
}
}
Parameter Order
You can mix query parameters with dependency parameters. Dependencies are resolved by type, so order is flexible:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
// Query parameters first
public static IEnumerable<DebitAccount> GetAccountsByOwnerWithLogging(
CustomerId ownerId,
bool includeZeroBalance,
IMongoCollection<DebitAccount> collection,
ILogger<DebitAccount> logger)
{
logger.LogInformation("Getting accounts for owner {OwnerId}, includeZero: {IncludeZero}",
ownerId, includeZeroBalance);
var filter = includeZeroBalance
? Builders<DebitAccount>.Filter.Eq(a => a.Owner, ownerId)
: Builders<DebitAccount>.Filter.And(
Builders<DebitAccount>.Filter.Eq(a => a.Owner, ownerId),
Builders<DebitAccount>.Filter.Gt(a => a.Balance, 0));
return collection.Find(filter).ToList();
}
// Dependencies first
public static IEnumerable<DebitAccount> GetAccountsByBalanceRange(
IMongoCollection<DebitAccount> collection,
ILogger<DebitAccount> logger,
decimal minBalance,
decimal maxBalance)
{
logger.LogInformation("Getting accounts with balance between {Min} and {Max}",
minBalance, maxBalance);
var filter = Builders<DebitAccount>.Filter.And(
Builders<DebitAccount>.Filter.Gte(a => a.Balance, minBalance),
Builders<DebitAccount>.Filter.Lte(a => a.Balance, maxBalance));
return collection.Find(filter).ToList();
}
}
Validation Attributes
Use validation attributes to ensure argument quality:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static IEnumerable<DebitAccount> SearchWithValidation(
[Required] [MinLength(3)] string searchTerm,
[Range(1, 100)] int pageSize,
[Range(0, int.MaxValue)] int page,
IMongoCollection<DebitAccount> collection)
{
// Validation is automatically applied by the framework
var filter = Builders<DebitAccount>.Filter.Regex(
a => a.Name,
new BsonRegularExpression(searchTerm, "i"));
return collection.Find(filter)
.Skip(page * pageSize)
.Limit(pageSize)
.ToList();
}
}
Observable Query Arguments
Observable queries can also accept arguments:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static ISubject<IEnumerable<DebitAccount>> GetAccountsByOwnerObservable(
CustomerId ownerId,
IMongoCollection<DebitAccount> collection)
{
return collection.Observe(a => a.Owner == ownerId);
}
public static ISubject<DebitAccount> GetAccountObservable(
AccountId id,
IMongoCollection<DebitAccount> collection)
{
return collection.Observe(a => a.Id == id);
}
}
URL Binding
Arguments are automatically bound from different parts of the HTTP request:
Route Parameters
Based on the method name and parameter names, route parameters are inferred:
// This would typically map to: GET /api/debitaccount/getaccountbyid/{id}
public static DebitAccount GetAccountById(AccountId id, IMongoCollection<DebitAccount> collection)
{
return collection.Find(a => a.Id == id).FirstOrDefault();
}
Query String Parameters
Parameters that aren't in the route become query string parameters:
// This would map to: GET /api/debitaccount/searchaccounts?nameFilter=abc&minBalance=100
public static IEnumerable<DebitAccount> SearchAccounts(
string nameFilter,
decimal? minBalance,
IMongoCollection<DebitAccount> collection)
{
// Implementation...
return collection.Find(_ => true).ToList();
}
Best Practices
- Use descriptive parameter names - They become part of your API contract
- Make optional parameters nullable - Use nullable types for optional arguments
- Provide default values - For commonly used optional parameters
- Use concept types - Leverage value objects for stronger typing
- Validate inputs - Use validation attributes for parameter validation
- Keep parameter lists reasonable - For many parameters, consider using parameter objects
- Order parameters logically - Group related parameters together
- Use appropriate types - Choose the most specific type that makes sense
- Handle null inputs gracefully - Check for null values and handle appropriately
URL Generation
Arc generates URLs based on your method names and parameters:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
// GET /api/debitaccount/getallaccounts
public static IEnumerable<DebitAccount> GetAllAccounts(IMongoCollection<DebitAccount> collection) => /* ... */;
// GET /api/debitaccount/getaccountbyid/{id}
public static DebitAccount GetAccountById(AccountId id, IMongoCollection<DebitAccount> collection) => /* ... */;
// GET /api/debitaccount/searchaccounts?name={name}&minBalance={minBalance}
public static IEnumerable<DebitAccount> SearchAccounts(string? name, decimal? minBalance, IMongoCollection<DebitAccount> collection) => /* ... */;
}
Note: The proxy generator automatically creates TypeScript types for your query arguments, making them strongly typed on the frontend as well.