Call .NET methods from JavaScript functions in ASP.NET Core Blazor

By Javier Calvarro Nelson, Daniel Roth, Shashikant Rudrawadi, and Luke Latham

A Blazor app can invoke JavaScript functions from .NET methods and .NET methods from JavaScript functions. These scenarios are called JavaScript interoperability (JS interop).

This article covers invoking .NET methods from JavaScript. For information on how to call JavaScript functions from .NET, see Call JavaScript functions from .NET methods in ASP.NET Core Blazor.

View or download sample code (how to download)

Static .NET method call

To invoke a static .NET method from JavaScript, use the DotNet.invokeMethod or DotNet.invokeMethodAsync functions. Pass in the identifier of the static method you wish to call, the name of the assembly containing the function, and any arguments. The asynchronous version is preferred to support Blazor Server scenarios. The .NET method must be public, static, and have the [JSInvokable] attribute. Calling open generic methods isn't currently supported.

The sample app includes a C# method to return an int array. The [JSInvokable] attribute is applied to the method.

Pages/JsInterop.razor:

<button type="button" class="btn btn-primary"
        onclick="exampleJsFunctions.returnArrayAsyncJs()">
    Trigger .NET static method ReturnArrayAsync
</button>

@code {
    [JSInvokable]
    public static Task<int[]> ReturnArrayAsync()
    {
        return Task.FromResult(new int[] { 1, 2, 3 });
    }
}

JavaScript served to the client invokes the C# .NET method.

wwwroot/exampleJsInterop.js:

window.exampleJsFunctions = {
  showPrompt: function (text) {
    return prompt(text, 'Type your name here');
  },
  displayWelcome: function (welcomeMessage) {
    document.getElementById('welcome').innerText = welcomeMessage;
  },
  returnArrayAsyncJs: function () {
    DotNet.invokeMethodAsync('BlazorWebAssemblySample', 'ReturnArrayAsync')
      .then(data => {
        data.push(4);
          console.log(data);
      });
  },
  sayHello: function (dotnetHelper) {
    return dotnetHelper.invokeMethodAsync('SayHello')
      .then(r => console.log(r));
  }
};

When the Trigger .NET static method ReturnArrayAsync button is selected, examine the console output in the browser's web developer tools.

The console output is:

Array(4) [ 1, 2, 3, 4 ]

The fourth array value is pushed to the array (data.push(4);) returned by ReturnArrayAsync.

By default, the method identifier is the method name, but you can specify a different identifier using the [JSInvokable] attribute constructor:

@code {
    [JSInvokable("DifferentMethodName")]
    public static Task<int[]> ReturnArrayAsync()
    {
        return Task.FromResult(new int[] { 1, 2, 3 });
    }
}

In the client-side JavaScript file:

returnArrayAsyncJs: function () {
  DotNet.invokeMethodAsync('{APP ASSEMBLY}', 'DifferentMethodName')
    .then(data => {
      data.push(4);
      console.log(data);
    });
}

The placeholder {APP ASSEMBLY} is the app's app assembly name (for example, BlazorSample).

Instance method call

You can also call .NET instance methods from JavaScript. To invoke a .NET instance method from JavaScript:

  • Pass the .NET instance by reference to JavaScript:
  • Invoke .NET instance methods on the instance using the invokeMethod or invokeMethodAsync functions. The .NET instance can also be passed as an argument when invoking other .NET methods from JavaScript.

Note

The sample app logs messages to the client-side console. For the following examples demonstrated by the sample app, examine the browser's console output in the browser's developer tools.

When the Trigger .NET instance method HelloHelper.SayHello button is selected, ExampleJsInterop.CallHelloHelperSayHello is called and passes a name, Blazor, to the method.

Pages/JsInterop.razor:

<button type="button" class="btn btn-primary" @onclick="TriggerNetInstanceMethod">
    Trigger .NET instance method HelloHelper.SayHello
</button>

@code {
    public async Task TriggerNetInstanceMethod()
    {
        var exampleJsInterop = new ExampleJsInterop(JS);
        await exampleJsInterop.CallHelloHelperSayHello("Blazor");
    }
}

