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

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

Invoke a static .NET method

To invoke a static .NET method from JavaScript (JS), use the JS functions DotNet.invokeMethod or DotNet.invokeMethodAsync. Pass in the name of the assembly containing the method, the identifier of the static .NET method, and any arguments.

In the following example:

  • The {ASSEMBLY NAME} placeholder is the app's assembly name.
  • The {.NET METHOD ID} placeholder is the .NET method identifier.
  • The {ARGUMENTS} placeholder are optional, comma-separated arguments to pass to the method, each of which must be JSON-serializable.
DotNet.invokeMethodAsync('{ASSEMBLY NAME}', '{.NET METHOD ID}', {ARGUMENTS});

DotNet.invokeMethod returns the result of the operation. DotNet.invokeMethodAsync returns a JS Promise representing the result of the operation.

The asynchronous function (invokeMethodAsync) is preferred over the synchronous version (invokeMethod) to support Blazor Server scenarios.

The .NET method must be public, static, and have the [JSInvokable] attribute.

In the following example:

  • The {<T>} placeholder indicates the return type, which is only required for methods that return a value.
  • The {.NET METHOD ID} placeholder is the method identifier.
@code {
    [JSInvokable]
    public static Task{<T>} {.NET METHOD ID}()
    {
        ...
    }
}

Calling open generic methods isn't supported with static .NET methods but is supported with instance methods, which are described later in this article.

In the following CallDotNetExample1 component, the ReturnArrayAsync C# method returns an int array. The [JSInvokable] attribute is applied to the method, which makes the method invokable by JS.

Pages/CallDotNetExample1.razor:

@page "/call-dotnet-example-1"

<h1>Call .NET Example 1</h1>

<p>
    <button onclick="returnArrayAsync()">
        Trigger .NET static method
    </button>
</p>

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

The <button> element's onclick HTML attribute is JavaScript's onclick event handler assignment for processing click events, not Blazor's @onclick directive attribute. The returnArrayAsync JS function is assigned as the handler.

The following returnArrayAsync JS function, calls the ReturnArrayAsync .NET method of the preceding CallDotNetExample1 component and logs the result to the browser's web developer tools console. BlazorSample is the app's assembly name.

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

<script>
  window.returnArrayAsync = () => {
    DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync')
      .then(data => {
        console.log(data);
      });
    };
</script>

When the Trigger .NET static method button is selected, the browser's developer tools console output displays the array data. The format of the output differs slightly among browsers. The following output shows the format used by Microsoft Edge:

Array(3) [ 1, 2, 3 ]

By default, the .NET method identifier for the JS call is the .NET method name, but you can specify a different identifier using the [JSInvokable] attribute constructor. In the following example, DifferentMethodName is the assigned method identifier for the ReturnArrayAsync method:

[JSInvokable("DifferentMethodName")]

In the call to DotNet.invokeMethod or DotNet.invokeMethodAsync, call DifferentMethodName to execute the ReturnArrayAsync .NET method:

  • DotNet.invokeMethod('BlazorSample', 'DifferentMethodName');
  • DotNet.invokeMethodAsync('BlazorSample', 'DifferentMethodName');

Note

The ReturnArrayAsync method example in this section returns the result of a Task without the use of explicit C# async and await keywords. Coding methods with async and await is typical of methods that use the await keyword to return the value of asynchronous operations.

ReturnArrayAsync method composed with async and await keywords:

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

For more information, see Asynchronous programming with async and await in the C# guide.

Invoke an instance .NET method

To invoke an instance .NET method from JavaScript (JS):

  • Pass the .NET instance by reference to JS by wrapping the instance in a DotNetObjectReference and calling Create on it.
  • Invoke a .NET instance method from JS using invokeMethod or invokeMethodAsync from the passed DotNetObjectReference. The .NET instance can also be passed as an argument when invoking other .NET methods from JS.
  • Dispose of the DotNetObjectReference.

Component instance examples

The following sayHello1 JS function receives a DotNetObjectReference and calls invokeMethodAsync to call the GetHelloMethod .NET method of a component.

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

<script>
  window.sayHello1 = (dotNetHelper) => {
    return dotNetHelper.invokeMethodAsync('GetHelloMessage');
  };
</script>

For the following CallDotNetExample2 component:

  • The component has a JS-invokable .NET method named GetHelloMessage.
  • When the Trigger .NET instance method button is selected, the JS function sayHello1 is called with the DotNetObjectReference.
  • sayHello1:
    • Calls GetHelloMessage and receives the message result.
    • Returns the message result to the calling TriggerDotNetInstanceMethod method.
  • The returned message from sayHello1 in result is displayed to the user.
  • To avoid a memory leak and allow garbage collection, the .NET object reference created by DotNetObjectReference is disposed in the Dispose method.

