Return Types
Model-bound queries support various return types, allowing you to structure your API responses according to your application’s needs.
Collections
Section titled “Collections”IEnumerable<T>
Section titled “IEnumerable<T>”The most common return type for queries that return multiple records:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static IEnumerable<DebitAccount> GetAllAccounts(IMongoCollection<DebitAccount> collection) => collection.Find(_ => true);}List<T> and Arrays
Section titled “List<T> and Arrays”You can also return concrete collection types:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static List<DebitAccount> GetAccountsList(IMongoCollection<DebitAccount> collection) => collection.Find(_ => true).ToList();
public static DebitAccount[] GetAccountsArray(IMongoCollection<DebitAccount> collection) => collection.Find(_ => true).ToArray();}Single Objects
Section titled “Single Objects”Return individual records or null when not found:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static DebitAccount? GetFirstAccount(IMongoCollection<DebitAccount> collection) => collection.Find(_ => true).FirstOrDefault();
public static DebitAccount GetAccountById(AccountId id, IMongoCollection<DebitAccount> collection) => collection.Find(a => a.Id == id).FirstOrDefault();}Custom Return Types
Section titled “Custom Return Types”Create custom types for complex query results:
public record AccountSummary(int TotalAccounts, decimal TotalBalance, decimal AverageBalance);
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static AccountSummary GetAccountSummary(IMongoCollection<DebitAccount> collection) { var accounts = collection.Find(_ => true).ToList(); return new AccountSummary( accounts.Count, accounts.Sum(a => a.Balance), accounts.Count > 0 ? accounts.Average(a => a.Balance) : 0); }}Async Return Types
Section titled “Async Return Types”Wrap any return type in Task<T> for asynchronous operations:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static async Task<IEnumerable<DebitAccount>> GetAllAccountsAsync(IMongoCollection<DebitAccount> collection) { var result = await collection.FindAsync(_ => true); return result.ToList(); }
public static async Task<DebitAccount?> GetAccountByIdAsync(AccountId id, IMongoCollection<DebitAccount> collection) { var result = await collection.FindAsync(a => a.Id == id); return result.FirstOrDefault(); }
public static async Task<AccountSummary> GetAccountSummaryAsync(IMongoCollection<DebitAccount> collection) { var accounts = await collection.Find(_ => true).ToListAsync(); return new AccountSummary( accounts.Count, accounts.Sum(a => a.Balance), accounts.Count > 0 ? accounts.Average(a => a.Balance) : 0); }}Observable Return Types
Section titled “Observable Return Types”For real-time queries that push updates to subscribers, return ISubject<T>:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static ISubject<IEnumerable<DebitAccount>> GetAccountsObservable( IMongoCollection<DebitAccount> collection) => collection.Observe(); // Leveraging MongoDB Extension methods
public static ISubject<DebitAccount> GetAccountObservable( AccountId id, IMongoCollection<DebitAccount> collection) => collection.ObserveSingle(a => a.Id == id);}Important: Do not combine
Task<T>withISubject<T>. Observable methods should returnISubject<T>directly, notTask<ISubject<T>>.
Complex Business Objects
Section titled “Complex Business Objects”Return rich domain objects with computed properties:
public record AccountDetails( AccountId Id, AccountName Name, CustomerId Owner, decimal Balance, decimal AvailableCredit, AccountStatus Status, DateTime LastActivity);
public record CustomerInfo(CustomerId Id, string Name, string Email);
public record AccountWithCustomerInfo( DebitAccount Account, CustomerInfo Customer, IEnumerable<string> RecentTransactions);
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static AccountDetails GetAccountDetails( AccountId id, IMongoCollection<DebitAccount> accountCollection, ICreditService creditService, ITransactionService transactionService) { var account = accountCollection.Find(a => a.Id == id).FirstOrDefault(); if (account is null) throw new AccountNotFoundException(id);
var availableCredit = creditService.GetAvailableCredit(id); var lastActivity = transactionService.GetLastActivity(id); var status = DetermineAccountStatus(account, lastActivity);
return new AccountDetails( account.Id, account.Name, account.Owner, account.Balance, availableCredit, status, lastActivity); }
private static AccountStatus DetermineAccountStatus(DebitAccount account, DateTime lastActivity) { if (account.Balance < 0) return AccountStatus.Overdrawn; if (lastActivity < DateTime.UtcNow.AddDays(-90)) return AccountStatus.Dormant; return AccountStatus.Active; }}Projection Types
Section titled “Projection Types”Create projection types for specific views of your data:
public record AccountListItem(AccountId Id, AccountName Name, decimal Balance);public record AccountSearchResult(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance, double RelevanceScore);
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static IEnumerable<AccountListItem> GetAccountListItems(IMongoCollection<DebitAccount> collection) { return collection.Find(_ => true) .Project(a => new AccountListItem(a.Id, a.Name, a.Balance)) .ToEnumerable(); }
public static IEnumerable<AccountSearchResult> SearchAccountsWithRelevance( string searchTerm, IMongoCollection<DebitAccount> collection) { // Implement search with relevance scoring var accounts = collection.Find(a => a.Name.Contains(searchTerm)).ToList();
return accounts.Select(a => new AccountSearchResult( a.Id, a.Name, a.Owner, a.Balance, CalculateRelevance(a.Name, searchTerm))); }
private static double CalculateRelevance(string accountName, string searchTerm) { // Simple relevance calculation if (accountName.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase)) return 1.0; if (accountName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)) return 0.8; return 0.5; }}Nullable Return Types
Section titled “Nullable Return Types”Use nullable return types when queries might not find results:
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ // Explicitly nullable - might not find account public static DebitAccount? FindAccountById(AccountId id, IMongoCollection<DebitAccount> collection) { return collection.Find(a => a.Id == id).FirstOrDefault(); }
// Non-nullable - should throw if not found public static DebitAccount GetAccountById(AccountId id, IMongoCollection<DebitAccount> collection) { var account = collection.Find(a => a.Id == id).FirstOrDefault(); return account ?? throw new AccountNotFoundException(id); }}Paged Results
Section titled “Paged Results”For large datasets, return paged results:
public record PagedResult<T>(IEnumerable<T> Items, int TotalCount, int Page, int PageSize){ public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize); public bool HasNext => Page < TotalPages - 1; public bool HasPrevious => Page > 0;}
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static PagedResult<DebitAccount> GetPagedAccounts( int page, int pageSize, IMongoCollection<DebitAccount> collection) { var totalCount = (int)collection.CountDocuments(_ => true); var items = collection.Find(_ => true) .Skip(page * pageSize) .Limit(pageSize) .ToList();
return new PagedResult<DebitAccount>(items, totalCount, page, pageSize); }}Error Responses
Section titled “Error Responses”Handle errors gracefully and return appropriate types:
public record QueryResult<T>(T? Data, bool Success, string? ErrorMessage){ public static QueryResult<T> Ok(T data) => new(data, true, null); public static QueryResult<T> Error(string message) => new(default, false, message);}
[ReadModel]public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance){ public static QueryResult<DebitAccount> TryGetAccountById( AccountId id, IMongoCollection<DebitAccount> collection, ILogger<DebitAccount> logger) { try { var account = collection.Find(a => a.Id == id).FirstOrDefault(); return account is not null ? QueryResult<DebitAccount>.Ok(account) : QueryResult<DebitAccount>.Error($"Account with ID {id} not found"); } catch (Exception ex) { logger.LogError(ex, "Error retrieving account {AccountId}", id); return QueryResult<DebitAccount>.Error("An error occurred while retrieving the account"); } }}Best Practices
Section titled “Best Practices”- Use appropriate collection types -
IEnumerable<T>for most cases, concrete types when needed - Consider nullability - Use nullable types when queries might not return results
- Use async for I/O - Wrap return types in
Task<T>for database operations - Create projection types - Use specific types for different views of your data
- Handle large datasets - Use paging for queries that might return many results
- Don’t mix async and observable - Use either
Task<T>orISubject<T>, never both - Use custom types for complex results - Create dedicated types for rich query responses
- Consider performance - Choose return types that support efficient data access
- Be consistent - Use similar return type patterns across related queries
- Document complex return types - Use XML documentation for non-obvious return structures