Update (Mar 17, 2020): I found out that you can use the tag helper srcInclude that enables you to use glob patterns such as this:
<script asp-src-include"~/app.*.js"></script>
. Hence, my lengthy hacky way below is unnecessary unless you're deploying hashed assets to a CDN.
Content
- TL;DR
- Introduction
- Alternative Approaches
- Problem Statement
- Solution
- Add a standard Angular Project with Angular CLI
- Update
package.json
Scripts to Add the Correct Deploy URL - Add a @RenderSection in _Layout.cshtml to Inject Stylesheets
- Add Partial Razor Templates to inject Assets in Development Environment
- Create a Script to Generate Razor Partials for Production Assets
- Edit your PROJECT.csproj File to Configure Required Tasks for Publishing Angular Assets
- Edit Your Startup File to Include Angular Assets in Production Mode and Enable Hot Reloading in Development Mode
- Final Step: Add Angular Components into a Razor Page
TL;DR
I describe my approach for adding Angular components in Razor pages in an Asp.Net project. My approach supports both live-reloading for Development mode and publishing hashed assets for production builds. A project example can be viewed here.
Introduction
Asp.net Core offers a variety of project templates to get started with. For web applications, in addition to MVC and Razor Pages templates, it also offers templates for frontend single-page applications with integration into asp.net core app.
Currently, the Angular template comes bundled with client-side routing and the backend is set up to delegate all routing to the frontend.
In my particular case, I just wanted to add simple Angular components on Razor pages without any client-side routing. I searched online for a solution but couldnβt find one that satisfied my requirements.
In this post, I will my approach to add angular components to Razor pages. My approach has its flaws. So if you can think of an improvement, please share your feedback.
Alternative Approaches
- Add angular apps to existing dot net core project: My post is largely inspired by this post. The author makes innovative use of tag helpers to inject Angular assets into a Razor page. They use a web scraping library to extract the Angular assets from Angular-generated
index.html
. The method is more foolproof and not brittle unlike my method of using a shell script. Still, it does not support live server reloading. - Add The latest Angular CLI project(7.x) to ASP.Net Core 2.1 project: A very simple and straightforward approach by simply hardcoding Angular assets into a Razor. The downside is that you cannot use it with hashed assets without having to manually hardcode them every time new hashes are generated.
Problem Statement
I want to set up an Angular project such that you can add angular components to Razor pages with no client-side routine. The setup must satisfy the same development experience the official angular template supports. Specifically, the setup must support local development with live reloading.
Solution
Add a standard Angular Project with Angular CLI
Navigate to your asp.net project directory and create an angular app using Angular CLI:
ng new ClientApp
Youβd be prompted if you wanna use routing, make sure you choose no.
Your project directory would look something like this:
βββ ClientApp
βββ Pages
βββ Program.cs
βββ Properties
βββ RazorPagesAngular.csproj
βββ Startup.cs
βββ appsettings.Development.json
βββ appsettings.json
βββ bin
βββ obj
βββ wwwroot
Update package.json
Scripts to Add the Correct Deploy URL
Update the start
and build
scripts in package.json
as follows:
"start": "ng serve --deployUrl=/ClientApp/dist/",
"build": "ng build --deployUrl=/ClientApp/dist/",
You can also update your
angular.json
file to setup the above config as shown here
Add a @RenderSection in _Layout.cshtml to Inject Stylesheets
See Final _Layout.cshtml
The razor template already includes a section for injecting addition javascript assets as shown below:
@RenderSection("Scripts", required: false)
However, you also need another section to inject the Angular generated stylesheets. Navigate to _Layout.cshtml
file and update the <head>
section as shown below:
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Skinshare.Web</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
@* A section to include angular generated stylesheets *@
@RenderSection("StyleSheets", required: false)
</head>
Add Partial Razor Templates to inject Assets in Development Environment
See final _AppStyleSheets.cshtml and _AppScripts.cshtml.
Create partial razor templates to inject the javascript and stylesheet assets into the razor page where the Angular component would be used.
You need two partial templates, one for adding javascript assets and one for adding stylesheets. These will inject Angular scripts and stylesheets during the Development environment only.
Pages
βββ Shared
βββ _AppScripts.cshtml
βββ _AppStyleSheets.cshtml
βββ _Layout.cshtml
βββ _ValidationScriptsPartial.cshtml
@* _AppScripts.cshtml *@
<script src="/ClientApp/dist/runtime.js" type="module"></script>
<script src="/ClientApp/dist/polyfills.js" type="module"></script>
<script src="/ClientApp/dist/vendor.js" type="module"></script>
<script src="/ClientApp/dist/main.js" type="module"></script>
@* _AppStyleSheets.cshtml *@
<link rel="stylesheet" href="/ClientApp/diststyles.css">
Create a Script to Generate Razor Partials for Production Assets
Adding development scripts is straightforward because when running Angular in development mode, The assetsβ file names never change (i.e theyβre always runtime.js
, polyfills.js
, etc). However, production assets are hashed on each new build. Therefore, They cannot be hardcoded as done in the previous section. Another challenge is figuring out the correct load order of production js assets. The only source of truth for the order is inspecting the generated index.html
file by the ng build
command.
I wrote a very brittle shell script to scrape the assets from the generated index.html
and creates addition razor templates to inject production assets. Copy the script below and paste it in <root_project_dir>/scripts/generate-clientapp-assets.zsh
:
#!/usr/bin/env zsh
pathToIndex="ClientApp/dist/index.html";
# Finds the line that contains the stylesheets
lineToStyleSheets=$(awk '/stylesheet/{ print NR; exit }' $pathToIndex ); # DANGER Super Brittle
# Finds the line that contains the js scripts
lineToScripts=$(awk '/script/{ print NR; exit }' $pathToIndex ); # DANGER Super Brittle
pathToPartials="Pages/Shared"
styleSheets=$(sed "${lineToStyleSheets}q;d" $pathToIndex);
styleSheets=$(echo $styleSheets | sed 's/<\/head>//g' );
scripts=$(sed "${lineToScripts}q;d" $pathToIndex);
scripts=$(echo $scripts | sed 's/<\/body>//g' );
echo $styleSheets > "$pathToPartials/_AppStyleSheetsProd.cshtml";
echo $scripts > "$pathToPartials/_AppScriptsProd.cshtml"
echo "Created prod prartials for _AppStyleSheetsProd.cshtml & _AppScriptsProd.cshtml";
The script does the following:
- Grabs the line in
index.html
that contains the stylesheets and copies it toPages/Shared/_AppStyleSheetsProd.cshtml
. - Grabs the line in
index.html
that contains the javascript assets and copies it toPages/Shared/_AppScriptsProd.cshtml
Donβt forget to add execute permissions to the script by running
chmod +x scripts/generate-clientapp-assets.zsh
Here is how your Pages/Shared
directory looks like after executing the script
Pages
βββ Shared
βββ _AppScripts.cshtml
βββ _AppScriptsProd.cshtml
βββ _AppStyleSheets.cshtml
βββ _AppStyleSheetsProd.cshtml
βββ _Layout.cshtml
βββ _ValidationScriptsPartial.cshtml
Edit your PROJECT.csproj File to Configure Required Tasks for Publishing Angular Assets
My csproj file is based on the one generated by dotnet angular template. I added an additional task CopyPartials
shown below to copy production assets into razor partials. The file can be viewed here.
<Target Name="CopyPartials" BeforeTargets="Compile" DependsOnTargets="BuildAsset" Condition=" '$(Configuration)' == 'Release' ">
<!-- Generate new _AppScriptsProd.cshtml and _AppStyleSheetsProd.cshtml based on the freshly created production build -->
<Exec Command="source scripts/generate-clientapp-assets.zsh" />
</Target>
Edit Your Startup File to Include Angular Assets in Production Mode and Enable Hot Reloading in Development Mode
The final Startup.cs looks like this
- Right below
app.UseStaticFiles();
insert the following:
if (!env.IsDevelopment())
{
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "ClientApp/dist")),
RequestPath = "/ClientApp/dist"
});
}
- Right below
app.UseEndpoints
invocation, insert the following:
if (env.IsDevelopment())
{
app.UseSpa(spa =>
{
spa.Options.SourcePath = null;
spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
});
}
Final Step: Add Angular Components into a Razor Page
Now that weβre donβt with all the setup, here is how I add my components:
@page
@model Skinshare.Web.Pages.Routines.CreateModel
@{
ViewData["Title"] = "Angular Component Example";
}
<!-- An angular component -->
<app-root></app-root>
@section StyleSheets {
<environment include="Development">
@{await Html.RenderPartialAsync("_AppStyleSheets");}
</environment>
<environment exclude="Development">
@{await Html.RenderPartialAsync("_AppStyleSheetsProd");}
</environment>
}
@section Scripts {
<environment include="Development">
@{await Html.RenderPartialAsync("_AppScripts");}
</environment>
<environment exclude="Development">
@{await Html.RenderPartialAsync("_AppScriptsProd");}
</environment>
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
For development mode, in a separate terminal, navigate to /ClientApp
and run npm start
. Then run your Dotnet app in development mode.
When you publish your app, the MS Build Task CopyPartials
will run right after production Angular assets are generated and before the Dotnet project is compiled. This will ensure that the production assets are injected using the _AppScriptsProd.cshtml
and _AppStyleSheetsProd.cshtml
partials.