In this blog I'm going to briefly explore theChannel
object in dotnet. I'll give a short introduction to what a Channel is, what you use it for and I'll show a small example how you might use the Channel object.
The what?
Microsoft describes channels as: "An implementation of the producer/consumer conceptual programming model". What it means is that this device stores work items as an ordered to-do list. A "worker" is then able to claim a work item and process it.
Why?
For dotnet applications, this device gives you an easy way to funnel work to a background worker while letting your request finish, even before the work is actually done. If, for example, you want to track every page view in your application, then you need to send a signal to your database on every request.According to microsoft, a well designed app could handle thousands of concurrent requests, but should your database also handle that much traffic? By handling work such as this in the background, you get two benefits:
- Your pages load faster
- The pressure on your external resources is reduced and more consistent
How?
I'm going to showcase the page view example from the previous chapter using some code snippets to illustrate the usage of theChannel
object. We're going to need 3 things:
- A communication model
- A queue object
- A consumer
Communication model
In order to transfer the information from the request thread to the background worker, we define a model:
publicrecordPageViewCommand(intPageId,DateTimeViewDateTime);
Queue
The queue is where we will use the channel. This object is a wrapper around the channel that will help us control the channel configuration and makes it easier for us to obtain the queue through dependency injection:
publicinterfaceIPageViewQueue{ValueTask<PageViewCommand>ReadAsync(CancellationTokencancellationToken=default);ValueTaskWriteAsync(PageViewCommanditem);}internalsealedclassPageViewQueue:IPageViewQueue{privatereadonlyChannel<PageViewCommand>_queue;publicClientErrorProcessorQueue(){// A bounded channel creates a channel with a maximum amount of items.// I use this option so that the amount of memory usage by the channel stays within reasonable limits.// I set the maximum amount of items to 10.000.varoptions=newBoundedChannelOptions(10000){// I configure the channel to discard any new items as long as the queue is full// I do this, because I find it acceptable to lose a few page views in case the application is extremely busy.// This ensures that I don't accidentally fill up my queue to the point that I run out of memoryFullMode=BoundedChannelFullMode.DropWrite};_queue=Channel.CreateBounded<PageViewCommand>(options);}publicValueTaskWriteAsync(PageViewCommanditem){ArgumentNullException.ThrowIfNull(item);return_queue.Writer.WriteAsync(item);}publicasyncValueTask<PageViewCommand>ReadAsync(CancellationTokencancellationToken=default){varresult=await_queue.Reader.ReadAsync(cancellationToken);returnresult;}}
Remember to register the queue in your dependency injection container:
services.AddSingleton<IPageViewQueue,PageViewQueue>();
Consumer
The consumer is the background worker that will read commands from the queue and execute them:
// 👇 BackgroundService is an implementation of `IHostedService` that starts a service on startup and cancels execution upon shutdowninternalsealedpartialclassPageViewWorker(IPageViewRepositorypageViewRepository,IPageViewQueuequeue,ILogger<PageViewWorker>logger):BackgroundService{protectedoverrideasyncTaskExecuteAsync(CancellationTokenstoppingToken){while(!stoppingToken.IsCancellationRequested){// You always want to wrap the whole operation inside a try/catch statement to ensure that the worker keeps running when something goes wrong.try{varitem=awaitqueue.ReadAsync(stoppingToken);awaitpageViewRepository.Insert(item.PageId,item.ViewDateTime);}catch(OperationCanceledExceptione){// No need to do anything here. The process is stopping, the exception signals cancellation}catch(Exceptione){LogBackgroundProcessingFailure(logger,e);}}}[LoggerMessage(LogLevel.Error,"Failed to process an item in the queue")]privatestaticpartialvoidLogBackgroundProcessingFailure(ILoggerlogger,Exceptione);}
Remember to register the worker in your dependency injection container:
services.AddHostedService<PageViewWorker>();
Producing work
All that is left is to throw work on the queue for processing. You may for example create a middleware:
publicclassPageViewMiddleware(RequestDelegatenext,IPageViewQueuequeue){// 👇 ICmsContext here is a made-up interface that resembles a device that returns a content page model corresponding to the current request from your preferred CMS.publicasyncTaskInvokeAsync(HttpContextcontext,ICmsContextcmsContext){varcurrentContent=cmsContext.GetCurrentContent();awaitqueue.WriteAsync(newPageViewCommand(currentContent.Id,DateTime.Now));awaitnext(context);}}
Alternatively, if you use a CDN for example, you may choose to create an API controller:
[ApiController][Route("api/[controller]/[action]")]publicclassPageViewController(IPageViewQueuequeue):ApiController{[HttpGet]publicasyncTask<IActionResult>Register(RegisterPageViewRequestrequest){awaitqueue.WriteAsync(newPageViewCommand(request.PageId,DateTime.Now));returnAccepted();}}
That is all you need to get started with channels. This blog is merely an introduction and a means to make you aware that this exists. If you want to read more in-depth information about this, I recommend checking outmicrosoft's documentation about channels. There is still a lot more to explore around channels.
If you're interested to see a practical example, I used this device inmy UrlTracker package for Umbraco CMS and it delegates client error tracking to a background worker to reduce database traffic during requests for better performance.
I hope you learned something new today and maybe I'll see you in my next blog! 😊
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse