CancellationToken in Blazor Pages?
Asked Answered
V

3

19

After living under a rock for 2 years employment wise, I am now confronted with Blazor at my new Workplace and have a lot of catch up to do after doing mostly ASP.NET Framework MVC prior to the 2 Years.

Trying myself on Blazor server side, I tried to apply my past knowledge which included cancellationtokens for async operations and i couldn't find much information about them in combination with Blazor.

Are they still a Best Practice or did they became Obsolete at some point? I did found this previously asked question which recommends creating a tokensource on the OnInitializedAsync() method and cancelling it on Dispose() which i honestly find a bit crude. (I would need to implement this for each page and you know... DRY)

I also found this Article about advanced Scenarios on Microsoft Docs that explains how to implement a Circuit Handler, which honestly is a bit beyond me right now and most likely way out of scope for my little home-project.

In comparison, in asp.net Framework MVC i would build a Controller like this:

namespace SampleWebsite.Controllers
{
    public class SampleController : ApiController
    {
        private readonly MyEntities _entities = new MyEntities();

        public async Task<IHttpActionResult> MyAsyncApi(CancellationToken cancellationToken)
        {
            var result = _entities.MyModel.FirstOrDefault(e => e.Id == 1, cancellationToken: cancellationToken);
            return OK(result);
        }
    }
}

The CancellationToken will be injected by asp.net Framework / Core and is directly linked to the current context connection-pipe. Hence, if the user closes the connection, the token becomes invalid.

I would have assumed that for asp.net core and blazor where dependency-injections is a big part of it, this would be the case here too, but i could not find any documentation about this here.

So, should cancellationtokens still be used at this point or does Microsoft do some magic in the background for asynchronous tasks? And if yes, what would be the best implementation?

EDIT: Here would be my Setup to clarify:

The Blazor-Component:

@page "/Index"
@inject IIndexService Service

@* Some fancy UI stuff *@

@code {
    private IEnumerable<FancyUiValue> _uiValues;

    protected override async Task OnInitializedAsync()
    {
        _uiValues = await Service.FetchCostlyValues();
    }
}

And the Injected Service-Class that does the heavy lifting:

public interface IIndexService
{
    Task<IEnumerable<FancyUiValue>> FetchCostlyValues();
}

public class IndexService : IIndexService
{
    public async Task<IEnumerable<FancyUiValue>> FetchCostlyValues()
    {
        var uiValues = await heavyTask.ToListAsync(); // <-- Best way to get a cancellationtoken here?
        return uiValues;
    }
}

My question is, what would be the best way to get a token in the specificed part of the code or would it be irrelevant because the Server would kill all running tasks when the connection (as example) ends?

Vanna answered 21/6, 2020 at 14:31 Comment(5)
Your edit helps but you still have no Exception handling. What do you want to do on an Error? Or on a TimeOut?Payson
@HenkHolterman: The question was about best practice for cancellation of Async Tasks in Blazor, not Exceptionhandling. I am not overly familiar with the two-way binary communication of Blazor, my tought-process revolves about "what if the User starts a costly async task (say: generating an PDF-report) and then navigates to another page?". From my MVC Knowledge, the async task will keep running and eat ressources, then returned and either ignored by the Controller on the parent-thread or send to and rejected by the Client.Vanna
Yes, for such a job (PDF) I would use a CancelSrc. And Dispose(). But normally you would use a service endpoint.Payson
Did you ever find your own answer to this question? This question could have been written by myself right now :)Karrikarrie
@Karrikarrie Yes... well sorta. See my posted Answer below :)Vanna
V
17

After 2 years of experience with Blazor, i figured that the only reliable way to pass an CancellationToken to a Task within an Object of a longer Lifetime (e.g. Singleton or Scoped Service) is the combination of IDisposeable and CancellationTokenSource

@page "/"
@implements IDisposable

*@ Razor Stuff *@

@code
{
    private CancellationTokenSource _cts = new();

    protected override async Task OnInitializedAsync()
    {
        await BusinessLogicSingleton.DoExpensiveTask(_cts.Token);
    }

    #region IDisposable

    public void Dispose()
    {
        _cts.Cancel();
        _cts.Dispose();
    }

    #endregion
}

On repeated use or just to comply to the DRY-Rule, you can also inherit from the ComponentBase Class and then use that Class for your Components that require to pass a CancellationToken:

public class CancellableComponent : ComponentBase, IDisposable
    {
        internal CancellationTokenSource _cts = new();

        public void Dispose()
        {
            _cts.Cancel();
            _cts.Dispose();
        }
    }
@page "/"
@inherits CancellableComponent

@* Rest of the Component *@

I also found that while you could Inject the IHttpContextAccessor and use the HttpContext.RequestAborted token which is the same that will be generated and injected in your ASP.Net MVC Method Calls, as of the current .Net6 Version it will never fire even after the Connection to the Client is severed and the providing HttpContext is disposed.

This may be a case for the Developer-Team on Github as i do see UseCases for it where the User is allowed to exit the Component while the Task keeps on going until the User leaves the Website completely.
(For such cases, my recommended Workaround would be to write your own CircuitHandler that will give you Events for when a Circuit is removed.)

Vanna answered 8/7, 2022 at 18:19 Comment(1)
Problem is that this will throw a ObjectDisposedException in some cases because Dispose() might be called before a task has finished.Sidewinder
S
10

Instead of adding a CancellationTokenSource to all components manually, you can create a base component that expose a CancellationToken and use this base component automatically in all components of the project

Implement your ApplicationComponentBase

public abstract class ApplicationComponentBase: ComponentBase, IDisposable
{
    private CancellationTokenSource? cancellationTokenSource;

    protected CancellationToken CancellationToken => (cancellationTokenSource ??= new()).Token;

    public virtual void Dispose()
    {
        if (cancellationTokenSource != null)
        {
            cancellationTokenSource.Cancel();
            cancellationTokenSource.Dispose();
            cancellationTokenSource = null;
        }
    }
}

Then add @inherits ApplicationComponentBase to _Imports.razor file

In page a call:

await Task.Delay(50000, CancellationToken);

Then try to navigate to another page, the task you called will be cancelled

Suribachi answered 2/11, 2022 at 7:22 Comment(0)
E
3

I'm pretty new to Blazor myself, but this was one of the first questions that occurred to me and I think I have a better solution.

The IServiceProvider automagically knows how to provide the IHostApplicationLifetime. This exposes a CancellationToken representing graceful server shutdown. Inject the application lifetime into your component base and use CreateLinkedTokenSource to create a CTS that you can cancel when the component is disposed:

    public abstract class AsyncComponentBase : ComponentBase, IDisposable
    {
        [Inject]
        private IHostApplicationLifetime? Lifetime { get; set; }
        private CancellationTokenSource? _cts;
        protected CancellationToken Cancel => _cts?.Token ?? CancellationToken.None;

        protected override void OnInitialized()
        {
            _cts = Lifetime != null ? CancellationTokenSource.CreateLinkedTokenSource(Lifetime.ApplicationStopping) : new();     
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
            if(_cts != null)
            {
                _cts.Cancel();
                _cts.Dispose();
                _cts = null;
            }
        }
    }
Erasmo answered 31/3 at 1:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.