Pages/CallDotNetExample2.razor:

@page "/call-dotnet-example-2"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 2</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;
    private DotNetObjectReference<CallDotNetExample2> objRef;

    public async Task TriggerDotNetInstanceMethod()
    {
        objRef = DotNetObjectReference.Create(this);
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }

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

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

To pass arguments to an instance method:

  1. Add parameters to the .NET method invocation. In the following example, a name is passed to the method. Add additional parameters to the list as needed.

    <script>
      window.sayHello2 = (dotNetHelper, name) => {
        return dotNetHelper.invokeMethodAsync('GetHelloMessage', name);
      };
    </script>
    
  2. Provide the parameter list to the .NET method.

    Pages/CallDotNetExample3.razor:

    @page "/call-dotnet-example-3"
    @implements IDisposable
    @inject IJSRuntime JS
    
    <h1>Call .NET Example 3</h1>
    
    <p>
        <label>
            Name: <input @bind="name" />
        </label>
    </p>
    
    <p>
        <button @onclick="TriggerDotNetInstanceMethod">
            Trigger .NET instance method
        </button>
    </p>
    
    <p>
        @result
    </p>
    
    @code {
        private string name;
        private string result;
        private DotNetObjectReference<CallDotNetExample3> objRef;
    
        public async Task TriggerDotNetInstanceMethod()
        {
            objRef = DotNetObjectReference.Create(this);
            result = await JS.InvokeAsync<string>("sayHello2", objRef, name);
        }
    
        [JSInvokable]
        public string GetHelloMessage(string passedName) => $"Hello, {passedName}!";
    
        public void Dispose()
        {
            objRef?.Dispose();
        }
    }
    

Class instance examples

The following sayHello1 JS function:

  • Calls the GetHelloMessage .NET method on the passed DotNetObjectReference.
  • Returns the message from GetHelloMessage to the sayHello1 caller.

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

<script>
  window.sayHello1 = (dotNetHelper) => {
    return dotNetHelper.invokeMethodAsync('GetHelloMessage');
  };
</script>

The following HelloHelper class has a JS-invokable .NET method named GetHelloMessage. When HelloHelper is created, the name in the Name property is used to return a message from GetHelloMessage.

HelloHelper.cs:

using Microsoft.JSInterop;

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

    public string Name { get; set; }

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

The CallHelloHelperGetHelloMessage method in the following JsInteropClasses3 class invokes the JS function sayHello1 with a new instance of HelloHelper.

JsInteropClasses3.cs:

using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses3 : IDisposable
{
    private readonly IJSRuntime js;
    private DotNetObjectReference<HelloHelper> objRef;

    public JsInteropClasses3(IJSRuntime js)
    {
        this.js = js;
    }

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

        return js.InvokeAsync<string>("sayHello1", objRef);
    }

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

To avoid a memory leak and allow garbage collection, the .NET object reference created by DotNetObjectReference is disposed in the Dispose method.

When the Trigger .NET instance method button is selected in the following CallDotNetExample4 component, JsInteropClasses3.CallHelloHelperGetHelloMessage is called with the value of name.

Pages/CallDotNetExample4.razor:

@page "/call-dotnet-example-4"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 4</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;
    private JsInteropClasses3 jsInteropClasses;

    private async Task TriggerDotNetInstanceMethod()
    {
        jsInteropClasses = new JsInteropClasses3(JS);
        result = await jsInteropClasses.CallHelloHelperGetHelloMessage(name);
    }

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

The following image shows the rendered component with the name Amy Pond in the Name field. After the button is selected, Hello, Amy Pond! is displayed in the UI:

Rendered 'CallDotNetExample4' component example

The preceding pattern shown in the JsInteropClasses3 class can also be implemented entirely in a component.

Pages/CallDotNetExample5.razor:

@page "/call-dotnet-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 5</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;
    private DotNetObjectReference<HelloHelper> objRef;

    public async Task TriggerDotNetInstanceMethod()
    {
        objRef = DotNetObjectReference.Create(new HelloHelper(name));
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }

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

To avoid a memory leak and allow garbage collection, the .NET object reference created by DotNetObjectReference is disposed in the Dispose method.

The output displayed by the CallDotNetExample5 component is Hello, Amy Pond! when the name Amy Pond is provided in the Name field.

