Using an IHttpAsyncHandler to call a WebService Asynchronously - c#

Here's the basic setup. We have an ASP.Net WebForms application with a page that has a Flash application that needs to access an external Web Service. Due to (security I presume) limitations in Flash (don't ask me, I'm not a Flash expert at all), we can't connect to the Web Service directly from Flash. The work around is to create a proxy in ASP.Net that the Flash application will call, which will in turn call the WebService and forward the results back to the Flash application.
The WebSite has very high traffic though, and the issue is, if the Web Service hangs at all, then the ASP.Net request threads will start backing up which could lead to serious thread starvation. In order to get around that, I've decided to use an IHttpAsyncHandler which was designed for this exact purpose. In it, I'll use a WebClient to asynchronously call the Web Service and the forward the response back. There are very few samples on the net on how to correctly use the IHttpAsyncHandler, so I just want to make sure I'm not doing it wrong. I'm basing my useage on the example show here: http://msdn.microsoft.com/en-us/library/ms227433.aspx
Here's my code:
internal class AsynchOperation : IAsyncResult
{
private bool _completed;
private Object _state;
private AsyncCallback _callback;
private readonly HttpContext _context;
bool IAsyncResult.IsCompleted { get { return _completed; } }
WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } }
Object IAsyncResult.AsyncState { get { return _state; } }
bool IAsyncResult.CompletedSynchronously { get { return false; } }
public AsynchOperation(AsyncCallback callback, HttpContext context, Object state)
{
_callback = callback;
_context = context;
_state = state;
_completed = false;
}
public void StartAsyncWork()
{
using (var client = new WebClient())
{
var url = "url_web_service_url";
client.DownloadDataCompleted += (o, e) =>
{
if (!e.Cancelled && e.Error == null)
{
_context.Response.ContentType = "text/xml";
_context.Response.OutputStream.Write(e.Result, 0, e.Result.Length);
}
_completed = true;
_callback(this);
};
client.DownloadDataAsync(new Uri(url));
}
}
}
public class MyAsyncHandler : IHttpAsyncHandler
{
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
var asynch = new AsynchOperation(cb, context, extraData);
asynch.StartAsyncWork();
return asynch;
}
public void EndProcessRequest(IAsyncResult result)
{
}
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
}
}
Now this all works, and I THINK it should do the trick, but I'm not 100% sure. Also, creating my own IAsyncResult seems a bit overkill, I'm just wondering if there's a way I can leverage the IAsyncResult returned from Delegate.BeginInvoke, or maybe something else. Any feedback welcome. Thanks!!

Wow, yeah you can make this a lot easier/cleaner if you're on .NET 4.0 by leveraging the Task Parallel Library. Check it:
public class MyAsyncHandler : IHttpAsyncHandler
{
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
// NOTE: the result of this operation is void, but TCS requires some data type so we just use bool
TaskCompletionSource<bool> webClientDownloadCompletionSource = new TaskCompletionSource<bool>();
WebClient webClient = new WebClient())
HttpContext currentHttpContext = HttpContext.Current;
// Setup the download completed event handler
client.DownloadDataCompleted += (o, e) =>
{
if(e.Cancelled)
{
// If it was canceled, signal the TCS is cacnceled
// NOTE: probably don't need this since you have nothing canceling the operation anyway
webClientDownloadCompletionSource.SetCanceled();
}
else if(e.Error != null)
{
// If there was an exception, signal the TCS with the exception
webClientDownloadCompletionSource.SetException(e.Error);
}
else
{
// Success, write the response
currentHttpContext.Response.ContentType = "text/xml";
currentHttpContext.Response.OutputStream.Write(e.Result, 0, e.Result.Length);
// Signal the TCS that were done (we don't actually look at the bool result, but it's needed)
taskCompletionSource.SetResult(true);
}
};
string url = "url_web_service_url";
// Kick off the download immediately
client.DownloadDataAsync(new Uri(url));
// Get the TCS's task so that we can append some continuations
Task webClientDownloadTask = webClientDownloadCompletionSource.Task;
// Always dispose of the client once the work is completed
webClientDownloadTask.ContinueWith(
_ =>
{
client.Dispose();
},
TaskContinuationOptions.ExecuteSynchronously);
// If there was a callback passed in, we need to invoke it after the download work has completed
if(cb != null)
{
webClientDownloadTask.ContinueWith(
webClientDownloadAntecedent =>
{
cb(webClientDownloadAntecedent);
},
TaskContinuationOptions.ExecuteSynchronously);
}
// Return the TCS's Task as the IAsyncResult
return webClientDownloadTask;
}
public void EndProcessRequest(IAsyncResult result)
{
// Unwrap the task and wait on it which will propagate any exceptions that might have occurred
((Task)result).Wait();
}
public bool IsReusable
{
get
{
return true; // why not return true here? you have no state, it's easily reusable!
}
}
public void ProcessRequest(HttpContext context)
{
}
}

