Eight months ago, I joined a large-scale database migration project. I had already written C# in small Unity AR applications and multiplayer game prototypes but nothing quite of this magnitude. My prior projects had tight scopes and short feedback loops. This codebase has hundreds of thousands of lines, a deep domain, and translation logic driven that stems from decades ago.
The codebase has taught me more about C# and software development than I expected, and today I am feeling much more confident about building with the language.
LINQ Changed How I Think About Data
In my Unity projects, I wrote C# in a very pythonic and object oriented way: loops, conditionals, manipulating mutable states, etc… LINQ (Language Integrated Query) was something I knew existed but never reached for. For some reason, the Microsoft documentation covering C# felt miserable when I was in college. Little did I know, LINQ is a really basic idea. LINQ is essentially letting you write SQL-style logic
(filtering, sorting, grouping, selecting) directly in C# code rather than in a separate query string.
On a migration project, you’re constantly filtering, grouping, ordering, and transforming collections. LINQ turned out to be the natural vocabulary for that work. Chaining .Where(), .Select(), .OrderBy(), and .GroupBy() let me express transformation logic in a way that read almost like a description of what the data should become rather than step-by-step instructions for how to get there.
The shift from “iterate and mutate” to “describe and transform” didn’t just make the code shorter (most of the time). It made it easier to reason about, easier to review, and easier to test. Once I internalized that style, I started seeing opportunities for it everywhere – even in code that wasn’t about migration.
Pattern Matching Is More Powerful Than I Expected
C#’s switch expressions were another revelation. In this project, we frequently need to map values from the source system to the target system – and those mappings can be deeply nested and conditional. Before this migration project I would have reached for chains of if/else blocks. But switch expressions with pattern matching let us write exhaustive, readable mappings .
A dramatic If/else approach versus the concise switch expression:

The intent becomes immediately obvious to the developer with switches + arrow notation.
Testing Taught Me the Domain
The codebase’s testing patterns ended up being my best teacher. The project very commonly uses a builder/factory pattern for constructing test source data. Reading existing tests taught me the domain faster than any documentation could have. The code is very human readable when applying these patterns.
The project also uses snapshot testing for integration tests. This taught me discipline: you can’t just make the test green and move on. You have to look at the diff and understand whether every change is intentional. It slowed me down early on, and that slowness was the valuable part.
What I’d Tell Someone Starting Out
If you’re a newer developer joining a mature codebase in an unfamiliar domain, here’s what worked for me:
Read the tests first. They’re executable documentation. Production code tells you what the system does. Tests tell you why and when – they encode the team’s understanding of edge cases and business rules. If your project doesn’t have valuable tests, then take the time to understand the code and add better coverage.
Write failing tests before you write production code. This was a team practice following TDD, but it became genuinely how I think now. Prove the problem exists before you start solving it.
Don’t optimize for speed. My early PRs were small and slow. That was fine. Understanding a complex mapping function is more valuable than writing one quickly. The speed comes later, after you’ve internalized the patterns.
Domain knowledge compounds. The first area I worked on took weeks. By the time I picked up the next one, I was moving faster — not because C# got easier, but because I understood the shape of the problem. The patterns repeat with variations, and each one builds on what you already know. If you want a fun way to sharpen that pattern recognition off the clock, Travis Henderson’s blog is a great place to start.
Modern Day
I’m at the point now where I can pick up a bug report, query the production database to find affected records, write a targeted test, and implement a fix within the same day (give or take). That’s not because I’m particularly fast. It’s because over time I built up a mental model around the repo, and new problems usually fit somewhere in that model.
C# fluency came with repetition. But the harder skill was learning how to navigate uncertainty: how to contribute meaningfully to a system I didn’t fully understand yet, and to trust that understanding would come through the work itself.