.NET 5 is here, and with the release comes a barrel of Blazor improvements. One of the upgrades Blazorinos should be most excited about is JavaScript isolation and object references within .NET.

This post will describe enhancing the existing Blazor API using extension methods to make importing JavaScript modules clearer. We’ll create a new type that will allow us to write C# code that looks similar to its JavaScript counterparts.

Most folks will either love this post or hate it because of the dynamic keyword.

What Is A JavaScript Module

From its humble beginning, the creators of JavaScript designed it to supplement the web experience. JavaScripts and the document object model (DOM) are closely tied. We all know that JavaScript has evolved past its initial design and bred an entire community of developers going outside the initially imagined bounds. With the growing needs of a burgeoning community, the language had to adapt to support its users’ needs.

One of the newest additions to JavaScript is JavaScript Modules. Modules allow JavaScript developers to group functionality, export specific interfaces, and allow for lazy-loading functionality at runtime. Language features include the keywords import and export. Let’s look at a simple module we’ll be exposing in our Blazor application.

export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

This particular module exports a showPrompt function. The advantage of JavaScript modules is we can import them into our JavaScript context without polluting the global namespace.

import { showPrompt} from './modules/prompt.js'
function cool() {
	showPrompt("hello, world!");
}

JavaScript modules support organization and a better understanding of the origin of functionality. That’s a general overview, and to learn more about JavaScript modules, I recommend the Mozilla.org documentation site.

Why is this important for Blazor developers?

.NET 5 and JavaScript Isolation

The Blazor story is one of interoperability with JavaScript and the Client UI technology of the web. To offer Blazor developers the web’s full power, the Blazor team had to support JavaScript modules. Luckily, with .NET 5, they did just that!

When developing a Razor Class Library, we can now import any JavaScript module and invoke its methods.

var module = await jsRuntime.InvokeAsync<JSObjectReference>("import", "./_content/MyComponents/exampleJsInterop.js");

In the above code, we a retrieving a handle to a JavaScript object, allowing us to pass around a reference to our module. We can then invoke any exported functions.

public async ValueTask<string> Prompt(string message)
{
    return await module.InvokeAsync<string>("showPrompt", message);
}

The idea behind Razor Class Library projects is to encapsulate the JavaScript parts away and provide consumers with a C# interface instead. What if you need to import a module from your Blazor App?

JavaScript Import Extension Method

Razor class libraries can expose C# interfaces, or they could be used to package a set of JavaScript libraries that developers could import when necessary. As you may have noticed from above, the current approach to importing requires some explicit knowledge of location, filenames, and structure.

I’ve been able to encapsulate much of that knowledge and return a dynamic interface. Taking the example that calls InvokeAsync, we can reduce the visible complexity to the following code.

@page "/"
@using Toasty.Core
@inject IJSRuntime JsRuntime;

<h1>Hello, world!</h1>

Welcome to your new app.

<button @onclick="Hi" class="btn btn-primary">Toast!</button>

@code { 
    private async Task Hi()
    {
        dynamic module = 
            await JsRuntime
                .Import<Example>("exampleJsInterop.js");
        // dynamic method invocation
        string value = await module.showPrompt<string>("hello world");
        Console.WriteLine(value);
    }
}

Let’s focus on the implementation of Hi.

    private async Task Hi()
    {
        dynamic module = 
            await JsRuntime
                .Import<Example>("exampleJsInterop.js");
        // dynamic method invocation
        string value = await module.showPrompt<string>("hello world");
        Console.WriteLine(value);
    }

First, we have an extension method for Import. The technique uses the generic type argument to read and determine the path to a file. Let’s see how that works.

public static class JsRuntimeExtensions
{
    public static async Task<JsObjectReferenceDynamic> Import<T>(
        this IJSRuntime jsRuntime,
        string pathFromWwwRoot)
    {
        var libraryName = typeof(T).Assembly.GetName().Name; 
        var module = await jsRuntime.InvokeAsync<JSObjectReference>(
            "import", 
            Path.Combine($"./_content/{libraryName}/", pathFromWwwRoot)
        );
        return new JsObjectReferenceDynamic(module);
    }
}

This method should reduce the chance of JavaScript errors in the event we rename the library. We may have also noticed we return a JsObjectReferenceDynamic type. The class is an implementation of DynamicObject, which allows us to call showPrompt directly in our C# code.

We go from this:

var result = await module.InvokeAsync<string>("showPrompt", message);

To this C# code:

// dynamic invocation
string value = await module.showPrompt<string>("hello world");

In my opinion, this looks pretty awesome, but I understand folks’ aversion to dynamic code. That said, we’re talking about JavaScript here; it’s already dynamic! Blazor is an interop love story, and part of love is accepting something for what it is, not what you want it to be.

How did we accomplish the dynamic interface? With a bit of reflection, of course.

public class JsObjectReferenceDynamic : DynamicObject
{
    public JSObjectReference Module { get; }

    public JsObjectReferenceDynamic(JSObjectReference module)
    {
        Module = module;
    }
    
    public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result)
    {
        var csharpBinder = binder.GetType().GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");
        var typeArgs = 
            csharpBinder!.GetProperty("TypeArguments")?.GetValue(binder, null) as IList<Type> ??
            Array.Empty<Type>();
        
        var jsObjectReferenceType = typeof(JSObjectReference);

        MethodInfo methodInfo;

        if (typeArgs.Any())
        {
            var method = jsObjectReferenceType
                .GetMethods()
                .First(x => x.Name.Contains(nameof(Module.InvokeAsync)));
        
            // only support one generic
            methodInfo = method.MakeGenericMethod(typeArgs.First());
        }
        else
        {
            methodInfo = jsObjectReferenceType
                .GetMethods()
                .First(x => x.Name.Contains(nameof(Module.InvokeVoidAsync)));
        }
        
        var task = methodInfo.Invoke(Module, new object[]{ binder.Name, args });
        result = task;
        return true;
    }
}

Let’s see it in action! We’ve already run the code once in the screenshot, so you’ll see the resulting output of “Type anything here”.

blazor app running with dynamic javascript module

Amazing.

Thinking Out Loud

Some folks immediately might pick this apart, heck, I might be one of those folks. Here are some of the issues I think folks might pick on.

  • use of dynamic can lead to runtime exceptions.
  • reflection is slow and scary
  • Blazor components encapsulate this, so why do it different?

I get it, these are all valid points, and on some level, I can agree, but let’s go through each argument.

The dynamic keyword can seem scary, but again, we’re talking about JavaScript, which is already dynamic. Any misspellings in strings or changing JavaScript interfaces will lead to exceptions regardless of how we invoke the method.

Reflection, while still slower, has gotten improvements in .NET 5. Additionally, we have not optimized the code above, and many of the type, PropertyInfo, and MethodInfo calls could be cached to improve our application’s performance profile.

The argument of encapsulation is the strongest argument against this approach. That said, we can still use this approach within our Razor Class Library components, making it easier to read code, process logic flows, and catch bugs.

Conclusion

The addition of JavaScript module support to Blazor is fantastic. It allows us to lazy-load dependencies when needed and keeps the global namespace clear of unnecessary noise. This post explored how to make the C# experience closer to JavaScript and the pros and cons of the approach I outlined.

For those who want to clone a working sample, you can clone the working project at my GitHub Repository.

If you’re a Blazor user, I’d love to hear your thoughts, negative or positive. Like I mentioned at the start of this post, I can see how it can be polarizing, so any feedback is welcome.