Skip to main content

Part 2: Create a Production-Grade .NET Solution

Before an agent can answer a single question, it needs a home. In this part you build that home the way production teams do: one solution, a pinned .NET SDK, shared build settings, every package version in one file, and two projects — a class library for the agent's "brain" and an ASP.NET Core API that will host it.

This is Part 2 of a 20-part series.

  1. Part 1: Overview, Architecture and Azure Setup
  2. Part 2: Create a Production-Grade .NET Solution (you are here)
  3. Part 3: Your First Agent API with Microsoft Foundry
  4. Part 4: Multi-Turn Conversations with Sessions
  5. Part 5: A Plug-and-Play Next.js Chat UI (coming soon)
  6. Part 6: Secure the API with Microsoft Entra ID (coming soon)
  7. Part 7: Give the Agent Enterprise Tools (coming soon)
  8. Part 8: Create Tickets and Test Your Tools (coming soon)
  9. Part 9: Connect an MCP Server (coming soon)
  10. Part 10: Human Approval for Sensitive Actions (coming soon)
  11. Part 11: Approval Cards in the Chat UI (coming soon)
  12. Part 12: Persistent Conversations (coming soon)
  13. Part 13: Memory That Knows the Employee (coming soon)
  14. Part 14: Guardrails — Keep Secrets Out of the Model (coming soon)
  15. Part 15: Audit Logs and Observability (coming soon)
  16. Part 16: Ticket Triage Workflow (coming soon)
  17. Part 17: Manager Approvals and Checkpoints (coming soon)
  18. Part 18: Agent Harness — Weekly Problem Report (coming soon)
  19. Part 19: Deploy to Azure Container Apps (coming soon)
  20. Part 20: Bring It to Microsoft Teams (coming soon)

There is no AI in this part yet — and that is on purpose. Everything the later parts add (the agent, its tools, sign-in, the chat page) plugs into the structure you create today, so it is worth getting right. Every step works in VS Code and in Visual Studio; where the two differ, the page shows a tab for each. Every output on this page is real, captured while building the project.

🎥 Detailed Video​

What You Will Build​

By the end of this part you will have this solution on your machine:

macOS-style window titled DewirideServiceDesk showing the finished folder tree: the .slnx solution file, global.json, Directory.Build.props, Directory.Packages.props, .gitignore, and a src folder holding the Dewiride.ServiceDesk.Agent class library and the Dewiride.ServiceDesk.Api project

DewirideServiceDesk/
├── DewirideServiceDesk.slnx the solution: lists every project
├── global.json pins the .NET SDK version
├── Directory.Build.props build settings every project shares
├── Directory.Packages.props every NuGet package version, in one place
├── .gitignore keeps build output out of Git
└── src/
├── Dewiride.ServiceDesk.Agent/ the agent's "brain" (class library)
└── Dewiride.ServiceDesk.Api/ the ASP.NET Core API that hosts it

And a running API that answers Healthy at http://localhost:5180/health.

Why a Production-Grade Structure?​

You could put everything in one project and start coding. It would work — until the second developer joins, the third project arrives, or a package update breaks one project but not the others. Each piece below solves one of those problems:

PieceWhat it doesWhy it matters in production
Solution file (.slnx)Groups all projects so you can build, run and open them togetherOne command builds everything; Visual Studio and VS Code open the whole system at once
global.jsonPicks which .NET SDK builds the codeEvery developer and every build server uses the same .NET version
Directory.Build.propsHolds settings shared by every projectUpgrading .NET later is a one-line change, not one change per project
Directory.Packages.propsHolds every NuGet package versionNo two projects can quietly use different versions of the same package
src/ folderKeeps source projects togetherTests (tests/) and the web page (web/) get their own folders later
Class library + APISplits the agent from the thing that hosts itThe same agent is reused by the API now, and by a background job and a Teams bot later

The last row is the "one brain, many hosts" idea from Part 1. The class library is the brain; the API is the first host.

What This Part Costs​

Nothing. This part only uses free tools on your own machine — no Azure resources are created or called.

Prerequisites​

The .NET 10 SDK. Install it from the .NET download page (choose the SDK, not just the runtime). Check what you have:

Terminal
dotnet --list-sdks
Output
10.0.403 [/usr/local/share/dotnet/sdk]

