Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Aurelia and .NET Core 2.0

0.00/5 (No votes)
20 Jun 2018 1  
How to take advantage of ASP .NET Core's Razor (.cshtml) views and have dynamic routes for Aurelia

Aurelia and .NET Core 2.0 (using Razor Views and Dynamic Routes)

Background

I am a freelance web developer and have my own custom framework / CMS which I use for my clients. A few years ago, I decided that I wanted to have the admin backend to be an SPA and so I decided on Rob Eisenberg's Durandal. This worked beautifully - only, I had to figure out how to build the routes dynamically since I had a plugin-style architecture... I could enable/disable plugins per site as needed. I managed to get that working and Durandal has served me well these last few years.

I am now moving my framework / CMS from ASP .NET MVC 5 to ASP .NET Core 2.0. With that, I have also started looking into migrating from Durandal to Aurelia. I was a little disappointed to find that although quite a few people have asked, "How to get Razor (.cshtml) views working with Aurelia?", the solutions appeared to be few and unsatisfactory. Thus, I spent a little time working out how to do this for myself and finally managed to do so. Not only do I have Aurelia working with Razor views, but I can build the routes dynamically as well. Read on to find out how I did it...

Basics

Let's start with the basics that apply to any Aurelia setup:

  1. Install NodeJS if you don't already have it.
  2. Install JSPM globally: npm install -g jspm
  3. Create a new .NET Core project in the usual way.
  4. Initialize JSPM: jspm init

NOTE: Do this from the root directory of the project (not the solution root).

The steps should look something like this:

$ jspm init

warn Running jspm globally, it is advisable to locally install jspm via npm install jspm --save-dev.

Package.json file does not exist, create it? [yes]:
Would you like jspm to prefix the jspm package.json properties under jspm? [yes]:
Enter server baseURL (public folder path) [./]:./wwwroot
Enter jspm packages folder [wwwroot\jspm_packages]:
Enter config file path [wwwroot\config.js]:
Configuration file wwwroot\config.js doesn't exist, create it? [yes]:
Enter client baseURL (public folder URL) [/]:
Do you wish to use a transpiler? [yes]:
Which ES6 transpiler would you like to use, Babel, TypeScript or Traceur? [babel]:
ok   Verified package.json at package.json
     Verified config file at wwwroot\config.js
     Looking up loader files...
       system-csp-production.js
       system.js
       system.src.js
       system-csp-production.src.js
       system-csp-production.js.map
       system.js.map
       system-polyfills.js.map
       system-polyfills.src.js
       system-polyfills.js

     Using loader versions:
       systemjs@0.19.46
     Looking up npm:babel-core
     Looking up npm:babel-runtime
     Looking up npm:core-js
     Updating registry cache...
ok   Installed babel as npm:babel-core@^5.8.24 (5.8.38)
     Looking up github:systemjs/plugin-json
     Looking up github:jspm/nodelibs-fs
     Looking up github:jspm/nodelibs-process
     Looking up github:jspm/nodelibs-path
ok   Installed github:systemjs/plugin-json@^0.1.0 (0.1.2)
ok   Installed github:jspm/nodelibs-fs@^0.1.0 (0.1.2)
     Looking up npm:process
ok   Installed github:jspm/nodelibs-process@^0.1.0 (0.1.2)
     Looking up npm:path-browserify
ok   Installed github:jspm/nodelibs-path@^0.1.0 (0.1.0)
ok   Installed npm:process@^0.11.0 (0.11.10)
ok   Installed npm:path-browserify@0.0.0 (0.0.0)
     Looking up github:jspm/nodelibs-assert
     Looking up github:jspm/nodelibs-vm
     Looking up npm:assert
ok   Installed github:jspm/nodelibs-assert@^0.1.0 (0.1.0)
     Looking up npm:util
ok   Installed npm:assert@^1.3.0 (1.4.1)
     Looking up npm:vm-browserify
     Looking up npm:inherits