Related

How to stop a Timer created in a .Net Core controller?

I've declared a System.Timers.Timer inside an Api Controller.
Next there is an Action that gets called by a Javascript client and its task is to make every second an HTTP GET request to an external server which sends back a JSON.
Then the JSON gets sent to the Javascript client via WebSocket.
Also I've created another Action that stops the timer when being called.
[Route("api")]
[ApiController]
public class PositionController : ControllerBase
{
private System.Timers.Timer aTimer = new System.Timers.Timer();
// ...
// GET api/position/state
[HttpGet("[controller]/[action]")]
public async Task<string> StateAsync()
{
try
{
Console.WriteLine("In StateAsync (GET)");
string json = "timer started";
aTimer.Elapsed += new ElapsedEventHandler(async (sender, args) =>
{
json = await Networking.SendGetRequestAsync("www.example.com");
Console.WriteLine($"Json in response:");
Console.WriteLine(json);
await _hubContext.Clients.All.SendAsync("ReceiveMessage", json);
});
aTimer.Interval = 1000;
aTimer.Enabled = true;
Console.WriteLine("----------------------------------");
return json;
}
catch (HttpRequestException error) // Connection problems
{
// ...
}
}
// GET api/position/stopstate
[HttpGet("[controller]/[action]")]
public async Task<string> StopStateAsync()
{
try
{
Console.WriteLine("In StopStateAsync (GET)");
string json = "timer stopped";
aTimer.Enabled = false;
Console.WriteLine("----------------------------------");
return json;
}
catch (HttpRequestException error) // Connection problems
{
// ...
}
}
// ...
}
The problem is, since ASP.NET Controllers (so .Net Core ones?) gets instancieted for every new request, when I call the Stop timer method, the timer doesn't stop because it's not the right Timer instance. So the system continues to make HTTP requests and Websocket transfers...
Is there a way to save and work on the Timer instance I need to stop from a different Controller instance or can I retrieve the original Controller instance?
Thanks in advance guys :)
You should really let your controllers do "controller" things. Running a timer in a controller breaks a controller's pattern.
You should look in to implementing an IHostedService that when injected will maintain a timer.
Here is a quick example:
TimerController.cs
[ApiController, Route("api/[controller]")]
public sealed class TimerController : ControllerBase
{
private readonly ITimedHostedService _timedHostedService;
public TimerController(ITimedHostedService timedHostedService)
{
_timedHostedService = timedHostedService;
}
// Just a tip: Use HttpPost. HttpGet should never change the
// state of your application. You can accidentally hit a GET,
// while POST takes a little more finesse to execute.
[HttpPost, Route("startTimer/{milliseconds}")]
public IActionResult StartTimer(int milliseconds)
{
_timedHostedService.StartTimer(milliseconds);
return Ok();
}
[HttpPost, Route("stopTimer")]
public IActionResult StopTimer()
{
_timedHostedService.StopTimer();
return Ok();
}
[HttpGet, Route("isTimerRunning")]
public IActionResult IsTimerRunning()
{
return Ok(new
{
result = _timedHostedService.IsTimerRunning()
});
}
}
TimedHostedService.cs
public interface ITimedHostedService
{
void StartTimer(int milliseconds);
void StopTimer();
bool IsTimerRunning();
}
public sealed class TimedHostedService : IHostedService, ITimedHostedService
{
private static Timer _timer;
private static readonly object _timerLock = new object();
public void StartTimer(int milliseconds)
{
lock(_timerLock)
{
_timer ??= new Timer(_ =>
{
// TODO: do your timed work here.
}, null, 0, milliseconds);
}
}
public bool IsTimerRunning()
{
lock(_timerLock)
{
return _timer != null;
}
}
public void StopTimer()
{
lock(_timerLock)
{
_timer?.Change(Timeout.Infinite, Timeout.Infinite);
_timer?.Dispose();
_timer = null;
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
StopTimer();
return Task.CompletedTask;
}
}
Then, inject it as so:
services.AddHostedService<TimedHostedService>();
services.AddTransient<ITimedHostedService, TimedHostedService>();
I haven't tested this, but it should work as-is.

gRPC keeping response streams open for subscriptions

I've tried to define a gRPC service where client can subscribe to receive broadcasted messages and they can also send them.
syntax = "proto3";
package Messenger;
service MessengerService {
rpc SubscribeForMessages(User) returns (stream Message) {}
rpc SendMessage(Message) returns (Close) {}
}
message User {
string displayName = 1;
}
message Message {
User from = 1;
string message = 2;
}
message Close {}
My idea was that when a client requests to subscribe to the messages, the response stream would be added to a collection of response streams, and when a message is sent, the message is sent through all the response streams.
However, when my server attempts to write to the response streams, I get an exception System.InvalidOperationException: 'Response stream has already been completed.'
Is there any way to tell the server to keep the streams open so that new messages can be sent through them? Or is this not something that gRPC was designed for and a different technology should be used?
The end goal service would be allows multiple types of subscriptions (could be to new messages, weather updates, etc...) through different clients written in different languages (C#, Java, etc...). The different languages part is mainly the reason I chose gRPC to try this, although I intend on writing the server in C#.
Implementation example
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Grpc.Core;
using Messenger;
namespace SimpleGrpcTestStream
{
/*
Dependencies
Install-Package Google.Protobuf
Install-Package Grpc
Install-Package Grpc.Tools
Install-Package System.Interactive.Async
Install-Package System.Linq.Async
*/
internal static class Program
{
private static void Main()
{
var messengerServer = new MessengerServer();
messengerServer.Start();
var channel = Common.GetNewInsecureChannel();
var client = new MessengerService.MessengerServiceClient(channel);
var clientUser = Common.GetUser("Client");
var otherUser = Common.GetUser("Other");
var cancelClientSubscription = AddCancellableMessageSubscription(client, clientUser);
var cancelOtherSubscription = AddCancellableMessageSubscription(client, otherUser);
client.SendMessage(new Message { From = clientUser, Message_ = "Hello" });
client.SendMessage(new Message { From = otherUser, Message_ = "World" });
client.SendMessage(new Message { From = clientUser, Message_ = "Whoop" });
cancelClientSubscription.Cancel();
cancelOtherSubscription.Cancel();
channel.ShutdownAsync().Wait();
messengerServer.ShutDown().Wait();
}
private static CancellationTokenSource AddCancellableMessageSubscription(
MessengerService.MessengerServiceClient client,
User user)
{
var cancelMessageSubscription = new CancellationTokenSource();
var messages = client.SubscribeForMessages(user);
var messageSubscription = messages
.ResponseStream
.ToAsyncEnumerable()
.Finally(() => messages.Dispose());
messageSubscription.ForEachAsync(
message => Console.WriteLine($"New Message: {message.Message_}"),
cancelMessageSubscription.Token);
return cancelMessageSubscription;
}
}
public static class Common
{
private const int Port = 50051;
private const string Host = "localhost";
private static readonly string ChannelAddress = $"{Host}:{Port}";
public static User GetUser(string name) => new User { DisplayName = name };
public static readonly User ServerUser = GetUser("Server");
public static readonly Close EmptyClose = new Close();
public static Channel GetNewInsecureChannel() => new Channel(ChannelAddress, ChannelCredentials.Insecure);
public static ServerPort GetNewInsecureServerPort() => new ServerPort(Host, Port, ServerCredentials.Insecure);
}
public sealed class MessengerServer : MessengerService.MessengerServiceBase
{
private readonly Server _server;
public MessengerServer()
{
_server = new Server
{
Ports = { Common.GetNewInsecureServerPort() },
Services = { MessengerService.BindService(this) },
};
}
public void Start()
{
_server.Start();
}
public async Task ShutDown()
{
await _server.ShutdownAsync().ConfigureAwait(false);
}
private readonly ConcurrentDictionary<User, IServerStreamWriter<Message>> _messageSubscriptions = new ConcurrentDictionary<User, IServerStreamWriter<Message>>();
public override async Task<Close> SendMessage(Message request, ServerCallContext context)
{
await Task.Run(() =>
{
foreach (var (_, messageStream) in _messageSubscriptions)
{
messageStream.WriteAsync(request);
}
}).ConfigureAwait(false);
return await Task.FromResult(Common.EmptyClose).ConfigureAwait(false);
}
public override async Task SubscribeForMessages(User request, IServerStreamWriter<Message> responseStream, ServerCallContext context)
{
await Task.Run(() =>
{
responseStream.WriteAsync(new Message
{
From = Common.ServerUser,
Message_ = $"{request.DisplayName} is listening for messages!",
});
_messageSubscriptions.TryAdd(request, responseStream);
}).ConfigureAwait(false);
}
}
public static class AsyncStreamReaderExtensions
{
public static IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IAsyncStreamReader<T> asyncStreamReader)
{
if (asyncStreamReader is null) { throw new ArgumentNullException(nameof(asyncStreamReader)); }
return new ToAsyncEnumerableEnumerable<T>(asyncStreamReader);
}
private sealed class ToAsyncEnumerableEnumerable<T> : IAsyncEnumerable<T>
{
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
=> new ToAsyncEnumerator<T>(_asyncStreamReader, cancellationToken);
private readonly IAsyncStreamReader<T> _asyncStreamReader;
public ToAsyncEnumerableEnumerable(IAsyncStreamReader<T> asyncStreamReader)
{
_asyncStreamReader = asyncStreamReader;
}
private sealed class ToAsyncEnumerator<TEnumerator> : IAsyncEnumerator<TEnumerator>
{
public TEnumerator Current => _asyncStreamReader.Current;
public async ValueTask<bool> MoveNextAsync() => await _asyncStreamReader.MoveNext(_cancellationToken);
public ValueTask DisposeAsync() => default;
private readonly IAsyncStreamReader<TEnumerator> _asyncStreamReader;
private readonly CancellationToken _cancellationToken;
public ToAsyncEnumerator(IAsyncStreamReader<TEnumerator> asyncStreamReader, CancellationToken cancellationToken)
{
_asyncStreamReader = asyncStreamReader;
_cancellationToken = cancellationToken;
}
}
}
}
}
The problem you're experiencing is due to the fact that MessengerServer.SubscribeForMessages returns immediately. Once that method returns, the stream is closed.
You'll need an implementation similar to this to keep the stream alive:
public class MessengerService : MessengerServiceBase
{
private static readonly ConcurrentDictionary<User, IServerStreamWriter<Message>> MessageSubscriptions =
new Dictionary<User, IServerStreamWriter<Message>>();
public override async Task SubscribeForMessages(User request, IServerStreamWriter<ReferralAssignment> responseStream, ServerCallContext context)
{
if (!MessageSubscriptions.TryAdd(request))
{
// User is already subscribed
return;
}
// Keep the stream open so we can continue writing new Messages as they are pushed
while (!context.CancellationToken.IsCancellationRequested)
{
// Avoid pegging CPU
await Task.Delay(100);
}
// Cancellation was requested, remove the stream from stream map
MessageSubscriptions.TryRemove(request);
}
}
As far as unsubscribing / cancellation goes, there are two possible approaches:
The client can hold onto a CancellationToken and call Cancel() when it wants to disconnect
The server can hold onto a CancellationToken which you would then store along with the IServerStreamWriter in the MessageSubscriptions dictionary via a Tuple or similar. Then, you could introduce an Unsubscribe method on the server which looks up the CancellationToken by User and calls Cancel on it server-side
Similar to Jon Halliday's answer, an indefinately long Task.Delay(-1) could be used and passed the context's cancellation token.
A try catch can be used to remove end the server's response stream when the task is cancelled.
public override async Task SubscribeForMessages(User request, IServerStreamWriter<Message> responseStream, ServerCallContext context)
{
if (_messageSubscriptions.ContainsKey(request))
{
return;
}
await responseStream.WriteAsync(new Message
{
From = Common.ServerUser,
Message_ = $"{request.DisplayName} is listening for messages!",
}).ConfigureAwait(false);
_messageSubscriptions.TryAdd(request, responseStream);
try
{
await Task.Delay(-1, context.CancellationToken);
}
catch (TaskCanceledException)
{
_messageSubscriptions.TryRemove(request, out _);
}
}

Async and await to synchronous method using Task

I am very new with this async/await concept, so I apologise for asking anything that is obvious.
I need to send email and the new API require me to use async and await. Problem is that a lot of my methods need to call this "send email" synchronously.
So, I create a synchronous wrapper method:
private Task SendEmailAsync(string email, string content)
{
...
...
...
}
private void SendEmail(string email, string content)
{
Task tsk = Task.Factory.StartNew(async () => await SendEmailAsync(email, content));
try { tsk.Wait(); }
catch (AggregateException aex)
{
throw aex.Flatten();
}
}
But for some reason, tsk.Wait() does not waiting for await SendEmailAsync(...) to finish. So, I need to add ManualResetEvent. Something like this
private void SendEmail(string email, string content)
{
ManualResetEvent mre = new ManualResetEvent(false);
Task tsk = Task.Factory.StartNew(async () =>
{
mre.Reset();
try { await SendEmailAsync(email, content); }
finally { mre.Set(); }
});
mre.WaitOne();
try { tsk.Wait(); }
catch (AggregateException aex)
{
throw aex.Flatten();
}
}
But any exception thrown by SendEmailAsync(...) will NOT be captured by tsk.Wait(). My question is:
Why tsk.Wait() does NOT wait for await SendEmailAsync(...)
How to catch exception thrown by await SendEmailAsync(...)
Thanks!
You can run asynchronous code in a synchronous manner by using the following extensions.
https://stackoverflow.com/a/5097066/5062791
public static class AsyncHelpers
{
/// <summary>
/// Execute's an async Task<T> method which has a void return value synchronously
/// </summary>
/// <param name="task">Task<T> method to execute</param>
public static void RunSync(Func<Task> task)
{
var oldContext = SynchronizationContext.Current;
var synch = new ExclusiveSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(synch);
synch.Post(async _ =>
{
try
{
await task();
}
catch (Exception e)
{
synch.InnerException = e;
throw;
}
finally
{
synch.EndMessageLoop();
}
}, null);
synch.BeginMessageLoop();
SynchronizationContext.SetSynchronizationContext(oldContext);
}
/// <summary>
/// Execute's an async Task<T> method which has a T return type synchronously
/// </summary>
/// <typeparam name="T">Return Type</typeparam>
/// <param name="task">Task<T> method to execute</param>
/// <returns></returns>
public static T RunSync<T>(Func<Task<T>> task)
{
var oldContext = SynchronizationContext.Current;
var synch = new ExclusiveSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(synch);
T ret = default(T);
synch.Post(async _ =>
{
try
{
ret = await task();
}
catch (Exception e)
{
synch.InnerException = e;
throw;
}
finally
{
synch.EndMessageLoop();
}
}, null);
synch.BeginMessageLoop();
SynchronizationContext.SetSynchronizationContext(oldContext);
return ret;
}
private class ExclusiveSynchronizationContext : SynchronizationContext
{
private bool done;
public Exception InnerException { get; set; }
readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false);
readonly Queue<Tuple<SendOrPostCallback, object>> items =
new Queue<Tuple<SendOrPostCallback, object>>();
public override void Send(SendOrPostCallback d, object state)
{
throw new NotSupportedException("We cannot send to our same thread");
}
public override void Post(SendOrPostCallback d, object state)
{
lock (items)
{
items.Enqueue(Tuple.Create(d, state));
}
workItemsWaiting.Set();
}
public void EndMessageLoop()
{
Post(_ => done = true, null);
}
public void BeginMessageLoop()
{
while (!done)
{
Tuple<SendOrPostCallback, object> task = null;
lock (items)
{
if (items.Count > 0)
{
task = items.Dequeue();
}
}
if (task != null)
{
task.Item1(task.Item2);
if (InnerException != null) // the method threw an exeption
{
throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
}
}
else
{
workItemsWaiting.WaitOne();
}
}
}
public override SynchronizationContext CreateCopy()
{
return this;
}
}
}
I should start by stating that you should follow #ChrFin's comment and try to refactor your code to make it asynchronous, instead of trying to run an existing async library synchronously (You'll even note an improvement in the application's performance).
To accomplish what you seek, you should use SendEmailAsync(email, content).GetAwaiter().GetResult().
Why not SendEmailAsync(email, content).Wait()? you may ask.
Well, there's a subtle difference. Wait() will wrap any runtime exception inside an AggregateException which will make your life harder if you attempt to catch the original one.
GetAwaiter().GetResult() will simply throw the original exception.
You code would look like this:
private void SendEmail(string email, string content)
{
try
{
SendEmailAsync(email, content).GetAwaiter().GetResult();
}
catch(Exception ex)
{
// ex is the original exception
// Handle ex or rethrow or don't even catch
}
}
I need to send email and the new API require me to use async and await. Problem is that a lot of my methods need to call this "send email" synchronously.
The best solution is to remove the "synchronous caller" requirement. Instead, you should allow async and await to grow naturally through your codebase.
for some reason, tsk.Wait() does not waiting for await SendEmailAsync(...) to finish.
That's because you're using Task.Factory.StartNew, which is a dangerous API.
I have tried to use Task.Result and Task.GetAwaiter().GetResult() but it resulted in deadlock.
I explain this deadlock in detail on my blog.
I have used the solution posted by ColinM and it worked fine.
That's a dangerous solution. I don't recommend its use unless you understand exactly how it works.
If you absolutely must implement the antipattern of invoking asynchronous code synchronously, then you'll need to use a hack to do so. I cover the various hacks in an article on brownfield async. In your case, you could probably just use the thread pool hack, which is just one line of code:
private void SendEmail(string email, string content) =>
Task.Run(() => SendEmailAsync(email, content)).GetAwaiter().GetResult();
However, as I stated at the beginning of this answer, the ideal solution is to just allow async to grow. Hacks like this limit the scalability of your web server.

