How to call async from sync method in blazor and wait for result?

Viewed 2224

In Blazor webassembly I have javascript function in js file:

function AksMessage(message) {
    return confirm(message);
}

In razor file:

[Inject]
public IJSRuntime JSRuntime { get; set; }
    
public async Task<bool> askMessage(msg)
{
    await JSRuntime.InvokeVoidAsync("AskMessage", msg);
}

Now in some not async function I want to call askMessage and get result if user clicked and returned false or true. How can I run it and wait for result from synchronous part of code? If I do:

var askmsg = Task.Run(async () => await askMessage("question"));

and askmsg.Result I have exception that monitors can not wait on this runtime.

2 Answers

I just solved this for setting bearer tokens on a gRPC interceptor which required some async code in WASM, you do it like this:

public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
    TRequest request,
    ClientInterceptorContext<TRequest, TResponse> context,
    AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
    var call = continuation(request, context);

    return new AsyncUnaryCall<TResponse>(
        call.ResponseAsync,
        GetMetadata(),
        call.GetStatus,
        call.GetTrailers,
        call.Dispose);
}

private async Task<Metadata> GetMetadata()
{
    try
    {
        if (!string.IsNullOrEmpty(await _authenticationManager.TryRefreshToken()))
            _snackbar.Add("Refreshed Token.", Severity.Success);
        var token = _authenticationManager.Token;
        var metadata = new Metadata();
        if (!string.IsNullOrEmpty(token))
        {
            metadata.Add("Authorization", $"Bearer {token}");
            var userIdentity = (await _authenticationStateProvider.GetAuthenticationStateAsync()).User.Identity;
            if (userIdentity!.IsAuthenticated)
                metadata.Add("User", userIdentity.Name!);
        }
        else
        {
            _authenticationManager.Logout().GetAwaiter().GetResult();
            _navigationManager.NavigateTo("/");
        }

        return metadata;
    }
    catch (Exception ex)
    {
        throw new InvalidOperationException("Failed to add token to request metadata", ex);
    }
}

EDIT: For some reason, although this async code runs, it doesn't actually set the headers onto the request so you have to do the following hack:

public class AuthenticationInterceptor : Interceptor
{
    private readonly IAuthenticationManager _authenticationManager;
    private readonly ISnackbar _snackbar;
    private readonly NavigationManager _navigationManager;
    private readonly AuthenticationStateProvider _authenticationStateProvider;
    private Metadata? _metadata;

    public AuthenticationInterceptor(
        IAuthenticationManager authenticationManager,
        ISnackbar snackbar,
        NavigationManager navigationManager,
        AuthenticationStateProvider authenticationStateProvider)
    {
        _authenticationManager = authenticationManager;
        _snackbar = snackbar;
        _navigationManager = navigationManager;
        _authenticationStateProvider = authenticationStateProvider;
    }

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        _ = new AsyncUnaryCall<TResponse>(
            null!,
            GetMetadata(context),
            null!,
            null!,
            null!); //  Doesn't actually send the request but runs the Task
                    // that provides the metadata to the field which can
                    // then be used to set the request headers
        
        var newOptions = context.Options.WithHeaders(_metadata!);

        var newContext = new ClientInterceptorContext<TRequest, TResponse>(
            context.Method,
            context.Host,
            newOptions);
 
        return base.AsyncUnaryCall(request, newContext, continuation);
    }

    private async Task<Metadata> GetMetadata<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context)
        where TRequest : class
        where TResponse : class
    {
        try
        {
            if (!string.IsNullOrEmpty(await _authenticationManager.TryRefreshToken()))
                _snackbar.Add("Refreshed Token.", Severity.Success);
            var token = _authenticationManager.Token;
            Console.WriteLine($"Token: {token}");
            var headers = new Metadata();
            if (!string.IsNullOrEmpty(token))
            {
                headers.Add(new Metadata.Entry("Authorization", $"Bearer {token}"));
                Console.WriteLine("Set metadata");
                var userIdentity = (await _authenticationStateProvider.GetAuthenticationStateAsync()).User.Identity;
                if (userIdentity!.IsAuthenticated)
                    headers.Add(new Metadata.Entry("User", userIdentity.Name!));
            }
            else
            {
                await _authenticationManager.Logout();
                _navigationManager.NavigateTo("/");
            }
            
            var callOptions = context.Options.WithHeaders(headers);
                
            return _metadata = callOptions.Headers!;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("Failed to add token to request headers", ex);
        }
    }
}

You can call async method from synchronous method and wait for it like this :

var askmsg = Task.Run(async () => await askMessage("question"));
var result = Task.WaitAndUnwrapException();

another solution is like this (sync method backs to its context):

var result = AsyncContext.RunTask(askMessage("question")).Result;
Related