CallHelloHelperSayHello invokes the JavaScript function sayHello with a new instance of HelloHelper.

JsInteropClasses/ExampleJsInterop.cs:

public class ExampleJsInterop : IDisposable
{
    private readonly IJSRuntime jsRuntime;
    private DotNetObjectReference<HelloHelper> objRef;

    public ExampleJsInterop(IJSRuntime jsRuntime)
    {
        this.jsRuntime = jsRuntime;
    }

    public ValueTask<string> CallHelloHelperSayHello(string name)
    {
        objRef = DotNetObjectReference.Create(new HelloHelper(name));

        return jsRuntime.InvokeAsync<string>(
            "exampleJsFunctions.sayHello",
            objRef);
    }

    public void Dispose()
    {
        objRef?.Dispose();
    }
}

wwwroot/exampleJsInterop.js:

window.exampleJsFunctions = {
  showPrompt: function (text) {
    return prompt(text, 'Type your name here');
  },
  displayWelcome: function (welcomeMessage) {
    document.getElementById('welcome').innerText = welcomeMessage;
  },
  returnArrayAsyncJs: function () {
    DotNet.invokeMethodAsync('BlazorWebAssemblySample', 'ReturnArrayAsync')
      .then(data => {
        data.push(4);
          console.log(data);
      });
  },
  sayHello: function (dotnetHelper) {
    return dotnetHelper.invokeMethodAsync('SayHello')
      .then(r => console.log(r));
  }
};

The name is passed to HelloHelper's constructor, which sets the HelloHelper.Name property. When the JavaScript function sayHello is executed, HelloHelper.SayHello returns the Hello, {Name}! message, which is written to the console by the JavaScript function.

JsInteropClasses/HelloHelper.cs:

public class HelloHelper
{
    public HelloHelper(string name)
    {
        Name = name;
    }

    public string Name { get; set; }

    [JSInvokable]
    public string SayHello() => $"Hello, {Name}!";
}

Console output in the browser's web developer tools:

Hello, Blazor!

To avoid a memory leak and allow garbage collection on a component that creates a DotNetObjectReference, adopt one of the following approaches:

  • Dispose of the object in the class that created the DotNetObjectReference instance:

    public class ExampleJsInterop : IDisposable
    {
        private readonly IJSRuntime js;
        private DotNetObjectReference<HelloHelper> objRef;
    
        public ExampleJsInterop(IJSRuntime js)
        {
            this.js = js;
        }
    
        public ValueTask<string> CallHelloHelperSayHello(string name)
        {
            objRef = DotNetObjectReference.Create(new HelloHelper(name));
    
            return js.InvokeAsync<string>(
                "exampleJsFunctions.sayHello",
                objRef);
        }
    
        public void Dispose()
        {
            objRef?.Dispose();
        }
    }
    

    The preceding pattern shown in the ExampleJsInterop class can also be implemented in a component:

    @page "/JSInteropComponent"
    @using {APP ASSEMBLY}.JsInteropClasses
    @implements IDisposable
    @inject IJSRuntime JS
    
    <h1>JavaScript Interop</h1>
    
    <button type="button" class="btn btn-primary" @onclick="TriggerNetInstanceMethod">
        Trigger .NET instance method HelloHelper.SayHello
    </button>
    
    @code {
        private DotNetObjectReference<HelloHelper> objRef;
    
        public async Task TriggerNetInstanceMethod()
        {
            objRef = DotNetObjectReference.Create(new HelloHelper("Blazor"));
    
            await JS.InvokeAsync<string>(
                "exampleJsFunctions.sayHello",
                objRef);
        }
    
        public void Dispose()
        {
            objRef?.Dispose();
        }
    }
    

    The placeholder {APP ASSEMBLY} is the app's app assembly name (for example, BlazorSample).

  • When the component or class doesn't dispose of the DotNetObjectReference, dispose of the object on the client by calling .dispose():

    window.myFunction = (dotnetHelper) => {
      dotnetHelper.invokeMethodAsync('{APP ASSEMBLY}', 'MyMethod');
      dotnetHelper.dispose();
    }
    

