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.

    Two solutions, each with its own projects BookShop.slnx a solution: one house Data library Api front door OrderWorker background jobs Tests quality check LibraryCatalog.slnx a separate house Data Api Tests shares nothing with the BookShop house

    A room is one of two kinds:

    • A library: nobody "lives" there, it just holds things that other rooms come to grab. Data is 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.)

    Two steps: build the room, then draw it on the blueprint Step 1: dotnet new The folder exists on disk. The .slnx does not list it yet. Step 2: dotnet sln add The .slnx now lists the project. Now the tools can see it.

    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, Shared or Utils, 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.

    References go one way Api Data Api has a key to Data Data must never point back to Api. Two projects that need each other can never be built.

    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>
    
    One settings file, every project inherits it Directory.Build.props one file at the top every project below inherits it Data Api OrderWorker Tests

    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) and webapi (answers requests).
    • Before building a project, ask whether others will need it. If yes, make it its own library.
    • A ProjectReference is a key to another project. Keys only go one way, never in a circle.
    • Directory.Build.props sets shared settings once for every project. Directory.Packages.props sets 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.

    1. 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?
    2. You create a new project with dotnet new classlib but never run dotnet sln add. The folder is there. What goes wrong, and why?
    3. Both Api and OrderWorker need the same code to work out shipping costs. Where would you put that code? Why not copy it into each project?
    4. What is a project reference? Why must Data never point back to Api when Api already points to Data?
    5. 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?
    ← HTTP and REST Concepts.NET Project Types →