Asp Net Core 6 Succinctly
Asp Net Core 6 Succinctly
NET Core 6
Succinctly
Dirk Strauss
Foreword by Daniel Jebaraj
Copyright © 2023 by Syncfusion, Inc.
ISBN: 978-1-64200-233-1
If you obtained this book from any other source, please register and download a free copy from
www.syncfusion.com.
The authors and copyright holders provide absolutely no warranty for any information provided.
The authors and copyright holders shall not be liable for any claim, damages, or any other
liability arising from, out of, or in connection with the information in this book.
Please do not use this book if the listed terms are unacceptable.
3
Table of Contents
4
Creating a minimal API .........................................................................................................51
Transient........................................................................................................................102
Singleton........................................................................................................................102
Scoped ..........................................................................................................................102
Finally .................................................................................................................................102
5
The Succinctly Series of Books
Daniel Jebaraj
CEO of Syncfusion, Inc.
When we published our first Succinctly series book in 2012, jQuery Succinctly, our goal was to
produce a series of concise technical books targeted at software developers working primarily
on the Microsoft platform. We firmly believed then, as we do now, that most topics of interest
can be translated into books that are about 100 pages in length.
We have since published over 200 books that have been downloaded millions of times.
Reaching more than 2.7 million readers around the world, we have more than 70 authors who
now cover a wider range of topics, such as Blazor, machine learning, and big data.
Each author is carefully chosen from a pool of talented experts who share our vision. The book
before you and the others in this series are the result of our authors’ tireless work. Within these
pages, you will find original content that is guaranteed to get you up and running in about the
time it takes to drink a few cups of coffee.
We are absolutely thrilled with the enthusiastic reception of our books. We believe the
Succinctly series is the largest library of free technical books being actively published today.
Truly exciting!
Our goal is to keep the information free and easily available so that anyone with a computing
device and internet access can obtain concise information and benefit from it. The books will
always be free. Any updates we publish will also be free.
We sincerely hope you enjoy reading this book and that it helps you better understand the topic
of study. Thank you for reading.
Please follow us on social media and help us spread the word about the Succinctly series!
6
About the Author
Dirk Strauss is a software developer from South Africa. He has extensive experience in
SYSPRO customization, with a focus on C# and web development. He is passionate about
writing code and sharing what he learns with others.
7
Chapter 1 Getting Started with
ASP.NET Core 6.0
The .NET 6.0 framework is a major release for Microsoft and allows developers to build almost
anything using one unified platform. The .NET Framework was released in 2002 for Windows
development, and later evolved into what we know as .NET Core. This lightweight, cross-
platform framework allows developers to build different types of applications.
Note: All the code for this book is available in this GitHub repository.
With the release of .NET 5.0 in 2020, Microsoft began to solidify its unified vision for the entire
platform, culminating in .NET 6.0. In this book, we will have a look at .NET 6.0 as it relates to
ASP.NET Core.
Here are some of the features highlighted by Microsoft program manager Richard Lander
regarding .NET 6 in the announcement blog:
Note: Visual Studio 2022 is a required update to use .NET 6.0, and all the code
samples in this book will be using Visual Studio 2022.
8
If you are using Visual Studio Code, you will need to install .NET 6.0 directly using a separate
download. If you want to use Visual Studio, you can download the Community edition (Figure 1),
which is free, and will be good enough for what we want to achieve in this book.
When performing the installation, make sure that you select the ASP.NET and web
development workload option, as shown in Figure 2. If you have already installed Visual Studio
2022, make sure that you have this workload added. If you want to install just .NET 6.0 without
Visual Studio, you can download the installer here.
9
Figure 2: Select the ASP.NET and Web Development Workload
You can verify the installed versions of .NET on your machine by typing the following in the
command prompt.
dotnet --info
You only need .NET 6.0 for the examples in this book, so as long as you have that installed,
you’re good to go.
10
Upgrading existing applications to ASP.NET Core 6.0
If your application is built on a recent version of .NET, upgrading your application to .NET 6.0 is
relatively easy. Upgrading applications built on ASP.NET Core 2.0 and later requires minimal
changes to references and configurations. Upgrading from any ASP.NET Core 1.x application
requires some structural and configuration changes, taking more effort to accomplish. If you
have an application built on the ASP.NET framework, you will have to make considerable
changes to the configuration and architecture.
Some developers choose to upgrade incrementally. In such cases, if you have an ASP.NET
Core 2.1 application, you’d upgrade it to ASP.NET Core 3.1 before upgrading to ASP.NET Core
6.0.
Check out Microsoft’s documentation on upgrading to ASP.NET Core by visiting this link. It
provides useful information about upgrading earlier versions of .NET Core applications. As luck
would have it, I have a book repository API that I wrote in ASP.NET Core 5.0 that I need to
upgrade to ASP.NET Core 6.0.
Tip: I would suggest going through this exercise of upgrading the book repository
project to ASP.NET Core 6.0. We will be using this project again later in the book to
add the data service when we look at minimal APIs. Remember, all the code is
available in the GitHub repository.
Let’s use this book repository and upgrade it to ASP.NET Core 6.0.
Upgrading BookRepository.Core
You will notice from Figure 3 that the solution contains three projects. Each one of these
projects will need to be upgraded to ASP.NET Core 6.0.
11
Figure 3: The BookRepositoryAPI Project
Let’s start by editing the csproj files for the BookRepository.Core project. Right-click the project
and choose the Edit Project File option.
12
In Code Listing 2, you will see the familiar TargetFramework tag specifying .NET 5.0. We also
have a package reference to include Microsoft.AspNetCore.Mvc.Versioning, but at the time
of writing this book, version 5.0.0 is the latest one available.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning"
Version="5.0.0" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning"
Version="5.0.0" />
</ItemGroup>
</Project>
To verify that the upgrade to .NET 6.0 was successful for the BookRepository.Core project,
right-click the project and perform a rebuild. If it succeeds, then the upgrade for that specific
project in the solution worked as expected.
Upgrading BookRepository
The next project that I need to update is the BookRepository project. Right-click the project and
edit the project file by clicking Edit Project File as shown in Figure 4. The project file is listed in
Code Listing 4.
13
Code Listing 4: The BookRepository csproj File
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Controllers\WeatherForecastController.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning"
Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore"
Version="5.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"
Version="5.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers;
buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer"
Version="5.0.11" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference
Include="..\BookRepository.Data\BookRepository.Data.csproj" />
</ItemGroup>
</Project>
In this project, we have a few more package references. As before, change the
TargetFramework from net5.0 to net6.0 and save your changes. Now go ahead and open the
NuGet package manager and view the Updates tab.
14
Figure 5: Update NuGet Packages for the BookRepository Project
Upgrade each of these packages to the latest versions and keep an eye on the Package
Manager Output window for any errors. After updating the NuGet packages, your csproj file
should look like the code in Code Listing 5.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Controllers\WeatherForecastController.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning"
Version="5.0.0" />
15
<PackageReference Include="Microsoft.EntityFrameworkCore"
Version="6.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"
Version="6.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers;
buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer"
Version="6.0.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference
Include="..\BookRepository.Data\BookRepository.Data.csproj" />
</ItemGroup>
</Project>
Give the BookRepository project a rebuild and if it works, the upgrade is successful.
Note: The versions of the package references in the csproj file are correct as of
the time of writing this book. These might, however, differ for you if you are
upgrading this sample project at a later time.
The last project that I need to upgrade is the BookRepository.Data project. Let’s do that next.
Upgrading BookRepository.Data
The last project that I need to update is the BookRepository.Data project. Right-click the project
and edit the project file by clicking Edit Project File as shown in Figure 4. The project file is
listed in Code Listing 6.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning"
Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore"
Version="5.0.11" />
16
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"
Version="5.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers;
buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational"
Version="5.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer"
Version="5.0.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference
Include="..\BookRepository.Core\BookRepository.Core.csproj" />
</ItemGroup>
</Project>
Just like the BookRepository project, we have a few more package references in this project.
Change the TargetFramework from net5.0 to net6.0 and save your changes.
17
Figure 6: The NuGet Packages for the BookRepository.Data Project
As before, we can see on the Updates tab that several NuGet Packages need to be upgraded.
Note: The order in which you update the NuGet packages matters. If you tried
updating the Microsoft.EntityFrameworkCore.Design package before the
Microsoft.EntityFrameworkCore.Relational package, you could receive an error because
Relational is a dependency of Design.
It is worth noting that each NuGet package lists any dependencies in its package description, as
shown in Figure 7.
18
Figure 7: Listed Package Dependencies
It is therefore required that the dependencies be updated before the package is updated;
otherwise, the update will fail.
Once this has been completed, your csproj file will look like the code in Code Listing 7. Rebuild
the project and check that everything went as planned.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning"
Version="5.0.0" />
19
<PackageReference Include="Microsoft.EntityFrameworkCore"
Version="6.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"
Version="6.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers;
buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational"
Version="6.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer"
Version="6.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference
Include="..\BookRepository.Core\BookRepository.Core.csproj" />
</ItemGroup>
</Project>
Finally, right-click the top-level solution and do a complete rebuild of the solution. If all builds
successfully, you have completely upgraded the ASP.NET Core 5.0 project to ASP.NET Core
6.0.
The web API uses a LocalDB database called BookRepo. You can see this in Figure 8. I will not
go into any detail on how to create this database and how to use migrations; I will leave this to
you as homework should you be curious.
20
Figure 8: The SQL Server Object Explorer
The next thing I need to check is what the debug URL is. Right-click the BookRepository
project and click Properties.
21
You will notice in Figure 9 that the properties look slightly different. Under the Debug section,
click the Open debug launch profiles UI link.
In the BookController.cs file in the BookRepository project, there are two GetBooks methods
with the [HttpGet] verb attribute. Each GetBooks method has a different MapToApiVersion
attribute. One is [MapToApiVersion("1.0")], and the other is [MapToApiVersion("1.1")].
Depending on the version of the route I call, the method will list all the books in the database
based on that particular version and logic.
Based on this information, I know that once I run the web API in Visual Studio 2022, the browser
will not be launched (because the Launch Browser checkbox was not selected), and I can list all
the books in the database by calling one of the following endpoints in Postman, as shown in
Code Listings 8 and 9.
https://localhost:44371/api/v1.0/book
22
To call version 1.1 of the API, just change v1.0 to v1.1 in the URL.
https://localhost:44371/api/v1.1/book
Calling one of these endpoints in Postman results in the JSON output shown in Figure 11, with
the list of books returned from the GetBooks method.
Now that I have tested the web API, I am satisfied that the upgrade process from ASP.NET
Core 5.0 to ASP.NET Core 6.0 was successful.
23
Chapter 2 Working with ASP.NET Core 6.0
With the release of .NET 6.0, there have been many improvements to existing features in
ASP.NET Core 6.0. There have also been a few new features added. This chapter will take a
peek at some of these.
This is fine, I suppose, when you are dealing with a small application. But if you are debugging a
large application consisting of multiple projects, Hot Reload becomes a welcome addition to
your developer tool belt.
This is especially true when the debug startup takes several seconds. There are some
limitations, and we will see them shortly.
To demonstrate Hot Reload, take a look at the BookRepository API we upgraded to .NET 6.0 in
Chapter 1. In the BookController.cs file, there is a method called GetBooks_1_1. The code is
illustrated in Code Listing 10.
24
Code Listing 10: The GetBooks_1_1 Controller Action
[HttpGet]
[MapToApiVersion("1.1")]
public async Task<ActionResult<List<BookModel>>> GetBooks_1_1()
{
try
{
var books = await _service.ListBooksAsync();
return (from book in books
let model = new BookModel()
{
Author = book.Author,
Description = book.Description,
Title = book.Title,
Publisher = book.Publisher,
ISBN = book.ISBN + " - for version 1.1"
}
select model).ToList();
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, "There
was a database failure");
}
}
Run the API and view the result in Postman by calling the URL
https://localhost:44371/api/v1.1/book. This will result in the output illustrated in Figure
13.
25
Figure 13: The Postman Result
Don’t stop your debugging session just yet. Modify the GetBooks_1_1 action and change the
text version 1.1 to API version 1.1, as shown in Code Listing 11.
[HttpGet]
[MapToApiVersion("1.1")]
public async Task<ActionResult<List<BookModel>>> GetBooks_1_1()
{
try
{
var books = await _service.ListBooksAsync();
return (from book in books
let model = new BookModel()
{
Author = book.Author,
Description = book.Description,
Title = book.Title,
Publisher = book.Publisher,
26
ISBN = book.ISBN + " - API version 1.1"
}
select model).ToList();
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, "There
was a database failure");
}
}
As illustrated in Figure 14, click Save (I know what you’re thinking, you will see how to change
this behavior shortly), and then click the little flame icon for Hot Reload.
27
Figure 15: The Updated API Result in Postman
You can change the behavior of Visual Studio so that you won’t have to click Save and then
manually start the Hot Reload. In the dropdown menu next to the Hot Reload button, select Hot
Reload on File Save (Figure 16).
With this option selected, there will automatically be a Hot Reload of your project whenever you
save your code changes. Now you can save a button click before doing a Hot Reload—and you
can save two button clicks by using just the keyboard shortcut Ctrl+S. This makes using Hot
Reload very convenient.
28
Limitations of Hot Reload
Hot Reload is not, unfortunately, a magic bullet; it does have some limitations. To illustrate this,
consider the code contained in the Models folder inside the BookModel class. The code is
illustrated in Code Listing 12.
using System.ComponentModel.DataAnnotations;
namespace BookRepository.Models
{
public class BookModel
{
[Required]
public string ISBN { get; set; }
[Required]
public string Title { get; set; }
public string Description { get; set; }
public string Publisher { get; set; }
public string Author { get; set; }
}
}
Ensure that you have selected Hot Reload on File Save as shown in Figure 16 and run your
Book Repository API.
Make a change to the Author property and mark it as Required, as seen in Code Listing 13.
After you have made the change, save it.
using System.ComponentModel.DataAnnotations;
namespace BookRepository.Models
{
public class BookModel
{
[Required]
public string ISBN { get; set; }
[Required]
public string Title { get; set; }
public string Description { get; set; }
public string Publisher { get; set; }
[Required]
public string Author { get; set; }
}
}
29
Figure 17: Limitations of Hot Reload
Visual Studio will display a message informing you that Hot Reload can’t be applied against the
changes you made (Figure 17). You can then rebuild and apply the changes or continue editing.
For the most part, Hot Reload is a great new feature, even with the limitations of not being able
to apply certain changes.
Under Build > Advanced, you will see the Language version available. Here you will see that
C# 10.0 is the version of C# that will be available to your project. Next, let’s have a look at some
of the new features introduced.
30
Global using statements
In C# 10, using directives have been simplified. You can now implement the new global
using directive, and in doing so, reduce the number of usings required at the top of each file.
Looking at the project in Figure 19, you will notice that I just added a file called GlobalUsings. I
also want to use a helper class called CommonHelperMethods in my controllers. This means that
I need to add the using EssentialCSharp10.Helpers; directive to each controller I want to
use my helper methods in. With global using directives, I can add this in a single place and
have it applied throughout my project.
Tip: It is important to note that you can put global usings in any .cs file. This
includes the Program.cs file. I just added a specific file called GlobalUsings to make
the intent clear.
That being said, the global using directive can be added to the beginning of any source code
file. It must appear:
31
It is also worth noting that you can combine the global modifier and the static modifier. This
means you can add global using static System.Console; to your GlobalUsings file. You
can also apply the global modifier to aliases in the case of global using Env =
System.Environment;.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Now you can use the types in often-used namespaces without having to add the using
directives yourself. You can see the generated file in your project’s obj directory. In my case, the
file generated in the obj directory was called EssentialCSharp10.GlobalUsings.g.cs, and
looking inside the file reveals the global using directives implicitly added for my project type.
// <auto-generated/>
global using global::Microsoft.AspNetCore.Builder;
global using global::Microsoft.AspNetCore.Hosting;
global using global::Microsoft.AspNetCore.Http;
global using global::Microsoft.AspNetCore.Routing;
global using global::Microsoft.Extensions.Configuration;
global using global::Microsoft.Extensions.DependencyInjection;
global using global::Microsoft.Extensions.Hosting;
global using global::Microsoft.Extensions.Logging;
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Net.Http.Json;
global using global::System.Threading;
global using global::System.Threading.Tasks;
32
If you don’t see the file, rebuild your solution and it will be generated.
File-scoped namespaces
Another very welcome feature is the addition of file-scoped namespaces. Consider the
extremely complex helper class I created, which is illustrated in Code Listing 16.
namespace EssentialCSharp10.Helpers
{
public static class CommonHelperMethods
{
public static string SanitizeString(string sInput)
{
// Do some string sanitization here
return sInput;
}
}
}
With file-scoped namespaces, I can add the namespace statement, followed by a semicolon,
and remove the curly braces as illustrated in Code Listing 17.
namespace EssentialCSharp10.Helpers;
public static class CommonHelperMethods
{
public static string SanitizeString(string sInput)
{
// Do some string sanitization here
return sInput;
}
}
This simplifies my code and negates an unnecessary level of nesting. You need to remember,
however, that file-scoped namespaces are only available in files with a single namespace. You
can’t add additional namespace declarations (nested or file-scoped).
33
Code Listing 18: Create a Constant from Constants in C# 9
The SupportMessage constant is created by adding the other constants to the string that you
build up. In C# 10, you can use string interpolation to create a constant.
The only thing you need to remember is that all the values you add in the braces {} must also
be constant strings. You can’t use other types, such as numeric or date values. This is because
they are sensitive to Culture.
Figure 20 illustrates the way developers included styles in their projects before CSS isolation.
Developers defined the styles in one or more CSS files that would apply them throughout the
application. To target specific elements, however, classes were used. This would result in a
rather large and unwieldy CSS file, especially when the site grew in complexity and functionality.
It also led to styling conflicts.
34
Figure 20: CSS Before CSS Isolation
35
With CSS isolation, developers can create CSS pages scoped to specific Razor pages in the
application. A global, site-wide CSS file can still be used, but developers can be more granular
in their styling approach. The global CSS file can handle general styling throughout the site,
while the isolated CSS files handle the styles specific to their page.
ASP.NET Core 6.0 also adds support for HTTP logging. By adding a specific middleware
component to the pipeline, developers can view raw logs of the incoming and outgoing HTTP
traffic. Let’s have a look at these two features in the next sections.
36
Looking at the CSS contained inside this file (Code Listing 20), you will see that apart from the
boilerplate CSS, I have added a specific style for H2 elements that will underline the text.
html {
font-size: 14px;
}
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px;
}
h2 {
text-decoration: underline;
}
Running the web application, you will see that I have an H2 element on the Home page (Figure
23). It is underlined because the site.css file specifies that all H2 elements need to be
underlined.
37
Figure 23: The Home Page Using the site.css
Heading over to then Privacy page (Figure 24), you will see that this same site-wide styling has
been applied to its H2 element.
38
I do, however, want to style the H2 element differently on the Privacy page. Right-click the
Home folder and select Add New Item from the context menu.
Choose a style sheet and name the file Privacy.cshtml.css, as seen in Figure 25. The naming
of this file is important. As you can see in Figure 26, when the file is added, it is nested under
the Privacy.cshtml file.
It is therefore isolated to the Privacy page only, and any styles that you add to this CSS file will
only apply to the Privacy page.
39
Figure 26: Added Privacy.cshtml.css Isolated File
Edit the Privacy.cshtml.css file and add the code in Code Listing 21.
h2 {
text-decoration: line-through;
}
You should now have CSS in the site.css file that adds text-decoration: underline to H2
elements, and CSS in the Privacy.cshtml.css file that adds text-decoration: line-
through to H2 elements on the Privacy page only.
Run your web application, and you will see that the Home page still applies the CSS for H2
elements as defined in the site.css file, as illustrated in Figure 27.
40
Figure 27: The Home Page Still Using the site.css
If you navigate to the Privacy page, you will see, as illustrated in Figure 28, that the isolated
CSS style has been applied only to the Privacy page.
Figure 28: The Privacy Page Using the Isolated CSS File
41
If you view the source in the browser developer tools, you will see that ASP.NET has generated
a styles.css file for you as shown in Figure 29.
Looking at the contents of this generated file in Figure 30, you will notice that the style you
added for the H2 element in the isolated Privacy page’s CSS file is contained in this generated
styles.css file with a specific selector.
The generated selector h2[b-l8tbgmvpl4] is applied to the H2 element on the Privacy page,
as seen in Figure 31.
42
Figure 31: The Elements Seen in Developer Tools
This ensures that styles you add to isolated CSS files are always applied to only that specific
page and will not conflict with other styles on your site.
Note: The book repository API project was upgraded from ASP.NET Core 5.0 to
ASP.NET Core 6.0 in Chapter 1. If you haven’t done so yet, ensure that you have
completed the upgrade.
By adding HTTP logging, we can monitor the requests and responses coming into the API. To
start, we will need to add some middleware to our Startup.cs file. Adding one line of code,
app.UseHttpLogging();, will allow ASP.NET Core 6.0 to log the requests and responses.
Your Configure method will look like the code in Code Listing 22.
43
_ = app.UseSwagger();
_ = app.UseSwaggerUI(c =>
c.SwaggerEndpoint("/swagger/v1/swagger.json", "BookRepository v1"));
}
_ = app.UseHttpLogging();
_ = app.UseHttpsRedirection();
_ = app.UseRouting();
_ = app.UseAuthorization();
_ = app.UseEndpoints(endpoints =>
{
_ = endpoints.MapControllers();
});
}
This adds the logging to our middleware pipeline with default settings.
The only other change we need to make is to ensure that the appsettings.json file specifies
the appropriate logging level of Information.
In Solution Explorer, edit the appsettings.json file and ensure that the log levels are set to
Information, as illustrated in Code Listing 23.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"BookConn": "Data Source=(localdb)\\MSSQLLocalDB;Initial
Catalog=BookRepo;Integrated Security=True"
}
}
44
The log level determines the severity of errors that are logged out to the console. In this
instance, we have lowered it to the value of Information instead of Warning.
Note: When logging output like this, there are performance and security
considerations to keep in mind. The danger here is that you might log sensitive
request data in some instances. Therefore, these settings need to be disabled or
configured correctly when moving to a production environment.
Run your API and in Postman make a GET request to the URL
https://localhost:44371/api/v1.1/book, keeping in mind that your URL port could be
different.
As you will see in Visual Studio’s Output window (Figure 32), the logs are displayed for the GET
request you sent from Postman.
Further down the Output window, you will see the 200 response info (Figure 33).
45
Figure 33: The 200 Response Information
The next request that I want to illustrate is a POST. By default, HTTP logging will not include
body information in the output. We need to explicitly change this behavior by configuring some
settings in the API.
Inside the ConfigureServices method, add the code as illustrated in Code Listing 24.
_ = services.AddHttpLogging(log =>
{
log.LoggingFields =
Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All;
});
This will log everything to the Output window, including the body information. Back in Postman,
construct a POST calling the URL https://localhost:44371/api/v1.1/book, passing it the
JSON body in Code Listing 25.
{
"isbn": "978-1-61268-019-4",
"title": "Rich Dad Poor Dad",
"description": "What the Rich Teach Their Kids About Money That the
Poor and Middle Class Do Not!",
"publisher": "Plata Publishing",
"author": "Robert T. Kiyosaki"
}
46
Figure 34: Add a New Book in Postman
When you perform the request, you will see it come through in the Output window of Visual
Studio with a method of POST (Figure 35).
When you scroll down a bit, you will see the request body sent through (Figure 36). This will be
the same as the JSON in Code Listing 25.
47
Figure 36: The POST Request Body
If all goes well, you will receive a 201 Created response (Figure 37).
To test if the book was created, perform a GET request using the URL
https://localhost:44371/api/v1.1/book/search?Isbn=978-1 and inspect the Output
window in Visual Studio (Figure 38).
48
Figure 38: The 200 Response from the GET Request
You will see that the book we just added is returned in the response body. ASP.NET Core 6.0
also adds better support for managing request and response headers in a strongly typed way.
Modify the Configure method as illustrated in Code Listing 26 and add the AcceptLanguage of
en-US.
_ = app.UseHttpLogging();
_ = app.UseHttpsRedirection();
49
_ = app.UseRouting();
_ = app.UseAuthorization();
_ = app.UseEndpoints(endpoints =>
{
_ = endpoints.MapControllers();
});
}
Performing a simple GET request and looking at the Output window in Visual Studio, you will see
the Accept-Language header is set to en-US.
These logging features improve the experience of analyzing the requests and responses going
in and out of your applications.
50
Chapter 3 Minimal APIs
Web APIs evolved from the MVC pattern that uses controllers and action methods. This has
been the standard way of creating APIs. With ASP.NET Core 6.0, one of the biggest new
features is the addition of minimal APIs. Minimal APIs provide an alternative API pattern for
developers to use. In this chapter, we will have a closer look at what minimal APIs are and how
to build one.
The “minimal” part of the name refers to the syntax, dependencies, and the overall structure of
the feature. With this said, minimal APIs do have some limitations:
• Minimal APIs do not support filters. This means that you can’t use action filters,
authorization filters, result filters, or exception filters.
• Form bindings are also not supported. According to some documentation, this will be
added in the future.
• Built-in validation support (for example, using the IModelValidator interface) is also not
supported.
The question now becomes: when should you use minimal APIs, and when should you use
controllers? The short answer is that it all depends on your use case. If you are creating a
complex back-end service for an enterprise-scale application, then a controller-based approach
will be best. This is especially true if you will need to use some features in your API that only
controllers provide.
If, however, you want to create a small and simple API that just returns some data, then a
minimal API approach should work for you. It keeps things simple and saves some time when
creating the API. You can also easily migrate your minimal API to a controller-based API without
too much effort.
It seems wrong to use the word “older,” but if you prefer to use the older approach to create
APIs, then rest assured that they are still fully supported in ASP.NET Core 6.0. With all this said,
let’s start by creating a minimal API in the next section.
51
Figure 40: Search for Web API in Create a New Project Dialog
As seen in Figure 41, I am just calling this API MinAPI, but you can call it what you like.
52
Figure 41: Choose Where to Create the Project
Click Next.
On the Additional information screen (Figure 42), you will see some options available to you.
Make sure that you select .NET 6.0 as the Framework, and very importantly, clear the option to
use controllers. Doing this will use the minimal API approach. You can also select the option to
allow Visual Studio 2022 to create the project without top-level statements. For this project,
however, we will be using top-level statements.
53
Figure 42: Clear the Use Controllers Option
Note: It does not matter if you select not to use top-level statements in your
projects. ASP.NET Core 6.0 is fully compatible with using Program.cs and Startup.cs,
just as it is with using a single Program.cs using top-level statements.
Click Create to create your minimal API with the standard Weather Forecast boilerplate code.
Have a look at the code contained in the Program.cs file (Code Listing 27).
54
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.Run();
Nice and minimal, isn’t it? Minimal APIs do not have controllers or action methods. Requests
are all handled via the methods in the Program file. Below the middleware pipeline setup (below
the app.UseHttpsRedirection(); line), you will see that an array is used to store the weather
summary.
The MapGet method will handle incoming GET requests to the weather service. It has a string of
/weatherforecast that maps the URL that the GET request should map to.
55
Note: You will notice the WithName method after the MapGet method that takes a
string GetWeatherForecast. This names the MapGet method for use with OpenAPI and
Swagger UI.
Before you run your API, have a look at the project properties by right-clicking the MinAPI
project and selecting Properties from the context menu.
The project properties will be displayed (Figure 43), and in the Debug > General tab, click
Open debug launch profiles UI.
In the Launch Profiles window, select IIS Express and scroll down until you see the Launch
browser option. Clear this option, as seen in Figure 44.
56
Scrolling down a little more, you will see the option to Use SSL. Ensure that this option is
selected, as shown in Figure 45, and make a note of the App SSL URL and port. This is the
URL that you will be using in Postman.
When you are done, close the Launch Profiles UI and run your API from Visual Studio, ensuring
that you have IIS Express selected in the Debug dropdown.
57
Figure 46: The GET Request in Postman
Because the App SSL URL is set to https://localhost:44349/, as seen in Figure 45, and
the endpoint URL path on the MapGet method (seen in Code Listing 27) is set to
/weatherforecast, we can create a GET request in Postman using the URL
https://localhost:44349/weatherforecast (as seen in Figure 46).
This returns the weather data from the weather forecast API as expected.
58
Figure 47: The Weather Forecast in Chrome
I like using Postman to test my APIs, but you can also use your browser to make the GET
request, as seen in Figure 47. Minimal APIs also have full Swagger support, as seen in Code
Listing 27, higher up in the Program file.
Go back to your properties and click to modify the Launch Profile for IIS Express, as shown in
Figure 48. Select the option Launch browser.
59
Figure 49: Viewing Your API in Swagger
Running your API again, you will see the browser launch and the Swagger page displayed for
your weather forecast API. Under the MinAPI name at the top of our page, click the link
https://localhost:44349/swagger/v1/swagger.json to view the Swagger JSON file. This file is
shown in Code Listing 28.
The file displays the details of the minimal API based on the OpenAPI specification. You can
see the details we set in the WithName method in Code Listing 27 show up in the operationId
in the Swagger JSON file in Code Listing 28.
Other extension methods, such as WithTags, are available for use in your API to add metadata,
so have a look at what there is on offer.
60
Code Listing 28: The Swagger JSON File
{
"openapi": "3.0.1",
"info": {
"title": "MinAPI",
"version": "1.0"
},
"paths": {
"/weatherforecast": {
"get": {
"tags": [
"MinAPI"
],
"operationId": "GetWeatherForecast",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/WeatherForecast"
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"WeatherForecast": {
"type": "object",
"properties": {
"date": {
"type": "string",
"format": "date-time"
},
"temperatureC": {
"type": "integer",
"format": "int32"
},
"summary": {
"type": "string",
"nullable": true
61
},
"temperatureF": {
"type": "integer",
"format": "int32",
"readOnly": true
}
},
"additionalProperties": false
}
}
}
}
All this was achieved without the need to write code for controllers or action methods. Minimal
APIs work with a very minimalistic approach to structure and design. This is, however, still just a
very basic API that only returns hardcoded data. We would ideally like to consume a service to
provide this information to our API. Let’s have a look at how to use services and dependency
injection in the next section.
To do this, right-click the MinAPI solution in Solution Explorer and click Add > Existing Project
from the context menu, as shown in Figure 50.
Navigate to the location on your hard drive where you have saved your converted
BookRepository project.
62
Note: If you didn’t go through the exercise of upgrading your book repository
project to ASP.NET Core 6.0 in Chapter 1, don’t worry. You can find the upgraded
project in the GitHub repo for this ebook.
From here, add the BookRepository.Core and BookRepository.Data projects to your MinAPI
project. When you have done this, your solution should look as illustrated in Figure 51.
You can see that the MinAPI solution now contains the projects we upgraded in the
BookRepository project in Chapter 1, but we can’t use them without a little more work on our
MinAPI project.
First, we need to add a reference to Entity Framework Core. Right-click the MinAPI project and
click Manage NuGet Packages from the context menu. On the NuGet Package Manager
screen, browse for and add the latest version of Microsoft.EntityFrameworkCore and
Microsoft.EntityFrameworkCore.Design to your MinAPI project.
63
Note: At the time of writing this book, version 6.0.6 is available, but if a newer
version is available for you, be sure to add this version to your MinAPI project.
The package references in your MinAPI solution should now look as illustrated in Figure 52.
With this bit added, we have most of the components ready to use the book repository service
to read the books in our database.
Open the appsettings.json file and add the connection string, as shown in Code Listing 29.
This will be used by the service to connect to the localdb database.
64
Code Listing 29: The appsettings.json File
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"BookConn": "Data Source=(localdb)\\MSSQLLocalDB;Initial
Catalog=MinAPIBookRepo;Integrated Security=True"
}
}
Now all that remains for us to do is add the service to our Program.cs file and consume it in a
MapGet method via dependency injection. Then, before we can call the service, we need to run
our database migrations to create the database.
Open the Program.cs file and register the data access service with the built-in dependency
injection container. It is added as a scoped service in the services collection. In essence, we are
telling our minimal API that whenever something in our project asks for IBookData, then it must
provide the SqlData class.
We also need to tell the DbContext about our database connection. The AddDbContextPool
specifies that DbContext pooling should be used, which allows for increased throughput
because the instances of DbContext are reused, instead of having new instances created per
request. You can see the code in Code Listing 30.
builder.Services.AddScoped<IBookData, SqlData>();
builder.Services.AddDbContextPool<BookRepoDbContext>(dbContextOptns =>
{
_ =
dbContextOptns.UseSqlServer(builder.Configuration.GetConnectionString("Book
Conn"));
});
Inject the service into a MapGet method as a parameter to our delegate. We can now use our
IBookData service in our MapGet method as seen in Code Listing 31. We also need to specify
the URL path of /listbooks and use the service to return the list of books from our data
service. Lastly, add the endpoint name as List Books. This will now be the REST endpoint we
will use to return a list of all books in our database.
65
Code Listing 31: Adding the End Point to List Books
The completed code for the Program.cs file is provided in Code Listing 32. If you come from the
Program.cs and Startup.cs file structure before top-level statements were introduced, the code
might feel slightly uncomfortable to write. It sure feels strange to me to be writing code like this
using top-level statements. This, however, is not a better way of writing your code. It’s just a
different, more succinct way of writing your code that produces the same result as before top-
level statements were introduced.
using BookRepository.Data;
using Microsoft.EntityFrameworkCore;
builder.Services.AddScoped<IBookData, SqlData>();
builder.Services.AddDbContextPool<BookRepoDbContext>(dbContextOptns =>
{
_ =
dbContextOptns.UseSqlServer(builder.Configuration.GetConnectionString("Book
Conn"));
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
app.UseHttpsRedirection();
66
app.MapGet("/listbooks", async (IBookData service) =>
{
return await service.ListBooksAsync();
})
.WithName("List Books");
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.Run();
We’re almost ready to call our service. If you do not already have a LocalDB instance containing
the database, you need to create that before we can go any further. Luckily for us, we have our
database migrations in the BookRepository.Data project.
LocalDB is usually installed when Visual Studio is installed. To check if LocalDB is installed, run
sqllocaldb info from the command prompt, as shown in Figure 53.
67
Figure 53: Check if LocalDB is Installed
You can then use sqllocaldb info mssqllocaldb in the command prompt to see more
information about the MSSQLLocalDB instance (Figure 54).
Next, you need to navigate to the BookRepository.Data directory in the command prompt.
Note: The next few steps will be dependent on where your projects exist in
relation to each other. Bear this in mind when you run the commands in the following
steps.
The project locations for the projects used in this book might be different for you than for me. My
projects are located under a single directory, and are as follows:
• BookRepositoryAPI
• BookRepositoryAPIv5_0
• DependencyInjection
• EssentialCSharp10
• IsolatedCSS
• MinAPI
The easiest way to get to the BookRepository.Data project is to right-click the project in your
solution and select Open Folder in File Explorer from the context menu. Copy the directory path
and change the directory to this location in the command prompt.
68
Then, run the following command seen in Code Listing 33, using the -s switch to specify the
MinAPI startup project relative to the BookRepository.Data project path.
Tip: If at any time you see the message that the Entity Framework tools version is
older than that of the runtime, update the tools by typing dotnet tool update --
global dotnet-ef in the Command Prompt.
After running the command in Code Listing 33, you will see the output illustrated in Figure 55.
Pay special attention to the database name returned. It should be MinAPIBookRepo, as defined
in the appsettings.json file in Code Listing 29. If you see this, you are ready to create your
database by running the command in Code Listing 34 from the command prompt.
After running the command, you will see the output in the command prompt, as illustrated in
Code Listing 35.
Build started...
69
Build succeeded.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 6.0.6 initialized 'BookRepoDbContext' using
provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.5' with options:
MaxPoolSize=1024
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (159ms) [Parameters=[], CommandType='Text',
CommandTimeout='60']
CREATE DATABASE [MinAPIBookRepo];
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (62ms) [Parameters=[], CommandType='Text',
CommandTimeout='60']
IF SERVERPROPERTY('EngineEdition') <> 5
BEGIN
ALTER DATABASE [MinAPIBookRepo] SET READ_COMMITTED_SNAPSHOT ON;
END;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (5ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
SELECT 1
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (20ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
SELECT 1
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (12ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
SELECT OBJECT_ID(N'[__EFMigrationsHistory]');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
SELECT [MigrationId], [ProductVersion]
FROM [__EFMigrationsHistory]
ORDER BY [MigrationId];
info: Microsoft.EntityFrameworkCore.Migrations[20402]
Applying migration '20211107110645_Initial'.
Applying migration '20211107110645_Initial'.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
CREATE TABLE [Books] (
70
[Id] int NOT NULL IDENTITY,
[ISBN] nvarchar(max) NULL,
[Title] nvarchar(max) NULL,
[Description] nvarchar(max) NULL,
[Publisher] nvarchar(max) NULL,
CONSTRAINT [PK_Books] PRIMARY KEY ([Id])
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20211107110645_Initial', N'6.0.6');
Applying migration '20211107125009_BookModelUpdate1'.
info: Microsoft.EntityFrameworkCore.Migrations[20402]
Applying migration '20211107125009_BookModelUpdate1'.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
ALTER TABLE [Books] ADD [Author] nvarchar(max) NULL;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20211107125009_BookModelUpdate1', N'6.0.6');
Done.
You will notice that a few things have happened here. The database MinAPIBookRepo was
created using the CREATE DATABASE command. A database table called Books was also
created. This table will match our Book entity located in the BookRepository.Core project.
Lastly, you will need to create some book data in your database. You can view the database in
Visual Studio by going to the View menu and clicking the SQL Server Object Explorer menu.
Expanding the Databases folder under MSSQLLocalDB, you will see the created
MinAPIBookRepo database. You can then expand the Tables folder, right-click the Books
table, and select View Data from the context menu. Here you can manually add some book
data to the table.
For your convenience, I have added an INSERT script to the GitHub repository called
MinAPIBookRepoData.sql that contains some book data. Open this script and copy the INSERT
statements. Then right-click the MinAPIBookRepo database and click New Query from the
context menu. Paste the copied INSERT script into the query window and run the script by
clicking Execute or pressing Ctrl+Shift+E.
Note: Phew, this was a lot of work to get here. Notice though that most of the
work was setting up the data service and creating the database. Implementing
dependency injection only comprised the code in Code Listings 30 and 31.
71
Run your API, and when your SwaggerUI loads, you will see the /listbooks endpoint listed
along with the /weatherforecast endpoint (Figure 56).
72
Execute the /listbooks endpoint in Swagger, and you will see the results as illustrated in
Figure 57.
73
As always, I’m a fan of Postman, and while your API is running, you can perform a GET request
in Postman by entering the URL https://localhost:44349/listbooks and clicking Send. Just
note that your port might be different from mine.
With the book data returned from our book repository using our minimal API, we have a fully
functioning REST endpoint that returns books stored in our LocalDB database. Hooking up the
data service was more work than actually implementing the dependency injection of the data
service. The next logical thing to do is to be able to add, update, and delete books from our
book repo database. Let’s have a look at how to implement these next.
Implementing the async methods in our endpoints is quite straightforward. Let’s take the
endpoints one by one and make each one functional, starting with adding a new book to the
repository.
74
{
_ = await service.SaveAsync(book);
})
.WithName("Add Book");
As shown in Code Listing 36, app.MapPost will use /book as the URL path for the incoming
HTTP verb POST. It also takes a parameter of Book that contains the book data from the POST
request body that .NET has mapped to the Book object. app.MapPost also takes the
IBookData service, which is required to call the SaveAsync method to save the book data
bound to the Book object via model binding.
Run the application and have a look at the Swagger UI that launches. It will show the new POST
endpoint as shown in Figure 59.
75
Expand the POST endpoint and you will see a sample request and expected responses, as
shown in Figure 60.
Click Try it out, add the code in Code Listing 37, and then click Execute.
76
Code Listing 37: Adding a Dummy Book
{
"isbn": "test777",
"title": "test777",
"description": "test777",
"publisher": "test777",
"author": "test777"
}
The code in Code Listing 37 is just dummy data, but we want to see the book created with our
minimal API. After creating a book, call the /listbooks endpoint, and you will see the book that
you just created returned from the GET request.
Note: Make a note of the ID returned here. We will be using it in our PUT request
later in the chapter.
The book data is somewhat silly, so we need a way to change this data.
The next endpoint that we want to add is the PUT. This will allow us to update a book in our book
repository. The code is almost identical to the POST in Code Listing 36. The only difference here
is that the endpoint URL has changed to /updatebook, and the WithName method specifies
Update Book.
77
Figure 61: The PUT Endpoint Listed in Swagger UI
Running the API again, you will see the newly created PUT endpoint listed, as seen in Figure 61.
78
Figure 62: Sample Request Body for the PUT
Expanding the PUT endpoint will allow you to see a sample request body. What I want to do
though, is change the book we added earlier using the POST in Code Listing 37. The book that I
added earlier was created with an ID of 1024. The ID of the book that you added will be different
than mine. This will be the ID you noted earlier. Use this ID to perform the PUT request and
change some of the data for the book, as seen in Code Listing 39.
79
Code Listing 39: Updating the Newly Added Book with PUT
{
"id": 1024,
"isbn": "test888",
"title": "test888",
"description": "test888",
"publisher": "test888",
"author": "test888"
}
Calling the GET endpoint after executing the PUT will return the modified book data.
Adding the /deletebook endpoint is, again, similar to the /updatebook endpoint. Only this
time, you need to add [FromBody] to tell .NET that the parameter should be bound using the
request body.
80
Figure 63: The Delete Endpoint in Swagger UI
Running your minimal API will display the /deletebook endpoint in Swagger UI, as seen in
Figure 63.
81
Figure 64: The Sample Request for the Delete
By expanding the endpoint and clicking Try it out, you can pass the same code as in Code
Listing 39 to the /deletebook endpoint to delete the dummy book we added earlier. As before,
your ID will differ from mine, and you should use the ID you noted earlier.
82
In reality, while this delete works, I would ideally like to pass the /deletebook endpoint just the
ID of the book to delete. This will make it easier when calling the API. I will leave it up to you to
figure this out, but I won't leave you high and dry.
The code in Code Listing 41 is the completed MapDelete endpoint that accepts a book ID as a
parameter. You need to complete the following:
• Modify the IBookData interface to tell it that any implementing classes need to contain a
DeleteAsync method that accepts a book ID as a parameter.
• Add the implementation to the SqlData class to find the book entity and delete it. Also,
handle errors when the supplied book ID is not found.
The complete code is on GitHub, so if you would like to see the solution, please have a look
there.
If we had to add a book, the Swagger UI would display the response shown in Figure 65.
83
Figure 65: The Current Add Book Response
What we should be doing, though, is returning a status code for the action that took place. In the
case of adding a book, we want to return a status 201 for created.
The modification is simple, as seen in Code Listing 43. The created book ID is returned from the
SaveAcync method, and Results.Created is used to display the result to the user. Adding
another book to the book repository will result in a nice 201 response, as shown in Figure 66.
84
Figure 66: The Improved Add Book Response
The endpoint used to update a book can also be improved. With the current endpoint, if I modify
the dummy book with ID 1029 added earlier, I will receive a very generic response. The code for
my modified book is illustrated in Code Listing 44.
As before, the ID of 1029 is probably not going to exist in your book repo, so be sure to update
a book returned when calling your /listbooks endpoint.
{
"id": 1029,
"isbn": "1455502782",
"title": "Arguably: Essays by Christopher Hitchens",
85
"description": "For nearly four decades, Hitchens has been telling us,
in pitch-perfect prose, what we confront when we grapple with first
principles-the principles of reason and tolerance and skepticism that
define and inform the foundations of our civilization-principles that, to
endure, must be defended anew by every generation.",
"publisher": "Twelve",
"author": "Christopher Hitchens"
}
Calling the /updatebook endpoint will return the response shown in Figure 67.
86
This endpoint response can also be improved, as illustrated in Code Listing 45. Here we are
simply saying after the PUT, return NoContent, which is a 204 status code.
Modifying a book with these code changes in place will result in the 204 being returned, as seen
in Figure 68.
The decision of which response to return here will likely differ from developer to developer. I like
to see the resource that was modified being returned in the response, but that is just my
preference. It is perfectly acceptable to also return Results.Ok from a PUT. I will leave this up to
you to experiment with.
87
It is, therefore, conceivable for APIs to call other APIs—that’s what we’ll be doing in this section.
Our minimal API contains a /weatherforecast endpoint that just returns some dummy data. If
we wanted to provide this functionality from our book repository, we would need to return real
weather data.
Luckily for us, there are more weather APIs out there than you can shake a stick at, and setting
up your minimal API to consume an external API is simple. In this example, I will be consuming
the AfriGIS Weather API. You will find information about this API on the AfriGIS website.
To talk to an external API, we need to use ASP.NET’s HTTP client. At the top of our
Program.cs file, we need to register an HTTP Client Factory as part of our setup code. The line
we need to add is builder.Services.AddHttpClient();. Once it’s added, your code should
look as illustrated in Code Listing 46.
using BookRepository.Core;
using BookRepository.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
builder.Services.AddScoped<IBookData, SqlData>();
builder.Services.AddDbContextPool<BookRepoDbContext>(dbContextOptns =>
{
_ =
dbContextOptns.UseSqlServer(builder.Configuration.GetConnectionString("Book
Conn"));
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
The next thing I need to do is create a WeatherResults entity. This will allow me to specify the
target type to deserialize to when calling the weather API from my minimal API. Add the
WeatherResults class to your BookRepository.Core project, as shown in Figure 69.
88
Figure 69: The Added WeatherResults Class
You can see the complete code for the WeatherResults class in Code Listing 47.
using System;
namespace BookRepository.Core
{
public class WeatherResults
{
public int code { get; set; }
public string source { get; set; }
public Result[] result { get; set; }
}
89
}
This will allow the JSON data that is returned from the AfriGIS Weather API to be deserialized
into an object that I can use.
Tip: To quickly create a class from JSON data, for example, copy the JSON you
want to deserialize. Then in Visual Studio, go to the Edit menu and select Paste Special
> JSON as Classes. This will create a class for you that is inferred from the JSON data
you copied. You can do the same for copied XML. Easy peasy lemon squeezy.
We can now start creating an endpoint that will call out to the AfriGIS Weather API in an
asynchronous way. You can find the complete code in Code Listing 48.
90
var response = await
client.GetFromJsonAsync<WeatherResults>($"{baseUrl}/{endP}/{auth}/?location
={latlong}&location_buffer={range}&station_count={count}");
return Results.Ok(response);
})
.WithName("Get SA Weather");
We start with adding app.MapGet, and give it a route of /sa-weather. We then provide an
async lambda that takes an IHttpClientFactory called factory. Using an async method
here is a good idea, especially when making an external API call.
Lastly, we do an async call to the AfriGIS Weather API using our WeatherResults as a type
parameter, and use the constructed URL to call the endpoint. We then just respond with
Results.Ok, returning the response from the AfriGIS Weather API.
When you run your API, you will see that the Swagger UI lists the /sa-weather endpoint, as
shown in Figure 70. Execute the GET in Swagger, and the JSON illustrated in Code Listing 49
will be returned.
{
“code”: 200,
“source”: “weather.measurements.api”,
“result”: [
{
“station_details”: {
“latitude”: -25.8277,
“longitude”: 28.2235,
“distance”: 3872,
“synop_no”: 68264,
“station_name”: “WATERKLOOF AIR FORCE BASE”
},
“station_readings”: [
{
“datetime”: “2022-07-09T07:00:00Z”,
“temperature”: 13.3,
“humidity”: 67,
“pressure”: 861.4,
“wind_direction”: 26,
“wind_speed”: 0.9,
91
“last_hours_rainfall”: 0
}
]
},
{
“station_details”: {
“latitude”: -25.752,
“longitude”: 28.2585,
“distance”: 6275,
“synop_no”: 68260,
“station_name”: “PRETORIA UNIVERSITY PROEFPLAAS”
},
“station_readings”: [
{
“datetime”: “2022-07-09T07:00:00Z”,
“temperature”: 13.5,
“humidity”: 63,
“pressure”: 874.1,
“wind_direction”: 69,
“wind_speed”: 0.9,
“last_hours_rainfall”: 0
}
]
},
{
“station_details”: {
“latitude”: -25.7663,
“longitude”: 28.2005,
“distance”: 7263,
“synop_no”: 68269,
“station_name”: “PRETORIA UNISA”
},
“station_readings”: [
{
“datetime”: “2022-07-09T07:00:00Z”,
“temperature”: 10.7,
“humidity”: 81,
“pressure”: 866.6,
“wind_direction”: 0,
“wind_speed”: 1.1,
“last_hours_rainfall”: 0
}
]
}
]
}
92
Figure 70: The AfriGIS Weather API Call in Swagger UI
Apart from the minor limitations with minimal APIs, they provide an alternative to traditional web
APIs. Minimal APIs use top-level statements, a single Program.cs file, and implicit usings to
minimize the boilerplate code required.
The MapGet, MapDelete, MapPut, and MapPost helper methods allow binding incoming
requests to handler methods for common verb types. Supporting basic parameter binding from
the URL, minimal APIs also support binding from the request body when using JSON.
Minimal APIs also support middleware pipelines such as cross-origin requests, logging, and
authentication. Minimal APIs allow us to provide results for API calls that help to improve the
developer experience. Last, but not least, dependency injection is also supported, allowing us to
inject services into the handler methods. In the next chapter, we will take a closer look at
dependency injection in ASP.NET Core 6.
93
Chapter 4 Why Use Dependency Injection?
Dependency injection (or DI) is a concept that you will come across sooner or later, if you
haven’t already. This design pattern supports the development of loosely coupled code. Let’s
have a look at how dependency injection works and why it is so beneficial.
To illustrate the architectural problem that dependency injection solves, let's have a look at an
application that uses the minimal API we created earlier to get the weather details from
AfriGIS’s Weather API. The application is a simple web application that displays the data
returned from the API call as shown in Figure 71.
Note: The previous chapter dealt with the creation of the minimal API to call an
external API to return weather data. The API must be deployed or running in Visual
Studio if you want to test against the service in this example. You can, however, just
create a dummy service that returns static data instead of doing a call to the minimal
API. Also note that the example is simply a way to illustrate dependency injection,
and not an example of how to correctly call an external web service (and all the error
handling that goes with it).
You can follow along in the code found on GitHub, or you can create your own weather service
manager to return dummy data. The real point here is to illustrate how dependency injection
solves the problem we have with a tightly coupled dependency.
94
Figure 71: The Weather Portal Web Application
The Razor page model called Index.cshtml.cs is shown in Code Listing 50. The OnGetAsync
method will populate the model that, in turn, is used to create the index page’s content.
Code Listing 50: The Razor Page for the Weather Portal
using Microsoft.AspNetCore.Mvc.RazorPages;
using WeatherPortal.Core;
namespace WeatherPortal.Pages
{
public class IndexModel : PageModel
{
public WeatherResults WeatherResults { get; set; } = new
WeatherResults();
95
public IndexModel()
{
The OnGetAsync method is currently using a class called WeatherManager to make a call to the
minimal API via the GetWeatherAsync method and returning the weather details to the page.
What we have here is a dependency between the OnGetAsync method of the IndexModel class
and the WeatherManager class.
This is because the OnGetAsync method is responsible for creating an instance of the
WeatherManager class on which it depends. You can see this dependency when you look at the
line var weatherManager = new WeatherManager(); in the OnGetAsync method.
As seen in Figure 71, the application works correctly and returns the data as expected, but there
are some problems with this approach. Because the OnGetAsync method is responsible for the
creation of the WeatherManager class, it is tightly coupled to the WeatherManager
implementation.
Creating interfaces
According to the dependency inversion principle, classes should rely on abstractions rather than
implementations. Because the IndexModel is tightly coupled to the WeatherManager
implementation, it does not meet this requirement.
We will therefore have to refactor our code to create an abstraction so that we can reduce the
coupling between classes and their dependencies. An interface will allow us to do this. The
code for the WeatherManager class is illustrated in Code Listing 51.
96
The implementation code in the WeatherManager class does not matter. You can just return
some hard-coded data if you like. I just thought it would be fun to use the minimal API created in
the previous chapter.
using Newtonsoft.Json;
using System.Net.Http.Headers;
namespace WeatherPortal.Core
{
public class WeatherManager
{
private static readonly HttpClient _client = new();
public WeatherManager()
{
_client.BaseAddress = new Uri("https://localhost:44349/");
_client.DefaultRequestHeaders.Accept.Clear();
_client.DefaultRequestHeaders.Accept.Add(new
MediaTypeWithQualityHeaderValue("application/json"));
}
To easily extract an interface from our WeatherManager class, place your cursor on the class
name and press Ctrl+. or right-click and select Quick Actions and Refactorings from the
context menu.
Next, click Extract interface (Figure 72) to create an interface called IWeatherManager, as
seen in Code Listing 52.
97
Figure 72: Extract an Interface from the WeatherManager Class
With the interface created, you will notice that the WeatherManager class now implements the
IWeatherManager interface, as seen in the excerpt in Code Listing 53.
namespace WeatherPortal.Core
{
public interface IWeatherManager
{
Task<WeatherResults> GetWeatherAsync();
}
}
You will notice in Code Listing 52 that the GetWeatherAsync method is a member of the
created IWeatherManager interface.
This easy refactoring is the first improvement made so that we can decouple the code moving
forward. With his improvement made, we can now think about using constructor injection to
inject the WeatherManager service into our methods.
98
Using constructor injections
Constructor injection will allow us to pass dependencies to our class via the constructor instead
of the class having to create instances of the dependencies it requires. To achieve constructor
injection, we can make dependencies parameters of the constructor. To illustrate this, consider
the code in Code Listing 54 for the IndexModel class.
using Microsoft.AspNetCore.Mvc.RazorPages;
using WeatherPortal.Core;
namespace WeatherPortal.Pages
{
public class IndexModel : PageModel
{
private readonly IWeatherManager _service;
public WeatherResults WeatherResults { get; set; } = new
WeatherResults();
Here we are applying a version of the inversion of control principle to invert control of the
creation of the WeatherManager dependency. We allow another component to take
responsibility for creating an instance of the dependency and passing that dependency to the
constructor of the class that depends on the WeatherManager. We are now able to achieve
loose coupling in our code.
A private readonly field called _service is created and assigned from within the constructor.
Our code now supports the dependency injection pattern. We now need to complete the
inversion of control so that ASP.NET Core can take responsibility for creating and injecting the
required WeatherManager dependency. For that, we need to register our service. Let’s do that
next.
99
Registering services in the ServiceCollection
The last thing we need to do is use the Microsoft dependency injection container built into
ASP.NET Core. The dependency injection container gets configured when the application starts
and we can register the services we need via the IServiceCollection. If we do not register
our dependencies with the IServiceCollection, we will experience runtime exceptions when
classes try to use these injected dependencies.
Services are registered in the Program.cs file. It is the entry point for our application, and as
seen in Code Listing 55, it contains top-level statements.
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
using WeatherPortal.Core;
100
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddScoped<IWeatherManager, WeatherManager>();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
What we are saying here is that the container will attempt to create and return an instance of
WeatherManager when resolving a request for IWeatherManager. In layman's terms, when a
class uses IWeatherManager, give it the WeatherManager implementation.
Running your Weather Portal application now will correctly resolve the required dependency
from within the dependency injection container and display the weather data returned from the
minimal API. You have successfully changed the application to make use of dependency
injection.
• Scoped
• Transient
• Singleton
In our application, we used Scoped, but it is important to know what each of these lifetimes
means and when to use them.
101
Transient
When specifying Transient, we are telling the container to create a new instance every time
the service is invoked. In other words, every class that receives a transient service via
dependency injection will receive its own unique instance of that dependency. This does mean a
bit more work for the garbage collector, but using Transient is the safest choice if you are
unsure of the lifetime required for your use case.
Singleton
When registering a service with a Singleton lifetime, we are telling the container to only create
a single instance of that service for the lifetime of the container (in other words, for the lifetime of
the web application). Because the service is registered for the lifetime of the container, it does
not require disposal or garbage collection.
Scoped
Considering Scoped services, we can think of them as sitting between Transient and
Singleton. A Scoped service instance will live for the length of the scope from which it is
resolved. Therefore, in an ASP.NET Core application, a scope is created for each request that
is handled.
Finally
A lot more can be said regarding dependency injection. In fact, it is a topic on its own, and does
require a more detailed look. In this book, I wanted to illustrate the bare bones of what
dependency injection is, and why it is something you need to use in your ASP.NET Core 6
applications.
102