Write an asynchrone IResoureHandler for CefSharp

I have written my own ResourceHandler. ProcessRequest() works asynchrone. After updating CefSharp from 43 (WPF) to 49 (WinForms) I have some problems with IRequest.IsDisposed.
It seems that the request is disposed before my Task is started. And if the request is disposed I have no more access to the post data.
public class MySchemeHandler : IResourceHandler {
// ...
public bool ProcessRequest(IRequest request, ICallback callback) {
// copy request???
Task.Run(() => {
try {
if (request.IsDisposed == true) // Copy post data before Task.Run()???
throw new ExpressDisposedException();
// ...
// Process(request, callback);
} catch(Exception ex) {
callback.Cancel();
} finally {
callback.Dispose();
}
});
return true;
}
}
So is there a way to avoid the disposing of IRequest. Is anywhere a complete example how to make it better.

ASP.NET Web API + Long running operation cancellation

Is there a way to figure out in ASP.NET Web API beta whether the HTTP request was cancelled (aborted by user of for any another reason)? I'm looking for opportunity to have a kind of cancellation token out-of-the-box that will signal that the request is aborted and therefore long-running ops should be aborted as well.
Possible related question - the use case for the CancellationTokenModelBinder class. What's the reason to have a separate binder for cancellation token?
You could check Response.IsClientConnected from time to time to see if the browser is still connected to the server.
I'd like to sum-up a bit. The only approach that seem to work is checking Response.IsClientConnected.
Here some technical details regarding what is going behind the stage:
here and here
This approach has some flaws:
Works only under IIS (no self-hosting, no Dev Server);
According to some SO answers may be slow (do not react immediately after client disconnected): here;
There are considerations regarding this call cost: here
At the end I came up with the following piece of code to inject CancellationToken based on IsClientConnected into the Web API controller:
public class ConnectionAbortTokenAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
private readonly string _paramName;
private Timer _timer;
private CancellationTokenSource _tokenSource;
private CancellationToken _token;
public ConnectionAbortTokenAttribute(string paramName)
{
_paramName = paramName;
}
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
object value;
if (!actionContext.ActionArguments.TryGetValue(_paramName, out value))
{
// no args with defined name found
base.OnActionExecuting(actionContext);
return;
}
var context = HttpContext.Current;
if (context == null)
{
// consider the self-hosting case (?)
base.OnActionExecuting(actionContext);
return;
}
_tokenSource = new CancellationTokenSource();
_token = _tokenSource.Token;
// inject
actionContext.ActionArguments[_paramName] = _token;
// stop timer on client disconnect
_token.Register(() => _timer.Dispose());
_timer = new Timer
(
state =>
{
if (!context.Response.IsClientConnected)
{
_tokenSource.Cancel();
}
}, null, 0, 1000 // check each second. Opts: make configurable; increase/decrease.
);
base.OnActionExecuting(actionContext);
}
/*
* Is this guaranteed to be called?
*
*
*/
public override void OnActionExecuted(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)
{
if(_timer != null)
_timer.Dispose();
if(_tokenSource != null)
_tokenSource.Dispose();
base.OnActionExecuted(actionExecutedContext);
}
}
If you added CancellationToken in to controller methods, it will be automatically injected by the framework, and when a client calls xhr.abort() the token will be automatically cancelled
Something similar to
public Task<string> Get(CancellationToken cancellationToken = default(CancellationToken))
For MVC you can also refer to
HttpContext.Current.Response.IsClientConnected
HttpContext.Response.ClientDisconnectedToken
For .NetCore
services.AddTransient<ICustomInterface>(provider => {
var accessor = provider.GetService<IHttpContextAccessor>);
accessor.HttpContext.RequestAborted;
});

Categories

Resources