ASP.NET Core 5 and Angular - Fourth Edition

By Valerio De Sanctis
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Looking Around

About this book

Learning full-stack development calls for knowledge of both front-end and back-end web development. ASP.NET Core 5 and Angular, Fourth Edition will get you started with Angular to build robust web applications as well as the ASP.NET Core 5 and Web API Controllers to implement API calls and server-side routing in the back-end. This fully revised edition includes a side-by-side comparison of the Visual Studio Angular app versus NG App and coverage of the Angular routing module.

You will learn how to build a data model with Entity Framework Core, alongside utilizing the Entity Core Fluent API and EntityTypeConfiguration class. As you progress, you will learn how to handle user input with Angular reactive forms and front-end and back-end validators for maximum effect. You will later explore the advanced debugging and unit testing features provided by xUnit.net (.NET 5) and Jasmine, as well as Karma for Angular.

Further, you will dive deep into progressive web applications (PWAs), learning about their technical requirements, testing, and converting SWAs to PWAs. Finally, you will learn how to deploy apps on Windows, Linux, and Azure using IIS, Kestrel, and nginx.

By the end of this book, you will be equipped with the skills you need to create, debug, and deploy efficient web applications using ASP.NET Core and Angular.

Publication date:
January 2021
Publisher
Packt
Pages
746
ISBN
9781800560338

 

Looking Around

Now that our project has been created, it's time to take a quick look around and try to understand some of the hard work that the .NET and Angular SPA template has done to make it work.

...Hey, wait a minute! Shouldn't we skip all these setup technicalities and just jump into coding?

As a matter of fact, yes, we'll definitely be doing that in a little while. However, before doing that, it's wise to highlight a couple of aspects of the code that have been put in place already so that we'll know how to move effectively within our project in advance: where to find the server-side and client-side code, where to put new content, how to change our initialization parameters, and so on. It will also be a good chance to review our basic knowledge of the Visual Studio environment and the packages we will need.

That's precisely what we're going to do in this chapter. More precisely, the following are the main topics we're going to cover:

  • Solution overview: A high-level summary of what we'll be dealing with
  • The ASP.NET back-end: Razor Pages, controllers, configuration files, and so on
  • The Angular front-end: The workspace, the ClientApp folder, the Angular initialization cycle, and so on
  • Creating a new app with the Angular CLI: Installation, creation, and testing
  • Getting to work: Caching concepts, removing some .NET controllers and Angular components that we no longer require, and so on

IMPORTANT! The sample code we're reviewing here is the code that comes with the default Angular SPA Visual Studio template shipped by .NET 5 SDK at the time of writing—the one created with the dotnet new angular command. In the (likely) event that this sample code is updated in future releases, ensure you get the former source code from the web using this book's official GitHub repository and use it to replace the contents of your project folder.

Caution: failing to do this could result in you working with different sample code from the code featured in this book.

 

Technical requirements

In this chapter, all of the previous technical requirements listed in Chapter 1, Getting Ready, will apply, with no additional resources, libraries, or packages.

The code files for this chapter can be found here: https://github.com/PacktPublishing/ASP.NET-Core-5-and-Angular/tree/main/Chapter_02/

 

Solution overview

The first thing that catches the eye is that, as we've already mentioned, the layout of a standard ASP.NET Core solution is quite different from what it used to be in ASP.NET 4 and earlier versions. However, provided that we already have some ASP.NET MVC experience, we should be able to distinguish the ASP.NET back-end part from the Angular front-end part, and also figure out how these two aspects can interact.

The ASP.NET back-end stack is contained in the following folders:

  • The Dependencies virtual folder, which basically replaces the old References folder and contains all the internal, external, and third-party references required to build and run our project. All the references to the NuGet packages that we'll add to our project will also be put there.
  • The /Controllers/ folder, which has been shipped with any MVC-based ASP.NET application since the preceding release of the MVC framework.
  • The /Pages/ folder, which contains a single Razor Page—Error.cshtml—to handle runtime and/or server errors (more on that later on).
  • The root-level files—Program.cs, Startup.cs, and appsettings.json—which will determine our web application's configuration, including the modules and middlewares, compilation settings, and publishing rules; we'll address them all in a while.

As for the Angular front-end, it comprises the following folders:

  • The /wwwroot/ folder, which will contain the compiled, ready-to-publish contents of our application: HTML, JS, and CSS files, along with fonts, images, and everything else we want our users to have access to in terms of static files.
  • The /ClientApp/ root folder, which hosts the Angular (and package manager) configuration files, as well as a couple of important sub-folders of which we're about to give an overview.
  • The /ClientApp/src/ folder, which contains the Angular app source code files. If we look at them, we can see that they all have a .ts extension, which means we'll be using the TypeScript programming language (we'll say more about this in a bit).
  • The /ClientApp/e2e/ folder, containing some sample end-to-end (E2E) tests built with the Protractor testing framework.

