The ASP.NET back-end
If you hail from the ASP.NET MVC framework(s), you might want to know why this template doesn't contain a /Views/
folder: where did our Razor views go?
As a matter of fact, this template doesn't make use of views. If we think about it, the reason is quite obvious: a Single-Page Application (SPA) might as well get rid of them since they are meant to operate within a single HTML page that gets served only once. In this template, such a page is the /ClientApp/src/folder/index.html
file—and, as we can clearly see, it's also a static page. The only server-side-rendered HTML page provided by this template is the /Pages/Error.cshtml
Razor Page, which is used to handle runtime and/or server errors that could happen before the Angular bootstrap phase.
Razor Pages
Those who have never heard of Razor Pages should spend 5-10 minutes taking a look at the following guide, which explains what they are and how they work: https://docs.microsoft.com/en-us/aspnet/core/razor-pages/
In a nutshell, Razor Pages were introduced in .NET Core 2.0 and represent an alternative way to implement the ASP.NET Core MVC pattern. A Razor Page is rather similar to a Razor view, with the same syntax and functionality, but it also contains the controller source code—which is placed in a separate file: such files share the same name as the page with an additional .cs
extension.
To better show the dependence between the .cshtml
and the .cshtml.cs
files of a Razor Page, Visual Studio conveniently nests the latter within the former, as we can see from the following screenshot:
Figure 2.1: Examining .cshtml and .cshtml.cs files
...Hey, wait a minute: where have I seen this movie before?
Yes, this definitely rings a bell: being a slimmer version of the standard MVC Controller + view approach, a Razor Page is pretty similar to an old .aspx
+ .aspx.cs
ASP.NET Web Form.
Advantages of using Razor Pages
As a matter of fact, one of the most important benefits of Razor Pages is the fact that they implement the Single Responsibility Principle in a seamless and effective way: each Razor Page is self-contained, as its view and controller are intertwined and organized together.
The Single Responsibility Principle (also known as SRP) is a computer programming good practice which advises that every module, class, or function should have responsibility for a single part of the functionality provided by the software and that this responsibility should also be entirely encapsulated by that class.
The approach enforced by Razor Pages is definitely easier to understand for a novice developer than the "standard" MVC model, which relies on the intertwined work of Controllers and Views; this also means that Razor Pages will be easier to develop, update, document, and test.
Controllers
If Razor Pages are so great, why we do still have a /Controller/
folder? Wouldn't it be better to just drop such a concept and switch to them from now on?
Well, it's not that simple: not all controllers are meant to serve server-rendered HTML pages (or views). For example, they can output a JSON output (REST APIs), XML-based response (SOAP web services), a static or dynamically created resource (JPG, JS, and CSS files), or even a simple HTTP response (such as an HTTP 301 redirect) without the content body. This basically means that Controllers still have a very important role, especially in web applications that strongly depend upon server-side JSON content coming from a REST API like those we're about to build.
Advantages of using Controllers
Among the many benefits of using Controllers, there's the fact that they allow a decoupling between what is meant to serve standard HTML content, which we usually call pages or views, and the rest of the HTTP response, which can be loosely defined as service APIs.
Such division enforces a separation of concerns between how we load the server-side pages (1%) and how we serve our server-side APIs (99%). The percentages shown are valid for our specific scenario: we're going to follow the SPA approach, which is all about serving and calling web APIs.
That's why we'll mostly deal with Controllers, whereas Razor Pages would mostly shine in a multi-page application scenario.
WeatherForecastController
By acknowledging all this, we can already infer that the single sample WeatherForecastController
contained in the /Controllers/
folder is there to expose a bunch of web APIs that will be used by the Angular front-end. To quickly check it out, hit F5 to launch the project in debug mode and execute the default route by typing the following URL: https://localhost:44334/WeatherForecast
.
The actual port number may vary, depending on the project configuration file: to set a different port for debug sessions, change the iisSettings
| iisExpress
| applicationUrl
and/or iisSettings
| iisExpress
| sslPort
values in the Properties/launchSettings.json
file.
This will execute the Get()
method defined in the WeatherForecastController.cs
file. As we can see by looking at the source code, such a method has an IEnumerable<WeatherForecast>
return value, meaning that it will return multiple objects of the WeatherForecast
type.
If you copy the preceding URL into the browser and execute it, you should see a JSON array of randomly generated data, as shown in the following screenshot:
Figure 2.2: JSON array of weather data
It's not difficult to imagine who'll be asking for these values.
Configuration files
Let's now take a look at root-level configuration files and their purpose: Program.cs
, Startup.cs
, and appsettings.json
. These files contain our web application's configuration, including the modules and middlewares, as well as environment-specific settings and rules.
The WeatherForecast.cs
file contains a strongly typed class designed to be returned from the Get
method of the WeatherForecastController
: this model can be seen as a View Model, as it will be serialized into JSON by the ASP.NET Core Framework. In our humble opinion, the template authors should have put it within the /ViewModel/
folder (or something like that) instead of leaving it at the root level. Anyway, let's just ignore it for now, since it's not a configuration file, and focus on the rest.
Program.cs
The Program.cs
file will most likely intrigue most seasoned ASP.NET programmers, as it's not something we usually see in a web application project. First introduced in ASP.NET Core 1.0, the Program.cs
file's main purpose is to create a HostBuilder
, an object that will be used by the .NET Core runtime to set up and build the IHost
, and which will host our web application.
IHost versus web server
That's great to know, but what is a host? In just a few words, it is the execution context of any ASP.NET Core app. In a web-based application, the host must implement the IHost
interface, which exposes a collection of web-related features and services and also a Start
method. The web host references the server that will handle requests.
The preceding statement can lead to the assumption that the web host and the web server are the same thing. However, it's very important to understand that they're not, as they serve very different purposes. Simply put, the host is responsible for application startup and lifetime management, while the server is responsible for accepting HTTP requests. Part of the host's responsibility includes ensuring that the application's services and the server are available and properly configured.
We can think of the host as being a wrapper around the server: the host is configured to use a particular server, while the server is unaware of its host.
For further info regarding the IHost
interface, the HostBuilder
class, and the purpose of the Setup.cs
file, take a look at the following guide: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/
If we open the Program.cs
file and take a look at the code, we can easily see that the HostBuilder
is built in an extremely straightforward manner, as follows:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace HealthCheck
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
The CreateDefaultBuilder(args)
method was introduced in ASP.NET Core 2.1 and is a great improvement on its 1.x counterpart, as it simplifies the amount of source code required to set up basic use cases, thus making it easier to get started with a new project.
To understand this better, let's take a look at the sample Program.cs
equivalent, like it was in ASP.NET Core 1.x:
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
host.Run();
}
}
The preceding code was intended to perform the following steps:
- Setting up the
Kestrel
web server - Setting up the content
root
folder, that is, where to look for theappsettings.json
file and other configuration files - Setting up
IIS integration
- Defining the
Startup
class to use (usually defined in theStartup.cs
file) - Finally, executing Build and Run on the now configured
IWebHost
In ASP.NET Core 1.x, all these steps must be called explicitly here and also manually configured within the Startup.cs
file; although such an "explicit" approach is still supported in ASP.NET Core 2.x, .NET Core 3.x, and .NET 5, using the CreateDefaultBuilder()
method is almost always a better way as it takes care of most of the job, and also lets us change the defaults whenever we want.
If you're curious about this method, you can even take a peek at the source code on GitHub: https://github.com/aspnet/MetaPackages/blob/master/src/Microsoft.AspNetCore/WebHost.cs
At the time of writing, the WebHost.CreateDefaultBuilder()
method implementation starts at line #148
.
As we can see, the CreateHostBuilder
method ends with a chained call to UseStartup<Startup>()
to specify the startup type that will be used by the web host. That type is defined in the Startup.cs
file, which is what we're going to talk about.
Startup.cs
If you're a seasoned .NET developer, you might already be familiar with the Startup.cs
file since it was first introduced in OWIN-based applications to replace most of the tasks previously handled by the good old Global.asax
file.
Open Web Interface for .NET (OWIN) comes as part of project Katana, a flexible set of components released by Microsoft back in 2013 for building and hosting OWIN-based web applications. For additional info, refer to the following link: https://www.asp.net/aspnet/overview/owin-and-katana
However, the similarities end here; the class has been completely rewritten to be as pluggable and lightweight as possible, which means that it will include and load only what's strictly necessary to fulfill our application's tasks.
More specifically, in .NET 5, the Startup.cs
file is the place where we can do the following:
- Add and configure services and Dependency Injection, in the
ConfigureServices()
method - Configure an HTTP request pipeline by adding the required middleware, in the
Configure()
method
To better understand this, let's take a look at the following lines taken from the Startup.cs
source code shipped with the project template we chose:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace HealthCheck
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime.
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// In production, the Angular files will
// be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
// This method gets called by the runtime.
// Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days.
// You may want to change this for production scenarios,
// see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA
// from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
}
}
The Startup
class contains the Configure()
method implementation, where, as we just said, we can set up and configure the HTTP request pipeline.
The code is very readable, so we can easily understand what happens here:
- The first bunch of lines features an
if-then-else
statement that implements two different behaviors to handle runtime exceptions in development and production, throwing the exception in the former case and showing an opaque error page to the end user in the latter; that's a neat way to handle runtime exceptions in very few lines of code. - Right after that, we can see the first block of middlewares:
HttpsRedirection
, to handle HTTP-to-HTTPS redirects;StaticFiles
, to serve static files placed under the/wwwroot/
folder; andSpaStaticFiles
, to serve static files in the/ClientApp/src/assets/
folder (theassets
folder of our Angular app). Without these last two middlewares, we won't be able to serve locally hosted assets such as JS, CSS, and images; this is the reason they are in a pipeline. Also, note how these methods are called with no parameters: this just means that their default settings are more than enough for us, so there's nothing to configure or override here. - After the three-pack, there's a call to the
EndpointRoutingMiddleware
, which adds route matching to the middleware pipeline. This middleware looks at the set of endpoints defined in the app and selects the best match based on each incoming HTTP request. - The
EndpointRoutingMiddleware
is followed by theEndpointsMiddleware
, which will add the required routing rule(s) to map certain HTTP requests to our web API controllers. We'll extensively talk about that in upcoming chapters, when we'll deal with server-side routing aspects; for now, let's just understand that there's an active mapping rule that will catch all HTTP requests resembling a controller name (and/or an optional action name, and/or an optional IDGET
parameter) and route them to that controller. That's precisely why we were able to call theWeatherForecastController.Get()
method from our web browser and receive a result. - Last but not least comes the
UseSpa
middleware, which gets added to the HTTP pipeline with two configuration settings:- The first one is pretty easy to understand: it's just the source path of the Angular app's root folder. In this template's scenario, it's the
/ClientApp/
folder. Let's keep a mental note of this folder's literal definition, because we'll come back to it later on. - The second one, which will only be executed in development scenarios, is way more complex. To explain it in a few words, the
UseAngularCliServer()
method tells .NET 5 to pass through all the requests addressed to the Angular app to an instance of the Angular CLI server: this is great for development scenarios because our app will always serve up-to-date CLI-built resources without having to run the Angular CLI server manually each time. At the same time, it's not ideal for production scenarios because of the additional overhead and an obvious performance impact.
- The first one is pretty easy to understand: it's just the source path of the Angular app's root folder. In this template's scenario, it's the
It's worth noting that middlewares added to the HTTP pipeline will process incoming requests in registration order, from top to bottom. This means that the StaticFile
middleware will take priority over the Endpoint
middleware, which will take place before the Spa
middleware, and so on. Such behavior is very important and could cause unexpected results if taken lightly, as shown in the following Stack Overflow thread: https://stackoverflow.com/questions/52768852/
Let's perform a quick test to ensure that we properly understand how these middlewares work:
- From Visual Studio's Solution Explorer, go to the
/wwwroot/
folder and add a newtest.html
page to our project. - Once done, fill it with the following contents:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Time for a test!</title> </head> <body> Hello there! <br /><br /> This is a test to see if the StaticFiles middleware is working properly. </body> </html>
Now, let's launch the application in debug mode—using the Run button or the F5 keyboard key—and point the address bar to the following URL: https://localhost:44334/test.html
.
Again, the TCP/IP port number may vary. Edit the Properties/launchSettings.json
file if you want to change it.
We should be able to see our test.html
file in all its glory, as shown in the following screenshot:
Figure 2.3: Viewing test.html
Based on what we learned a moment ago, we know that this file is being served thanks to the StaticFiles
middleware. Let's now go back to our Startup.cs
file and comment out the app.UseStaticFiles()
call to prevent the StaticFiles
middleware from being loaded:
app.UseHttpsRedirection();
// app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
Once done, run the application again and try to go back to the previous URL, as shown in the following screenshot:
Figure 2.4: Trying to view test.html
As expected, the test.html
static file isn't served anymore. The file is still there, but the StaticFile
middleware is not registered and cannot handle it. Therefore, the now-unhandled HTTP request goes all the way through the HTTP pipeline until it reaches the Spa
middleware, which acts as a catch-all and tosses it to the client-side Angular app. However, since there is no client-side routing rule that matches the test.html
pattern, the request is eventually redirected to the app's starting page.
The last part of the story is fully documented in the browser's Console log, as shown in the preceding screenshot. The Cannot match any routes
error message comes from Angular, meaning that our request passed through the whole ASP.NET Core back-end stack.
Now that we've proved our point, we can bring the StaticFiles
middleware back in place by removing the comments and moving on.
For additional information regarding the StaticFiles
middleware and static file handling in .NET Core, visit the following URL: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/static-files.
All in all, since the Startup.cs
file shipped with the Angular SPA template already has everything we need, we can leave it as it is for now.
Thanks to this brief overview, we should now be fully aware of how the HTTP request received by our web application will be handled. Let's try to wrap everything up:
- Each request will be received by the ASP.NET Core back-end, which will try to handle it at the server-side level by checking the various middlewares registered in the HTTP pipeline (in registration order). In our specific scenario, we'll first check the static files in the
/wwwroot/
folder, then the static files in the/ClientApp/src/assets/
folder, and then those in the routes mapped to our web API controllers/endpoints. - If one of the aforementioned middlewares is able to match and handle the request, the ASP.NET Core back-end will take care of it. Conversely, the
Spa
middleware will pass the request through to the Angular client-side app, which will handle it using its client-side routing rules (more on them later on).
appsettings.json
The appsettings.json
file is just a replacement for the good old Web.config
file; the XML syntax has been replaced by the more readable and considerably less verbose JSON format. Moreover, the new configuration model is based upon key/value settings that can be retrieved from a wide variety of sources, including, but not limited to, JSON files, using a centralized interface.
Once retrieved, they can be easily accessed within our code using Dependency Injection via literal strings (using the IConfiguration
interface):
public SampleController(IConfiguration configuration)
{
var myValue = configuration["Logging:IncludeScopes"];
}
Alternatively, we can achieve the same result with a strongly typed approach using a custom POCO
class (we'll get to that later on).
It's worth noting that there's also an appsettings.Development.json
file nested below the main one. Such a file serves the same purpose as the old Web.Debug.config
file, which was widely used during the ASP.NET 4.x period. In a nutshell, these additional files can be used to specify additional configuration key/value pairs (and/or override existing ones) for specific environments.
To better understand the concept, let's take a look at the two files' contents.
The following is the appsettings.json
file:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
And here's the appsettings.Development.json
file:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
As we can see, the Logging.LogLevel.Default
value for our app is set to Warning
in the first file. However, whenever our app runs in development mode, the second file will overwrite the value, setting it to Debug
, and add the System
and Microsoft
log levels, setting them both to Information
.
Back in .NET Core 1.x, this overriding behavior had to be specified manually within the Startup.cs
file. In .NET Core 2, the Host.CreateDefaultBuilder()
method within the Program.cs
file takes care of that automatically, by assuming that you can rely on this default naming pattern and don't need to add another custom .json
configuration file.
Assuming that we understood everything here, we're done inspecting the ASP.NET Core back-end part; it's time to move on to the Angular front-end folders and files.