Component instance method call

To invoke a component's .NET methods:

  • Use the invokeMethod or invokeMethodAsync function to make a static method call to the component.
  • The component's static method wraps the call to its instance method as an invoked Action.

Note

For Blazor Server apps, where several users might be concurrently using the same component, use a helper class to invoke instance methods.

For more information, see the Component instance method helper class section.

In the client-side JavaScript:

function updateMessageCallerJS() {
  DotNet.invokeMethodAsync('{APP ASSEMBLY}', 'UpdateMessageCaller');
}

The placeholder {APP ASSEMBLY} is the app's app assembly name (for example, BlazorSample).

Pages/JSInteropComponent.razor:

@page "/JSInteropComponent"

<p>
    Message: @message
</p>

<p>
    <button onclick="updateMessageCallerJS()">Call JS Method</button>
</p>

@code {
    private static Action action;
    private string message = "Select the button.";

    protected override void OnInitialized()
    {
        action = UpdateMessage;
    }

    private void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        StateHasChanged();
    }

    [JSInvokable]
    public static void UpdateMessageCaller()
    {
        action.Invoke();
    }
}

To pass arguments to the instance method:

  • Add parameters to the JS method invocation. In the following example, a name is passed to the method. Additional parameters can be added to the list as needed.

    function updateMessageCallerJS(name) {
      DotNet.invokeMethodAsync('{APP ASSEMBLY}', 'UpdateMessageCaller', name);
    }
    

    The placeholder {APP ASSEMBLY} is the app's app assembly name (for example, BlazorSample).

  • Provide the correct types to the Action for the parameters. Provide the parameter list to the C# methods. Invoke the Action (UpdateMessage) with the parameters (action.Invoke(name)).

    Pages/JSInteropComponent.razor:

    @page "/JSInteropComponent"
    
    <p>
        Message: @message
    </p>
    
    <p>
        <button onclick="updateMessageCallerJS('Sarah Jane')">
            Call JS Method
        </button>
    </p>
    
    @code {
        private static Action<string> action;
        private string message = "Select the button.";
    
        protected override void OnInitialized()
        {
            action = UpdateMessage;
        }
    
        private void UpdateMessage(string name)
        {
            message = $"{name}, UpdateMessage Called!";
            StateHasChanged();
        }
    
        [JSInvokable]
        public static void UpdateMessageCaller(string name)
        {
            action.Invoke(name);
        }
    }
    

    Output message when the Call JS Method button is selected:

    Sarah Jane, UpdateMessage Called!
    

Component instance method helper class

The helper class is used to invoke an instance method as an Action. Helper classes are useful when:

  • Several components of the same type are rendered on the same page.
  • A Blazor Server app is used, where multiple users might be using a component concurrently.

In the following example:

  • The JSInteropExample component contains several ListItem components.
  • Each ListItem component is composed of a message and a button.
  • When a ListItem component button is selected, that ListItem's UpdateMessage method changes the list item text and hides the button.

MessageUpdateInvokeHelper.cs:

using System;
using Microsoft.JSInterop;

public class MessageUpdateInvokeHelper
{
    private Action action;

    public MessageUpdateInvokeHelper(Action action)
    {
        this.action = action;
    }

    [JSInvokable("{APP ASSEMBLY}")]
    public void UpdateMessageCaller()
    {
        action.Invoke();
    }
}

The placeholder {APP ASSEMBLY} is the app's app assembly name (for example, BlazorSample).

In the client-side JavaScript:

window.updateMessageCallerJS = (dotnetHelper) => {
    dotnetHelper.invokeMethodAsync('{APP ASSEMBLY}', 'UpdateMessageCaller');
    dotnetHelper.dispose();
}

The placeholder {APP ASSEMBLY} is the app's app assembly name (for example, BlazorSample).

Shared/ListItem.razor:

@inject IJSRuntime JS

