Tutorials / Build a REST API with C# and .NET · Lesson 15 of 15

On this page

    Create the Solution and the API Project

    We have covered many concepts: HTTP, JSON, dependency injection, async code, SQLite, unit tests and how a team works. Now it is time to start coding.

    This lesson is only setup. We will create the real bookshop project and arrange it properly, but we will not write any code of our own yet. Good setup now saves a lot of work later.

    What you need

    • The .NET 10 SDK (the tools that build and run .NET code).
    • We recommend the latest Visual Studio (the free Community edition is fine). When you install it, tick the ASP.NET and web development workload.

    You do not need Visual Studio to build .NET apps. Almost everything can be done with two simple tools:

    • a terminal, where you type commands to create projects, build them and run them, and
    • a text editor (even Notepad), where you write the files.

    Many developers still prefer Visual Studio, because it does a lot of this for you with a few clicks, and it shows your files, errors and tests in one window.

    So we show both ways for each step, and you can follow whichever you like:

    • Terminal: the commands to type. They work in any terminal on Windows, macOS or Linux.
    • Visual Studio: the same step as a list of things to click.

    Pick one and stay with it. Both give you exactly the same files on disk.


    The plan

    By the end of this lesson you will have this:

    BookShop/
      BookShop.slnx                  the solution
      Directory.Build.props          shared settings (one place)
      Directory.Packages.props       shared package versions (one place)
      src/
        BookShop.Api/                the API project
    

    There is no tests/ folder yet. We add it in lesson 17, when we write the first test.


    Create the solution and the API project

    You learned in lesson 01 that a solution is the house and a project is a room. Let us build the house, then the first room: BookShop.Api, a web API (lesson 02).

    Terminal

    Make a folder for the whole thing and go into it. Then run the three commands below, all from that same folder. You do not need to change folder between them.

    mkdir BookShop
    cd BookShop
    
    dotnet new sln -n BookShop
    dotnet new webapi -n BookShop.Api -o src/BookShop.Api
    dotnet sln add src/BookShop.Api
    
    • dotnet new sln -n BookShop creates the solution file.
    • dotnet new webapi ... -o src/BookShop.Api creates the API project inside a src folder.
    • dotnet sln add puts the project into the solution. Creating a project does not do this for you (lesson 01).

    The first command prints a short message:

    The template "Solution File" was created successfully.
    

    (The solution file is called BookShop.slnx with the .NET 10 SDK. Older versions make BookShop.sln instead. They work the same way for us.)

    Visual Studio

    1. Start Visual Studio and choose Create a new project.
    2. Search for Blank Solution and choose it, then Next.
    3. Name it BookShop, pick a folder, and choose Create. You now have an empty solution.
    4. In Solution Explorer (the panel that lists your files), right-click the solution, then Add > New Project.
    5. Choose ASP.NET Core Web API, then Next.
    6. Name it BookShop.Api. For Location, choose a new src folder inside the BookShop folder, then Next.
    7. On the Additional information page, make sure:
      • Framework is .NET 10.0 (Long Term Support).
      • Enable OpenAPI support is ticked.
      • Use controllers is not ticked. We use the lighter "minimal API" style.
    8. Choose Create.

    Visual Studio may show a different solution file name (.sln or .slnx) depending on its version. Do not worry about it.

    What was created

    Look at the folder. The project is inside src, and the solution sits at the top:

    BookShop/
      BookShop.slnx
      src/
        BookShop.Api/
          BookShop.Api.csproj
          Program.cs
          appsettings.json
          appsettings.Development.json
          BookShop.Api.http
          Properties/
            launchSettings.json
    

    (Visual Studio can add one or two extra files. That is fine.)


    Shared settings: Directory.Build.props

    The last lesson ended with a question: how can you change a version number once, and have every project pick it up? This file is the answer.

    Directory.Build.props is a small settings file that you put in a folder. Every project in that folder, and in the folders below it, reads it automatically when it builds. You do not point the projects at it, and there is nothing to connect. The name is fixed, and it must be spelled exactly like this, because the build tool looks for it by name. (The build tool is called MSBuild. It is the program that compiles .NET projects, and it runs when you build or run.)

    So instead of every project saying "I use .NET 10", the file says it once for all of them. Next November, you change one line.

    Let us see what it replaces. Open the project file, BookShop.Api.csproj. In Visual Studio, right-click the project in Solution Explorer and choose Edit Project File. In a terminal, open it in any text editor.

    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
      </ItemGroup>
    
    </Project>
    

    (Your version number can be a little different. That is fine.)

    Remember lesson 13: .NET releases every year, so these lines will change. A solution will soon have several projects, and each one would need its own copy. Instead, we write these three settings once, in a file above all the projects:

    • TargetFramework is the .NET version. net10.0 means .NET 10.
    • Nullable makes the compiler warn you about values that might be missing (null).
    • ImplicitUsings adds the most common using lines for you, so files stay short.

    Create a file named exactly Directory.Build.props in the BookShop folder, next to the solution file:

    <Project>
      <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
    </Project>
    

    Now delete the whole <PropertyGroup> block from BookShop.Api.csproj. The project no longer states these settings itself:

    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
      </ItemGroup>
    
    </Project>
    

    Here is what just happened, side by side. The three settings moved out of the project file and into the shared file. The package line stayed:

    Three settings move from the project file to Directory.Build.props BookShop.Api.csproj before TargetFramework Nullable ImplicitUsings PackageReference Directory.Build.props TargetFramework Nullable ImplicitUsings BookShop.Api.csproj after PackageReference move stays

    Build it to prove it still works:

    dotnet build
    
    Build succeeded.
        0 Warning(s)
        0 Error(s)
    

    The project still builds as .NET 10, and it never mentioned it. MSBuild looked up the folders, found Directory.Build.props, and applied it. This is the "master thermostat" from lesson 01.

    In Visual Studio, the file works as soon as it exists in that folder. Create it with any text editor, or with Notepad. Then, if you want to see it in Solution Explorer, right-click the solution, choose Add > New Solution Folder and name it Solution Items. Right-click that folder and choose Add > Existing Item, then pick the file. This only makes the file visible. The build finds it by its place on disk, not by this list.


    Shared package versions: Directory.Packages.props

    The project file still has one version number, on the Microsoft.AspNetCore.OpenApi line. This is the second half of the answer to the question from lesson 13.

    A package is a ready-made library, installed through NuGet (lesson 01). Almost every real project uses several packages, and each package has a version number. Now picture a solution with five projects that all use the same package. If every project writes its own version, they can slowly drift apart: one project on version 10.0.12, another on 10.0.9. Nobody notices until something behaves strangely, and an upgrade means hunting through every project file.

    Five projects, five copies of the version, one has drifted Api 10.0.12 Data 10.0.12 Pricing 10.0.9 Orders 10.0.12 Tests 10.0.12 Same package, five copies of the number. One has drifted.

    Directory.Packages.props fixes this the same way as the file before it: one shared file, found automatically by the build. Think of a restaurant menu with prices. The menu lists each dish and its price once. A customer then orders by dish name only, and never says the price. The file is the menu, and each project is a customer.

    It has two parts:

    • PackageVersion is a line on the menu: a package and its one agreed version.
    • PackageReference is an order in a project file: "I use this package". It has the name and no version.

    There is also one switch, ManagePackageVersionsCentrally. It tells the build to take versions from the menu, and to complain if a project writes its own.

    Create Directory.Packages.props next to Directory.Build.props. Use the same version number you saw in your project file:

    <Project>
      <PropertyGroup>
        <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
      </PropertyGroup>
      <ItemGroup>
        <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
      </ItemGroup>
    </Project>
    

    Then remove the version from the project file. The project now only orders the package by name:

    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" />
      </ItemGroup>
    
    </Project>
    

    Again, side by side. This time only the version number moves:

    The version number moves from the project file to Directory.Packages.props BookShop.Api.csproj before PackageReference OpenApi Version 10.0.12 asks for the package and picks the version Directory.Packages.props PackageVersion OpenApi Version 10.0.12 BookShop.Api.csproj after PackageReference OpenApi version moves name stays

    Now run dotnet build again. It should succeed as before.

    Why this matters more as the project grows

    Right now the menu has one line. That looks like a lot of ceremony for one package. But a real API soon uses many packages: one for the database, one for testing, one for logging, one for login tokens, and more. Later in this course we add several of them. A real company solution can easily use fifty.

    Here is the same file a few lessons from now. The package names and version numbers are only examples:

    <Project>
      <PropertyGroup>
        <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
      </PropertyGroup>
      <ItemGroup>
        <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
        <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.12" />
        <PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.12" />
        <PackageVersion Include="MSTest" Version="4.0.0" />
      </ItemGroup>
    </Project>
    

    Without this file, each of those versions would be written in every project that uses the package. With several projects and many packages, upgrading means opening every project file, finding every line and checking that nothing was missed. It is easy to get one wrong, and a mismatch can break the build or cause odd behaviour.

    With this file, there is one list and one place to look. To upgrade a package, you change one number in one file, and every project in the solution gets it on the next build. To see all the packages the solution uses, you read one short file.

    The two files in one minute

    File What it holds One change affects
    Directory.Build.props Shared settings: the .NET version, Nullable, ImplicitUsings every project
    Directory.Packages.props Shared package versions: one line for each package every project

    Both are plain, short files. You write them once, on day one, and they sit quietly in the background. Yet they remove one of the most boring and error-prone jobs of a growing solution: chasing the same number through dozens of files. This is a small habit that many professional teams use, and it costs you almost nothing to start with it now.

    Looking at the project file again, it has become a few lines. Everything shared lives in the two files above. An upgrade next November is a change in those two files, not in every project. From now on, when a lesson adds a package with dotnet add package, or with Manage NuGet Packages in Visual Studio, the tools add the version to Directory.Packages.props for you.


    A tour of Program.cs

    You did not write this file. The template created it when you made the project, and it already works. We only read it, to understand what each part does.

    Open Program.cs. It is the starting point of the app. It has no class and no Main method: C# lets a file start with plain statements (called top-level statements), and writes the wrapper for you.

    The whole file does four things, in this order:

    The four steps of Program.cs 1. Builder start setting up 2. Services register parts 3. Build lock the setup 4. Run handle requests before Build() after Build()

    1 and 2. The builder and the services

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddOpenApi();
    

    The builder collects everything the app needs before it starts. builder.Services is the list of services, the same idea as in lesson 03: parts that the app can hand out to whoever asks. AddOpenApi() registers one part, the one that describes the API for tools like Swagger (lesson 15). Every service we write in later lessons is registered in this section.

    3. Build

    var app = builder.Build();
    

    Build() turns the setup into a real, ready app. After this line the list of services is closed. That is why all registrations must come before it.

    4. The pipeline, the endpoints and Run

    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi();
    }
    
    app.UseHttpsRedirection();
    
    app.MapGet("/weatherforecast", () =>
    {
        // the template fills this in: it builds five fake forecasts
    })
    .WithName("GetWeatherForecast");
    
    app.Run();
    

    This part says how the running app treats a request. (The MapGet body is shortened here. Your file has the full code where the comment is.)

    • UseHttpsRedirection() sends visitors who use plain http to the secure https address. It is a step that every request passes through, in the order the steps are written. That ordered chain is called the pipeline. We add more steps to it in later lessons, such as the error handler in lesson 19.
    • MapOpenApi() is an endpoint: it answers requests for the API description. IsDevelopment() means it is only active on your own machine, not on the live server.
    • MapGet("/weatherforecast", ...) is another endpoint. It answers a GET request for the path /weatherforecast. It is the sample that the template ships with, so you can see something work. We replace it in the next lesson.
    • Run() starts the app and waits for requests. It does not return until you stop the app.

    The other files

    The template also made some files next to Program.cs:

    • Properties/launchSettings.json tells your computer which address to use when you run the project.
    • BookShop.Api.http is a file with sample requests. We come back to it in the next lesson.
    • appsettings.json and appsettings.Development.json are the app's settings files. They matter more, so they get their own section.

    The settings files

    Every real app has settings: how much to write in the log, where the database is, what the connection string is. You could write them straight into the C# code. Do not. A setting in the code is a setting you can only change by editing the code, building again and deploying a new version.

    Settings often differ between places the app runs. Your laptop uses a small test database. The live server uses a big real one. If the values live in a file, you change the file, run the app again, and the code stays the same. Nobody has to rebuild anything.

    So appsettings.json is a plain text file with the app's settings, written as JSON (lesson 08). The template's first version holds logging settings. In a later lesson, the database connection goes here too. .NET reads the file when the app starts, and the code asks for a setting by name.

    Why is there a second file, appsettings.Development.json? Because the same app runs in different environments, such as Development (your own machine) and Production (the live server). The two files work in layers:

    1. appsettings.json is always read. It holds the settings every environment shares.
    2. Then the file for the current environment is read on top. Any setting it repeats replaces the one from the first file.

    Here is an example with one setting, the log level (how much detail the app writes to its log). The values are made up to show the idea:

    How the two settings files combine appsettings.json LogLevel: Warning appsettings .Development.json LogLevel: Debug in Development Debug appsettings.json LogLevel: Warning Development file ignored in Production Warning

    On your machine, the environment is Development, so both files apply. That is set in launchSettings.json: open it and look for ASPNETCORE_ENVIRONMENT. On a live server the environment is usually Production, so appsettings.Development.json is simply ignored. A typical use: write much more detail in the log while you develop, and keep it quiet in production.


    Run it

    Time to see it working.

    Terminal

    dotnet run --project src/BookShop.Api
    
    info: Microsoft.Hosting.Lifetime[14]
          Now listening on: http://localhost:5159
    info: Microsoft.Hosting.Lifetime[0]
          Application started. Press Ctrl+C to shut down.
    

    The port number (5159 here) is different on every machine. Use the one your terminal shows. Open http://localhost:5159/weatherforecast in your browser. Press Ctrl+C in the terminal to stop the app.

    Visual Studio

    1. Press Ctrl+F5 (run without debugging). Visual Studio may ask you to trust a development certificate. Choose Yes.
    2. Find the address the app is listening on. It is printed in the console window that opens.
    3. Open /weatherforecast on that address in your browser.
    4. Stop the app with Shift+F5, or close the console window.

    What you should see

    The browser shows a short list of weather data in JSON (lesson 08):

    [{"date":"2026-10-22","temperatureC":-6,"summary":"Freezing","temperatureF":22}, ...]
    

    (The values are random and change on every call.) It is nothing to do with books. It only proves that the project builds, starts and answers a request. Next lesson, we replace it with the first real bookshop endpoint.


    Recap

    Here is the folder we ended up with. Some files were generated for us, and two we wrote ourselves:

    BookShop/
      BookShop.slnx                         generated: the solution
      Directory.Build.props                 written by us: shared settings
      Directory.Packages.props              written by us: shared package versions
      src/
        BookShop.Api/
          BookShop.Api.csproj               generated, then shortened by us
          Program.cs                        generated: the app's starting point
          appsettings.json                  generated: settings
          appsettings.Development.json      generated: settings for your machine
          BookShop.Api.http                 generated: sample requests
          Properties/
            launchSettings.json             generated: how to run it locally
    

    What we did, in short:

    • We made one solution with an API project in a src folder, using commands or Visual Studio. Creating a project does not add it to the solution by itself, so dotnet sln add is a separate step.
    • We moved the shared settings out of the project into Directory.Build.props (TargetFramework, Nullable, ImplicitUsings), and the package versions into Directory.Packages.props. Projects now ask for a package by name only. When .NET moves on, we change two files, not every project.
    • We read Program.cs, which has four steps: create the builder, register services, Build(), then set up the pipeline and endpoints and Run().
    • We learned that settings belong in appsettings.json, not in the code, and that appsettings.Development.json adds or replaces settings on your own machine.
    • We ran the app. The /weatherforecast endpoint is only a sample. We replace it next lesson.

    Questions to think about

    The answers are not given here on purpose, so check yourself against the lesson.

    1. In one sentence, what is Directory.Build.props for?
    2. Which file holds the version numbers of the packages? What does the project file say about a package instead?
    3. Your solution has ten projects and you want to move to a newer .NET version. Which file do you change, and how many times?
    4. Put these three in the order they happen in Program.cs: Build(), Run(), adding services.
    5. Why is it better to keep a setting such as a database address in appsettings.json than inside the C# code?

    Next lesson: the first real endpoint, and a page where we can try it.

    ← .NET Versions and Why Upgrades Are Routine