What I Learned From Building My First C#/.NET Project

I’m thrilled to announce the first public release of tsk — a cross-platform, single-binary command-line todo manager written in C#/.NET. This project began as a way for me to explore C# and .NET, but it quickly evolved into a hands-on masterclass in software architecture, modern development workflows, and real-world release engineering.
This post is a behind-the-scenes look at what I learned building tsk from scratch: the language, the patterns, the tooling, and the workflows that brought it all together.
📘 Learning C# from the Ground Up
Before tsk, I had never written a single line of code in C#. Coming from a primarily Python and Type-/JavaScript background, I found C# to be both powerful and expressive. Here are some takeaways:
- Strong typing: I love the strong and strict typing. TypeScript is… nice… but it’s plain as day to me now that it’s not really a strongly-typed language — not in the same way that C#, Jave, or Go are. TypeScript feels a lot more like “syntactical sugar” after having used a language like C#.
- String interpolation and pattern matching: These made CLI output code
cleaner than I’d expected. I love the
$""
interpolation and@""
verbatim syntax. - Good architecture: C# nudges you to build things better than you can get away with using loosey-goosey languages. Can you write bad C# code? Of course. But I do feel like it’s harder. And, conversely, it’s easier to write better code with a first-party DI container, and all the other C#/.NET goodies.
- Great tooling: As someone whose primary OS is a Linux distro, I was
nervous about C#/.NET’s connection to the Microsoft ecosystem — mostly because
I thought I’d be fighting an uphill battle developing in C# on Linux. But I
quickly found that this is absolutely not the case in 2025. The
dotnet
SDK CLI is really powerful, and while VS Code doesn’t have all the Visual Studio bells and whistles, with the C# Dev Kit extension, you can be extremely productive.
🧱 Object-Oriented Programming in Practice
One of my goals was to really get to grips with OOP. Instead of just reading about inheritance and encapsulation, I applied them:
- I created a
TodoItem
entity and surrounded it with real-world responsibilities: parsing, validation, mutation. - I used interfaces to define behaviors and abstract away implementation details.
- I kept entity methods focused and pure wherever possible.
- I used dependency injection and factory methods to make code more loosely coupled and easily testable
This reinforced not just the what of OOP, but the why — how it leads to testability, flexibility, and clarity when used well.
🧱 Clean Architecture: Layering the Right Way
I structured the app using a layered architecture:
Tsk.Domain/
: Core business logic, entities, and validationTsk.Infrastructure/
: File I/O, serializationTsk.CLI/
: Presentation layer usingSpectre.Console.Cli
(boy is that a great library!)
Even though it’s a small app, this separation of concerns made the codebase feel maintainable and scalable from day one. It also helped me understand:
- How layers talk to each other (via interfaces and dependency injection)
- How to write code that is decoupled from its environment
This was my first real attempt at “clean architecture,” and it felt fantastic.
It definitely was slow going at first, but once I hit the CLI
layer, it
really started to fly, as I could build confidently knowing that all tests were
passing and everything was well-architected.
💉 Dependency Injection
I tried manual DI in a previous Node project. I saw how powerful it was, but manually injecting dependencies becomes really clunky really fast. But this project really helped me see just why DI is so powerful. As soon as I need to:
- inject a mock repository in tests
- swap between file-based and future database backends
- pass shared config or services into CLI commands
… I realized why it’s such a core part of modern development.
I wired up a lightweight DI system using
Microsoft.Extensions.DependencyInjection
and Spectre.Console.Cli
’s custom
type resolver, and organized services cleanly across the layers.
🔁 Git + GitHub Workflow: Working Like a Pro
This was my first project with a structured, intentional Git and GitHub workflow:
- Feature branches: Every feature or fix got its own branch.
- PRs and reviews: Even solo, I used PRs to document and stage changes.
- Issue tracking: I tracked work using issues with clear references in commit messages.
- Semantic commits: I followed clear and conventional commit messages.
- GitHub Actions: Every PR and merge to main triggered an Action to run automated tests.
🚀 Automated GitHub Releases
One of the most satisfying parts was shipping tsk as:
- A single-file, self-contained binary
- Built and uploaded via GitHub Actions
- Prebuilt for Linux, macOS, and Windows (x64 + ARM64)
I learned:
- How to use
dotnet publish
to produce standalone binaries - How to target multiple runtimes in a matrix build
- How to zip/tar artifacts depending on OS
- How to write a clean
release.yml
workflow that tags, builds, and publishes automatically
This transformed tsk from “just a project” to a real, distributable tool.
🧪 Testing and Mocking
I wrote real tests, not just toy ones. I learned how to:
- Use
xUnit
andMoq
to test the core logic - Separate unit tests from file integration tests
It also made me think more carefully about interfaces and design for testability.
📝 Documentation That Matters
I put real effort into:
- Writing a professional, readable
README.md
with TOC, usage, and install instructions - Including aliases, metadata flags, and even the plaintext DSL
- Creating a proper
CHANGELOG.md
using Keep a Changelog format - Writing a GitHub release with full notes, features, and highlights
This was a great exercise in writing for other developers — or future me.
🛣 What’s Next?
This is only the beginning. For future versions, I’d like to add:
- Filtering and sorting (in-memory and on-disk)
- Config file support
- Export options (JSON, CSV)
- Possibly a GUI wrapper or TUI mode
- Other niceties, like overdue tasks highlighting
And I want to keep refining my design patterns and release automation.
Final Thoughts
Building tsk taught me more than I expected: not just about C# or CLIs, but about shipping software the right way. I went from a blank directory to a tested, documented, versioned, cross-platform release in just a couple weeks.
I was a little tentative about C# at first, mostly because I thought it was very
closely tied to the whole Microsoft ecosystem. But with the move to .NET
Core (now just “.NET”), it’s truly cross-platform. Developing on Linux in VS
Code was very smooth with the C# Dev Kit extension and the dotnet
SDK.
I will definitely be back for more.
Check out the project: https://github.com/bradleyburgess/tsk