<li>
    @message
    <button @onclick="InteropCall" style="display:@display">InteropCall</button>
</li>

@code {
    private string message = "Select one of these list item buttons.";
    private string display = "inline-block";
    private MessageUpdateInvokeHelper messageUpdateInvokeHelper;

    protected override void OnInitialized()
    {
        messageUpdateInvokeHelper = new MessageUpdateInvokeHelper(UpdateMessage);
    }

    protected async Task InteropCall()
    {
        await JS.InvokeVoidAsync("updateMessageCallerJS",
            DotNetObjectReference.Create(messageUpdateInvokeHelper));
    }

    private void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }
}

Pages/JSInteropExample.razor:

@page "/JSInteropExample"

<h1>List of components</h1>

<ul>
    <ListItem />
    <ListItem />
    <ListItem />
    <ListItem />
</ul>

Share interop code in a class library

JS interop code can be included in a class library, which allows you to share the code in a NuGet package.

The class library handles embedding JavaScript resources in the built assembly. The JavaScript files are placed in the wwwroot folder. The tooling takes care of embedding the resources when the library is built.

The built NuGet package is referenced in the app's project file the same way that any NuGet package is referenced. After the package is restored, app code can call into JavaScript as if it were C#.

For more information, see ASP.NET Core Razor components class libraries.

Avoid circular object references

Objects that contain circular references can't be serialized on the client for either:

  • .NET method calls.
  • JavaScript method calls from C# when the return type has circular references.

For more information, see the following issues:

Size limits on JS interop calls

In Blazor WebAssembly, the framework doesn't impose a limit on the size of JS interop inputs and outputs.

In Blazor Server, JS interop calls are limited in size by the maximum incoming SignalR message size permitted for hub methods, which is enforced by HubOptions.MaximumReceiveMessageSize (default: 32 KB). JS to .NET SignalR messages larger than MaximumReceiveMessageSize throw an error. The framework doesn't impose a limit on the size of a SignalR message from the hub to a client.

When SignalR logging isn't set to Debug or Trace, a message size error only appears in the browser's developer tools console:

Error: Connection disconnected with error 'Error: Server returned an error on close: Connection closed with an error.'.

When SignalR server-side logging is set to Debug or Trace, server-side logging surfaces an InvalidDataException for a message size error.

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

System.IO.InvalidDataException: The maximum message size of 32768B was exceeded. The message size can be configured in AddHubOptions.

Increase the limit by setting MaximumReceiveMessageSize in Startup.ConfigureServices. The following example sets the maximum receive message size to 64 KB (64 * 1024):

services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

Increasing the SignalR incoming message size limit comes at the cost of requiring more server resources, and it exposes the server to increased risks from a malicious user. Additionally, reading a large amount of content in to memory as strings or byte arrays can also result in allocations that work poorly with the garbage collector, resulting in additional performance penalties.

One option for reading large payloads is to send the content in smaller chunks and process the payload as a Stream. This can be used when reading large JSON payloads or if data is available in JavaScript as raw bytes. For an example that demonstrates sending large binary payloads in Blazor Server that uses techniques similar to the InputFile component, see the Binary Submit sample app.

Consider the following guidance when developing code that transfers a large amount of data between JavaScript and Blazor:

  • Slice the data into smaller pieces, and send the data segments sequentially until all of the data is received by the server.
  • Don't allocate large objects in JavaScript and C# code.
  • Don't block the main UI thread for long periods when sending or receiving data.
  • Free any memory consumed when the process is completed or cancelled.
  • Enforce the following additional requirements for security purposes:
    • Declare the maximum file or data size that can be passed.
    • Declare the minimum upload rate from the client to the server.
  • After the data is received by the server, the data can be:
    • Temporarily stored in a memory buffer until all of the segments are collected.
    • Consumed immediately. For example, the data can be stored immediately in a database or written to disk as each segment is received.

JS modules

For JS isolation, JS interop works with the browser's default support for EcmaScript modules (ESM) (ECMAScript specification).

Additional resources