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 handy in the future: the first one of them will involve the server-side aspects of our application, while the latter will be performed on the client-side. Both will help us acknowledge whether we really understood everything there is to know before proceeding to the subsequent chapters.
Getting to work
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) and put the following URL in the address line, so we can have another good look at it.
Right after that, without stopping the application, open the test.html file and add the following lines to its existing content (new lines are highlighted):
<!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.
<br /><br />
IT DOES, BUT THE FILES ARE CACHED ON CLIENTS BY DEFAULT!
</body>
</html>
Save the file, then go back 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 it will force a page refresh from the server, which is not what we want; you will see that the preceding changes won't be reflected by your browser, which means that you hit a client-cached version of that page.
Caching static files on the clients can be a good thing in production servers, but is definitely annoying during development. Luckily enough, as we said earlier, the Webpack middleware will automatically fix this issue for all the TypeScript files, and also for all the static assets we'll serve through Webpack itself. However, what about the other ones? We'll most likely have some static HTML files, favicons, image files, audio files, or anything else that we would like to be directly served by the web server.
Is there a way to fine-tune the caching behavior for static files? If so, can we also set up different behaviors for the debug/development and release/production scenarios?
The answer is yes for both questions; let's see how we can do that.
A blast from the past
Back in ASP.NET 4, we could easily disable static files caching 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" />
<add name="Pragma" value="no-cache" />
<add name="Expires" value="-1" />
</customHeaders>
</httpProtocol>
That would be it; we can even restrict such behavior to the debug environment by adding these lines to the Web.debug.config file.
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. Now that we understood the basics, let's see whether we can solve that caching issue by taking advantage of the new configuration model.
Back to the future
The first thing to do is to understand how we can modify the default HTTP headers for static files; as a matter of fact, we can do that by adding a custom set of options to the app.UseDefaultFiles() 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 lines are highlighted):
app.UseStaticFiles(new StaticFileOptions()
{
OnPrepareResponse = (context) =>
{
// Disable caching for all static files.
context.Context.Response.Headers["Cache-Control"] = "no-cache,
no-store";
context.Context.Response.Headers["Pragma"] = "no-cache";
context.Context.Response.Headers["Expires"] = "-1";
}
});
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.
However, we're not done yet; now that we learned how to change the default behavior, we just need to change these static values with some convenient references pointing to the appsettings.Development.json file. To do that, we can add the following key/value section to the appsettings.Development.json file in the following way (new lines highlighted):
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"Console": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
},
"StaticFiles": {
"Headers": {
"Cache-Control": "no-cache, no-store",
"Pragma": "no-cache",
"Expires": "-1"
}
}
}
Then, change the preceding Startup.cs code accordingly (modified lines highlighted):
app.UseStaticFiles(new StaticFileOptions()
{
OnPrepareResponse = (context) =>
{
// Disable caching for all static files.
context.Context.Response.Headers["Cache-Control"] =
Configuration["StaticFiles:Headers:Cache-Control"];
context.Context.Response.Headers["Pragma"] =
Configuration["StaticFiles:Headers:Pragma"];
context.Context.Response.Headers["Expires"] =
Configuration["StaticFiles:Headers:Expires"];
}
});
Ensure that you add these values to the non-development version of the appsettings.json file as well, otherwise the application won't find them (when executed outside a development environment) and throw an error.
Since this will most likely happen in a production environment, we can take the chance to relax these caching policies a bit:
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
},
"StaticFiles": {
"Headers": {
"Cache-Control": "max-age=3600",
"Pragma": "cache",
"Expires": null
}
}
}
That's about it. Learning how to use this pattern is strongly advisable, as it's a great and effective way to properly configure our application's settings.
Testing it up
Let's see whether our new caching strategy is working as expected. Run the application in debug mode, and then issue a request to the test.html page by typing the following URL in the browser address bar:
http://localhost:<port>/test.html
We should be able to see the updated contents with the phrase we wrote earlier; if not, press F5 from the browser to force a page retrieval from the server:
Now, without stopping the application, edit the test.html page and update its contents in the following way (updated lines are highlighted):
<!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.
<br /><br />
It seems like it works, and now it doesn't even cache those files!
</body>
</html>
Right after that, go back to the browser, select the address bar, and press Enter; again, ensure that you did not press the refresh button or the F5 key, or you'll have to start over. If everything worked properly, we will immediately see the updated contents on screen:
We did it! Our server-side task was successfully completed.
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 dive deeper into that within this book, we can suggest reading the following great articles showing three different approaches to achieve 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 meant to work with .NET Core 1.x; however, they can still be very usable in .NET Core 2. That said, if we were to choose, we would probably go with the latter, as we found it to be the most clean and clever one.
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 be just a rather trivial 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 them with our own content.
Trimming down the component list
If we navigate through the /ClientApp/app/components/ folder, we can take another close look at the components that are currently in place:
- The /app/ folder contains the files related to the AppComponent, which is the main application component file; it's the one in charge to dynamically load all the other components, hence we definitely want to keep it.
- The /home/ folder contains the files related to HomeComponent, which hosts the Home View contents. Do you remember the introductory text shown on the browser when we run the project? This is where we can find (and update) it. Our SPA will most likely need a home as well, so it's better to keep it too.
- The /navmenu/ folder contains the files related to NavMenuComponent, which handles the layout and the functionalities of the navigation menu to the left. Even if we will make a lot of changes to this menu, keeping it as a working base would be a good idea.
The /counter/ and /fetchdata/ folders contain two sample components, which demonstrate how to implement two very common Angular features: respectively, affect the DOM in real time and fetch data from the web server. Although they can still use them as valuable code samples, keeping them within our client code will eventually confuse us, hence it's better to move these two folders outside the project - or just entirely delete them - to prevent the Visual Studio TypeScript compiler from messing with the .ts files contained there.
However, 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 './components/fetchdata/fetchdata.component'.
Error TS2307 (TS) Cannot find module './components/counter/counter.component'.
Both errors will point to the app.module.shared.ts file, which, as we already know, contains the references of all the TypeScript files used by our Angular application and required by either the client (for browser rendering) and the server (to enable server-side rendering). If we open the file, we can clearly see where the problem is:
To fix it, we need to remove the offending references. However, when we do that, the following TypeScript errors will be raised:
Error TS2304 (TS) Cannot find name 'CounterComponent'.
Error TS2304 (TS) Cannot find name 'FetchDataComponent'.
Error TS2304 (TS) Cannot find name 'CounterComponent'.
Error TS2304 (TS) Cannot find name 'FetchDataComponent'.
All these issues will also point to the app.module.shared.ts file, which now has four names without a valid reference:
Remove all the four lines containing the errors to fix them.
Once done, our updated AppModuleShared file should look like this:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './components/app/app.component';
import { NavMenuComponent } from './components/navmenu/navmenu.component';
import { HomeComponent } from './components/home/home.component';
@NgModule({
declarations: [
AppComponent,
NavMenuComponent,
HomeComponent
],
imports: [
CommonModule,
HttpModule,
FormsModule,
RouterModule.forRoot([
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: '**', redirectTo: 'home' }
])
]
})
export class AppModuleShared {
}
Since we're here, those who don't know how Angular works should spend a couple of minutes to understand how an AppModule class actually works. We already know why we got three files instead of one--to allow SSR--but we never talked about the source code.
The AppModule class(es)
Angular Modules, also known as NgModules, have been introduced in Angular 2 RC5 and are a great and powerful way to organize and bootstrap any Angular application; they help developers consolidate their own set of components, directives, and pipes into reusable blocks.
Every Angular application since v2 RC5 must have at least one module, which is conventionally called root module and 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 TS files) required by the application.
- The root NgModule declaration, which--as we can see--is basically an array of named arrays, each one containing a set of Angular objects that serves a common purpose: directives, components, pipes, modules, providers, and so on. The last one of them contains the component we want to bootstrap, which in most scenarios--including ours--is the main application component, the AppComponent.
Updating the NavMenu
If we run our project in debug mode, we can see that our code changes don't 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, nothing will happen; this is hardly a surprise, since we just moved these components out of the way. To avoid confusion, let's remove these links from the menu as well.
Open the /ClientApp/app/components/navmenu/navmenu.component.html file and delete the offending lines. Once done, the updated source code should look as follows:
<div class='main-nav'>
<div class='navbar navbar-inverse'>
<div class='navbar-header'>
<button type='button' class='navbar-toggle' data-
toggle='collapse' data-target='.navbar-collapse'>
<span class='sr-only'>Toggle navigation</span>
<span class='icon-bar'></span>
<span class='icon-bar'></span>
<span class='icon-bar'></span>
</button>
<a class='navbar-brand' [routerLink]="
['/home']">TestMakerFree</a>
</div>
<div class='clearfix'></div>
<div class='navbar-collapse collapse'>
<ul class='nav navbar-nav'>
<li [routerLinkActive]="['link-active']">
<a [routerLink]="['/home']">
<span class='glyphicon glyphicon-home'></span>
Home
</a>
</li>
</ul>
</div>
</div>
</div>
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 firstly ran the project? Let's change it with our own content.
Open the /ClientApp/app/components/home/home.component.html file and replace its whole content with the following:
<h1>Greetings, stranger!</h1>
<p>This is what you get for messing up with .NET Core and Angular.</p>
Save, run the project in debug mode and get ready to see the following:
The Counter and Fetch data menu links are gone, and our Home View welcome text couldn't be sleeker.
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 fast Webpack will handle our modifications--is good enough.