Let's talk

← all posts

Building a Family Tree App with Blazor: Hard Problems & Solutions

3 min read
BlazorCase StudyWeb Development

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:

  1. Reads all Person + Relationship data from the database
  2. Builds a nuclear-family graph (couples as nodes, children as edges)
  3. Places each family unit in 2D space using a depth-first traversal
  4. Computes every person's pixel coordinates
  5. 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 mousedownmousemovemouseup
  • On mousemove, JavaScript updates the element's style.left and style.top directly
  • 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

  1. 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.
  2. Let each layer own its domain. JavaScript owns gestures, C# owns data. Mixing them causes latency and race conditions.
  3. Measure before optimizing. The photo upload slowdown wasn't obvious until I enabled Azure monitor and saw 90% of time was network I/O.
  4. 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!

Related Posts