Using jQuery and Bootstrap from a CDN with fallback scripts in ASP.NET Core 3.0 (2022)

In this post, I show how to use the Link Tag Helper and Script Tag Helper in Razor with the asp-fallback attribute to serve files from a Content Delivery Network (CDN), falling back to local scripts if the CDN is unavailable.

Using a CDN with a fallback was the default approach in the ASP.NET Core templates for .NET Core 2.x, but in 3.x the templates were significantly simplified and now only serve from local files.

Using a CDN for common libraries

The first thing to discuss is why you might want to use a CDN for serving your application's client-side dependencies.

A CDN is just another server that hosts common files, often used for client-side assets like CSS stylesheets, JavaScript libraries, or images. Using a CDN can speed up your applications for several reasons:

  • CDNs are typically globally distributed, so can give very low latencies for downloading files, wherever in the world your users are. That can make a big difference if your application is only hosted in one region, and users are sending requests from the other side of the world!
  • It offloads network traffic from your servers, reducing the load on your server.
  • By sending requests for client-side assets to a CDN, you may see higher overall network throughput for your application. Browsers limit the number of simultaneous connections they make to a server (commonly 6). If you host your files on a CDN, the connections to the CDN don't count towards your server limit, leaving more connections to download in parallel from your app.
  • Other applications may have already downloaded common libraries from the CDN. If the file is already cached by the browser, it may not need to make a request at all, significantly speeding up your application.

If you need to include common libraries such as Bootstrap or jQuery, then it can make a lot of sense to serve these from a CDN. These libraries are publicly hosted on many different CDNs, so using any of the common ones can be a big win for your application's performance.

There are a couple of downsides or considerations when using CDNs

  • By using a CDN you're trusting them to deliver code to your user's browser. You need to be careful that if a CDN is compromised with malicious JavaScript, your website doesn't run it on your page. That can put both you and your users at risk.
  • If a CDN is unavailable, you should fallback to serving the scripts from your own website, as otherwise a CDN going down could break your application, as shown below.

Using jQuery and Bootstrap from a CDN with fallback scripts in ASP.NET Core 3.0 (2)

(Video) Install and use Bootstrap in ASP NET Core

I'm going to describe how to tackle that second point in this post, but the solutions will also cover the first point too. For more details on the security side, see this post by Scott Helme on adding a Content-Security Policy (CSP) to your application, and using Sub Resource Integrity (SRI) checks.

Whether you consider adding a fallback worthwhile will depend very much on the application you're building. Using a fallback adds complexity to your site that you may not need. The Tag Helper approach I show here also requires injecting inline-JavaScript, which may be at-odds with your CSP.

The current ASP.NET Core templates - no CDN for you

As part of the ASP.NET Core 3.x updates, the default templates were updated to use Bootstrap 4 (instead of version 3). They were also simplified significantly, and as part of that, CDN support was removed. If you look at the default _Layout.cshtml for a Razor Pages or MVC application in ASP.NET Core 3.0, you'll see something like the following (I've only kept the pertinent <link> and <script> tags in the example below):

<!DOCTYPE html><html lang="en"><head> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" /></head><body> @RenderBody() <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> @RenderSection("Scripts", required: false)</body></html>

As you can see, the default layout references the following files:

  • bootstrap.min.css - The core Bootstrap CSS files. Version 4.3.1 as of .NET Core 3.1
  • site.css - The custom CSS for your website.
  • jquery.min.js - jQuery version 3.3.1 - required by Bootstrap.
  • bootstrap.bundle.min.js - The bootstrap jQuery plugins (bundled with Popper.js)
  • site.js - The custom JavaScript for your website.

In addition, for client-side validation you need to add the jQuery validation libraries. These are specified in the separate _ValidationScriptsPartial.cshtml file:

<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script><script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

All these libraries are included in the default templates in the wwwroot/lib folders, but if you'd rather serve these files from a CDN, then you should consider keeping these files as a fallback.

Using fallback Tag Helpers to test for failed file loading from a CDN