ok   Installed github:jspm/nodelibs-vm@^0.1.0 (0.1.0)
ok   Installed npm:util@0.10.3 (0.10.3)
     Looking up npm:indexof
ok   Installed npm:inherits@2.0.1 (2.0.1)
ok   Installed npm:vm-browserify@0.0.4 (0.0.4)
ok   Installed npm:indexof@0.0.1 (0.0.1)
     Looking up github:jspm/nodelibs-buffer
     Looking up github:jspm/nodelibs-util
     Looking up npm:buffer
ok   Installed github:jspm/nodelibs-buffer@^0.1.0 (0.1.1)
     Looking up npm:base64-js
     Looking up npm:ieee754
ok   Installed npm:buffer@^5.0.6 (5.1.0)
     Downloading npm:base64-js@1.3.0
ok   Installed npm:ieee754@^1.1.4 (1.1.11)
ok   Installed github:jspm/nodelibs-util@^0.1.0 (0.1.0)
ok   Installed npm:base64-js@^1.0.2 (1.3.0)
ok   Installed core-js as npm:core-js@^1.1.4 (1.2.7)
ok   Installed babel-runtime as npm:babel-runtime@^5.8.24 (5.8.38)
ok   Loader files downloaded successfully

Pay special attention to set the baseURL to ./wwwroot.

OPTIONAL: When working with JSPM, I recommend using the following Visual Studio extension: Package Installer.

Creating the "skeleton navigation" Project

  1. Install the following packages with JSPM:
    aurelia-binding
    aurelia-bootstrapper
    aurelia-dependency-injection
    aurelia-event-aggregator
    aurelia-framework
    aurelia-history
    aurelia-http-client
    aurelia-loader
    aurelia-loader-default
    aurelia-logging
    aurelia-metadata
    aurelia-pal
    aurelia-pal-browser
    aurelia-path
    aurelia-route-recognizer
    aurelia-router
    aurelia-task-queue
    aurelia-templating
    aurelia-templating-resources
    aurelia-templating-router
    bootstrap
    font-awesome
    jquery
  2. Open your _Layout.cshtml and set the contents to the following:
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    </head>
    <body>
        @RenderBody()
    </body>
    </html>

    Don't bother adding anything inside the <body>, as Aurelia will simply remove it to replace with the current "view".

  3. Create a folder named aurelia-app under your wwwroot.
  4. Copy the JavaScript and HTML for the views from the official skeleton-navigation project. In this case, we want the following files copied to your new aurelia-app folder:
    welcome.js

    NOTE: I renamed this to index.js in my demo project. The rest of this article assumes you do the same.

    Additionally, add the following JavaScript into a file called flickr.js also under the aurelia-app folder:

    import {inject} from 'aurelia-framework';
    import {HttpClient} from 'aurelia-http-client';
    
    @inject(HttpClient)
    export class Flickr{
        heading = 'Flickr';
        images = [];
        url = 
        'http://api.flickr.com/services/feeds/photos_public.gne?tags=mountain&tagmode=any&format=json';
    
        constructor(http){
            this.http = http;
        }
    
        activate(){
            return this.http.jsonp(this.url).then(response => {
                this.images = response.content.items;
            });
        }
    }

    and then the following files should be copied directly under wwwroot:

    app.js
    main.js
  5. Next, change your HomeController to look as follows:
    public class HomeController : Controller
    {
        [Route("")]
        public IActionResult Host()
        {
            return View();
        }
    
        [Route("index")]
        public IActionResult Index()
        {
            return PartialView();
        }
    
        [Route("app")]
        public IActionResult App()
        {
            return PartialView();
        }
    
        [Route("nav-bar")]
        public IActionResult NavBar()
        {
            return PartialView();
        }
    
        [Route("flickr")]
        public IActionResult Flickr()
        {
            return PartialView();
        }
    }

    NOTE: It is important to return PartialView() instead of View(), otherwise ASP.NET will obviously return your view combined with the contents of the _Layout.cshtml.

  6. Under Views\Home, delete any existing views and add new ones as follows:
    App.cshtml
    Flickr.cshtml
    Host.cshtml
    Index.cshtml
    NavBar.cshtml
  7. Copy the following source files in the skeleton-navigation project to the following destinations in your own project:
    /src/skeleton/src/app.html -> App.cshtml
    /src/skeleton/src/nav-bar.html -> NavBar.cshtml
    /src/skeleton/src/welcome.html -> Index.cshtml
    /src/skeleton/views/home/Index.cshtml -> Host.cshtml
  8. Add the following to Flickr.cshtml:
    <template>
        <section class="au-animate">
            <h2>${heading}</h2>
            <div class="row au-stagger">
                <div class="col-sm-6 col-md-3 flickr-img au-animate" repeat.for="image of images">
                    <a class="thumbnail">
                        <img src.bind="image.media.m" style="height:200px" />
                    </a>
                </div>
            </div>
        </section>
    </template>

