Real applications have relationships. A Todo has TodoItems. An Order has OrderLines. An Invoice has InvoiceItems. In VSA, parent-child relationships raise an important architectural question: should child entities have their own handlers and endpoints, or should they be nested inside the parent's request DTOs? Both the Blazor CRM and MVC Project Manager Todo implementations face this question, and they answer it differently — offering two production-proven patterns you can choose from.
The Blazor CRM uses separate child handlers: CreateTodoItemHandler, UpdateTodoItemHandler, and DeleteTodoItemHandler live in the Cqrs/ folder alongside the parent handlers. Each has its own endpoint under /api/todo/todo-item. The MVC Project Manager uses nested request DTOs: the CreateTodoRequest contains a List<CreateTodoItemRequest> Items property, and the parent handler creates, updates, and deletes child entities in a single transaction. Both patterns work — the choice depends on your transactional and API design requirements.
Pattern 1: Separate Child Handlers (Blazor CRM)
The Blazor CRM treats TodoItems as independent entities with their own CRUD handlers. Each handler is a self-contained VSA file with its own request, response, and logic:
public class CreateTodoItemRequest
{
public string? TodoId { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public DateTime? StartTime { get; set; }
public DateTime? EndTime { get; set; }
public bool IsCompleted { get; set; }
}
public record CreateTodoItemCommand(CreateTodoItemRequest Data) : IRequest<CreateTodoItemResponse>;
public class CreateTodoItemHandler : IRequestHandler<CreateTodoItemCommand, CreateTodoItemResponse>
{
public async Task<CreateTodoItemResponse> Handle(CreateTodoItemCommand request, CancellationToken ct)
{
var entity = new TodoItem
{
TodoId = request.Data.TodoId, Name = request.Data.Name,
Description = request.Data.Description,
StartTime = request.Data.StartTime, EndTime = request.Data.EndTime,
IsCompleted = request.Data.IsCompleted
};
_context.TodoItem.Add(entity);
await _context.SaveChangesAsync(ct);
return new CreateTodoItemResponse { Id = entity.Id, Name = entity.Name };
}
}
The endpoint exposes these as separate routes: POST /api/todo/todo-item, POST /api/todo/todo-item/update, POST /api/todo/todo-item/delete/{id}. The Blazor UI uses MudDialog components to manage child items — each dialog calls the appropriate endpoint independently. This pattern is ideal when child entities are managed independently from their parent, such as adding items to an existing Todo without reloading the entire parent.
Pattern 2: Nested Request DTOs (MVC Project Manager)
The MVC Project Manager embeds child item data directly in the parent's request DTO. The CreateTodoRequest contains everything — the Todo fields plus a list of items, image attachments, and file attachments — creating a single atomic transaction:
public class CreateTodoRequest
{
public string? Name { get; set; }
public string? Description { get; set; }
public TodoPriority Priority { get; set; }
public TodoCategory Category { get; set; }
public int Progress { get; set; }
public List<CreateTodoItemRequest> Items { get; set; } = new();
public List<CreateTodoImageAttachmentRequest> ImageAttachments { get; set; } = new();
public List<CreateTodoFileAttachmentRequest> FileAttachments { get; set; } = new();
}
The handler processes everything in one transaction: create the Todo, then loop through Items and create each one. If anything fails, the entire operation rolls back. This pattern is ideal when the parent and children must be created atomically — you never want a Todo without its items, or items without their parent.
Update Strategies: Replace vs Diff
For updates, the two patterns diverge further. The Blazor CRM updates each TodoItem individually through its own update handler. The MVC Project Manager uses a diff-based approach: the UpdateTodoRequest sends the complete list of items, and the handler compares it against the database to determine what was added, modified, or removed. Existing items with an Id are updated, items with no Id are added, and existing items not in the request are soft-deleted. This diff-based approach is powerful because the client sends the full desired state, and the server figures out what changed — no need for separate add/remove endpoints.
Cascade Deletes: Hard vs Soft
The Blazor CRM uses hard deletes with EF Core cascade. Deleting a Todo removes its items via the database relationship. The MVC Project Manager uses soft deletes with explicit cascade in the handler — the DeleteTodoHandler loads the entity with items and attachments, soft-deletes each child, then soft-deletes the parent. Soft delete preserves data for audit and recovery; hard delete is simpler and requires no special handling. In both patterns, the key VSA principle holds: all the logic for the parent-child relationship lives in the Todo feature folder.