The Link and Script Tag Helpers support the concept of configuring a fallback test for files loaded from a CDN. You can add asp-fallback-* attributes to a link, and the tag helper automatically generates some JavaScript to check if the file was downloaded from the CDN correctly.

For example, lets just take the first <link> from _layout.cshtml:

(Video) Using sweetalert/toastr javascript library in Asp.Net Core MVC

<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />

You could update this link to load the Bootstrap CSS file from a CDN (cdnjs in this example) by changing the href:

<link rel="stylesheet" href="" />

However, if the CDN is unavailable, your site will look very broken. You can provide a fallback for a CSS stylesheet link, by adding the following attributes:

  • asp-fallback-test-class - The CSS class to apply to a test element. Should be a class specified in the linked stylesheet, that won't exist otherwise.
  • asp-fallback-test-property - The CSS property to check on the test element.
  • asp-fallback-test-value - The value of the CSS property that the test element should have, if the linked stylesheet didn't load correctly.
  • asp-fallback-href - The URL of the file to load if the test fails.

For the Bootstrap example, you could apply the .sr-only class, and check that the position property has the value absolute using the following:

<link rel="stylesheet" href="" asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css" asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />

When it renders, this generates the following markup and inline JavaScript (the JavaScript is minified in practice, I've de-mangled and simplified it to make it a bit easier to understand below):

<link rel="stylesheet" href="" /><meta name="x-stylesheet-fallback-test" content="" class="sr-only" /><script> function checkValid(property, value, localLink, attributes) { var elements = document.getElementsByTagName("SCRIPT"); var style = getStyle(elements); if (style && style[property] !== value) { document.write('<link href="' + localLink + '" ' + attributes + "/>") } } function getStyle(elements){ var previousEl = elements[elements.length - 1].previousElementSibling; return document.defaultView && document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(previousEl) : previousEl.currentStyle; } checkValid("position", "absolute", "/lib/bootstrap/dist/css/bootstrap.min.css", "rel=\u0022stylesheet\u0022");</script>

As you can see, that's a lot of extra JavaScript to check for a fallback. The version for <script> tags is a lot simpler. You just need two attributes for that:

  • asp-fallback-test - the JavaScript code to run that should evaluate to a "truthy" value if the script was loaded correctly.
  • asp-fallback-src - The URL of the file to load if the test fails.

For this script file reference:

<script src="" asp-fallback-src="~/lib/jquery/dist/jquery.min.js" asp-fallback-test="window.jQuery"></script>

You'll get the following generated HTML:

<script src=""></script><script>(window.jQuery||document.write("\u003Cscript src=\u0022/lib/jquery/dist/jquery.min.js\u0022\u003E\u003C/script\u003E"));</script>

The JavaScript generated is pretty simple - run the test, and if it fails, add a new <script> tag with the correct URL.

(Video) Bootstrap navigation menu in asp net core application

That gives us everything we need to update our layout files to use a CDN with a local falback.

Updating the templates to use a CDN with a fallback

I'll start with _Layout.cshtml first, from the start of this post.

<!DOCTYPE html><html lang="en"><head> <environment include="Development"> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" /> <link rel="stylesheet" href="~/css/site.css" /> </environment> <environment exclude="Development"> <link rel="stylesheet" href="" asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css" asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> </environment></head><body> @RenderBody() <environment include="Development"> <script src="~/lib/jquery/dist/jquery.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> </environment> <environment exclude="Development"> <script src="" asp-fallback-src="~/lib/jquery/dist/jquery.min.js" asp-fallback-test="window.jQuery" crossorigin="anonymous" integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"> </script> <script src="" asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js" asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal" crossorigin="anonymous" integrity="sha384-xrRywqdh3PHs8keKZN+8zzc5TX0GRTLCcmivcbNJWm2rs5C8PRhcEn3czEjhAO9o"> </script> <script src="~/js/site.js" asp-append-version="true"></script> </environment> @RenderSection("Scripts", required: false)</body></html>

There's a lot in there, but here are the highlights:

Next up is the _ValidationScriptsPartial.cshtml file, which uses a similar approach:

<environment include="Development"> <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script> <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script></environment><environment exclude="Development"> <script src="" asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js" asp-fallback-test="window.jQuery && window.jQuery.validator" crossorigin="anonymous" integrity="sha384-rZfj/ogBloos6wzLGpPkkOr/gpkBNLZ6b6yLy4o+ok+t/SAKlL5mvXLr0OXNi1Hp"> </script> <script src="" asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js" asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive" crossorigin="anonymous" integrity="sha384-ifv0TYDWxBHzvAk2Z0n8R434FL1Rlv/Av18DXE43N/1rvHyOG4izKst0f2iSLdds"> </script></environment>

With that in place, lets try it out.

Testing the fallbacks

The easiest way to test that your fallback behaviour is working correctly, is to actively block the CDN files from loading. You can achieve that in Chrome or Edge by opening dev-tools (F12) and right-clicking the network file in question. From that you can choose "Block Request URL":

Using jQuery and Bootstrap from a CDN with fallback scripts in ASP.NET Core 3.0 (7)

If you go through and block all the CDN URLs (or the domains) and reload the page, it should load fine. The blocked URLs are shown as blocked in the network tab, but the fallback tests wil fail, and use the local URLs instead:

(Video) Razor Pages for ASP.NET Core - Full Course (.NET 6)

Using jQuery and Bootstrap from a CDN with fallback scripts in ASP.NET Core 3.0 (8)


There's one thing to watch out for though - for the integrity attribute to work correctly, the local file must be exactly the same as the CDN version. When I tested blocking CDN files initially, the fallback tests failed, but so did loading the local files:

Using jQuery and Bootstrap from a CDN with fallback scripts in ASP.NET Core 3.0 (9)

SRI requires referenced files to be byte-for-byte identical. In my case, the local files used CRLF instead of the LF used in the CDN. I fixed it by overwriting the local files with the ones from the CDN, and ensuring that git preserved the LF, by adding this to the project .gitattributes file:

**/wwwroot/lib/** text eol=lf

That ensures that the files in wwwroot/lib are always checked-out with LF line endings, even on windows, and should help avoid SRI issues!


In this post I showed how you could update the default ASP.NET templates to load CSS stylesheets and JavaScript libraries from a CDN. I showed how to use Tag Helpers to add fallback tests, so that if the CDN is unreachable, then your library files will be loaded from the local files instead.

As part of the update, I added SRI hashes to ensure that if the CDN files are compromised (as has happened in several high-profile cases), your application will refuse to run the compromised files. With the fallbacks configured, your application will be protected and will continue to function. Win win!

(Video) Asp.Net Core MVC Image Upload and Retrieve


1. ASP.NET Core autocomplete search using jquery💡 like google 🔍
(Dot Net)
2. (#40) Link tag helper in core | What if CDN is not working? | Asp.Net Core tutorial
3. ASP.NET Core 6 .NET 6 for Beginners (Full Course) [2022]
(Fast and Easy Programming)
4. ASP.NET Core Web API CRUD with Angular 11
5. John Bristowe | A Deep Dive into Kendo UI | ComponentsConf 2019
6. Extend IdentityUser in ASP NET Core

You might also like

Latest Posts

Article information

Author: Carmelo Roob

Last Updated: 09/21/2022

Views: 5900

Rating: 4.4 / 5 (65 voted)

Reviews: 88% of readers found this page helpful

Author information

Name: Carmelo Roob

Birthday: 1995-01-09

Address: Apt. 915 481 Sipes Cliff, New Gonzalobury, CO 80176

Phone: +6773780339780

Job: Sales Executive

Hobby: Gaming, Jogging, Rugby, Video gaming, Handball, Ice skating, Web surfing

Introduction: My name is Carmelo Roob, I am a modern, handsome, delightful, comfortable, attractive, vast, good person who loves writing and wants to share my knowledge and understanding with you.