Organizing a C# Project
Move beyond one file: understand projects, namespaces, dependencies, configuration, and tests without creating architecture for its own sake.
Before this lesson
Explain source files, projects, solutions, assemblies, and namespaces
Separate domain logic from console and storage concerns
Create a test project and run focused unit tests from the .NET CLI
Add dependencies and configuration deliberately
The short answer
A .NET project file defines how source is built and which packages it uses. Organize code by cohesive responsibility, use namespaces to prevent naming collisions, keep configuration outside source, and add tests around rules that can fail.
Namespaces organize names, not files on disk
A namespace qualifies type names and avoids collisions: StudyTracker.Domain.Session differs from AnotherProduct.Session. Folder structure often mirrors namespaces because that helps navigation, but the compiler does not require the paths to match.
using imports a namespace for shorter references; it does not copy code or install a library. A package reference brings an external compiled dependency into the project. These are different operations even though both can lead to new type names becoming available.
| Concept | Purpose | Typical artifact |
|---|---|---|
| Source file | Human navigation and declarations | StudySession.cs |
| Namespace | Qualify and group type names | StudyTracker.Domain |
| Project | One build unit and dependency set | StudyTracker.csproj |
| Assembly | Compiled output | StudyTracker.dll |
| Solution | Group related projects | StudyTracker.sln |
Separate policy from input, output, and storage
Console.ReadLine, file paths, and display colors are delivery details. The rule that a study session must have positive minutes is domain logic. Keeping rules in focused classes or methods lets a later web interface reuse them and lets tests call them without simulating a terminal.
A practical small structure might have Program coordinate the use case, Domain types enforce rules, and a file repository handle persistence. Do not create layers whose only work is passing the same values onward. Every boundary should make a dependency, policy, or ownership decision clearer.
StudyTracker/
StudyTracker.csproj
Program.cs
Domain/
StudySession.cs
StudyLog.cs
Storage/
FileStudySessionRepository.cs
Tests/
StudyTracker.Tests.csprojPackages, configuration, and tests are part of the program
NuGet packages provide reusable libraries. Add one only after checking ownership, maintenance, license, security posture, target-framework compatibility, and whether the standard library already solves the problem. Pin and review dependency updates rather than pasting package commands from old tutorials blindly.
Configuration that varies by environment—paths, service endpoints, feature settings—should not be hardcoded with secrets in source control. Tests should target decisions, calculations, and invariants. A method such as CalculateShipping is easier to test than a Main method that reads input and prints inside the calculation.
Write the first unit test around a boundary
A unit test calls a small piece of code, supplies a controlled input, and checks one observable result. It should fail for a useful reason when the rule is wrong. Start with a boundary because boundaries concentrate mistakes: for free shipping at 5000 cents, test just below it, exactly at it, and above it.
Keep the calculation outside Program so the test can call it without simulating console input. The following commands create a class library and an xUnit test project, connect the test project to the production project, and run the suite. Run them from a new empty directory. The solution filename can vary with the installed SDK; the dotnet sln add commands work against the solution created in that directory.
dotnet new sln --name Shipping
dotnet new classlib --name Shipping.Domain
dotnet new xunit --name Shipping.Domain.Tests
dotnet sln add Shipping.Domain/Shipping.Domain.csproj
dotnet sln add Shipping.Domain.Tests/Shipping.Domain.Tests.csproj
dotnet add Shipping.Domain.Tests/Shipping.Domain.Tests.csproj reference Shipping.Domain/Shipping.Domain.csproj
dotnet testThe theory below expresses three boundary examples without duplicating the test body. First make the test fail by returning the wrong amount, then implement the smallest rule that makes every example pass. A red-green-refactor loop proves that the test can detect failure before you trust its success.
namespace Shipping.Domain;
public static class ShippingCalculator
{
public static int CalculateShippingInCents(int subtotalInCents) =>
subtotalInCents >= 5000 ? 0 : 599;
}Place that class in Shipping.Domain/ShippingCalculator.cs. Then replace the generated test file with this test class:
using Shipping.Domain;
using Xunit;
public class ShippingCalculatorTests
{
[Theory]
[InlineData(4999, 599)]
[InlineData(5000, 0)]
[InlineData(5001, 0)]
public void Calculates_shipping_at_free_shipping_boundary(
int subtotalInCents,
int expectedShippingInCents)
{
int actual = ShippingCalculator.CalculateShippingInCents(subtotalInCents);
Assert.Equal(expectedShippingInCents, actual);
}
}Test behavior that callers depend on: returned values, state changes, and deliberate exceptions. Avoid asserting private methods or the exact internal steps of an algorithm. Those tests resist safe refactoring and confuse implementation with the contract.
Quick knowledge check
Answer before you reveal.
01Does adding a using directive install a NuGet package?
No. using shortens namespace references; package installation is a project dependency change.
02Must folders match namespaces?
No, but aligning them is a useful navigation convention in many projects.
Exercise
Practice challenge
Create a local .NET solution with a console project and a test project. Move a shipping calculation into a focused class, keep Console input in Program, and test totals below, at, and above the free-shipping boundary.
Requirements
- dotnet build succeeds from the solution directory
- The calculation project code has no Console calls
- Tests cover normal and boundary cases
- No secret or machine-specific absolute path is committed
Optional extension: Add a storage interface and file implementation only after identifying which behavior a test double or second implementation would replace.
Open in C# compilerLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
When should I create a class library project?
When code has a useful independent boundary, dependency set, reuse case, or test/deployment reason. One small console project can contain several well-organized files.
Which test framework should I use?
xUnit, NUnit, and MSTest are established choices. Follow current project or team conventions; the essential skill is designing testable code and meaningful cases.
What is dependency injection configuration?
It is the application startup code that chooses concrete implementations and supplies them to constructors. Framework containers can automate lifetime and resolution for larger apps.