Let's quickly review the most important parts of this structure.

 

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:

  1. Setting up the Kestrel web server
  2. Setting up the content root folder, that is, where to look for the appsettings.json file and other configuration files
  3. Setting up IIS integration
  4. Defining the Startup class to use (usually defined in the Startup.cs file)
  5. 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; and SpaStaticFiles, to serve static files in the /ClientApp/src/assets/ folder (the assets 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 the EndpointsMiddleware, 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 ID GET parameter) and route them to that controller. That's precisely why we were able to call the WeatherForecastController.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.

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:

  1. From Visual Studio's Solution Explorer, go to the /wwwroot/ folder and add a new test.html page to our project.
  2. 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:

Immagine che contiene screenshot

Descrizione generata automaticamente

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:

Immagine che contiene screenshot

Descrizione generata automaticamente

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:

  1. 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.
  2. 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.

 

The Angular front-end

The front-end part of the template will probably be seen as more complex to understand, because Angular—just like most client-side frameworks—has evolved at a dramatic pace, thus experiencing many breaking changes in its core architecture, toolchain management, coding syntax, template, and setup.

For this very reason, it's very important to take our time understanding the role of the various files shipped with the template. This brief overview will start with root-level configuration files, which will also be updated with the latest versions of the Angular packages (and their dependencies) that we'll need to use.

Workspace

The Angular workspace is the filesystem place containing the Angular files: a collection of application files, libraries, assets, and so on. In our template, as in most ASP.NET Core and Angular projects, the workspace is located within the /ClientApp/ folder, which is defined as the workspace root.

The workspace is usually created and initialized by the CLI command used to create the app. Do you remember the dotnet new command we used in Chapter 1, Getting Ready? That's what we're talking about: the Angular part of the template was created by that command. We could achieve that same result with the Angular CLI, using the ng new command.

Any CLI commands operating on the app and/or their libraries (such as adding or updating new packages) will be executed from within the workspace folder.

angular.json

The most important role within the workspace is played by the angular.json file, created by the CLI in the workspace root. This is the workspace configuration file and contains workspace-wide and project-specific configuration defaults for all build and development tools provided by the Angular CLI.

It's worth noting that all the paths defined within this file are meant to be relative to the workspace root folder; in our scenario, for example, src/main.ts will resolve to /ClientApp/src/main.ts.

The first few properties at the top of the file define the workspace and project configuration options:

  • version: The configuration file version.
  • newProjectRoot: The path where new projects are created, relative to the workspace root folder. We can see that this value is set to the projects folder, which doesn't even exist. That's perfectly normal since our workspace is meant to contain two Angular projects in two already defined folders: our HealthCheck Angular app, located in the /ClientApp/src/ folder, and end-to-end tests, located in the /ClientApp/e2e/ folder. Therefore, there is no need to define a newProjectRoot—and it's also important to not use an existing folder to avoid the risk of overwriting some existing stuff.
  • projects: A container item that hosts a sub-section for each project in the workspace, containing project-specific configuration options.
  • defaultProject: The default project name—any CLI command that doesn't specify a project name will be executed on this project.

It's worth noting that the angular.json file follows a standard generic-to-specific cascading rule. All configuration values set at the workspace level will be the default values for any project and can be overridden by those set at the project level. These, in turn, can be overridden by command-line values available when using the CLI.

It's also worth mentioning that, before Angular 8, manually modifying the angular.json file was the only way to make changes to the workspace config.

That's all we need to know, at least for the time being. All the configuration values are already good enough for our scenario, hence, we'll just leave them as they are for now.

Up to Angular 7, manually modifying the angular.json file was the only way to make changes to the workspace config. This changed with Angular 8 with the introduction of the workspace API, which now allows us to read and modify these configurations much more conveniently. For additional info regarding this new feature, we suggest taking a look at the following page: https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/core/README.md#workspaces

package.json

The package.json file is the Node Package Manager (npm) configuration file. It basically contains a list of npm packages that the developer wants to be restored before the project starts. Those who already know what npm is and how it works can skip to the next section, while those who don't should definitely keep reading.

npm started its life as the default package manager for the JavaScript runtime environment known as Node.js. During recent years, though, it has also been used to host a number of independent JavaScript projects, libraries, and frameworks of any kind, including Angular. Eventually, it became the de facto package manager for JavaScript frameworks and tooling. Those who have never used it can think of it as the NuGet for the JavaScript world.

Although npm is mostly a command-line tool, the easiest way to use it from Visual Studio is to properly configure a package.json file containing all the npm packages we want to get, restore, and keep up-to-date later on. These packages get downloaded in the /node_modules/ folder within our project directory, which is hidden by default within Visual Studio; however, all retrieved packages can be seen from the npm virtual folder. As soon as we add, delete, or update the package.json file, Visual Studio will automatically update that folder accordingly.

In the Angular SPA template we've been using, the shipped package.json file contains a huge number of packages—all Angular packages—plus a good bunch of dependencies, tools, and third-party utilities such as Karma (a great test runner for JavaScript/TypeScript).

Before moving ahead, let's take a further look at our package.json file and try to get the most out of it. We can see how all packages are listed within a standard JSON object entirely made up of key-value pairs. The package name is the key, while the value is used to specify the version number. We can either input precise build numbers or use the standard npmJS syntax to specify auto-update rules bound to custom version ranges using supported prefixes, such as the following:

  • The Tilde (~): A value of "~1.1.4" will match all 1.1.x versions, excluding 1.2.0, 1.0.x, and so on.
  • The Caret (^): A value of "^1.1.4" will match everything above 1.1.4, excluding 2.0.0 and above.

