dotnet-cqrs/docs/http-integration/naming-conventions.md

434 lines
9.0 KiB
Markdown

# 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:
```csharp
[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
```csharp
[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
```csharp
// 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
```csharp
[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
```csharp
[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
```csharp
// 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
```csharp
public record CreateUserCommandV1 { }
// POST /api/command/createUserV1
public record CreateUserCommandV2 { }
// POST /api/command/createUserV2
```
### Route Prefix Versioning
```csharp
// Configure different route prefixes for different versions
app.MapSvrntyCommands("v1/commands");
app.MapSvrntyCommands("v2/commands");
```
## Best Practices
### Naming Guidelines
#### ✅ DO
```csharp
// 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
```csharp
// 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
```csharp
// ❌ Bad - Potential conflict
[CommandName("users/{id}")]
public record UpdateUserCommand { }
[CommandName("users/active")]
public record GetActiveUsersQuery { }
// Is "users/active" an ID or a specific route?
```
```csharp
// ✅ 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:
```csharp
// 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
```csharp
// ✅ 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:
```csharp
[CommandName("users with spaces")] // Not recommended
// Becomes: /api/command/users%20with%20spaces
```
## Organization Patterns
### By Feature
```csharp
// 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
```csharp
// 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
```csharp
// 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:
```json
{
"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
```csharp
[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();
}
```
## See Also
- [HTTP Integration Overview](README.md)
- [Endpoint Mapping](endpoint-mapping.md)
- [HTTP Configuration](http-configuration.md)
- [Command Attributes](../core-features/commands/command-attributes.md)
- [Query Attributes](../core-features/queries/query-attributes.md)