You need at least one line starting with 10.0. Having several is normal — the next steps make sure the right one is used.

A code editor. Pick one tab — the choice is remembered for every tab on this page:

  • Visual Studio Code on Windows, macOS or Linux.
  • The C# Dev Kit extension. It adds a Solution Explorer, IntelliSense and run and debug support for .NET.
  • The REST Client extension. It sends the test requests in Step 14.

To install an extension, open the Extensions view (Ctrl+Shift+X, or Cmd+Shift+X on macOS), search for its name and select Install.

A terminal. You run a few short commands with the .NET command line (CLI) — in Terminal on macOS, PowerShell on Windows, any Linux shell, or the terminal inside your editor. Every command works exactly as shown in all of them, so you can copy and paste without changing anything. The one place where macOS / Linux and Windows differ (Step 14) has a tab for each.

Part 1 is not needed yet

This part does not touch Azure. You need the Azure setup from Part 1 in Part 3, when the agent connects to its AI model.

Step 1: Create the Solution Folder and Solution File​

Open a terminal in the folder where you keep your code — for example a source folder in your home folder. Then create a folder for the solution, move into it, and create the solution file:

Terminal
mkdir DewirideServiceDesk
cd DewirideServiceDesk
dotnet new sln --name DewirideServiceDesk
Output
The template "Solution File" was created successfully.
Part of the commandWhat it does
dotnet newCreates something from a template — a ready-made starting point
slnThe template for a solution file
--name DewirideServiceDeskThe file name. Without it, .NET uses the folder name — the same here, but explicit is clearer

The folder now contains one file, DewirideServiceDesk.slnx:

DewirideServiceDesk/DewirideServiceDesk.slnx
<Solution>
</Solution>

That is the whole file — an empty list of projects. .slnx is the new XML solution format that .NET 10 creates by default. It replaces the older .sln format, which was hard to read and full of generated IDs. Visual Studio 2026 and the C# Dev Kit both open it.

tip

From now on, run every command from inside this DewirideServiceDesk folder — the solution root. All paths on this page are relative to it.

Step 2: Open the Solution in Your Editor​

Open the solution now, so you can watch each file appear as you create it:

In the same terminal, run:

Terminal
code .

The . means "this folder", so VS Code opens the DewirideServiceDesk folder. If your terminal says code is not recognized, open VS Code yourself and use File → Open Folder… to pick the DewirideServiceDesk folder. (On macOS you can also enable the command: open the Command Palette with Cmd+Shift+P and run Shell Command: Install 'code' command in PATH.)

Then open VS Code's own terminal with Terminal → New Terminal. It starts in the DewirideServiceDesk folder. Run the rest of this part's commands there.

Step 3: Pin the .NET SDK with global.json​

Your machine can have several .NET SDKs installed side by side. When you run dotnet, it normally picks the newest one — so the day you install a preview of .NET 11, every project would quietly start building with it. A global.json file stops that:

Terminal
dotnet new globaljson --sdk-version 10.0.100 --roll-forward latestFeature
Output
The template "global.json file" was created successfully.
DewirideServiceDesk/global.json
{
"sdk": {
"rollForward": "latestFeature",
"version": "10.0.100"
}
}
SettingWhat it means
"version": "10.0.100"The lowest SDK allowed: the first .NET 10 SDK
"rollForward": "latestFeature"Use the newest installed .NET 10 SDK (10.0.1xx, 2xx, 3xx and so on) — but never jump to .NET 11

See which SDK the solution now uses:

Terminal
dotnet --version
Output
10.0.403

global.json accepts any .NET 10 SDK from 10.0.100 up and picks the newest one installed — on my machine, 10.0.403. Yours may print a different 10.0 number — any is fine. Visual Studio respects this file too.

Step 4: Add a .gitignore​

Building a .NET project creates bin and obj folders full of generated files. Editors add their own settings folders. None of that belongs in source control. The .NET SDK ships a ready-made ignore list:

Terminal
dotnet new gitignore
Output
The template "dotnet gitignore file" was created successfully.

The new .gitignore file has a few hundred lines that cover Visual Studio, VS Code, Rider, build output and more. It also ignores .env files, which matters in Part 5 when the chat page gets its own settings file.

Using Git?

If you track your code with Git, this is a good moment to run git init. This series does not require Git, so the rest of the page does not depend on it.

