VSATutorialSeptember 2026 · 9 min read

VSA Todo App — Part 6: Parent-Child Relationships & Nested Operations

TL;DR

Part 6 handles the parent-child relationship between Todo and TodoItem in VSA. We compare two production patterns: separate child handlers with dedicated endpoints (Blazor CRM) vs nested request DTOs where child data is embedded in the parent request (MVC Project Manager). Covers cascade deletes, child validation, and when to choose each approach.

Part 6 of 20 in the VSA Todo App Tutorial Series|← Previous: Part 5|Next: Part 7 →

Help Us Grow

Love this tutorial? Explore our ready-to-use enterprise starter kits built with VSA in .NET 10.

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.

Key Takeaways

  • Separate child handlers (Blazor CRM) enable independent CRUD operations on child entities via dedicated endpoints
  • Nested request DTOs (MVC PM) create parent and children in a single atomic transaction — all or nothing
  • Diff-based updates compare the client's desired state against the database to determine adds, updates, and deletes
  • Cascade deletes can be hard (EF Core cascade) or soft (handler-level cascade with audit preservation)
  • Both patterns live in the same VSA feature folder — the architecture doesn't change, only the handler design

Frequently Asked Questions

Q: Should child entities have their own handlers in VSA?

It depends on your transaction boundaries. Use separate handlers when children are managed independently (add items to an existing Todo without touching the parent). Use nested DTOs when parent and children must be created or updated atomically. Both patterns live in the same feature folder.

Q: How do I cascade deletes in VSA?

For hard deletes, configure EF Core cascade delete on the relationship. For soft deletes, load the entity with its children using Include, then soft-delete each child before soft-deleting the parent in the handler. The MVC Project Manager's DeleteTodoHandler demonstrates the soft-delete cascade pattern.

Q: What's the diff-based update pattern in VSA?

The client sends the full desired state of child entities. The handler loads existing children, then compares: entities with an Id that exist in the request are updated, entities with no Id are added, and existing entities not in the request are deleted. This eliminates the need for separate add/remove child endpoints.

Q: How to validate child entities in VSA?

Use FluentValidation's RuleForEach(x => x.Items).ChildRules(item => { ... }). This validates each item in the collection independently and returns per-item error messages. Combine with .When() for conditional rules on updates. The validator lives in the feature's Cqrs/ folder.

Part 6 of 20 in the VSA Todo App Tutorial Series|← Previous: Part 5|Next: Part 7 →

Ready to study real VSA code?

Every Indotalent product is a complete .NET 10 application built with Vertical Slice Architecture. Complete source code — $21 each.

Explore Products

Looking for a ready-to-use traditional monolithic multilayered clean architecture?

Monolithic Clean Architecture — 1,300+ devs, 500+ forks. Clean Arch + CQRS + Repository Pattern. Free & open source for commercial use.

Star on GitHub