Getting to work
Now that we've got a general picture of our new project, it's time to do something. Let's start with two simple exercises that will also come in handy in the future. The first of these will involve the server-side aspects of our application, while the second will be performed on the client side. Both will help us discover whether we have really understood everything there is to know before proceeding to subsequent chapters.
Static file caching
Let's start with the server-side task. Do you remember the /wwwroot/test.html
file we added when we wanted to check how the StaticFiles
middleware works? We will use it to do a quick demonstration of how our application will internally cache static files.
The first thing we have to do is to run the application in debug mode (by clicking on the Run button or pressing the F5 key). When the app is fully loaded, press CTRL+SHIFT+J to open the Developer tools bar and navigate to the Network tab, which will show the browser's network activity.
Once done, put the following URL in the address line to load the test.html
page: http://localhost:<port>/test.html
If we did everything properly, we should be able to identify the HTML page's response in the Network tab's bottom-most table:
Figure 2.16: Examining the HTML page's response
Click to that row and a new tab panel will open to the right part of the screen.
Go to the Headers tab, where we'll be able to see the HTTP headers of the response that brought us that static page:
Figure 2.17: Examining the response headers
As we can see, the HTTP request doesn't have many cache-related headers; it definitely seems that the default settings of StaticFilesMiddleware
only add the etag
and last-modified
header values to the requested static resources.
Are these headers good enough to prevent our browser from fully downloading that static HTML file upon each request, even if its content doesn't change? Let's find out.
While keeping the Network tab open, click to the browser address bar and press Enter again to issue another HTTP request to the test.html
file.
Ensure that you don't use F5 or the Refresh button, as doing so would force a page refresh on the server through an explicit request header added by the browser, which is not what we want.
The Network table will get refreshed as well and will eventually show the new response result for the test.html
file:
Figure 2.18: The new response result for test.html
As we can see, the HTTP status code is not 200 OK
, like it was on our first attempt. It has changed to 304 Not Modified
, which means that the browser got it from its cache instead of re-downloading it.
That's precisely what the ETag
header is there for. For those who have never heard of it, let's try to explain how it works. However, before doing that, it could be useful to spend a couple of minutes performing a brief recap of the whole HTTP cache request-response cycle.
How the HTTP cache works
When we use a browser client to surf the World Wide Web, we basically instruct it to connect to a server (typically a web server or a reverse proxy) and retrieve the content that we want. This retrieval process is performed through one or more HTTP requests ("please give me that resource") that get answered by the service using one or more corresponding HTTP responses ("here it is"). Both the request and the response are equipped with a set of metadata, known as headers, that contains contextual information regarding the request and returned content. As we can easily guess, the request's headers are set by the browser, while the response's headers are added by the server.
One of the many purposes of such headers is to control the browser's cache, which is a temporary internal storage where it stores the requested resources (HTML pages, JS/CSS files, images, videos, and so on) according to the caching rules determined by the headers.
As a matter of fact, all the requests issued by the browser are first directed to its cache to check whether there's a valid cached response that can fulfill each request without having to re-download it from the server. In case there's a match, the requested resource is read from the cache, thus eliminating the need for a file transfer and dramatically reducing its overall latency and bandwidth costs.
The browser's cache's behavior is determined by a combination of these request and response headers, which in turn depend on how their actors (browser and server) have been configured; however, unless we change the browser's default cache-handling configuration – which is something that 99.9% of average internet users won't ever do – the part that matters the most is the response headers set by the server. The browser will just act accordingly using its default behavior, which is almost always good enough, and what a typical web developer should reasonably expect.
The ETag
and last-modified
response headers are two of them; let's see how their presence will influence our browser's caching behavior.
The ETag response header
The ETag
response header is calculated by the server using collision-resistant hash functions of the requested resource's content, in order to uniquely identify it and then add it to the response header with the sole purpose of getting stored (together with the resource) in the browser's cache.
When the browser requests that resource a second time, if the ETag
response header is present and the other cache-related headers are not present (or expired), it adds its value (using an If-None-Match
request header) to the request that will immediately be issued to the server to fetch that resource again.
When the server receives such a request, it will check the received ETag
value against the requested file. If the file has changed, it will serve it from scratch, otherwise it will send a 304 Not Modified
response, thus instructing the browser to use the cached data instead.
For further information about the ETag
response header, check out the following URL: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
The Last-Modified response header
The Last-Modified
response header works in a similar way to ETag
. However, instead of relying on a content-based strategy to determine whether a resource has changed, it uses the resource's last modified time.
The workflow is also very similar: instead of the If-None-Match
request header used by the ETag
, the browser sets its value to an If-Modified-Since
header to allow the server to perform the check.
For further information about the Last-Modified
response header, check out the following URL: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified
Improving our caching strategy
As we've seen a short while ago, both the ETag
and the Last-Modified
tags are being set by default by the StaticFileMiddleware
. Now, we know why we got a 304 Not Modified
response on our second request.
Being able to rely on these response headers is indeed a great way to make our user's HTTP request much more efficient, and we get it for free since it's a built-in feature: yay!
However, it's very important to understand that when the ETag
and/or Last-Modified
headers come into action, the browser is still sending an HTTP request to your server, albeit very small: this means that it will need to wait for the server's response, even if it's a mere 304 Not Modified
response. There could be many scenarios where such a downside is non-trivial, such as when we have high latency connections (mobile devices, bad WI-FI signal, and so on).
In order to properly deal with such circumstances, we should find a way to make the browser assume that the file is the same for a certain amount of time without performing a check (and hence a request) to the server; this is precisely what the Cache-Control
header is for.
The Cache-Control header
The Cache-Control
HTTP header holds a series of directives that instruct the browser about how to handle the caching for that given resource.
Here's the standard set of Cache-Control
directives that can be used in an HTTP response:
- Cache-Control: must-revalidate
- Cache-Control: no-cache
- Cache-Control: no-store
- Cache-Control: no-transform
- Cache-Control: public
- Cache-Control: private
- Cache-Control: proxy-revalidate
- Cache-Control: max-age=<seconds>
- Cache-Control: s-maxage=<seconds>
As we can see by looking at the directives' names, this header can be used to specify if, how, and for how long the browser (and/or other intermediate parties, such as proxies) should cache the response before having to perform a request to the server, including the one to check for the ETag
and/or Last-Modified
value changes.
That's precisely what we need! However, the StaticFilesMiddleware
doesn't natively add such headers to our server-side responses. Let's see how we can implement it.
For further information about the Cache-Control
header and its set of directives, check out the following URL: https://developer.mozilla.org/it/docs/Web/HTTP/Headers/Cache-Control
A blast from the past
Back in ASP.NET 4.x (and earlier versions), we could easily implement the Cache-Control
header by adding some lines to our main application's Web.config
file, such as the following:
<caching enabled="false" />
<staticContent>
<clientCache cacheControlMode="DisableCache" />
</staticContent>
<httpProtocol>
<customHeaders>
<add name="Cache-Control" value="no-cache, no-store" />
</customHeaders>
</httpProtocol>
We could even restrict such behavior to the debug environment by adding these lines to the Web.debug.config
file.
However, we can't use the same approach in .NET Core, as the configuration system has been redesigned from scratch and is now quite different from the previous versions. As we said earlier, the Web.config
and Web.debug.config
files have been replaced by the appsettings.json
and appsettings.Development.json
files, which also work in a completely different way.
Back to the future
Now that we understand the basics, let's see whether we can solve that caching issue by taking advantage of the new configuration model.
The first thing to do is to understand how we can modify default HTTP headers for static files. As a matter of fact, we can do that by adding a custom set of configuration options to the app.UseStaticFiles()
method call in the Startup.cs
file that adds the StaticFiles
middleware to the HTTP request pipeline.
In order to do that, open Startup.cs
, scroll down to the Configure
method, and replace that single line with the following code (new/modified parts are highlighted):
app.UseStaticFiles(new StaticFileOptions()
{
OnPrepareResponse = (context) =>
{
// Disable caching for all static files.
context.Context.Response.Headers["Cache-Control"] =
"max-age=3600";
}
});
That wasn't hard at all; we just added some additional configuration values to the method call, wrapping them all within a dedicated StaticFileOptions
object instance.
As we can see, we've used the max-age
directive, which is a great way to set a fixed amount of time: the value is expressed in seconds, meaning that our static content will be cached for 1 hour.
However, we're not done yet; now that we've learned how to change the default behavior, we just need to change these static values with some convenient references pointing to the appsettings.json
file.
To do that, we can add the following key/value section to the appsettings.json
file in the following way (new lines highlighted):
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"StaticFiles": {
"Headers": {
"Cache-Control": "max-age=3600"
}
}
}
Then, change the preceding Startup.cs
code accordingly (modified lines are highlighted):
app.UseStaticFiles(new StaticFileOptions()
{
OnPrepareResponse = (context) =>
{
// Retrieve cache configuration from appsettings.json
context.Context.Response.Headers["Cache-Control"] =
Configuration["StaticFiles:Headers:Cache-Control"];
}
});
Now that we've set a caching policy to the generic appsettings.json
file, it might be wise to selectively overwrite that rule for the development environment, where we don't need any cache.
In order to do that, open the appsettings.Development.json
file and define a different Cache-Control
directive in the following way (new lines highlighted):
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"StaticFiles": {
"Headers": {
"Cache-Control": "no-cache"
}
}
}
That's it. As we can see, we've used the no-cache
directive, which tells the browser to always validate the stored resource with the server before using it. This would be perfect for our development environment, since we don't want to cache any static resource unless the server confirms that the content has not been changed (using ETag
and/or Last-Modified
checks).
Now that we've performed these changes, we're going to have those static files cached for an hour in our production environment without the risk of creating refresh issues during the development phase. Learning how to properly use this pattern is strongly advisable, as it's a great and effective way to properly configure our application's settings.
The strongly typed approach(es)
The approach that we chose to retrieve the appsettings.json
configuration values makes use of the generic IConfiguration
object, which can be queried using the preceding string-based syntax. This approach is rather practical; however, if we want to retrieve this data in a more robust way, for example, in a strongly typed fashion, we can—and should—implement something better. Although we won't cover this in more depth in this book, we suggest you read the following great articles, showing three different approaches for achieving this result:
The first one, written by Rick Strahl, explains how to do that using the IOptions<T>
provider interface: https://weblog.west-wind.com/posts/2016/may/23/strongly-typed-configuration-settings-in-aspnet-core
The second, by Filip W, explains how to do that with a simple POCO
class, thus avoiding the IOptions<T>
interface and the extra dependencies required by the preceding approach: https://www.strathweb.com/2016/09/strongly-typed-configuration-in-asp-net-core-without-ioptionst/
The third, by Khalid Abuhakmeh, shows an alternative way to use a standard POCO
class and directly register it as a Singleton
with the ServicesCollection
, while also (optionally) shielding it from unwanted modifications due to development mistakes: https://rimdev.io/strongly-typed-configuration-settings-in-asp-net-core-part-ii/
All of these approaches were originally meant to work with .NET Core 1.x; however, they can still be used with .NET 5 (at the time of writing). That said, if we were to choose, we would probably go with the final option, as we find it to be the cleanest and cleverest.
Client app cleanup
Now that our server-side journey has come to an end, it's time to challenge ourselves with a quick client-side exercise. Don't worry—it will just be a rather simple demonstration of how we can update the Angular source code that lies within the /ClientApp/
folder to better suit our needs. More specifically, we will remove all the stuff we don't need from the sample Angular app shipped with our chosen Angular SPA template and replace it with our own content.
We can never say it enough, so it's worth repeating again: the sample source code explained in the following sections is taken from the ASP.NET Core with Angular (C#) project template originally shipped with the .NET 5 SDK, as explained in the following URL: https://docs.microsoft.com/en-US/aspnet/core/client-side/spa/angular?view=aspnetcore-5.0&tabs=visual-studio
This project template might be updated in the future and become different from what we've seen in this chapter; for this very reason, it's important to check it against the code published in this book's GitHub repo. If you find relevant differences between the book's code and yours, feel free to get the one from the repository and use that instead.
Trimming down the component list
The first thing we have to do is delete Angular components that we don't want to use.
Go to the /ClientApp/src/app/
folder and delete the counter
and the fetch-data
folders, together with all the files they contain.
Although they can still be used as valuable code samples, keeping these components within our client code will eventually confuse us, hence it's better to delete them in order to prevent the Visual Studio TypeScript compiler from messing with the .ts
files contained there. Don't worry; we'll still be able to check them out via the book's GitHub project.
As soon as we do that, the Visual Studio Error List view will immediately raise two blocking TypeScript-based issues:
Error TS2307 (TS) Cannot find module './counter/counter.component'.
Error TS2307 (TS) Cannot find module './fetch-data/fetch-data.component'.
All of these errors will point to the app.module.ts
file, which, as we already know, contains the references of all the TypeScript files used by our Angular application. If we open the file, we'll immediately be able to see the issues:
Figure 2.19: Examining the app.module.ts file
In order to fix them, we need to remove the two offending import
references (lines 10-11). Right after that, two more errors will appear:
Error TS2304 (TS) Cannot find name 'CounterComponent'.
Error TS2304 (TS) Cannot find name 'FetchDataComponent'.
This can be fixed by removing the two offending component names from the declarations
array (lines 18-19, which became 16-17 after the previous deletion) and from the RouterModule
configuration (lines 27-28, or 23-24 after the deletion).
Once done, our updated app.module.ts
file should look like this:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
@NgModule({
declarations: [
AppComponent,
NavMenuComponent,
HomeComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' }
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Since we're here, those who don't know how Angular works should spend a couple of minutes to better understand how an AppModule
class actually works.
The AppModule source code
Angular modules, also known as NgModules, were introduced in Angular 2 RC5 and are a great, powerful way to organize and bootstrap any Angular application; they help developers consolidate their own set of components, directives, and pipes into reusable blocks. As we said previously, every Angular application since v2 RC5 must have at least one module, which is conventionally called a root module and is thus given the AppModule
class name.
AppModule
is usually split into two main code blocks:
- A list of import statements, pointing to all the references (in the form of TypeScript files) required by the application.
- The root
NgModule
block, which, as we can see, is basically a collection of named arrays, each one containing a set of Angular objects that serve a common purpose: directives, components, pipes, modules, providers, and so on. The last one contains the component we want to bootstrap, which, in most scenarios—including ours—is the main application component, theAppComponent
.
Adding the AppRoutingModule
Now that we know the gist of how AppModule
actually works, we can better understand why dealing with routing in a separate, top-level module is considered a best practice. Since we're cleansing our app's workspace, let's take the chance to "steal" such neat behavior from the NG app and embrace such best practice as well.
From Solution Explorer, navigate to the /ClientApp/src/app/
folder and create a new TypeScript file, calling it app-routing.module.ts
. Once done, fill it with the following content:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
const routes: Routes = [
{ path: '', component: HomeComponent, pathMatch: 'full' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Once done, open the app.module.ts
file and replace the following import
statement (around line 5):
import { RouterModule } from '@angular/router';
with this one:
import { AppRoutingModule } from './app-routing.module';
Right after that, scroll down to lines 21-23 and replace the entire RouterModule
configuration block:
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
])
],
With a single reference to the newly created AppRoutingModule
:
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
AppRoutingModule
],
Here's the full app.module.ts
source code after the trimming:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
@NgModule({
declarations: [
AppComponent,
NavMenuComponent,
HomeComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
What we did should be quite straightforward. We've removed the Angular's RouterModule
references and configuration from the AppModule
and replaced them with a single reference to our brand-new AppRoutingModule
, which will take care of routing from now on.
Reasons for using a dedicated routing module
We've repeated many times that using a separate, dedicated routing module is considered an Angular best practice, but we still don't know the reasons. What are the benefits that will compensate the additional work required to reference/import all the components twice, like we've just done with HomeComponent
?
As a matter of fact, there are no real benefits for small and/or sample apps. When that's the case, most developers will probably choose to skip the routing module and merge the routing configuration directly into the AppModule
itself, just like the sample VS app did in the first place.
However, such an approach is only convenient when the app's configuration is minimal. When the app starts to grow, its routing logic will eventually become much more complex, thanks to some advanced features (such as specialized guard and resolver services) that, sooner or later, we'll want (or have) to implement. When something like this happens, a dedicated routing module will help us to keep the source code clean, simplify and streamline the testing activities, and increase the overall consistency of our app.
Updating the NavMenu
Let's go back to what we were doing. If we run our project in debug mode, we can see that our recent code changes – the deletion of CounterComponent
and FetchDataComponent
and the creation of the AppRoutingModule
– do not prevent the client app from booting properly. We didn't break it this time – yay!
However, if we try to use the navigation menu to go to the Counter
and/or Fetch data
by clicking the links from the main view, nothing will happen. This is hardly a surprise since we've moved these components out of the way. To avoid confusion, let's remove these links from the menu as well.
Open the /ClientApp/app/nav-menu/nav-menu.component.html
file, which is the UI template for the NavMenuComponent
. As we can see, it does contain a standard HTML structure containing the header part of our app's main page, including the main menu.
It shouldn't be difficult to locate the HTML part we need to delete in order to remove the links to CounterComponent
and FetchDataComponent
—both of them are contained within a dedicated HTML <li>
element:
[...]
<li class="nav-item" [routerLinkActive]="['link-active']">
<a class="nav-link text-dark" [routerLink]="['/counter']"
>Counter</a
>
</li>
<li class="nav-item" [routerLinkActive]="['link-active']">
<a class="nav-link text-dark" [routerLink]="['/fetch-data']"
>Fetch data</a
>
</li>
[...]
Delete the two <li>
elements and save the file.
Once done, the updated HTML structure of the NavMenuComponent
code should look as follows:
<header>
<nav
class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"
>
<div class="container">
<a class="navbar-brand" [routerLink]="['/']">HealthCheck</a>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target=".navbar-collapse"
aria-label="Toggle navigation"
[attr.aria-expanded]="isExpanded"
(click)="toggle()"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse"
[ngClass]="{ show: isExpanded }"
>
<ul class="navbar-nav flex-grow">
<li
class="nav-item"
[routerLinkActive]="['link-active']"
[routerLinkActiveOptions]="{ exact: true }"
>
<a class="nav-link text-dark" [routerLink]="['/']">Home</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
While we're here, let's take the chance to get rid of something else. Do you remember the Hello, World!
introductory text shown by the browser when we first ran the project? Let's replace it with our own content.
Open the /ClientApp/src/app/home/home.component.html
file and replace its entire contents with the following:
<h1>Greetings, stranger!</h1>
<p>This is what you get for messing up with ASP.NET and Angular.</p>
Save, run the project in debug mode, and get ready to see the following:
Figure 2.20: Looking at our home view
The Counter
and Fetch data
menu links are gone, and our Home View
welcome text couldn't be sleeker.
Now that we've removed any references from the front-end, we can do the same with the following back-end files, which we don't need anymore:
WeatherForecast.cs
Controllers/WeatherForecastController.cs
Locate these two files using Visual Studio's Solution Explorer and delete them.
It's worth noting that, once we do that, we will no longer have any .NET controllers available in our web application. That's perfectly fine since we don't have Angular components that need to fetch data either. Don't worry, though—we're going to add them back in upcoming chapters!
That's about it for now. Rest assured, we can easily do the same with other components and completely rewrite their text, including the navigation menu. We'll do that in the following chapters, where we'll also update the UI layout, add new components, and so on. For the time being, understanding how easy it is to change the content—and also how rapidly Visual Studio, ASP.NET, and Angular will react to our modifications—is good enough.