Implementing a Angular project with AspNet Core

diogenespolanco

Diógenes Polanco

Posted on November 4, 2019

Implementing a Angular project with AspNet Core

This article will help us understand how to use the .NET Core command line interface to create and run an ASP.NET Core web application.

This way of implementing an Angular project provides a convenient starting point for ASP.NET Core applications that use Angular and the Angular CLI to implement a rich client-side user interface (UI).

The mechanism is equivalent to creating an ASP.NET Core project to act as an API back-end and an angular CLI project to act as an UI. This route offers the convenience of hosting both types of projects in a single application project. Consequently, the application project can be built and published as a single unit, which also allows us to deploy it to nopcommerce as a plugin.

You'll learn how to:

  • Create a web app project
  • How to use the SPA subdirectory approach
  • Add pages, images, styles, modules
  • Run the app
  • Publish and deploy
Create a web app project

Open a command shell, and enter the following command:

dotnet new angular

Run dotnet build to verify the app builds correctly. On the first run, the build process restores npm dependencies, which can take several minutes. Subsequent builds are much faster.

Run dotnet run to start the app. A message similar to the following is logged:

Now listening on: http://localhost:<port>

Navigate to this URL in a browser.

In ASP.NET Core, supporting SPAs works via a middleware to then point to this directory (no matter what you call it). It does this by injecting the middleware as the last step in Startup’s Configure method:

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 test
{
    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");
                }
            });
        }
    }
}

This involves two calls: UseSpaStaticFiles and UseSpa. These calls set up the ability to get calls from a generated index.html that Angular-CLI builds for you. In this case, you’ll see that during development, it will launch ‘npm start’ for you; that is part of how it launches the development build of the Angular app.

While the middleware is a good option if you’re building a single, monolithic SPA without much use for other web pages, I think for most developers, integrating ASP.NET Core into a traditional MVC application makes the most sense. You can typically keep an SPA as a separate folder and treat it as a child-project, without having to do a lot of integration between the two projects.

How to use the SPA subdirectory approach

suppose you have an Angular project in a subdirectory called ClientApp.

Because this directory has its own package.json, you can just use the CLI of the library you’re working with to do builds and such. You could even develop it in isolation without invoking the ASP.NET Core project, but since I’m usually adding the SPA to an existing Razor view, I generally just run them both.

One issue with this method is that the build directory for the SPA project is usually in this directory. You could use the SpaStaticFiles middleware to solve this, but I usually just change the configuration of the SPA to build into my wwwroot directory. For example, this is what that looks like in the angular.json file (for the Vue.js CLI):

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "test": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {},
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "progress": true,
            "extractCss": true,
            "outputPath": "dist",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": ["src/assets"],
            "styles": [
              "node_modules/bootstrap/dist/css/bootstrap.min.css",
              "src/styles.css"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "test:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "test:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "test:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.spec.json",
            "karmaConfig": "src/karma.conf.js",
            "styles": ["styles.css"],
            "scripts": [],
            "assets": ["src/assets"]
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
            "exclude": ["**/node_modules/**"]
          }
        },
        "server": {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist-server",
            "main": "src/main.ts",
            "tsConfig": "src/tsconfig.server.json"
          },
          "configurations": {
            "dev": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": true
            },
            "production": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false
            }
          }
        }
      }
    },
    "test-e2e": {
      "root": "e2e/",
      "projectType": "application",
      "architect": {
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "test:serve"
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": "e2e/tsconfig.e2e.json",
            "exclude": ["**/node_modules/**"]
          }
        }
      }
    }
  },
  "defaultProject": "test"
}

In this case, I’m just telling the Vue.js CLI to output the build to a subdirectory of the js folder in wwwroot, so I don’t need to configure ASP.NET Core to look into the SPA directory directly. Angular and React both support this kind of output directory, if you don’t want to use the SpaStaticFiles middleware.

By using one or more subdirectories with their own build, you can have many SPA projects (even if they’re different frameworks) in one ASP.NET Core project. But it does have some limitations involving deployment and dependency management. By doing this, each project will have it’s own node_modules directory which means that build times can get very lengthy.

Because you’re configuring the output directory into wwwroot, you don’t need to specify including a directory when you deploy the project, but you will need to tell the MSBuild (e.g. csproj file) to build the SPA project before you deploy. The trick here is to add a Target to your .csproj file:

  <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build -- --prod" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build:ssr -- --prod" Condition=" '$(BuildServerSideRenderer)' == 'true' " />

    <!-- Include the newly-built files in the publish output -->
    <ItemGroup>
      <DistFiles Include="$(SpaRoot)dist\**; $(SpaRoot)dist-server\**" />
      <DistFiles Include="$(SpaRoot)node_modules\**" Condition="'$(BuildServerSideRenderer)' == 'true'" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>%(DistFiles.Identity)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
        <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>

The Target specifies that it should consider something before it computes the files to publish (so before it looks at wwwroot for the files to include). The two ‘Exec’ lines are just a way to run the install and build steps in the ClientApp directory. This is only executed during a publish, so it won’t impact your development cycle.

The big drawback is more about Visual Studio than about ASP.NET Core. Because the project.json is in a subdirectory (not in the root of the ASP.NET Core project), you can’t get the Task Runner Explorer to find the build. This means you’ll have to run it separately. If you’re using Visual Studio Code, Rider, or other IDE, you might already be used to opening console/terminal windows and doing the build yourself so this might not be much of a hardship.

The other limitation is related to this one, more than one package.json to manage. This is the one that hurts me the most. Most of my ASP.NET Core projects already use NPM to manage other dependencies (both dev and production dependencies). Having to manage two or more package.json files makes me a little frustrated. That’s why I usually resolve to use the last of the options — fully integrate the SPA into the ASP.NET Core project.

Add pages, images, styles, modules

The ClientApp directory contains a standard Angular CLI app. See the official Angular documentation for more information.

There are slight differences between the Angular app created by this template and the one created by Angular CLI itself (via ng new); however, the app's capabilities are unchanged. The app created by the template contains a Bootstrap-based layout and a basic routing example.

Run the app

If you have the ng tool installed globally, you can run any of its commands. For example, you can run ng lintng test, or any of the other Angular CLI commands. There's no need to run ng serve though, because your ASP.NET Core app deals with serving both server-side and client-side parts of your app. Internally, it uses ng serve in development.

If you don't have the ng tool installed, run npm run ng instead. For example, you can run npm run ng lint or npm run ng test.

In a command prompt, switch to the ClientApp subdirectory:

cd ClientApp && npm start

Use npm start to launch the Angular CLI development server, not ng serve, so that the configuration in package.json is respected. To pass additional parameters to the Angular CLI server, add them to the relevant scripts line in your package.json file.

Publish and deploy

In development, the app runs in a mode optimized for developer convenience. For example, JavaScript bundles include source maps (so that when debugging, you can see your original TypeScript code). The app watches for TypeScript, HTML, and CSS file changes on disk and automatically recompiles and reloads when it sees those files change.

In production, serve a version of your app that's optimized for performance. This is configured to happen automatically. When you publish, the build configuration emits a minified, ahead-of-time (AoT) compiled build of your client-side code. Unlike the development build, the production build doesn't require Node.js to be installed on the server (unless you have enabled server-side rendering (SSR)).

I will soon be publishing the code on GitHub and some extra steps to deploy it in the cloud ...

💖 💪 🙅 🚩
diogenespolanco
Diógenes Polanco

Posted on November 4, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related