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

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();
}

See Also