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 BookShopcreates the solution file.dotnet new webapi ... -o src/BookShop.Apicreates the API project inside asrcfolder.dotnet sln addputs 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
- Start Visual Studio and choose Create a new project.
- Search for Blank Solution and choose it, then Next.
- Name it
BookShop, pick a folder, and choose Create. You now have an empty solution. - In Solution Explorer (the panel that lists your files), right-click the solution, then Add > New Project.
- Choose ASP.NET Core Web API, then Next.
- Name it
BookShop.Api. For Location, choose a newsrcfolder inside theBookShopfolder, then Next. - 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.
- 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:
TargetFrameworkis the .NET version.net10.0means .NET 10.Nullablemakes the compiler warn you about values that might be missing (null).ImplicitUsingsadds the most commonusinglines 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:
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.
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:
PackageVersionis a line on the menu: a package and its one agreed version.PackageReferenceis 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:
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:
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 plainhttpto the securehttpsaddress. 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.jsontells your computer which address to use when you run the project.BookShop.Api.httpis a file with sample requests. We come back to it in the next lesson.appsettings.jsonandappsettings.Development.jsonare 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:
appsettings.jsonis always read. It holds the settings every environment shares.- 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:
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
- Press Ctrl+F5 (run without debugging). Visual Studio may ask you to trust a development certificate. Choose Yes.
- Find the address the app is listening on. It is printed in the console window that opens.
- Open
/weatherforecaston that address in your browser. - 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
srcfolder, using commands or Visual Studio. Creating a project does not add it to the solution by itself, sodotnet sln addis a separate step. - We moved the shared settings out of the project into
Directory.Build.props(TargetFramework,Nullable,ImplicitUsings), and the package versions intoDirectory.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 thatappsettings.Development.jsonadds or replaces settings on your own machine. - We ran the app. The
/weatherforecastendpoint 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.
- In one sentence, what is
Directory.Build.propsfor? - Which file holds the version numbers of the packages? What does the project file say about a package instead?
- Your solution has ten projects and you want to move to a newer .NET version. Which file do you change, and how many times?
- Put these three in the order they happen in
Program.cs:Build(),Run(), adding services. - Why is it better to keep a setting such as a database address in
appsettings.jsonthan inside the C# code?
Next lesson: the first real endpoint, and a page where we can try it.