Step 5: Create the Shared Build Files​

Two more files sit at the solution root and affect every project. Create both now:

Terminal
dotnet new buildprops
dotnet new packagesprops
Output
The template "MSBuild Directory.Build.props file" was created successfully.
The template "MSBuild Directory.Packages.props file" was created successfully.

Directory.Build.props is where shared build settings live. The template starts empty:

DewirideServiceDesk/Directory.Build.props
<Project>
<!-- See https://aka.ms/dotnet/msbuild/customize for more details on customizing your build -->
<PropertyGroup>


</PropertyGroup>
</Project>

MSBuild — the engine behind dotnet build and Visual Studio — automatically reads a file with exactly this name for every project in this folder and all folders below it. You will fill it in Step 8.

Directory.Packages.props turns on Central Package Management:

DewirideServiceDesk/Directory.Packages.props
<Project>
<PropertyGroup>
<!-- Enable central package management, https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management -->
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
</ItemGroup>
</Project>

With ManagePackageVersionsCentrally set to true, a project file only says which NuGet packages it uses. The version of each package lives here, in one list. This solution has no packages yet; in Part 3, dotnet add package writes the first versions into this file for you.

This matters more than it looks. Microsoft Agent Framework ships new versions almost every week, and this series pins exact versions. With central package management, moving the whole solution to a new version means changing one line.

Step 6: Create the Two Projects​

First, the class library — the agent's brain. A class library is code that other projects use; it does not run on its own:

Terminal
dotnet new classlib --name Dewiride.ServiceDesk.Agent --output src/Dewiride.ServiceDesk.Agent
Output
The template "Class Library" was created successfully.

Processing post-creation actions...
Restoring /Users/you/source/DewirideServiceDesk/src/Dewiride.ServiceDesk.Agent/Dewiride.ServiceDesk.Agent.csproj:
Restore succeeded.

Next, the API — the host. The ASP.NET Core Empty template (web) gives a working web app with nothing extra in it:

Terminal
dotnet new web --name Dewiride.ServiceDesk.Api --output src/Dewiride.ServiceDesk.Api
Output
The template "ASP.NET Core Empty" was created successfully.

Processing post-creation actions...
Restoring /Users/you/source/DewirideServiceDesk/src/Dewiride.ServiceDesk.Api/Dewiride.ServiceDesk.Api.csproj:
Restore succeeded.

Your paths will be different — on Windows they start with something like C:\Users\you\source\. "Restore" means .NET checked which packages each project needs (none yet).

OptionWhat it does
--nameThe project name. It also becomes the default namespace in the code
--output src/...Where to create the project. Keeping projects under src/ leaves room for tests/ and web/ later

The names follow Microsoft's .NET naming guideline for namespaces: Company.Product.Feature. Dewiride is the company, ServiceDesk the product, and Agent or Api the part. Later projects follow the same pattern.

Why the Empty template rather than the Web API template? The Web API template adds a sample weather forecast and extra packages that you would delete straight away. Starting empty means every line in the project is one you added on purpose.

The class library template adds a placeholder file, Class1.cs, that you do not need. Delete it — right-click it in your editor and choose Delete, or run:

Terminal
rm src/Dewiride.ServiceDesk.Agent/Class1.cs

(rm works in PowerShell too.) The library is now empty. That is fine: it compiles, and Part 3 fills it with the agent.

Step 7: Add the Projects to the Solution​

The projects exist on disk, but the solution does not know about them yet. Add both:

Terminal
dotnet sln add src/Dewiride.ServiceDesk.Agent src/Dewiride.ServiceDesk.Api
Output
Project `src/Dewiride.ServiceDesk.Agent/Dewiride.ServiceDesk.Agent.csproj` added to the solution.
Project `src/Dewiride.ServiceDesk.Api/Dewiride.ServiceDesk.Api.csproj` added to the solution.

Now tell the API that it uses the class library. This is called a project reference:

Terminal
dotnet add src/Dewiride.ServiceDesk.Api reference src/Dewiride.ServiceDesk.Agent
Output
Reference `..\Dewiride.ServiceDesk.Agent\Dewiride.ServiceDesk.Agent.csproj` added to the project.