This is another scenario where IntelliSense comes in handy, as it will also visually explain the actual meaning of these prefixes.

For an extensive list of available npmJS commands and prefixes, it's advisable to check out the official npmJS documentation at: https://docs.npmjs.com/files/package.json

Upgrading (or downgrading) Angular

As we can see, the Angular SPA template uses fixed version numbers for all Angular-related packages; this is definitely a wise choice since we have no guarantees that newer versions will seamlessly integrate with our existing code without raising some potentially breaking changes and/or compiler errors. Needless to say, the version number will naturally increase with the passage of time, because template developers will definitely try to keep their good work up to date.

That said, here are the most important Angular packages and releases that will be used throughout this book (not including a small bunch of additional packages that will be added later on):

"@angular/animations": "11.0.1",
"@angular/common": "11.0.1",
"@angular/compiler": "11.0.1",
"@angular/core": "11.0.1",
"@angular/forms": "11.0.1",
"@angular/platform-browser": "11.0.1",
"@angular/platform-browser-dynamic": "11.0.1",
"@angular/platform-server": "11.0.1",
"@angular/router": "11.0.1",
"@angular-devkit/build-angular": "0.1100.1",
"@angular/cli": "11.0.1",
"@angular/compiler-cli": "11.0.1",
"@angular/language-service": "11.0.1"

The former group can be found in the dependencies section, while the latter is part of the devDependencies section. As we can see, the version number is mostly the same for all packages and corresponds to the latest Angular final release available at the time of writing.

The version of Angular that we use in this book was released a few weeks before this book hit the shelves. We did our best to use the latest available (non-beta, non-rc) version to give the reader the best possible experience with the most recent technology available. That said, that freshness will eventually decrease over time and this book's code will start to become obsolete. When this happens, try not to blame us for that!

If we want to ensure the highest possible level of compatibility between our project and this book's source code, we should definitely adopt that same release, which, at the time of writing, also corresponds to the latest stable one. We can easily perform the upgrade—or downgrade—by changing the version numbers; as soon as we save the file, Visual Studio should automatically fetch new versions through npm. In the unlikely scenario that it doesn't, manually deleting the old packages and issuing a full rebuild should be enough to fix the issue.

As always, we're free to overwrite such behavior and get newer (or older) versions of these packages, assuming that we properly understand the consequences and according to the Disclaimer in Chapter 1, Getting Ready.

If you encounter problems while updating your package.json file, such as conflicting packages or broken code, ensure that you download the full source code from the official GitHub repository of this book, which includes the same package.json file that has been used to write, review, and test this book. It will definitely ensure a great level of compatibility with the source code you'll find here.

Upgrading (or downgrading) the other packages

As we might expect, if we upgrade (or downgrade) Angular to 11.0.1, we also need to take care of a series of other npm packages that might need to be updated (or downgraded).

Here's the full package list (including the Angular packages) we'll be using in our package.json file throughout the book, split into dependencies, devDependencies, and optionalDependencies sections. The relevant packages are summarized in the following snippet—be sure to triple-check them!

{
  "name": "healthcheck",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "build:ssr": "ng run HealthCheck:server:dev",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "11.0.1",
    "@angular/common": "11.0.1",
    "@angular/compiler": "11.0.1",
    "@angular/core": "11.0.1",
    "@angular/forms": "11.0.1",
    "@angular/platform-browser": "11.0.1",
    "@angular/platform-browser-dynamic": "11.0.1",
    "@angular/platform-server": "11.0.1",
    "@angular/router": "11.0.1",
    "aspnet-prerendering": "3.0.1",
    "bootstrap": "4.5.3",
    "core-js": "3.8.0",
    "jquery": "3.5.1",
    "oidc-client": "1.10.1",
    "popper.js": "1.16.1",
    "rxjs": "6.6.3",
    "zone.js": "0.10.3"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "0.1100.1",
    "@angular/cli": "11.0.1",
    "@angular/compiler-cli": "11.0.1",
    "@angular/language-service": "11.0.1",
    "@types/jasmine": "3.6.2",
    "@types/jasminewd2": "2.0.8",
    "@types/node": "14.14.10",
    "codelyzer": "6.0.1",
    "jasmine-core": "3.6.0",
    "jasmine-spec-reporter": "5.0.2",
    "karma": "5.2.3",
    "karma-chrome-launcher": "3.1.0",
    "karma-coverage-istanbul-reporter": "3.0.3",
    "karma-jasmine": "4.0.1",
    "karma-jasmine-html-reporter": "1.5.4",
    "typescript": "4.0.5"
  },
  "optionalDependencies": {
    "node-sass": "4.14.1",
    "postcss": "8.1.10",
    "protractor": "7.0.0",
    "ts-node": "9.0.0",
    "tslint": "6.1.3"
  }
}

It's advisable to perform a manual command-line npm install followed by an npm update from the project's root folder right after applying these changes to the package.json file in order to trigger a batch update of all the project's npm packages. Sometimes, Visual Studio doesn't update the packages automatically and doing that using the GUI can be tricky.