Routing

  1. Create a folder in your project called Infrastructure (or whatever you prefer). Add the following:

    AureliaRoute.cs

    public struct AureliaRoute
    {
        public string Route { get; set; }
    
        public string Name { get; set; }
    
        public string ModuleId { get; set; }
    
        public object Nav { get; set; }
    
        public string Title { get; set; }
    }

    IAureliaRouteProvider.cs

    public interface IAureliaRouteProvider
    {
        string Area { get; }
    
        IEnumerable<AureliaRoute> Routes { get; }
    }
    
    public class AureliaRouteProvider : IAureliaRouteProvider
    {
        public string Area => "Admin";
    
        public IEnumerable<AureliaRoute> Routes
        {
            get
            {
                var routes = new List<AureliaRoute>();
    
                routes.Add(new AureliaRoute { Route = "", Name = "index", 
                ModuleId = "/aurelia-app/index", Nav = true, Title = "Home" });
                routes.Add(new AureliaRoute { Route = "flickr", Name = "flickr", 
                ModuleId = "/aurelia-app/flickr", Nav = true, Title = "Flickr" });
    
                return routes;
            }
        }
    }

    The IAureliaRouteProvider interface will allow us to define routes we wish to have in our Aurelia app. In my case, each of my plugins could have an instance of IAureliaRouteProvider. And note that each ModuleId contains /aurelia-app/ as a prefix. This is important and you will see why at the end of this article where we use Aurelia's convertOriginToViewUrl function to tell it where to find our views.

  2. We need to register our instances of IAureliaRouteProvider on application startup, so we can resolve all of them when needed. I am using Autofac, so I register our one instance as follows:
    builder.RegisterType<AureliaRouteProvider>().As<IAureliaRouteProvider>().InstancePerDependency();
  3. Now that we have our IAureliaRouteProvider registered, we need to resolve it and configure Aurelia to use it. First, we modify HomeController as follows:
    public class HomeController : Controller
    {
        private readonly IEnumerable<IAureliaRouteProvider> routeProviders;
    
        public HomeController(
            IEnumerable<IAureliaRouteProvider> routeProviders)
        {
            this.routeProviders = routeProviders;
        }
    
        // ...
        // ...
        // ...
    
        [Route("get-spa-routes")]
        public JsonResult GetSpaRoutes()
        {
            var routes = routeProviders
                .Where(x => x.Area == "Admin")
                .SelectMany(x => x.Routes);
    
            return Json(routes);
        }
    }

    And finally, we modify our app.js as follows:

    import { HttpClient } from 'aurelia-http-client';
    import { PLATFORM } from 'aurelia-pal';
    import $ from 'jquery';
    
    export class App {
        async configureRouter(config, router) {
            config.title = 'Aurelia';
    
            this.router = router;
    
            let http = new HttpClient();
            let response = await http.get("/get-spa-routes");
    
            $(response.content).each(function (index, item) {
                this.router.addRoute({
                    route: item.route,
                    name: item.name,
                    moduleId: PLATFORM.moduleName(item.moduleId),
                    title: item.title,
                    nav: item.nav
                });
            });
    
            this.router.refreshNavigation();
        }
    }