Read the command as "add to the API a reference to the Agent library". The direction matters: the API depends on the brain, never the other way round. That keeps the brain free of any web code, so a background job or a Teams bot can use it later without dragging an API along.

Open DewirideServiceDesk.slnx again. The CLI filled it in:

DewirideServiceDesk/DewirideServiceDesk.slnx
<Solution>
<Folder Name="/src/">
<Project Path="src/Dewiride.ServiceDesk.Agent/Dewiride.ServiceDesk.Agent.csproj" />
<Project Path="src/Dewiride.ServiceDesk.Api/Dewiride.ServiceDesk.Api.csproj" />
</Folder>
</Solution>

Because the projects live under src/, the CLI also created a solution folder called src. Solution Explorer — in both editors — now shows a src folder containing the two projects.

Step 8: Move Shared Settings into Directory.Build.props​

Open both project files. They contain the same three settings. The library's project file:

src/Dewiride.ServiceDesk.Agent/Dewiride.ServiceDesk.Agent.csproj (as created)
<Project Sdk="Microsoft.NET.Sdk">

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

</Project>

And the API's, which now also has the project reference from Step 7:

src/Dewiride.ServiceDesk.Api/Dewiride.ServiceDesk.Api.csproj (as created)
<Project Sdk="Microsoft.NET.Sdk.Web">

<ItemGroup>
<ProjectReference Include="..\Dewiride.ServiceDesk.Agent\Dewiride.ServiceDesk.Agent.csproj" />
</ItemGroup>

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

</Project>

Two copies of the same settings will drift apart sooner or later. Give them one home instead.

To open the project files: in VS Code, select them in the Explorer. In Visual Studio, double-click a project in Solution Explorer to open its .csproj. To open Directory.Build.props in Visual Studio for now, use File → Open → File… — Step 9 makes it appear in Solution Explorer.

1. Add the three settings to Directory.Build.props. Put them inside the empty <PropertyGroup>:

DewirideServiceDesk/Directory.Build.props (add inside <PropertyGroup>)
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
SettingWhat it means
TargetFrameworkWhich .NET the code runs on. net10.0 is .NET 10, a long-term support (LTS) release
ImplicitUsingsAdds the most common using lines (such as System and System.Linq) to every file for you
NullableMakes the compiler warn you when a value might be null — it catches a whole class of crashes before they happen

The complete file:

DewirideServiceDesk/Directory.Build.props
<Project>
<!-- See https://aka.ms/dotnet/msbuild/customize for more details on customizing your build -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

2. Remove the duplicates from the project files. Replace the whole content of the library's project file with:

src/Dewiride.ServiceDesk.Agent/Dewiride.ServiceDesk.Agent.csproj
<Project Sdk="Microsoft.NET.Sdk">

</Project>

And the API's with:

src/Dewiride.ServiceDesk.Api/Dewiride.ServiceDesk.Api.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

<ItemGroup>
<ProjectReference Include="..\Dewiride.ServiceDesk.Agent\Dewiride.ServiceDesk.Agent.csproj" />
</ItemGroup>

</Project>

The library's project file really is almost empty now. Everything it needs comes from Directory.Build.props. What is left in each file is what makes it different:

  • Sdk="Microsoft.NET.Sdk" — an ordinary .NET project.
  • Sdk="Microsoft.NET.Sdk.Web" — a web project. It brings in ASP.NET Core and the web server.
  • The ProjectReference — only the API uses the library.

When you move to a newer .NET version one day, you change net10.0 once here (and the SDK in global.json), and every project follows.

Step 9: Show the Shared Files in Solution Explorer​

global.json, .gitignore and the two Directory.*.props files affect the whole solution, but Visual Studio's Solution Explorer only lists what the solution file mentions — so these files stay hidden there. The standard fix is a solution folder, by convention called Solution Items, that lists them. VS Code's normal Explorer already shows every file, but adding the folder keeps the solution complete for anyone on your team who opens it in Visual Studio:

Open DewirideServiceDesk.slnx from the Explorer and add a Solution Items folder as the first entry inside <Solution>:

DewirideServiceDesk/DewirideServiceDesk.slnx (add inside <Solution>, above the src folder)
<Folder Name="/Solution Items/">
<File Path=".gitignore" />
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
<File Path="global.json" />
</Folder>

Save the file.

Either way, the complete solution file now reads:

