Tutorials / Build a REST API with C# and .NET · Lesson 2 of 15
On this page
Solution and Project Planning
Solutions and projects
Before writing any code, you need somewhere to put it. In .NET, code lives in projects, and projects are grouped into a solution.
A picture that helps: a solution is a house, and each project is a room in it.
Throughout this tutorial we will build the back end of an online bookshop. Its solution has these projects:
Data: a storage room. It will hold the code that talks to the database.Api: the front door. This is where requests from the outside come in.OrderWorker: a room that quietly does jobs in the background, such as sending order emails.Tests: a quality-check room that tests the others.
A second program, say a catalogue for a library, would be a completely separate house. It has its own rooms and shares nothing with the bookshop, because the two have nothing to do with each other.
A room is one of two kinds:
- A library: nobody "lives" there, it just holds things that other rooms come to grab.
Datais a library. - A runnable app: a room people actually walk into and use, like
Api.
Same house or different house? Ask: "Would these projects realistically want to share code?" If yes, add a room to the same house. If no, build a separate house.
Creating a solution
Building the house, with nothing inside it yet, is one command:
dotnet new sln -n BookShop
This creates BookShop.slnx, the blueprint of the house. Right now it is blank:
<Solution>
</Solution>
Each time you add a room, the blueprint gets one new line. You will almost never edit this file by hand. The dotnet sln command does it for you.
Adding a project to a solution
This is the step that trips people up, so go slowly: building a room and adding it to the blueprint are two separate steps.
# Step 1: build the room
dotnet new classlib -n BookShop.Data -o src/BookShop.Data
# Step 2: mark it on the blueprint
dotnet sln add src/BookShop.Data/BookShop.Data.csproj
(-o src/BookShop.Data puts the new project in a src folder. Run both commands from the folder that contains BookShop.slnx. The .csproj file is the project's own settings file.)
After step 2 the blueprint looks like this:
<Solution>
<Project Path="src/BookShop.Data/BookShop.Data.csproj" />
</Solution>
If you skip step 2, it is like building a room that never made it onto the official floor plan. The room is real, and it works if you stand in it. But anyone looking at the plan, such as Visual Studio or dotnet build, has no idea it exists.
The basic project types
dotnet new <template> is you choosing what kind of room to build. Four are enough for now:
| Template | Command | Analogy | What we use it for |
|---|---|---|---|
| classlib | dotnet new classlib |
A storage closet: nobody lives there, but other rooms grab things from it | Data |
| console | dotnet new console |
A light switch: flip it, something happens, done | DiExperiment, a small playground we create later to practise dependency injection |
| worker | dotnet new worker |
A washing machine: runs in the background until you stop it | OrderWorker |
| webapi | dotnet new webapi |
The front door with a doorbell: listens for people (requests) knocking | Api |
A closet (classlib) cannot run by itself. It only exists so other rooms can use what is inside. Everything else in the table is something you can switch on. The next lesson covers these project types in more detail.
Planning for reuse
Before you add a room, ask: "Will more than one other room need to use this?" If yes, build it as its own closet from the start. Do not stuff it into whichever room needed it first.
Think of a toolbox. Instead of everyone keeping their own copy of the screwdriver in their own room, where the copies drift apart, you keep one shared toolbox that anyone can open.
For the bookshop, the closets might be:
| Library (closet) | What it holds | Who uses it |
|---|---|---|
Data |
Database code | Api, OrderWorker |
Security |
Password hashing and encryption helpers | Api, OrderWorker |
PaymentClient |
Code that calls a payment provider's web API | Api, OrderWorker |
EmailClient |
Code that sends emails | OrderWorker |
One closet can be used by several rooms, which is the point of keeping it separate. But a room does not need every closet. Api might never open EmailClient at all.
💡 Rule of thumb: if you are tempted to build one big closet called
Common,SharedorUtils, stop. That usually means several unrelated things are about to get tangled together, like tossing kitchen tools, garden tools and paperwork into the same box.
Referencing between projects
When a closet exists, another room needs a key to use it. That key is a project reference:
dotnet add src/BookShop.Api reference src/BookShop.Data
(The command takes the project folders. It finds the .csproj file inside each one.) A new line appears in Api's .csproj. It is literally the key to the Data closet:
<ItemGroup>
<ProjectReference Include="..\BookShop.Data\BookShop.Data.csproj" />
</ItemGroup>
Keys only go one way. Api holds a key to Data, but Data must never hold a key back to Api.
If both pointed at each other, neither could be built first. It is like two doors that can only open if the other one is already open.
Shared settings for every project
Imagine a house with five rooms where every room needs the same thermostat setting and the same brand of light bulb. Going from room to room, setting each one by hand and keeping them matching forever, is slow and error-prone. .NET has two files for this: Directory.Build.props and Directory.Packages.props.
Directory.Build.props is a master thermostat for the whole house. Put it once in the folder above your projects, and every project inherits its settings:
<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
You never connect it to each room. When you build, MSBuild (the tool that compiles .NET projects) looks up the folder tree for this file and applies it. Change the setting once, and every project feels it on the next build.
Directory.Packages.props does the same job for package versions. A package is a ready-made library from the internet, installed through NuGet. Without a central rule, one project might quietly use a different version of a package than another, and nobody notices until something breaks:
<!-- Directory.Packages.props -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageVersion Include="Moq" Version="4.20.0" />
</ItemGroup>
</Project>
(The version numbers here are only examples.)
Each project then asks for the package without a version, because the house-wide rule already decided it:
<PackageReference Include="Microsoft.EntityFrameworkCore" />
One rule, one place to change it, and every project updates together.
Wrap-up
- A solution is a house and a project is a room. Put projects in the same solution only if they would realistically share code.
- Building a project and adding it to the solution are two steps. Do not forget
dotnet sln add. - Four project types:
classlib(a library that cannot run by itself),console(run once),worker(runs in the background) andwebapi(answers requests). - Before building a project, ask whether others will need it. If yes, make it its own library.
- A
ProjectReferenceis a key to another project. Keys only go one way, never in a circle. Directory.Build.propssets shared settings once for every project.Directory.Packages.propssets shared package versions once.
Questions to think about
Before the next lesson, try answering these in your own words. There are no answers given here on purpose. If you can explain them out loud to a friend, you are ready to move on.
- What is a solution and what is a project? You are also building a small to-do app that has nothing to do with the bookshop. Would it go in the same solution? How do you decide?
- You create a new project with
dotnet new classlibbut never rundotnet sln add. The folder is there. What goes wrong, and why? - Both
ApiandOrderWorkerneed the same code to work out shipping costs. Where would you put that code? Why not copy it into each project? - What is a project reference? Why must
Datanever point back toApiwhenApialready points toData? - Six projects all use the same package, but one of them quietly uses an older version. What problem could that cause? Which file helps prevent it?