The Challenge
What starts as "just display a family tree" quickly becomes complicated. My goal was to build ArborKin—a private, multi-user family tree app where each user can browse their ancestors and descendants in an interactive, zoomable canvas.
Sounds simple, right? Wrong. Here's what I had to solve:
- Tree Layout: Every person needs pixel-perfect positioning (X, Y coordinates) relative to their parents and siblings
- Drag State: Users want to reposition nodes and overlays; Blazor re-renders every 50ms, so position state kept in .NET gets wiped
- Photo Handling: Uploading photos to Azure Blob Storage was slow—taking 5+ minutes per image
- Real-time Collaboration: Multiple family members viewing/editing the same tree concurrently
Hard Problem #1: Tree Layout Without JavaScript
Most genealogy tools use D3.js or similar to compute node positions dynamically in the browser. I wanted something different: compute all positions in C# before rendering a single pixel.
Why? Predictable, testable, no layout thrashing.
The Solution: A custom FamilyTreeLayoutEngine (C# class) that:
- Reads all Person + Relationship data from the database
- Builds a nuclear-family graph (couples as nodes, children as edges)
- Places each family unit in 2D space using a depth-first traversal
- Computes every person's pixel coordinates
- Returns fully-positioned data to the frontend
Result: SVG canvas renders instantly, zero JavaScript layout math needed. The engine has 500+ lines of C# and 50+ unit tests covering cross-family marriages, orphans, siblings marrying-out, and other edge cases.
Hard Problem #2: Drag State Ownership
Users drag the hero overlay and toolbar to new positions. Blazor's re-render cycle is fast but destructive—every time component parameters change, the DOM gets rebuilt and custom CSS transforms reset.
The Naive Approach: Store drag positions in C# component state.
Result: Draggy, laggy UX. Every 50ms, positions reset.
The Fix: JavaScript owns drag state.**
- JavaScript listens for
mousedown→mousemove→mouseup - On
mousemove, JavaScript updates the element'sstyle.leftandstyle.topdirectly - On
mouseup, JavaScript invokes a C# method ([JSInvokable]) with the final coordinates - C# persists to localStorage + database
- Next page load, positions restore from localStorage instantly
Pattern: JavaScript owns gestures, C# owns state. Blazor never knows the drag happened until it's done.
Hard Problem #3: Photo Upload Latency
Every byte traveled over SignalR (Blazor Server's WebSocket protocol). A 2MB photo meant 2MB of SignalR round-trips. At 500KB/sec, that's 4+ seconds. Multiply by 1-2 retries on slow connections, and you hit 5-10 minutes.
The Fix: Blazor JavaScript interop to upload directly to Azure Blob Storage.
- Frontend generates a SAS (Shared Access Signature) URL on the server
- Browser uploads directly to Azure Blob Storage using the SAS token
- Server doesn't touch the bytes—just the URL
- Result: 200KB/sec → 10 seconds for 2MB (local network speed, not server bottleneck)
The Stack
Frontend: Blazor Server (C#, Razor components)
Backend: ASP.NET Core (C#, Entity Framework Core)
Database: Azure SQL Database (serverless)
Storage: Azure Blob Storage (photos, exports)
Layout: Custom C# tree layout engine
Deployment: Azure App Service (free tier F1)Total: ~8k lines of C#, 60+ EF migrations, 75 unit tests.
Metrics
- Users: 6 (family members, invite-only)
- Data: 56 people, 80+ relationships, 40+ photos
- Performance: Tree renders in <150ms; page load <2s on 3G throttle
- Uptime: 99.8% (one planned migration outage)
- Bugs Fixed: 45 (most during MVP phase; last 3 months ~2/month)
Lessons
- Layout engines are complex. If you're visualizing a graph (org chart, family tree, dependency graph), expect to spend 20-30% of your time on positioning logic.
- Let each layer own its domain. JavaScript owns gestures, C# owns data. Mixing them causes latency and race conditions.
- Measure before optimizing. The photo upload slowdown wasn't obvious until I enabled Azure monitor and saw 90% of time was network I/O.
- Test edge cases early. A person with two spouses, an orphan sibling, cross-family marriages—these are rare but they break layouts badly if you don't handle them upfront.
What's Next?
ArborKin is live and used by my family. Future priorities: multi-person photo tagging, life events (births, migrations, military service), and a public sharing mode for branches of the tree.
If you're building a Blazor app with complex UI interactions, I hope this breakdown helps. Feel free to reach out if you have questions!