For this very reason, a convenient update-npm.bat batch file has been added to this book's source code repository on GitHub (inside the /ClientApp/ folder) to handle that without having to type the preceding command manually. Such a batch file can be launched either by opening a command prompt or by installing the Open Command Line Visual Studio extension by Mads Kristensen. This extension adds a neat "Execute File" command in the Visual Studio contextual menu that opens when we right-click to a file from the Solution Explorer.

Those who run into npm and/or ngcc compilation issues after the npm update command can also try to delete the /node_modules/ folder and then perform an npm install from scratch.

Upgrading the Angular code

It's worth noting that our updated package.json file doesn't include some of the packages that were present in the Visual Studio default ASP.NET and Angular SPA project template. The reason for that is quite simple: those packages are either deprecated, obsolete, or not required by the code samples we'll be working with from now on.

At the same time, since we're upgrading an existing (albeit minimalistic) Angular app, excluding some of them might lead to compiler errors and a broken source code. As a matter of fact, if we try to run our project now that we're changed our package.json file (and updated the /node_modules/ folders accordingly using npm), we'll definitely get some TypeScript errors coming from the /ClientApp/src/app/app.server.module.ts file, as shown in the following screenshot:

Immagine che contiene screenshot

Descrizione generata automaticamente

Figure 2.5: Errors after trying to run our project

These errors are due to the fact that we've removed the @nguniversal/module-map-ngfactory-loader JS library, which has been obsolete since Angular 9. In order to fix them, we need to perform the following updates to the app.server.module.ts file:

  • Remove the whole line starting with import { ModuleMapLoaderModule } (line 3)
  • Remove ModuleMapLoaderModule from the imports array (line 8)

Here is the /ClientApp/src/app/app.server.module.ts file's updated source code:

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
@NgModule({
  imports: [AppModule, ServerModule],
    bootstrap: [AppComponent]
})
export class AppServerModule { }

