Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
ASP.NET Core 5 and Angular

You're reading from   ASP.NET Core 5 and Angular Full-stack web development with .NET 5 and Angular 11

Arrow left icon
Product type Paperback
Published in Jan 2021
Publisher Packt
ISBN-13 9781800560338
Length 746 pages
Edition 4th Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Valerio De Sanctis Valerio De Sanctis
Author Profile Icon Valerio De Sanctis
Valerio De Sanctis
Arrow right icon
View More author details
Toc

Table of Contents (15) Chapters Close

Preface 1. Getting Ready 2. Looking Around FREE CHAPTER 3. Front-End and Back-End Interactions 4. Data Model with Entity Framework Core 5. Fetching and Displaying Data 6. Forms and Data Validation 7. Code Tweaks and Data Services 8. Back-End and Front-End Debugging 9. ASP.NET Core and Angular Unit Testing 10. Authentication and Authorization 11. Progressive Web Apps 12. Windows, Linux, and Azure Deployment 13. Other Books You May Enjoy
14. Index

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:

Immagine che contiene testo

Descrizione generata automaticamente

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:

Immagine che contiene tavolo

Descrizione generata automaticamente

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, the AppComponent.

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:

Immagine che contiene screenshot

Descrizione generata automaticamente

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.

You have been reading a chapter from
ASP.NET Core 5 and Angular - Fourth Edition
Published in: Jan 2021
Publisher: Packt
ISBN-13: 9781800560338
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at R$50/month. Cancel anytime