In the preceding CallDotNetExample5 component, the .NET object reference is disposed. If a class or component doesn't dispose the DotNetObjectReference, dispose it from the client by calling dispose on the passed DotNetObjectReference:

window.jsFunction = (dotnetHelper) => {
  dotnetHelper.invokeMethodAsync('{ASSEMBLY NAME}', '{.NET METHOD ID}');
  dotnetHelper.dispose();
}

In the preceding example:

  • The {ASSEMBLY NAME} placeholder is the app's assembly name.
  • The {.NET METHOD ID} placeholder is the .NET method identifier.

Component instance .NET method helper class

A helper class can invoke a .NET instance method as an Action. Helper classes are useful in the following scenarios:

  • When several components of the same type are rendered on the same page.
  • In a Blazor Server apps, where multiple users concurrently use the same component.

In the following example:

  • The CallDotNetExample6 component contains several ListItem components, which is a shared component in the app's Shared folder.
  • 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.

The following MessageUpdateInvokeHelper class maintains a JS-invokable .NET method, UpdateMessageCaller, to invoke the Action specified when the class is instantiated. BlazorSample is the app's assembly name.

MessageUpdateInvokeHelper.cs:

using System;
using Microsoft.JSInterop;

public class MessageUpdateInvokeHelper
{
    private Action action;

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

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

The following updateMessageCaller JS function invokes the UpdateMessageCaller .NET method. BlazorSample is the app's assembly name.

Inside the closing </body> tag of wwwroot/index.html (Blazor WebAssembly) or Pages/_Host.cshtml (Blazor Server):

<script>
  window.updateMessageCaller = (dotnetHelper) => {
    dotnetHelper.invokeMethodAsync('BlazorSample', 'UpdateMessageCaller');
    dotnetHelper.dispose();
  }
</script>

The following ListItem component is a shared component that can be used any number of times in a parent component and creates list items (<li>...</li>) for an HTML list (<ul>...</ul> or <ol>...</ol>). Each ListItem component instance establishes an instance of MessageUpdateInvokeHelper with an Action set to its UpdateMessage method.

When a ListItem component's InteropCall button is selected, updateMessageCaller is invoked with a created DotNetObjectReference for the MessageUpdateInvokeHelper instance. This permits the framework to call UpdateMessageCaller on that ListItem's MessageUpdateInvokeHelper instance. The passed DotNetObjectReference is disposed in JS (dotnetHelper.dispose()).

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("updateMessageCaller",
            DotNetObjectReference.Create(messageUpdateInvokeHelper));
    }

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

StateHasChanged is called to update the UI when message is set in UpdateMessage. If StateHasChanged isn't called, Blazor has no way of knowing that the UI should be updated when the Action is invoked.

The following CallDotNetExample6 parent component includes four list items, each an instance of the ListItem component.

Pages/CallDotNetExample6.razor:

@page "/call-dotnet-example-6"

<h1>Call .NET Example 6</h1>

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

The following image shows the rendered CallDotNetExample6 parent component after the second InteropCall button is selected:

  • The second ListItem component has displayed the UpdateMessage Called! message.
  • The InteropCall button for the second ListItem component isn't visible because the button's CSS display property is set to none.

Rendered 'CallDotNetExample6' component example

Location of JavaScipt

Load JavaScript (JS) code using any of approaches described by the JS interop overview article:

For information on isolating scripts in JS modules, see the JavaScript isolation in JavaScript modules section.

Warning

Don't place a <script> tag in a component file (.razor) because the <script> tag can't be updated dynamically.

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.

Size limits on JavaScript interop calls

This section only applies to Blazor Server apps. In Blazor WebAssembly, the framework doesn't impose a limit on the size of JavaScript (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"
    }
  }
}

Error:

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 JS 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.

Note

Documentation links to the ASP.NET Core reference source load the repository's main branch, which represents the product unit's current development for the next release of ASP.NET Core. To select the branch for a different release, use the Switch branches or tags dropdown list to select the branch. For example, select the release/5.0 branch for the ASP.NET Core 5.0 release.

Consider the following guidance when developing code that transfers a large amount of data between JS and Blazor in Blazor Server apps:

  • 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 JS and C# code.
  • Don't block the main UI thread for long periods when sending or receiving data.
  • Free consumed memory 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.

JavaScript isolation in JavaScript modules

Blazor enables JavaScript (JS) isolation in standard JavaScript modules (ECMAScript specification).

JS isolation provides the following benefits:

  • Imported JS no longer pollutes the global namespace.
  • Consumers of a library and components aren't required to import the related JS.

For more information, see Call JavaScript functions from .NET methods in ASP.NET Core Blazor.

Additional resources