DewirideServiceDesk/DewirideServiceDesk.slnx
<Solution>
<Folder Name="/Solution Items/">
<File Path=".gitignore" />
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
<File Path="global.json" />
</Folder>
<Folder Name="/src/">
<Project Path="src/Dewiride.ServiceDesk.Agent/Dewiride.ServiceDesk.Agent.csproj" />
<Project Path="src/Dewiride.ServiceDesk.Api/Dewiride.ServiceDesk.Api.csproj" />
</Folder>
</Solution>

A solution folder is only a label in Solution Explorer. Nothing moves on disk, and the build ignores the files listed there.

Step 10: Add a Health Endpoint​

Open src/Dewiride.ServiceDesk.Api/Program.cs. This file starts the API. The template wrote four lines:

src/Dewiride.ServiceDesk.Api/Program.cs (as created)
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
LineWhat it does
WebApplication.CreateBuilder(args)Prepares the app: reads settings, sets up logging, and gives you a place to register services
builder.Build()Turns that preparation into a runnable app
app.MapGet("/", ...)Answers web requests to / with the text Hello World!
app.Run()Starts the web server and waits for requests until you stop it

Every production API needs a way to answer one question: "are you up and running?" In Part 19, Azure Container Apps asks exactly that, many times a minute, to decide whether to send traffic to the API or restart it. ASP.NET Core has this built in, as health checks.

1. Register the health check service. Below the first line, add:

src/Dewiride.ServiceDesk.Api/Program.cs (add below CreateBuilder)
// Health checks answer one question: "is this API up and running?"
builder.Services.AddHealthChecks();

builder.Services is the app's list of services — a pattern called dependency injection that the rest of the series uses all the time. AddHealthChecks() adds the service that runs the checks.

2. Replace the Hello World line with a health endpoint:

src/Dewiride.ServiceDesk.Api/Program.cs (replace the MapGet line)
app.MapHealthChecks("/health");

MapHealthChecks("/health") answers requests to /health with Healthy while the API works. An API does not need a home page, so the Hello World line goes.

The complete file:

src/Dewiride.ServiceDesk.Api/Program.cs
var builder = WebApplication.CreateBuilder(args);

// Health checks answer one question: "is this API up and running?"
builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/health");

app.Run();

Step 11: Give the API a Fixed Address​

Open src/Dewiride.ServiceDesk.Api/Properties/launchSettings.json. It tells dotnet run, VS Code and Visual Studio how to start the API on your machine:

src/Dewiride.ServiceDesk.Api/Properties/launchSettings.json (as created)
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5029",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7059;http://localhost:5029",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

The template picked random port numbers — yours will be different from 5029 and 7059. The rest of the series (the test requests, the chat page in Part 5) needs to know where the API lives, so give it a fixed address that everybody shares.

SettingWhat it means
profilesNamed ways to start the app. http comes first, so dotnet run uses it unless you choose another one
commandName: "Project"Run the project itself on the built-in Kestrel web server
dotnetRunMessagesShow the Building... message when you start the app with dotnet run
launchBrowserOpen a browser when the app starts. Visual Studio does this; dotnet run ignores it
applicationUrlThe address the API listens on
ASPNETCORE_ENVIRONMENTMarks this as the Development environment. Later parts switch on developer-only settings with it

1. Fix the ports. In the http profile, change applicationUrl to:

launchSettings.json (http profile)
"applicationUrl": "http://localhost:5180",

and in the https profile:

launchSettings.json (https profile)
"applicationUrl": "https://localhost:7180;http://localhost:5180",

2. Open the health page when the browser launches. Add a launchUrl line below launchBrowser in both profiles:

launchSettings.json (both profiles)
"launchUrl": "health",

The complete file:

src/Dewiride.ServiceDesk.Api/Properties/launchSettings.json
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "health",
"applicationUrl": "http://localhost:5180",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "health",
"applicationUrl": "https://localhost:7180;http://localhost:5180",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

launchSettings.json is only used on your machine — it is never published or deployed. In Azure (Part 19), the platform decides the address and handles HTTPS for you.

Step 12: Build the Solution​

Build everything once, from the solution root:

Terminal
dotnet build
Output
Restore complete (0.7s)
Dewiride.ServiceDesk.Agent net10.0 succeeded (1.6s) → src/Dewiride.ServiceDesk.Agent/bin/Debug/net10.0/Dewiride.ServiceDesk.Agent.dll
Dewiride.ServiceDesk.Api net10.0 succeeded (0.7s) → src/Dewiride.ServiceDesk.Api/bin/Debug/net10.0/Dewiride.ServiceDesk.Api.dll

Build succeeded in 3.2s

dotnet build found the .slnx file in the folder and built both projects — the library first, because the API depends on it. Both say net10.0, which comes from Directory.Build.props. There are no warnings. The times on your machine will differ.

In Visual Studio you can also use Build → Build Solution (Ctrl+Shift+B); the result appears in the Output window.

Step 13: Run the API​

In VS Code's terminal, from the solution root, run:

Terminal
dotnet run --project src/Dewiride.ServiceDesk.Api

--project says which project to start — the solution has two, and only the API can run.

The API starts and writes this log:

Output
Using launch settings from src/Dewiride.ServiceDesk.Api/Properties/launchSettings.json...
Building...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5180
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: /Users/you/source/DewirideServiceDesk/src/Dewiride.ServiceDesk.Api
Log lineWhat it tells you
Using launch settings from ...dotnet run used the http profile from Step 11
Now listening on: http://localhost:5180The fixed address works
Hosting environment: DevelopmentASPNETCORE_ENVIRONMENT came through from the launch profile
Content root pathThe folder the API reads its settings files from

Leave the API running for the next step.

Step 14: Call the API from an .http File​

An .http file is a plain text file of web requests that your editor can send for you. It lives next to the code, so the whole team shares the same test requests — and you will add to this one in almost every part of the series.

Create a file named Dewiride.ServiceDesk.Api.http in the src/Dewiride.ServiceDesk.Api folder:

In the Explorer, right-click the Dewiride.ServiceDesk.Api folder (inside src), select New File… and type Dewiride.ServiceDesk.Api.http.

Put this in the file:

src/Dewiride.ServiceDesk.Api/Dewiride.ServiceDesk.Api.http
@baseUrl = http://localhost:5180

# Is the API up and running?
GET {{baseUrl}}/health

###
LineWhat it does
@baseUrl = http://localhost:5180A variable for the API's address, so it is written in one place
# Is the API up and running?A comment
GET {{baseUrl}}/healthThe request: a GET to /health. {{baseUrl}} is replaced by the variable's value
###Marks the end of a request. Later parts add more requests below it

Now send the request:

The REST Client extension adds a small Send Request link above the GET line. Select it. The response opens in a new tab beside your file.

Response
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Thu, 29 Oct 2026 16:08:39 GMT
Server: Kestrel
Cache-Control: no-store, no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Transfer-Encoding: chunked

Healthy

200 OK with the word Healthy: your API is up. Notice the Cache-Control: no-store, no-cache header — health checks are never cached, so a monitor always sees the current state. (Each tool lays out the headers a little differently, but the content is the same.)

Prefer the terminal? Send the same request with curl:

Terminal
curl -i http://localhost:5180/health

Stop the API when you are done. In VS Code's terminal, press Ctrl+C. In Visual Studio, press Ctrl+C in the console window, or close it. The log ends with:

Output
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...

Your Solution Now Looks Like This​

DewirideServiceDesk/
├── .gitignore
├── DewirideServiceDesk.slnx
├── Directory.Build.props
├── Directory.Packages.props
├── global.json
└── src/
├── Dewiride.ServiceDesk.Agent/
│ └── Dewiride.ServiceDesk.Agent.csproj
└── Dewiride.ServiceDesk.Api/
├── Properties/
│ └── launchSettings.json
├── appsettings.Development.json
├── appsettings.json
├── Dewiride.ServiceDesk.Api.csproj
├── Dewiride.ServiceDesk.Api.http
└── Program.cs

You will also see bin and obj folders inside each project. They are build output, recreated by every build, and .gitignore keeps them out of source control. The two appsettings files are untouched templates for now; Part 3 puts the agent's settings in appsettings.json.

Common Mistakes​