We are almost done!

Configuring Aurelia to Use MVC routes Instead of .html Files

The last thing we need to do is to get Aurelia to look at our .NET Core routes (defined in HomeController) instead of looking for .html files, which it does by default. The way to do this is to open our main.js and override the ViewLocator.prototype.convertOriginToViewUrl function with our own implementation. The following was my first attempt and although it works, it does not allow for some view URLs that should be left as the default for serving actual .html files. For example, the aurelia-kendoui-bridge.

export function configure(aurelia) {
    aurelia.use
        .standardConfiguration()
        .developmentLogging();

    ViewLocator.prototype.convertOriginToViewUrl = function (origin) {
        var viewUrl = null;
        var idx = origin.moduleId.indexOf('aurelia-app');

        if (idx != -1) {
            viewUrl = origin.moduleId.substring(idx + 11).replace(".js", '');
        }
        else {
            var split = origin.moduleId.split("/");
            viewUrl = split[split.length - 1].replace(".js", '');
        }
        return viewUrl;
    }

    aurelia.start().then(a => a.setRoot("./app", document.body));
}

NOTE: Originally, I just split the moduleId by "/" and took the last part, but that caused problems for me when having routes that NEED to have a slash in them. So instead, I decided to add an /aurelia-app/ prefix to my module IDs (see this in the routes defined in the AureliaRouteProvider instance).

In my second attempt, I allowed for .html files under jspm_packages:

ViewLocator.prototype.convertOriginToViewUrl = function (origin) {

    var viewUrl = null;
    var idx = origin.moduleId.indexOf('aurelia-app');

    // The majority of views should be under /wwwroot/aurelia-app
    if (idx != -1) {
        viewUrl = origin.moduleId.substring(idx + 11).replace(".js", '');
    }
    // JSPM packages may need to load HTML files (example: aurelia-kendoui-bridge)
    // TODO: This is not perfect.. (what if the view we want to show is normal HTML, 
    // but not in jspm_packages folder?)
    else if (origin.moduleId.indexOf('jspm_packages') !== -1) {
        viewUrl = origin.moduleId.replace(".js", '.html');
    }
    else {
        // This is for any js files in top-level of /wwwroot directory (should point to HomeController).
        var split = origin.moduleId.split("/");
        viewUrl = split[split.length - 1].replace(".js", '');
    }

    return viewUrl;
}

As you can see, this was an improvement, but still not quite perfect. At this point, I have requested the Aurelia team to create a new viewUrl property on the routes. We can have Aurelia still look for .html files by default, but if there’s a viewUrl property in the route definition, then use that instead and we won't have to implement ViewLocator.prototype.convertOriginToViewUrl. This way, modules that are not being served by ASP.NET controllers and have actual HTML files (for example: aurelia-kendo-bridge and others) won’t be affected... and it will allow me to have the .js file location decoupled from the .html file location, so I can put them wherever I want and additionally, I can then use embedded JavaScript as well, which is a requirement in some parts of my CMS.

