Chapter 1. ASP.NET Core Primer
.NET Core is not just yet another .NET version. It represents a complete overhaul of everything we may have learned as .NET developers. This is a brand new, “1.0” product that is finally going to bring .NET development into the open source community as a fully cross-platform development stack.
This chapter will break down the essential components of ASP.NET Core and .NET Core. In classic Microsoft fashion, there are a dozen new terms and labels to learn, and those have changed multiple times between the betas and release candidates, so the internet is awash with confusing, misleading, or downright incorrect information.
By the end of the chapter, you’ll have a better idea of what ASP.NET Core is and how it fits into the new cross-platform framework architecture. You will also have set your workstation up with all of the prerequisites so that you’ll be ready to dive into the rest of the book.
Distilling the Core
I’d love to be able to jump straight to the canonical and mandatory “hello world” application using .NET Core. However, Core (I will use “.NET Core” and “Core” interchangeably throughout the book) represents such an enormous shift in architecture, design, and tooling that we need to take a minute to at least cover some of the terminology that has changed from previous versions of .NET.
Even if you’ve never used .NET before and Core is your first exposure, you’ll find this terminology everywhere you search, so knowing what it all means is essential.
CoreCLR
The CoreCLR is a lightweight, cross-platform runtime that provides many of the same features that the Common Language Runtime (CLR) provides on the Windows desktop or server, including:
- Garbage collection
-
A garbage collector is responsible for the cleanup of unused object references in a managed application. If you’ve used any of the previous versions of .NET (or Java), then you should be familiar with the concept. Despite the differences between the CLR and CoreCLR, they both follow the same fundamental principles when it comes to garbage collection.
- JIT compilation
-
As with previous versions of .NET, the Just-in-Time (JIT) compiler is responsible for compiling the Intermediate Language (IL) code in the .NET assemblies into native code on demand. This holds true now for Windows, Linux, and macOS.
- Exception handling
-
For a number of reasons beyond the scope of this book, exception handling (e.g.,
try/catch
statements) is a part of the runtime and not the base class library.
In the first version of .NET, the CLR was a large, monolithic thing that provided the basic services required by .NET applications. Over time it grew larger and more tightly coupled to Windows. It eventually grew so large that Microsoft had to split the CLR in two, allowing developers to choose full or light versions because the whole thing was usually too bloated for most practical uses. Here, developers generally chose based on whether they were building server or client applications.
With .NET Core, the CoreCLR is now the smallest possible thing that can provide runtime services to .NET Core applications. It is essentially a bootstrapper. Everything not responsible for the most primitive parts of the cross-platform runtime are part of CoreFX (discussed next) or available as completely separate add-on libraries.
CoreFX
People who have been developing .NET applications for some time now should be familiar with the concept of the base class library (BCL)—the sum total of all .NET libraries that comprise the framework. If you installed something like “.NET Framework v3.5” on a server, then you would get every possible class that came with the framework. This led to developers expecting everything to exist on their servers, and unfortunately to developers treating their servers like pets (more on why this is bad later).
The legacy .NET Framework is an enormous beast, with thousands of classes. When deploying applications to a server, the entire framework has to be installed, regardless of how much of it your application actually uses.
CoreFX is a set of modular assemblies (available as NuGet packages and completely open source, available on GitHub) from which you can pick and choose. Your application no longer needs to have every single class library assembly installed on the target server. With CoreFX, you can use only what you need, and in true cloud-native fashion you should vendor (bundle) those dependencies with your application and expect nothing of your target deployment environment. The burden of dependency management is now reversed—the server should have nothing to do with it.
This represents an enormous shift in the way people think about .NET development. Building .NET applications is no longer about closed-source, vendor-locked development on Windows. Today, it’s a lean, use-only-what-you-need model that is absolutely in line with patterns and practices of modern microservice development and how the open source community at large views the art of building software.
.NET Platform Standard
Prior to .NET Core, .NET developers were familiar with the concept of Portable Class Libraries (PCLs). These allowed developers to compile their assemblies to target an intersection of architecture and platform (e.g., a Windows Phone 8 DLL and a DLL that could be used by an ASP.NET app on the server). This resulted in multiple different DLLs that were each tagged with where they could be deployed.
The .NET Platform Standard (often just called .NET Standard) aims to simplify this process and allow for a more manageable architecture to support .NET Core’s cross-platform goals for binary portability. For more information on .NET Standard, check out the documentation on GitHub.
It may also help to think of .NET Standard in terms of interfaces. You can think of each version of .NET Standard as a collection of interfaces that can either be implemented by the traditional .NET Framework (v.4x–vNext) or by the .NET Core libraries. As you evaluate which NuGet packages you want to use, you’ll be looking at which version of the standard they use. If they don’t conform to some version of .NET Standard, they’re not compatible with .NET Core.
Table 1-1 shows the compatibility and equivalencies between .NET Standard, .NET Core, and the existing .NET Framework versions at the time of writing this book (table contains data taken from the official Microsoft documentation).
Platform | ||||||||
---|---|---|---|---|---|---|---|---|
netstandard |
1.0 | 1.1 | 1.2 | 1.3 | 1.4 | 1.5 | 1.6 | 2.0 |
netcoreapp (.NET Core) |
1.1 | 2.0 | ||||||
|
4.5 | 4.5.1 | 4.6 | 4.6.1 | 4.6.2 | vNext | 4.6.2 |
ASP.NET Core
ASP.NET Core is a collection of small, modular components that can be plugged into your application to let you build web applications and microservices. Within ASP.NET Core you will find APIs for routing, JSON serialization, and rigging up MVC controllers and views.
Historically, ASP.NET came with the .NET Framework—you could not separate the two. After the split between lightweight and heavyweight frameworks, you could install versions of the .NET Framework that did not include ASP.NET.
Now, much in line with the way the rest of the open source software (OSS) community has been doing things for years, all of the components you need to convert a console app into a web app or service are simply modules you add as dependencies. As with everything that is part of Core, it is 100% open source. You can find all of the source code to ASP.NET Core at https://github.com/aspnet.
Installing .NET Core
As mentioned before, you no longer need to install ASP.NET as it is nothing more than a collection of modules from which you can choose to add functionality to your Core app. What you’ll need to install is the .NET Core command-line tools as well as an SDK. The distinction between the tooling and the SDK is important, because you can have more than one SDK (e.g., v1.0 and v1.1) installed and managed by a single version of the command-line tools.
This new modular design is a more modern approach to open source frameworks and is exactly how you’ll see frameworks for other languages managed and distributed. For folks coming to .NET Core from the OSS world, this should feel natural and second-nature. For developers who have spent a good portion of their careers installing ASP.NET on server after server, this is a new (and hopefully refreshing) experience.
To install .NET Core, simply follow the instructions at the main website. Make sure you install the newest version of the SDK (the tooling) and the newest version of the runtime.
There are different instructions for each operating system, but when you’re done, you should be able to execute the following command without error:
$ dotnet --version 1.0.3
Your version may vary slightly from the preceding output, but the executable should be in your path and it should produce a version number. This book was written against version 1.0.3 of the SDK and version 1.1.1 of the runtime.
.NET Core has a very active community and a pretty rapid release cycle, so it’s quite possible that newer versions of the tooling and runtime will be available by the time you read this.
If this works, then you can be reasonably confident that you’ve got the basic requirements for .NET Core installed on your workstation. Double-check this with Microsoft’s installation instructions to make sure you have the latest version of the tools.
All of the samples in this book assume that your projects will be managed with project files in the form of <project name>.csproj. Note that if you do some basic internet searching for .NET Core samples, you may run into samples that use the project.json file format. These are old and deprecated and not compatible with the 1.x versions of the SDK.
If you ended up with a version of dotnet
that is earlier than the one shown in the preceding snippet, you may need to download a specific version manually from GitHub.
The requirements for this book are that you have a runtime version of 1.1 or greater and an SDK/tools version of 1.0.2 or better.
Tool Versions
Depending on what directory you’re in when you run the dotnet
command, the version output may vary. If a global.json file is a peer or in a parent directory and specifies a fixed SDK version, you will see this version, even if the dotnet
command-line tool is a higher version. To see the highest version of the tooling/SDK you have available, run the dotnet --version
command from a root or temporary directory that has no nearby global.json file.
One side effect of the modularity of .NET Core that many developers may take some time getting used to is the difference between the SDK (tools/CLI) version and the runtime version. The latest runtime version at the time this book was written was 1.1.1. On a Mac, you can use the following command to see which versions of the runtime are available to you:
$ ls -F /usr/local/share/dotnet/shared/Microsoft.NETCore.App/ 1.0.1/ 1.0.3/ 1.0.4/ 1.1.0/ 1.1.0-preview1-001100-00/ 1.1.1/
If you see 1.1.1 in this directory, and you’re using 1.0.2 or newer of the SDK, then you should be fine for the rest of this book.
If you do not see 1.1.1 in the directory, you’re going to want to download it. The list of runtimes is available directly on Microsoft’s .NET Core page.
If you’re using a Windows machine, you should be able to find your installed runtimes in the following directory: Program Files\dotnet\shared\Microsoft.NETCore.App.
.NET Core is extremely lightweight and, as I mentioned earlier, only includes the bare minimum necessary to get you going. All of the dependencies your applications need are going to be downloaded via the dotnet restore
command by examining your project file. This is essential for cloud-native application development because having vendored (locally bundled) dependencies is mandatory for deploying immutable artifacts to the cloud, where you should assume virtually nothing about the virtual machine hosting your application.
Building a Console App
Before we can get to any of the really interesting stuff, we need to make sure that we can create and build the world’s simplest sample—the oft-derided yet canonical “hello world.”
The dotnet
command-line tool has an option that will create a bare-bones scaffold for a simple console application. If you type dotnet new
without any parameters, it will give you a list of the templates you can use. For this sample, we’re going to use console
.
Note that this will create project files in the current directory. So, make sure you’re where you want to be before you run the command:
$ dotnet new console Welcome to .NET Core! --------------------- Learn more about .NET Core @ https://aka.ms/dotnet-docs. Use dotnet --help to see available commands or go to https://aka.ms/dotnet-cli-docs. Telemetry -------------- The .NET Core tools collect usage data in order to improve your experience. The data is anonymous and does not include commandline arguments. The data is collected by Microsoft and shared with the community. You can opt out of telemetry by setting a DOTNET_CLI_TELEMETRY_OPTOUT environment variable to 1 using your favorite shell. You can read more about .NET Core tools telemetry @ https://aka.ms/dotnet-cli- telemetry. Configuring... ------------------- A command is running to initially populate your local package cache, to improve restore speed and enable offline access. This command will take up to a minute to complete and will only happen once. Decompressing 100% 2828 ms Expanding 100% 4047 ms Created new C# project in /Users/kevin/Code/DotNET/sample.
If this isn’t your first time using the latest version of the command-line tools you will see far less spam. Worth noting is the telemetry opt-out message. If you’re uncomfortable with Microsoft collecting information about your compilation habits anonymously, then go ahead and modify the profile for your favorite shell or terminal to include setting DOTNET_CLI_TELEMETRY_OPTOUT
to 1
.
Once the project is created, you can type dotnet restore
, which analyzes the project dependencies and downloads whatever packages are necessary. This step is required every time you modify the project file:
$ dotnet restore Restoring packages for /Users/kevin/Code/DotNET/sample/sample.csproj... Writing lock file to disk. Path: /Users/kevin/Code/DotNET/sample/obj/ project.assets.json Restore completed in 743.6987ms for /Users/kevin/Code/DotNET/sample/ sample.csproj. NuGet Config files used: /Users/kevin/.nuget/NuGet/NuGet.Config Feeds used: https://api.nuget.org/v3/index.json
Assuming nothing went wrong, you can now run the application and you’ll see the text “Hello World!” emitted to your terminal window (you may experience a delay of a few seconds if this is the first time you’ve compiled this app to a binary):
$ dotnet run Hello World!
Our project consists of two files: the project file (which defaults to <directory name>.csproj) and Program.cs, listed in Example 1-1.
Example 1-1. Program.cs
using
System
;
namespace
ConsoleApplication
{
class
Program
{
static
void
Main
(
string
[]
args
)
{
Console
.
WriteLine
(
"Hello World!"
);
}
}
}
Make sure that you can run all of the dotnet
commands and execute the application and see the expected output before continuing. On the surface this looks just like any other console application written for previous versions of .NET. In the next section, we’ll start to see immediate differences as we incorporate ASP.NET Core.
If you looked at the .csproj file, you might’ve noticed that it declares which version of netcoreapp
it’s targeting (1.0).
To make sure that your tools are working properly and your environment is suitable for all of the rest of the code samples in the book (which use v1.1 of the runtime), let’s edit this .csproj file so that it looks like this:
<Project
Sdk=
"Microsoft.NET.Sdk"
>
<PropertyGroup
>
<OutputType
>
Exe
</OutputType>
<TargetFramework
>
netcoreapp1.1
</TargetFramework>
</PropertyGroup>
</Project>
We’ve upped the NET Core version to 1.1 and changed the dependency on Microsoft.NETCore.App
to version 1.1.0. One muscle memory you’ll want to start building right away is the need to run dotnet restore
after every .csproj file change:
$ dotnet restore Restoring packages for /Users/kevin/Code/DotNET/sample/sample.csproj... Generating MSBuild file /Users/kevin/Code/DotNET/sample/obj/ \ sample.csproj.nuget.g.props. Writing lock file to disk. Path: /Users/kevin/Code/DotNET/sample/obj/ \ project.assets.json Restore completed in 904.0985ms for /Users/kevin/Code/DotNET/sample/ \ sample.csproj. NuGet Config files used: /Users/kevin/.nuget/NuGet/NuGet.Config Feeds used: https://api.nuget.org/v3/index.json
Now you should be able to run the application again. There should be no visible change and there should be no problem compiling it.
If you’ve been following along, take a look at your bin/Debug directory. You should see one subdirectory called netcoreapp1.0 and another one called netcoreapp1.1. This is because you built your application for two different target frameworks. If you were to remove the bin directory and rerun restore
and then run
, you’d only see the netcoreapp1.1 directory.
Building Your First ASP.NET Core App
Adding ASP.NET Core functionality to a console application is actually quite easy. You could start off with a template from inside Visual Studio, or you could use Yeoman on the Mac to create a new ASP.NET project.
However, I want to show just how small the gap is from a console “hello world” to a web-based “hello world” without using any templates or scaffolding. My opinion is that templates, scaffolding, and wizards should be useful, but if your framework requires these things then it has too high a complexity burden. One of my favorite rules of thumb is:
However inconvenient, if you cannot build your entire app with a simple text editor and command-line tools, then you’re using the wrong framework.
Adding ASP.NET Packages to the Project
First, we’re going to want to add a few package references to our project:
Microsoft.AspNetCore.Mvc
Microsoft.AspNetCore.Server.Kestrel
Microsoft.Extensions.Logging
(three different packages)Microsoft.Extensions.Configuration.CommandLine
Whether you choose to edit the project file on your own or use Visual Studio or VSCode to add the references is up to you.
Throughout the early history of .NET Core, the format of the project file changed. Everything from the initial alphas all the way up through the release candidates and 1.0 general availability made use of a file called project.json. During the “preview3” release of v1.0 of the tools, Microsoft created a cross-platform version of the MSBuild tool and embedded that in the command-line tools. As a result, at the time this book went to print, we now have a <project>.csproj project file format that works with this new MSBuild.
Here’s what our hellobook.csproj file looks like with the new dependencies:
<Project
Sdk=
"Microsoft.NET.Sdk"
>
<PropertyGroup>
<OutputType>
Exe</OutputType>
<TargetFramework>
netcoreapp1.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference
Include=
"Microsoft.AspNetCore.Mvc"
Version=
"1.1.1"
/>
<PackageReference
Include=
"Microsoft.AspNetCore.Server.Kestrel"
Version=
"1.1.1"
/>
<PackageReference
Include=
"Microsoft.Extensions.Logging"
Version=
"1.1.1"
/>
<PackageReference
Include=
"Microsoft.Extensions.Logging.Console"
Version=
"1.1.1"
/>
<PackageReference
Include=
"Microsoft.Extensions.Logging.Debug"
Version=
"1.1.1"
/>
<PackageReference
Include=
"Microsoft.Extensions.Configuration.CommandLine"
Version=
"1.1.1"
/>
</ItemGroup>
</Project>
Adding the Kestrel Server
We’re going to extend the existing sample so that whenever you issue an HTTP request, you get “Hello, world” in response. We will return that phrase regardless of what URL is requested or what HTTP method is used.
Let’s take a look at our new Program.cs main entry point, in Example 1-2.
Example 1-2. Program.cs
using
System
;
using
Microsoft.AspNetCore.Hosting
;
using
Microsoft.AspNetCore.Builder
;
using
Microsoft.Extensions.Configuration
;
namespace
HelloWorld
{
class
Program
{
static
void
Main
(
string
[]
args
)
{
var
config
=
new
ConfigurationBuilder
()
.
AddCommandLine
(
args
)
.
Build
();
var
host
=
new
WebHostBuilder
()
.
UseKestrel
()
.
UseStartup
<
Startup
>()
.
UseConfiguration
(
config
)
.
Build
();
host
.
Run
();
}
}
}
In this new Main
method, the first thing we do is initialize the configuration sub-system. We can use the ConfigurationBuilder
to accept configuration settings from JSON files, from environment variables, and, as our sample shows, from the command line. Samples in forthcoming chapters will show more varied use of the configuration system.
Once we’ve got our configuration built, we then use the WebHostBuilder
class to set up our web host. We’re not using Internet Information Services (IIS) or the Hostable Web Core (HWC) on Windows. Instead, we’re using a cross-platform, bootstrapped web server called Kestrel. For ASP.NET Core, even if you deploy to Windows and IIS, you’ll still be using the Kestrel server underneath it all.
Adding a Startup Class and Middleware
In classic ASP.NET, we had a global.asax.cs file that we could use to accomplish work during the various startup phases of the application. With ASP.NET Core, we can use the UseStartup<>
generic method to define a startup class that handles the new startup hooks.
The startup class is expected to be able to support the following methods:
- A constructor that takes an
IHostingEnvironment
variable - The
Configure
method, used to configure the HTTP request pipeline and the application - The
ConfigureServices
method, used to add scoped services to the system to be made available via dependency injection
As hinted at by the .UseStartup<Startup>()
line in Example 1-2, we need to add a Startup
class to our project. This class is shown in Example 1-3.
Example 1-3. Startup.cs
using
Microsoft.AspNetCore.Builder
;
using
Microsoft.AspNetCore.Hosting
;
using
Microsoft.Extensions.Logging
;
using
Microsoft.AspNetCore.Http
;
namespace
HelloWorld
{
public
class
Startup
{
public
Startup
(
IHostingEnvironment
env
)
{
}
public
void
Configure
(
IApplicationBuilder
app
,
IHostingEnvironment
env
,
ILoggerFactory
loggerFactory
)
{
app
.
Run
(
async
(
context
)
=>
{
await
context
.
Response
.
WriteAsync
(
"Hello, world!"
);
});
}
}
}
The Use
method adds middleware to the HTTP request processing pipeline. Everything about ASP.NET Core is configurable, modular, and extremely extensible. This is due in large part to the adoption of the middleware pattern, which is embraced by web frameworks for many other languages. Developers who have built web services and applications using other open source frameworks will likely be familiar with the concept of middleware.
ASP.NET Core middleware components (request processors) are set up as a chain or pipeline and are given a chance to perform their processing in sequence during each request. It is the responsibility of the middleware component to invoke the next component in the sequence or terminate the pipeline if appropriate.
As we’ve shown in Example 1-3, the simplest possible ASP.NET application has a single middleware component that handles all requests.
Middleware components can be added to request processing using the following three methods:
Map
-
Map
adds the capability to branch a request pipeline by mapping a specific request path to a handler. You can also get even more powerful functionality with theMapWhen
method that supports predicate-based branching. Use
-
Use
adds a middleware component to the pipeline. The component’s code must decide whether to terminate or continue the pipeline. Run
-
The first middleware component added to the pipeline via
Run
will terminate the pipeline. A component added viaUse
that doesn’t invoke the next component is identical toRun
, and will terminate the pipeline.
We’ll be playing with middleware components extensively throughout the rest of this book. As I’ve mentioned, this modular ability to manipulate the HTTP request handling pipeline is key to our ability to make powerful microservices.
Running the App
To run this sample, you can simply type dotnet run
from the command line. You should see something very similar to the following when you’ve run the app. Make sure you’ve done a dotnet restore
prior to this:
$ dotnet run Hosting environment: Production Content root path: /Users/kevin/Code/DotNET/sample/bin/Debug/netcoreapp1.1 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down.
You can exercise this service easily using the following terminal commands. Note that any URL you try, as long as it’s a valid URL that curl
understands, will invoke the middleware and give you a response:
$ curl localhost:5000 Hello, world! $ curl localhost:5000/will/any/url/work? Hello, world!
Out of the box, Windows doesn’t come with the curl
command. If you have Windows 10 and have enabled the Linux subsystem, then you can use curl
from a bash prompt running within Windows. Otherwise, you can just open this URL in a browser or use your favorite REST client testing tool, like the Chrome plug-in Postman.
If you weren’t playing the home game and typing the sample as you read the chapter, you can get the full code from the GitHub repo.
Summary
This chapter got you started with .NET Core. You were able to download and install the latest tools (despite the confusing difference between tooling versions and runtime versions), and you created a console app.
We then converted this console application into a simple web application using middleware that responds with “Hello, world!” to all requests. This was easy to do with just a few changes to a project file and adding a few lines of code. Don’t worry if not all of the code made sense yet; it’ll get much clearer as subsequent chapters go into more detail.
At this point, you should have most of the tools you need for the rest of the book and be ready to dive in!
Get Building Microservices with ASP.NET Core now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.