MistakeWhat happensFix
Running a command outside the solution rooterror MSB1003: Specify a project or solution filecd into the DewirideServiceDesk folder first
Forgetting --project with dotnet runCouldn't find a project to run. — the solution root holds a solution, not a runnable projectUse dotnet run --project src/Dewiride.ServiceDesk.Api
Leaving the three settings in the project files after Step 8Nothing breaks today, but the copies drift apart laterKeep each setting only in Directory.Build.props
Adding Version="..." to a package in a .csprojerror NU1008 — versions belong in Directory.Packages.propsLet dotnet add package write the version, or move it by hand
Opening the folder in Visual Studio instead of the .slnx fileVisual Studio shows plain files instead of projectsUse Open a project or solution and pick DewirideServiceDesk.slnx
Using Visual Studio 2022It cannot target .NET 10Use Visual Studio 2026
Sending the .http request before starting the APIThe request fails with a connection errorStart the API (Step 13), then send the request again
Starting the API twiceThe second copy fails because the port is takenStop the first copy, or use the one that is already running

Troubleshooting​

SymptomCauseFix
A compatible .NET SDK was not found. with Requested SDK version: 10.0.100No .NET 10 SDK is installed, so global.json cannot be satisfiedInstall the .NET 10 SDK, then open a new terminal
MSBUILD : error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file.The terminal is in the wrong foldercd into DewirideServiceDesk, or pass the path, as in dotnet build DewirideServiceDesk.slnx
Failed to bind to address http://127.0.0.1:5180: address already in use.Another copy of the API — or another app — already uses port 5180Stop the other copy (Ctrl+C in its terminal). If another app needs 5180, pick a new port in launchSettings.json and in the .http file
error NU1008: The following PackageReference items cannot define a value for VersionA project file names a package version while central package management is onRemove Version="..." from the .csproj and put the version in Directory.Packages.props
No Send Request link in VS CodeThe REST Client extension is missing, or the file does not end in .httpInstall REST Client; check the file name
code is not recognizedVS Code's command-line launcher is not on your PATHUse File → Open Folder…, or install the code command as shown in Step 2
Visual Studio does not load the projectsAn older Visual Studio, or the ASP.NET workload is missingUse Visual Studio 2026 with the ASP.NET and web development workload

Frequently Asked Questions​

Why create a class library that is empty?​

Because the structure is the point of this part. The library is where the agent lives from Part 3 on. Creating it now — and wiring the reference in the right direction — means the API never grows agent code that would later have to be moved out.

Can I use a .sln file instead of .slnx?​

Yes. dotnet new sln --format sln creates the older format, and every command on this page works with it. .slnx is the default in .NET 10 because it is short, readable XML that merges cleanly in Git.

Why not use the Web API template?​

The Web API template includes a sample weather forecast endpoint and an OpenAPI package. Neither is needed here, and deleting sample code is a poor start for a production project. The Empty template gives you a clean API to build up step by step.

Do I need HTTPS on my machine?​

No. The http profile is the default, and everything in this series works with it locally. When the API is deployed in Part 19, Azure provides HTTPS in front of it. The https profile is still there if you want it: dotnet run --project src/Dewiride.ServiceDesk.Api --launch-profile https (you may first need to trust the development certificate with dotnet dev-certs https --trust).

Why is there no Swagger or OpenAPI page?​

The API's main endpoint arrives in Part 3 and speaks AG-UI, a streaming protocol built for chat front ends rather than a classic request/response API. You test it with the .http file and, from Part 5, with the chat page. An OpenAPI description can be added later for the regular endpoints that come with the workflows.

Should I commit launchSettings.json?​

Yes. It holds no secrets — only how to start the API on a developer machine — and sharing it means everyone uses the same port. Secrets never go in this file; this series keeps them in Azure Key Vault.

Where is the AI?​

In Part 3. You now have a solid, empty house; next you move the brain in.

Conclusion​

You built the foundation of the whole series, the way a production team would:

  • a .slnx solution with src/ projects and a Solution Items folder,
  • a pinned .NET 10 SDK in global.json,
  • shared build settings in Directory.Build.props,
  • central package management ready for its first package,
  • an empty class library for the agent, referenced by an ASP.NET Core API,
  • a /health endpoint on a fixed address, tested from an .http file in VS Code or Visual Studio.

Next: Part 3: Your First Agent API with Microsoft Foundry — you will add Microsoft Agent Framework, connect the brain to your Foundry model without any API keys, and stream the agent's first answers through the API.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.