Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
ASP.NET Core 5 and Angular

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

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

Table of Contents (15) Chapters Close

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

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.

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