At the time of writing, it is uncertain when this viewUrl will be implemented on the Aurelia routes. So I came up with a temporary solution. I decided I would modify the IAureliaRouteProvider interface to allow for a dictionary of mappings from module IDs to view URLs.

  1. The first step is to do just that:
    public interface IAureliaRouteProvider
    {
        string Area { get; }
    
        IEnumerable<AureliaRoute> Routes { get; }
    
        IDictionary<string, string> ModuleIdToViewUrlMappings { get; }
    }
    
    // NOTE: the "aurelia-app" route prefix is required (see main.js for the reason why)
    public class AureliaRouteProvider : IAureliaRouteProvider
    {
        public string Area => "Admin";
    
        public IEnumerable<AureliaRoute> Routes
        {
            get
            {
                var routes = new List<AureliaRoute>();
    
                routes.Add(new AureliaRoute { Route = "", Name = "index", 
                ModuleId = "/aurelia-app/index", Nav = true, Title = "Home" });
                routes.Add(new AureliaRoute { Route = "flickr", Name = "flickr", 
                ModuleId = "/aurelia-app/flickr", Nav = true, Title = "Flickr" });
    
                return routes;
            }
        }
    
        public IDictionary<string, string> ModuleIdToViewUrlMappings => new Dictionary<string, string>
        {
            // HomeController
            { "aurelia-app/index", "index" },
            { "aurelia-app/app", "app" },
            { "aurelia-app/nav-bar", "nav-bar" },
            { "aurelia-app/flickr", "flickr" },
    
            // etc...
        };
    }
  2. Secondly, we need to add one more action in our HomeController:
    [Route("get-moduleId-to-viewUrl-mappings")]
    public JsonResult GetModuleIdToViewUrlMappings()
    {
        var mappings = routeProviders
            .Where(x => x.Area == "Admin")
            .SelectMany(x => x.ModuleIdToViewUrlMappings);
    
        return Json(mappings);
    }
  3. And finally, we go back to main.js to make the following change:
    ViewLocator.prototype.convertOriginToViewUrl = function (origin) {
        let viewUrl = null;
    
        let storageKey = "moduleIdToViewUrlMappings";
        let mappingsJson = window.localStorage.getItem(storageKey);
        let mappings = null;
    
        if (mappingsJson) {
            mappings = JSON.parse(mappingsJson);
        }
    
        if (!mappings) {
            // NOTE: We are not using Aurelia's HTTP Client here, 
            // because we need to query synchronously..
            // and we can't use async/await because that causes Aurelia 
            // to throw an error - it doesn't
            // seem to like async on this "convertOriginToViewUrl" function. Thus, we resort to using
            // jQuery's $.ajax function instead and set "async: false"
            $.ajax({
                url: "/get-moduleId-to-viewUrl-mappings",
                type: "GET",
                dataType: "json",
                async: false
            }).done(function (content) {
                window.localStorage.setItem(storageKey, JSON.stringify(content));
                mappings = content;
            }).fail(function (jqXHR, textStatus, errorThrown) {
                console.log(textStatus + ': ' + errorThrown);
            });
        }
    
        if (mappings) {
            let idx = origin.moduleId.indexOf('aurelia-app');
    
            if (idx != -1) {
                let moduleId = origin.moduleId.substring(idx).replace(".js", '');
                let item = mappings.find(x => x.key === moduleId);
    
                if (item) {
                    viewUrl = item.value;
                }
                else {
                    // Any module ID which contains "aurelia-app" is an ASP.NET 
                    // route and thus SHOULD have a mapping from module ID to view URL.
                    // However, we can't find one here, so we take a guess that 
                    // it is most likely the same as the module ID but without any extension.
                    // NOTE: Since "moduleId" has been set to "origin.moduleId" 
                    // minus the ".js" extension and "aurelia-app" prefix, 
                    // we'll use that for the view URL,
                    // as that is what it is likely to be in most cases. 
                    // If not, there will be an error in the console and we will know to provide
                    // a mapping in IAureliaRouteProvider
                    viewUrl = moduleId;
                }
            }
        }
    
        if (!viewUrl) {
            viewUrl = origin.moduleId.replace(".js", '.html'); // Default
        }
    
        //console.log('View URL: ' + viewUrl);
        return viewUrl;
    }

And that's it! You can download a fully working demo, which also includes the Todo List and Contact Manager tutorials (as well as a bonus demo page for the Aurelia-Kendo-Bridge which uses an OData backend) from my GitHub project: aurelia-razor-netcore2-skeleton.

Enjoy!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here