Introduction
After a series of previews, Microsoft announced the final release of .NET Core 2.1, which includes newer version for ASP.NET Core and Entity Framework Core. As you probably know, the official Angular SPA template was using Angular 4 and just hours after the .NET Core 2.1 release, I tried the new Angular template and it was using version 5, and that was quite disappointing.
Background
During the last couple of months, many developers around the world have been trying to mix ASP NET Core and Angular 6 and, yes, I've read and tried almost all recipes on every blog post without good results, some recipes worked for me at a 85%, some others didn't work at all, so I decided to start my own recipe from scratch. I did it combining steps and techniques seen in all of those articles and it worked for me.
At that point, I was asked for the recipe by a friend of mine but due to the lack of time I decided to bring him a template to let him create a working project with few commands. That happened on Tuesday evening and I created a GitHub repository to share this with everybody on Wednesday morning just before starting to work, but just at that time, .NET Core 2.1 was released and I felt forced to update the repo to have it a 100% up-to-date.
Using the Code
I'll explain in this article how I did it, how it evolved, the challenges I faced and how I fixed but at this point, if you are curious, you might go to that repository, clone it, install the template and start developing Angular 6 with ASP NET Core 2.1.
This template includes:
- NET Core API 2.1 (basic MVC, with
ValuesController
as usual) - Swagger (just navigate to your '
/swagger
') - Angular 6 project with Angular CLI (6.0.7) including:
MDBootstrap
(https://mdbootstrap.com/) ngx-mapbox-gl
(https://github.com/Wykks/ngx-mapbox-gl) rxjs
& rxjs-compat
- Docker support (just select
docker-compose
as startup project and run it)
Install from the git repo
- Git clone this repo
- Install the template
dotnet new --install .\content
- Create a directory where you would like to store the project
- Type `
dotnet new angular6 -n
<name_of_your_project> - Open the solution and run the project
- Enjoy coding!
How I Did It?
Well, best recipes agreed on the same steps to start from scratch, and those are:
- Update your environment (npm, Node and Angular CLI)
- Create a new Angular project
- Get inside new generated project folder and create, with dotnet CLI, a new Angular SPA project with 'dotnet new angular
-n
ProjectName' - Refactor to mix both ecosystems
That final step was the most challenging one, on the one hand, you have the ClientApp folder generated by the SPA template but that includes an Angular 4 application and also you have the src folder with the ng-generated
Angular 6 application, so, the first step for me was to get rid of every Angular 4 stuff, this includes webpack.config removal and many .ts refactors that, today, I'm unable to list. To be honest, I got into a try-failure loop that consumed my spare time during the last couple of weeks.
How It Evolved?
And finally, I got a working version that satisfied me, without tricks, a clean solution with a clean folder structure that would allow me to grow on both sides of my developments, frontend with Angular 6 and backend with .NET Core, version 2 at that time. And I saved a snapshot just in case.
Then I added Swagger, with Swasbucle's NuGet package and I could see my page meanwhile I was able to access the swagger interface and call an API Controller. And I saved a snapshot just in case.
Then I went for web styling, and I was tried Bootstrap, Angular Material and Bootstrap Material Design UI KIT (aka MDBootstrap), I decided to go for MDBootstrap because it is the one I like the most. And it worked, well, There are few warnings at compile time but I can live with it. And I added a dummy navbar and a dummy footer, and also a dummy animation, and everything worked... And I saved a snapshot just in case.
And as in my project I need to work with maps, even though I already knew the answer, I asked for best maps integration in Angular and I was told to go for mapbox by using ngx-mapbox-gl
. And I went for it, I installed, configured it basically, I inserted a map in the site and it worked... And I saved a snapshot just in case.
(NOTE: I made a newbie mistake, but it doesn't matter, the repo has my mapbox token, it will work for everyone but I have to update the example to avoid that and then update the Readme with some quickstart instructions.)
And then, on Wednesday at 5 a.m, I decided to share this with the world so I created a template project and I renamed few things to allow the template to create a working solution with three steps:
- Clone the repo
- Install the template
- Create new project (dotnet new angular6)
And then .NET Core 2.1 was released, but as the SPA came with Angular 5.2 instead of Angular 6, I decided to update my project to be Angular 6 and Core 2.1, and it worked, and I pushed it to the repo.
And then, I was asked if I had been able to run this in docker, and, well, I really hadn't tried that out yet with this solution and, indeed, that had been one of the challenging things at the beginning because I was able to run everything locally but not inside a Docker container. I added Docker support to the project and I spent few hours improving the docker file stages until I reached a clean small set of stages that did the work quite quick and I pushed that to the repo.
Code Details
Mainly two files are important here. Startup.cs and angular.json because those files are the ones that orchestrates the behaviour and interaction between both worlds, dotnet and angular.
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.Swagger;
using System;
using System.IO;
namespace AspNetCore2Ang6Template
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSpaStaticFiles(c =>
{
c.RootPath = "wwwroot";
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = "AspNetCore2Ang6Template API",
Description = "A simple example ASP.NET Core Web API",
Contact = new Contact { Name = "Juan García Carmona",
Email = "d.jgc.it@gmail.com", Url = "https://wisegeckos.com" },
});
var basePath = AppContext.BaseDirectory;
var xmlPath = Path.Combine(basePath, "AspNetCore2Ang6Template.xml");
c.IncludeXmlComments(xmlPath);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(name: "default", template: "{controller}/{action=index}/{id}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "wwwroot";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
}
}
As you can see, startup mixes MVC and SPA using as source path the well-known folder 'wwwroot'.
Angular.json (just important part):
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"AspNetCore2Ang6Template": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {
":component": {
"styleext": "scss"
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "wwwroot",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"node_modules/font-awesome/scss/font-awesome.scss",
"node_modules/angular-bootstrap-md/scss/bootstrap/bootstrap.scss",
"node_modules/angular-bootstrap-md/scss/mdb-free.scss",
"node_modules/mapbox-gl/dist/mapbox-gl.css",
"src/styles.scss"
],
"scripts": [
"node_modules/chart.js/dist/Chart.js",
"node_modules/hammerjs/hammer.min.js"
]
},
"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": "AspNetCore2Ang6Template:build"
},
"configurations": {
"production": {
"browserTarget": "AspNetCore2Ang6Template:build:production"
}
}
}, [ETC...]
- It establishes source root as src, which means that the application source code is under src folder.
- It also adds scss support, because it was a requirement for MDBootstrap.
- It determines that the output path is wwwroot, which is what makes more sense and, hell, I haven't seen this in other places, at least not in a direct and clean approach.
- It replaces environment configuration when building in production mode.
Also, I'm proud of this Docker file because I spent, as I said before, a couple of hours, mainly because I hadn't understood multi-stage docker files.
FROM microsoft/dotnet:2.1-sdk as build-env
WORKDIR /app
# Setup node
ENV NODE_VERSION 8.11.1
ENV NODE_DOWNLOAD_SHA 0e20787e2eda4cc31336d8327556ebc7417e8ee0a6ba0de96a09b0ec2b841f60
RUN curl -SL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz"
--output nodejs.tar.gz \
&& echo "$NODE_DOWNLOAD_SHA nodejs.tar.gz" | sha256sum -c - \
&& tar -xzf "nodejs.tar.gz" -C /usr/local --strip-components=1 \
&& rm nodejs.tar.gz \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs\
&& npm i -g @angular/cli npm i -g @angular/cli
# Copy csproj and restore as distinct layers
COPY src/AspNetCore2Ang6Template/ ./AspNetCore2Ang6Template/
RUN cd AspNetCore2Ang6Template \
&& dotnet restore \
&& npm install
RUN cd AspNetCore2Ang6Template \
&& dotnet publish -c Release -o out
# build runtime image
FROM microsoft/dotnet:2.1-aspnetcore-runtime
WORKDIR /app
# Setup node
ENV NODE_VERSION 8.11.1
ENV NODE_DOWNLOAD_SHA 0e20787e2eda4cc31336d8327556ebc7417e8ee0a6ba0de96a09b0ec2b841f60
RUN curl -SL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz"
--output nodejs.tar.gz \
&& echo "$NODE_DOWNLOAD_SHA nodejs.tar.gz" | sha256sum -c - \
&& tar -xzf "nodejs.tar.gz" -C /usr/local --strip-components=1 \
&& rm nodejs.tar.gz \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs\
&& npm i -g @angular/cli
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "AspNetCore2Ang6Template.dll"]
Article Improvements
- It uses newest microsoft/dotnet image as build environment.
- It installs on a single step, node and npm and finally Angular cli.
- In the next step, it gets into the folder and, again, in a single RUN, it restores project dependencies, the NuGet packages by dotnet restore and the npm with npm install.
- Then we switch to the runtime environment, microsoft/dotnet:2.1-aspnetcore-runtime, and we install node npm and angular cli in this environment too.
- We copy build environment output here.
- And we run our DLL.
Note that to whoever uses this template, at every place, during project creation with dotnet new, the dotnet finds AspNetCore2Ang6Template
keyword, it will replace that with your project name. I used this approach because I've found many POCs on GitHub that at the end implies huge refactorings. I'd say that with three steps, create a project that looks yours from scratch seems a better idea.
Final Words
I'm sure that this can help many other developers out there but also, I'm willing to give direct help, so don't hesitate to leave a comment or use a direct approach by contacting me by email or LinkedIn (it's easy to find me). As always, I hope it helps.
History
- Article created on 02/June/2018 by Juan GarcĂa Carmona
- Few minutes later: added Startup.cs and Angular.json code
- And few minutes later, I added the Docker file and its explanation