Once done, we need to perform another minor, yet necessary, change to upgrade our existing project structure to the new naming conventions introduced by Angular 10 and 11:

  • Locate the /ClientApp/browserlist file
  • Rename it to /ClientApp/.browserlistrc (don't forget the starting dot!)

For additional information regarding these changes, refer to this section of the Angular 9 to 10 official migration guide at the following URL: https://update.angular.io/?l=3&v=9.0-10.0

For further reference and/or future updates, please also check the updated source code in this book's official GitHub repository, which will always contain the latest improvements, bug fixes, compatibility fixes, and so on.

tsconfig.json

The tsconfig.json file is the TypeScript configuration file. Again, those who already know what TypeScript is won't need to read all this, although those who don't should.

In fewer than 100 words, TypeScript is a free, open source programming language developed and maintained by Microsoft that acts as a JavaScript superset; this means that any JavaScript program is also a valid TypeScript program. TypeScript also compiles to JavaScript, so it can seamlessly work on any JavaScript-compatible browser without external components. The main reason to use it is to overcome JavaScript's syntax limitations and overall shortcomings when developing large-scale applications or complex projects. Simply put, it makes the developer's life easier when they are forced to deal with non-trivial JavaScript code.

In this project, we will definitely use TypeScript for a number of good reasons; the most important ones are as follows:

  • TypeScript has a number of features over JavaScript, such as static typing, classes, and interfaces. Using it in Visual Studio also gives us the chance to benefit from the built-in IntelliSense, which is a great benefit and often leads to a remarkable productivity boost.
  • For a large client-side project, TypeScript will allow us to produce more robust code, which will also be fully deployable anywhere a plain JavaScript file would run.

Not to mention the fact that the Angular SPA template we chose already uses TypeScript. Hence, we can say that we already have a foot in the water!

Jokes aside, we're not the only ones praising TypeScript; this has been acknowledged by the Angular team itself, considering the fact that the Angular source code has been written using TypeScript since Angular 2, as was proudly announced by Microsoft in the following MDSN blog post in March 2015: https://devblogs.microsoft.com/typescript/angular-2-built-on-typescript/

This was further emphasized in this great post by Victor Savkin (cofounder of Narwhal Technologies and acknowledged Angular consultant) on his personal blog in October 2016: https://vsavkin.com/writing-angular-2-in-typescript-1fa77c78d8e8

Getting back to the tsconfig.json file, there's not much to say; the option values used by the Angular SPA template are more or less what we need to configure both Visual Studio and the TypeScript compiler (TSC) to properly transpile the TypeScript code files included in the /ClientApp/ folder.

For additional info about the tsconfig.json file and all the available options, visit the following URL: https://angular.io/config/tsconfig

Other workspace-level files

There are also other notable files created by the CLI in the workspace root. Since we'll not be changing them, we'll just briefly mention them in the following list:

  • .editorconfig: Workspace-specific configuration for code editors.
  • .gitignore: A text file that tells Git—a version-control system you most likely know quite well—which files or folders to ignore in the workspace. These are intentionally untracked files that shouldn't be added to the version control repository.
  • README.md: Introductory documentation for the workspace. The .md extension stands for Markdown, a lightweight markup language created by John Gruber and Aaron Swartz in 2004.
  • package-lock.json: Provides version information for all packages installed in the /node_modules/ folder by the npm client. If you plan to replace npm with Yarn, you can safely delete this file (the yarn.lock file will be created instead).

    Yarn is a package manager for the JavaScript programming language developed and released by Facebook in October 2016 to address some of the limitations that npm had at the time, and is meant to be a drop-in replacement for npm. For further info, read here: https://yarnpkg.com/

  • /node_modules/: A folder containing all the npm packages for the entire workspace. This folder will be populated with packages defined in the package.json file located on the workspace root, which will be visible to all projects.
  • tslint.json: Default TSLint configuration options for all projects in the workspace. These general rules will be integrated and/or overwritten with the project-specific tslint.json file included in the project root folder.

TSLint is an extensible static analysis tool that checks TypeScript code for readability, maintainability, and functionality errors; it's very similar to JSLint, which performs the same tasks for JavaScript code. The tool is widely supported across modern editors and build systems and can be customized with your own linting rules, configurations, and formatters.

For additional info, check out the following URL: https://palantir.github.io/tslint/

The /ClientApp/src/ folder

It's now time to pay a visit to our sample Angular app and see how it works. Rest assured, we won't stay for long; we just want to get a glimpse of what's under the hood.

By expanding the /ClientApp/src/ directory, we can see that there are the following sub-folders:

  • The /ClientApp/src/app/ folder, along with all its subfolders, contains all the TypeScript files related to our Angular app; in other words, the whole client-side application source code is meant to be put here.
  • The /ClientApp/src/assets/ folder is meant to store all the application's images and other asset files. These files will be copied and/or updated as-is in the /wwwroot/ folder whenever the application is built.
  • The /ClientApp/src/environment/ folder contains build configuration options that target specific environments; this template, just like any Angular new project default, includes an environment.ts file (for development) and an environment.prod.ts file (for production).

There is also a bunch of root-level files:

  • browserslistrc: Configures the sharing of target browsers and Node.js versions among various front-end tools.
  • index.html: The main HTML page that is served when someone visits your site. The CLI automatically adds all JavaScript and CSS files when building your app, so you typically don't need to add any <script> or <link> tags here manually.
  • karma.conf.js: Application-specific Karma configuration. Karma is a tool used to run Jasmine-based tests. We can safely ignore the whole topic for now, as we'll get to it later on.
  • main.ts: The main entry point for your application. Compiles the application with the JIT compiler and bootstraps the application's root module (AppModule) to run in the browser. You can also use the AOT compiler without changing any code by appending the --aot flag to CLI build and serve commands.
  • polyfills.ts: Provides polyfill scripts for improving browser support.
  • styles.css: A list of CSS files that supply styles for a project.
  • test.ts: The main entry point for the project's unit tests.
  • tsconfig.*.json: Project-specific configuration options for various aspects of our app: .app.json for application-level, .server.json for server-level, and .spec.json for tests. These options will override those set in the generic tsconfig.json file in the workspace root.
  • tslint.json: The TSLint configuration for the current project.

The /app/ folder

Our template's /ClientApp/src/app/ folder follows Angular folder structure best practices and contains our project's logic and data, thus including all Angular modules, services, and components, as well as templates and styles. It's also the only sub-folder worth investigating, at least for the time being.

AppModule

As we briefly anticipated in Chapter 1, Getting Ready, the basic building blocks of an Angular application are NgModules, which provide a compilation context for components. The role of NgModules is to collect related code into functional sets. Therefore, the whole Angular app is defined by a set of one or more NgModules.

An Angular app requires a root module—conventionally called AppModule—that tells Angular how to assemble the application, thus enabling bootstrapping and starting the initialization life cycle (see the diagram that follows). The remaining modules are known as feature modules and serve a different purpose. The root module also contains a reference list of all available components.

The following is a schema of the standard Angular Initialization Cycle, which will help us better visualize how it works:

Figure 2.6: The Angular initialization cycle

As we can see, the main.ts file bootstraps app.module.ts (AppModule), which then loads the app.component.ts file (AppComponent); the latter, as we'll see in a short while, will then load all the other components whenever the application needs them.

The root module of the sample Angular app created by our template can be found in the /ClientApp/src/app/ folder and is defined within the app.module.ts file. If we take a look at the source code, we can see that it contains a bunch of import statements and some arrays referencing components, other modules, providers, and so on. This should be no mystery since we just said that the root module is basically a reference file.

Server-side AppModule for SSR

As we can see, the /ClientApp/src/app/ folder also contains an app.server.module.ts file, which will be used to enable the Angular Universal Server-Side Rendering (SSR)—a technology that renders Angular applications on the server, provided that the back-end framework supports it. The template generated this file because .NET natively supports such convenient features.

The following is the improved Angular initialization schema when using SSR:

Figure 2.7: The Angular universal initialization cycle

That's about it, at least for now. If you feel like you're still missing something here, don't worry—we'll come back to this soon enough to help you understand all of this better.

To avoid losing too much time on the theoretical aspects of ASP.NET Core and Angular, we won't enter into the details of SSR. For a more detailed look at different techniques and concepts surrounding Angular Universal and SSR, we suggest checking out the following article: https://developers.google.com/web/updates/2019/02/rendering-on-the-web

AppComponent

If NgModules are Angular building blocks, Components can be defined as the bricks used to put the app together, to the extent that we can say that an Angular app is basically a tree of components working together.

Components define views, which are sets of screen elements that Angular can choose between and modify according to your program logic and data, and use services, which provide specific functionality not directly related to views. Service providers can also be injected into components as dependencies, thus making the app code modular, reusable, and efficient.

The cornerstone of these components is conventionally called AppComponent, which is also the only component that—according to Angular folder structure conventions—should be placed in the /app/ root folder. All other components should be put in a sub-folder, which will act as a dedicated namespace.

As we can see, our sample AppComponent consists of two files:

  • app.component.ts: Defines the component logic, that is, the component class source code.
  • app.component.html: Defines the HTML template associated with the AppComponent. Any Angular component can have an optional HTML file containing its UI layout structure instead of defining it within the component file itself. This is almost always a good practice unless the component comes with a very minimal UI.

Since the AppComponent is often lightweight, it doesn't have other optional files that could be found in other components, such as:

  • <*>.component.css: Defines the base CSS style sheet for a component. Just like the .html file, this file is optional and should always be used unless the component doesn't require UI styling.
  • <*>.component.spec.ts: Defines a unit test for the component.

Other components

Other than AppComponent, our template contains four more components, each one in a dedicated folder, as follows:

  • CounterComponent: Placed in the counter subfolder
  • FetchDataComponent: Placed in the fetch-data subfolder
  • HomeComponent: Placed in the home subfolder
  • NavMenuComponent: Placed in the nav-menu subfolder

As we can see by looking at the source files within their respective subfolders, only one of them has some defined tests: CounterComponent, which comes with a counter.component.spec.ts file containing two tests. It might be useful to run them to see whether the Karma + Jasmine testing framework that has been set up by our template actually works. However, before doing that, it might be wise to take a look at these components to see how they are meant to function within the Angular app.

In the next sections, we'll take care of both of these tasks.

Testing the app

Let's start by taking a look at these components to see how they are meant to work.

HomeComponent

As soon as we hit F5 to run the app in debug mode, we'll be greeted by HomeComponent, as seen in the following screenshot:

Immagine che contiene screenshot

Descrizione generata automaticamente

Figure 2.8: Viewing HomeComponent

As the name clearly suggests, the HomeComponent could be considered the home page of our app; however, since the page concept might be rather misleading when dealing with single-page apps, we'll call them views instead of pages throughout the book. The word view basically refers to the combined HTML template generated by the Angular component (including all sub-components) that corresponds to a given navigation route.

NavMenuComponent

Do we have sub-components already? Yes, we do. The NavMenuComponent is a perfect example of that, since it doesn't have a dedicated route for itself, but is rendered as part of other components within their corresponding view.

More precisely, it's the top portion of each view, as we can see from the following screenshot:

Figure 2.9: Examining the NavMenuComponent subcomponent

The main purpose of the NavMenuComponent is to let users navigate through the main views of the app. In other words, it's where we implement all first-level navigation routes defined in AppModule, all pointing to a given Angular component.

First-level navigation routes are those that we want our users to reach with a single click, that is, without having to navigate through other components first. In the sample app we're reviewing now, there are three of them:

  • /: Pointing to the HomeComponent
  • /counter: Pointing to the CounterComponent
  • /fetch-data: Pointing to the FetchDataComponent

As we can see, these navigation routes have been implemented in the NavMenuComponent by using anchor links placed within a single unordered list; a bunch of <a> elements placed inside a <ul> / <li> structure, which is rendered on the right-hand side of the component, and at the top-right corner of any component containing it.

Let's now review the design to handle the two remaining first-level navigation routes: CounterComponent and FetchDataComponent.

CounterComponent

The CounterComponent shows an incrementing counter that we can increase by pressing an Increment button:

Figure 2.10: Viewing CounterComponent

The FetchDataComponent is an interactive table populated with the JSON array generated by the server-side web API via WeatherForecastController, which we saw a while ago when we were examining the back-end part of our project:

Immagine che contiene screenshot

Descrizione generata automaticamente

Figure 2.11: Viewing FetchDataComponent

The spec.ts file(s)

If we take a look at the source files within the preceding component's subfolders, we can see that the CounterComponent comes with a counter.component.spec.ts file. Those files, as per the Angular naming convention, are meant to contain unit tests for the counter.component.ts source file and are run using the Jasmine JavaScript test framework through the Karma test runner.

For additional info regarding Jasmine and Karma, check out the following guides:

Jasmine: https://jasmine.github.io/

Karma: https://karma-runner.github.io/

Angular Unit Testing: https://angular.io/guide/testing

While we're there, it could be useful to give them a run to see whether the Jasmine + Karma testing framework that has been set up by our template actually works.

Our first app test

Before running the test, it may be useful to understand a little more about Jasmine and Karma. If you don't know anything about them, don't worry—you will soon. For now, just know that Jasmine is an open source testing framework for JavaScript that can be used to define tests, while Karma is a test runner tool that automatically spawns a web server that will execute JavaScript source code against Jasmine-made tests and output their respective (and combined) results on a command line.

In this quick test, we'll basically launch Karma to execute the source code of our sample Angular app against the Jasmine tests defined by the template in the counter.component.spec.ts file; this is actually a much easier task than it might seem.

Open Command Prompt, navigate to the <project>/ClientApp/ folder, and then execute the following command:

> npm run ng test

This will call the Angular CLI using npm.

IMPORTANT: Chrome needs to be installed, otherwise the test won't work.

Alternatively, we can install the Angular CLI globally using the following command:

> npm install -g @angular/cli

Once done, we'll be able to directly call it in the following way:

> ng test

In the unlikely event that the npm command returns a program not found error, check that the Node.js/npm binary folder is properly set within the PATH variable. If it's not there, be sure to add it, and then close and re-open the command-line window and try again.

Right after we hit Enter, a new browser window should open with the Karma console and a list of results for the Jasmine tests, as shown in the following screenshot:

Immagine che contiene screenshot

Descrizione generata automaticamente

Figure 2.12: Results of the Jasmine test

As we can see, both tests have been completed successfully—that's everything we need to do for now. There's no need to peek at the counter.component.spec.ts source code since we're going to ditch it—together with all the template components—and create new ones (with their own tests).

For the sake of simplicity, we're going to stop here with Angular app tests for the time being; we'll discuss them in far greater depth in Chapter 9, ASP.NET Core and Angular Unit Testing.

 

Creating a new app with the Angular CLI

In the previous sections, we've spent a good amount of time reviewing and learning the structure of the sample Angular app created by Visual Studio's ASP.NET and Angular default template. However, there are other – and arguably even better – ways to create a sample Angular app from scratch; as a matter of fact, the approach recommended by the Angular development team is entirely based on creating the app through the Angular Command-Line Interface, better known as the Angular CLI.

For reasons of space, we'll only scratch the surface of the Angular CLI throughout the whole book, limiting its usage to what we need here and there. Those who want to know more about this powerful tool can take a look at the following URL: https://cli.angular.io/

In this paragraph, we'll see how to take this alternative route, so that we can compare the resulting apps and highlight their similarities and differences. More specifically, here's what we're going to do:

  • Install the Angular CLI (unless we've done that already)
  • Create a new Angular app using the ng create command
  • Compare the Angular CLI generated source code files with those generated by Visual Studio
  • Modify the .NET's Startup.cs file so that we can choose which Angular app to run
  • Take a look at the alternative Angular app

Are we ready? Let's do this!

Installing the Angular CLI

If we haven't installed the Angular CLI yet, we can do so using the following command:

> npm install -g @angular/[email protected]

The -g option will ensure that the CLI is installed globally, which will allow us to run the ng command-line tool in any folder; the @11.0.1 at the end will force npm to install the specific version used by this book.

Those who are bold enough to use the latest version can do that by running the following command:

npm install -g @angular/[email protected]

However, since we'll use the CLI to create a new Angular app that we'll mostly use for code review, it's strongly advisable to install the specific version used by this book.

Creating a new Angular app

When the Angular CLI has been installed, open a command prompt and navigate to the root-level folder of our existing HealthCheck app, such as /Projects/HealthCheck/.

Once there, issue the following command:

> ng new HealthCheck

This command will start a terminal wizard that will ask you the following questions:

Do you want to enforce stricter type checking and stricter bundle budgets in the workspace? This setting helps improve maintainability and catch bugs ahead of time. For more information, see https://angular.io/strict.

Answer NO by typing the N key.

Would you like to add Angular routing?

Answer YES by typing the Y key.

Which stylesheet format would you like to use? (use the arrow keys)

Select SCSS using the keyboard's arrow keys.

Right after that, the code generation process will start, with all the required npm packages being downloaded and installed accordingly:

Text

Description automatically generated

Figure 2.13: Creating a new Angular app

Wait for the tool to complete the task, and then do the following:

  • Type cd HealthCheck to enter the newly created folder
  • Execute the npm install and npm update commands to update the npm packages

At the end of these tasks, reload your Visual Studio project and include the new /HealthCheck/ folder so that you'll be able to access it from the VS GUI, as shown in the following screenshot:

Immagine che contiene screenshot

Descrizione generata automaticamente

Figure 2.14: Examining our VS and NG Angular app folders

Now we have two differently generated Angular apps that we can compare; for the sake of convenience, we'll call them the VS app (Visual Studio app) and the NG app (Angular CLI app) from now on.

Comparing the Angular apps

If we take a look at the filesystem structure, we'll immediately be able to see a lot of similarities between the two apps: both of them feature roughly the same set of configuration files, with some minor differences related to the folder nesting level. For example, the NG app has the karma.conf.js file and all the various tsconfig.json files in the root folder, while the VS app has them within the /src/ folder, and so on. However, for the most part, the two apps seem very similar in terms of overall structure: this is definitely a good thing, because it means that our front-end code review is still valid!

If we take a closer look and start to compare the various file contents, we'll see that the major differences are in the following files:

  • package.json, which will potentially contain different package versions (the Angular CLI command will always use the latest Angular version available)
  • tsconfig.json, which will definitely contain some small differences in terms of TypeScript configuration and won't likely have the angularCompilerOptions section that we've added early on
  • All the files contained in the /app/ folder, due to the fact that the two sample apps have a very different look and feature set
  • The NG app features a separate app-routing.module.ts file (AppRoutingModule) that contains Angular's RouterModule, while the VS app implements the routing patterns within the main app.module.ts file (AppModule).

Loading and configuring the router in a separate, top-level module dedicated to routing and imported by the root AppModule has been an Angular best practice since Angular 10. Hence, we can definitely say that the NG app has a better approach; we'll use it to our advantage later on.

Luckily enough, none of these differences will prevent our project from running, as we'll be able to see in a short while.

Updating the Startup.cs file

If we want to launch the NG app instead of the VS app, we need to perform some changes to the Startup.cs file, which, as we know, is responsible for loading the front-end.

Open that file, scroll down to the ConfigureServices method, and change the following line (line 27 or so):

configuration.RootPath = "ClientApp/dist";

in the following way:

configuration.RootPath = "HealthCheck/dist";

Right after that, scroll down to the Configure method and do the same with the following line (line 66 or so):

spa.Options.SourcePath = "ClientApp";

Change it as follows:

spa.Options.SourcePath = "HealthCheck";

Alternatively, you can declare a new ClientApp read-only variable at the top of the file:

public class Startup
{
    /// <summary>
    /// The name of the folder that hosts the Angular app
    /// </summary>
    public static readonly string ClientApp = "HealthCheck";
[...]

And then change the above literals into more convenient references in the following way:

configuration.RootPath = string.Format("{0}/dist", ClientApp);
[...]
spa.Options.SourcePath = ClientApp;

Both of these approaches achieve what we want: they tell ASP.NET to run the NG app instead of the VS app.

Testing the NG Angular app

Let's now take a look at the NG app by hitting F5 to issue a debug run. If we've done everything properly, we should be greeted by the following welcome screen:

Immagine che contiene screenshot

Descrizione generata automaticamente

Figure 2.15: Testing the Angular app

Here we go: truth be told, the app created by the Angular CLI looks much prettier than the Visual Studio sample!

However, for the sake of simplicity, we'll just revert the changes that we just made to the Startup.cs file and switch back to the /ClientApp/, which will also be the app that we'll be using in the next chapters.

Those who want to keep using the new NG app are free to do that. All the code samples, updates and exercises that we'll encounter throughout the whole book are fully compatible with both apps… after all, we've just seen that they're basically the same thing! The only real difference will involve some minor path-related differences, which shouldn't be an issue for most readers. That said, the choice is yours: if you don't mind such additional work and a minor risk of headaches, you might as well stay with the NG app and keep your Startup.cs file as it is, otherwise switch back to the ClientApp literal before going further.

 

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.

 

Summary

In this second chapter, we spent some valuable time exploring and understanding our sample project's core components, how they work together, and their distinctive roles. For the sake of simplicity, we split the analysis into two parts: the .NET back-end ecosystem and the Angular front-end architecture, each with its own configuration files, folder structure, naming conventions, and overall scope.

At the end of the day, we can definitely say that we met the end goal of this chapter and learned a fair number of useful things: we know the location and purpose of both server-side and client-side source code files, we are able to remove existing content and insert new stuff, we are aware of the caching system and other setup parameters, and so on.

A relevant part of the chapter was dedicated to the Angular CLI: we've spent a good amount of time learning how to create a sample app using the ng new command and to understand the similarities and the differences between a "canonical" Angular app and the sample app provided by the Visual Studio Angular SPA project template. Such time was very well spent, since now we know how to set up a new ASP.NET Core and Angular project without having to rely on the default templates created by Microsoft.

Last but not least, we also took the time to perform some quick tests to see whether we're ready to hold our ground against what's coming in upcoming chapters: setting up an improved request-response cycle, building our own controllers, defining additional routing strategies, and more.

 

Suggested topics

Razor Pages, separation of concerns, the single responsibility principle, JSON, web hosts, Kestrel, ASP.NET middlewares, Dependency Injection, the Angular workspace, Jasmine, Karma, unit tests, Server-Side Rendering (SSR), TypeScript, Angular architecture, the Angular initialization cycle, browser cache, and client cache.

 

References

About the Author

  • Valerio De Sanctis

    Valerio De Sanctis is a skilled IT professional with 20 years of experience in lead programming, web-based development, and project management using ASP.NET, PHP, Java, and JavaScript-based frameworks. He held senior positions at a range of financial and insurance companies, most recently serving as Chief Technology and Security Officer at a leading IT service provider for top-tier insurance groups. He's an active member of the Stack Exchange Network, providing advice and tips on the Stack Overflow, ServerFault, and SuperUser communities; he is also a Microsoft Most Valuable Professional (MVP) for Developer Technologies. He's the founder and owner of Ryadel and author of many best-selling books on back-end and front-end web development.

    Browse publications by this author
Book Title
Access this book, plus 7,500 other titles for FREE
Access now