9.0 KiB
9.0 KiB
Naming Conventions
URL naming and customization for HTTP endpoints.
Default Naming Rules
The framework automatically converts command/query class names to endpoint URLs.
Conversion Process
1. Take class name
2. Remove "Command" or "Query" suffix
3. Convert to lowerCamelCase
4. Preserve numbers and special characters
Examples
| Class Name | Endpoint |
|---|---|
CreateUserCommand |
/api/command/createUser |
GetUserByIdQuery |
/api/query/getUserById |
UpdateUserProfileCommand |
/api/command/updateUserProfile |
SearchProductsQuery |
/api/query/searchProducts |
DeleteOrderCommand |
/api/command/deleteOrder |
GetTop10ProductsQuery |
/api/query/getTop10Products |
Edge Cases
| Class Name | Endpoint | Notes |
|---|---|---|
UserCommand |
/api/command/user |
No "Command" suffix to remove |
CreateUser2Command |
/api/command/createUser2 |
Numbers preserved |
Update_User_Command |
/api/command/update_User |
Underscores preserved |
CreateUserV2Command |
/api/command/createUserV2 |
Version number preserved |
Custom Naming
[CommandName] Attribute
Override the default endpoint name:
[CommandName("users/create")]
public record CreateUserCommand
{
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
}
// Endpoint: POST /api/command/users/create
[QueryName] Attribute
[QueryName("users/search")]
public record SearchUsersQuery
{
public string Keyword { get; init; } = string.Empty;
}
// Endpoints:
// GET /api/query/users/search
// POST /api/query/users/search
RESTful Patterns
Resource-Based Naming
// Create
[CommandName("users")]
public record CreateUserCommand { }
// POST /api/command/users
// Update
[CommandName("users/{id}")]
public record UpdateUserCommand
{
public int Id { get; init; }
}
// POST /api/command/users/{id}
// Delete
[CommandName("users/{id}")]
public record DeleteUserCommand
{
public int Id { get; init; }
}
// POST /api/command/users/{id}
// Get
[QueryName("users/{id}")]
public record GetUserQuery
{
public int Id { get; init; }
}
// GET /api/query/users/{id}
// POST /api/query/users/{id}
// List
[QueryName("users")]
public record ListUsersQuery
{
public int Page { get; init; }
public int PageSize { get; init; }
}
// GET /api/query/users
Note: While you can use path parameters in custom names, all commands use POST, so this doesn't provide true REST semantics. Consider using traditional ASP.NET Core controllers if you need full REST compliance.
Hierarchical Naming
Nested Resources
[CommandName("orders/{orderId}/items")]
public record AddOrderItemCommand
{
public int OrderId { get; init; }
public int ProductId { get; init; }
public int Quantity { get; init; }
}
// POST /api/command/orders/{orderId}/items
[QueryName("orders/{orderId}/items")]
public record GetOrderItemsQuery
{
public int OrderId { get; init; }
}
// GET /api/query/orders/{orderId}/items
Domain Grouping
[CommandName("catalog/products/create")]
public record CreateProductCommand { }
[CommandName("catalog/categories/create")]
public record CreateCategoryCommand { }
[CommandName("sales/orders/create")]
public record CreateOrderCommand { }
[CommandName("sales/invoices/create")]
public record CreateInvoiceCommand { }
Resulting endpoints:
POST /api/command/catalog/products/create
POST /api/command/catalog/categories/create
POST /api/command/sales/orders/create
POST /api/command/sales/invoices/create
Versioning Strategies
URL Path Versioning
// Version 1
[CommandName("v1/users/create")]
public record CreateUserCommandV1
{
public string Name { get; init; } = string.Empty;
}
// Version 2
[CommandName("v2/users/create")]
public record CreateUserCommandV2
{
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty; // New field
}
Endpoints:
POST /api/command/v1/users/create
POST /api/command/v2/users/create
Class Name Versioning
public record CreateUserCommandV1 { }
// POST /api/command/createUserV1
public record CreateUserCommandV2 { }
// POST /api/command/createUserV2
Route Prefix Versioning
// Configure different route prefixes for different versions
app.MapSvrntyCommands("v1/commands");
app.MapSvrntyCommands("v2/commands");
Best Practices
Naming Guidelines
✅ DO
// Clear, descriptive names
[CommandName("users/create")]
[CommandName("products/search")]
[CommandName("orders/cancel")]
// Logical grouping
[CommandName("catalog/products")]
[CommandName("catalog/categories")]
// Version when needed
[CommandName("v2/users/create")]
❌ DON'T
// Too vague
[CommandName("do")]
[CommandName("action")]
// Inconsistent naming
[CommandName("users/create")] // Good
[CommandName("CreateProduct")] // Bad - inconsistent casing
[CommandName("delete-order")] // Bad - inconsistent separator
// Too nested
[CommandName("api/v1/domain/subdomain/resource/action")]
URL Patterns
✅ DO
- Use lowercase with hyphens or underscores
- Keep URLs short and meaningful
- Use nouns for resources
- Use verbs for actions when necessary
- Be consistent across your API
users/create
products/search
orders/{id}/cancel
❌ DON'T
- Mix casing styles
- Use unnecessary nesting
- Include file extensions (.json, .xml)
- Use special characters
Users/Create // Mixed case
api/v1/app/users // Unnecessary nesting
users.json // File extension
users@create // Special character
Route Conflicts
Avoiding Conflicts
// ❌ Bad - Potential conflict
[CommandName("users/{id}")]
public record UpdateUserCommand { }
[CommandName("users/active")]
public record GetActiveUsersQuery { }
// Is "users/active" an ID or a specific route?
// ✅ Good - Clear separation
[CommandName("users/{id}")]
public record UpdateUserCommand { }
[QueryName("users/filter/active")]
public record GetActiveUsersQuery { }
Route Ordering
More specific routes should be registered before generic ones:
// Register specific routes first
[QueryName("products/featured")]
public record GetFeaturedProductsQuery { }
[QueryName("products/sale")]
public record GetSaleProductsQuery { }
// Then generic routes
[QueryName("products/{id}")]
public record GetProductQuery { public int Id { get; init; } }
Special Characters
Allowed Characters
// ✅ Allowed
[CommandName("users/create")] // Slash
[CommandName("users-create")] // Hyphen
[CommandName("users_create")] // Underscore
[CommandName("users.create")] // Dot
// Works but not recommended
[CommandName("users~create")] // Tilde
URL Encoding
If you must use special characters, they will be URL-encoded:
[CommandName("users with spaces")] // Not recommended
// Becomes: /api/command/users%20with%20spaces
Organization Patterns
By Feature
// User Management
[CommandName("users/create")]
[CommandName("users/update")]
[CommandName("users/delete")]
[QueryName("users/list")]
[QueryName("users/search")]
// Product Catalog
[CommandName("products/create")]
[CommandName("products/update")]
[QueryName("products/list")]
[QueryName("products/search")]
By Domain
// E-commerce domain
[CommandName("ecommerce/orders/create")]
[CommandName("ecommerce/orders/cancel")]
[QueryName("ecommerce/orders/list")]
// Inventory domain
[CommandName("inventory/products/add")]
[CommandName("inventory/products/remove")]
[QueryName("inventory/products/list")]
Flat Structure
// Simple applications
public record CreateUserCommand { } // createUser
public record GetUserQuery { } // getUser
public record UpdateUserCommand { } // updateUser
public record DeleteUserCommand { } // deleteUser
Documentation
Swagger Grouping
Commands and queries are automatically grouped by tags:
{
"tags": [
{ "name": "Commands" },
{ "name": "Queries" }
]
}
Custom names appear in Swagger UI:
Commands
POST /api/command/users/create
POST /api/command/products/update
Queries
GET /api/query/users/search
POST /api/query/users/search
Testing
URL Testing
[Fact]
public async Task CreateUser_WithCustomName_MapsCorrectly()
{
var command = new { name = "John", email = "john@example.com" };
// Use custom route
var response = await _client.PostAsJsonAsync(
"/api/command/users/create", // Custom name from [CommandName]
command);
response.EnsureSuccessStatusCode();
}