Blazor server-side: subscribe to event - c#

I am having trouble to understand how can I subscribe to an event in a Blazor page.
I have this class which implements Quartz's .NET IJobListener interface:
public class GlobalJobListener : IJobListener
{
public event TaskExecution Started;
public event TaskExecution Vetoed;
public event TaskExecutionComplete Executed;
public GlobalJobListener(string name)
{
Name = name;
}
public GlobalJobListener()
{
}
public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var task = new Task(() => Started?.Invoke());
task.Start();
task.Wait();
return task;
}
public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var task = new Task(() => Vetoed?.Invoke());
task.Start();
task.Wait();
return task;
}
public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, CancellationToken cancellationToken = default(CancellationToken))
{
var task = new Task(() => Executed?.Invoke(jobException));
task.Start();
task.Wait();
return task;
}
public string Name { get; }
}
In ConfigureServices:
services.AddScoped<GlobalJobListener>();
Then in my .razor page:
[Inject]
protected GlobalJobListener listener { get; set; }
And OnInitializedAsync():
protected override async Task OnInitializedAsync()
{
scheduler = quartz.Scheduler;
if (scheduler != null)
{
listener.Started += BeforeStart;
listener.Executed += AfterEndAsync;
}
}
private void BeforeStart()
{
Log.Information("\t" + "Started: " + DateTime.Now.ToString("dd-MMM-yyyy hh:mm:ss tt"));
}
And BeforeStart() never triggers, but JobToBeExecuted() triggers OK. I can't manage to see what I am doing wrong.
Thanks a lot.

Related

Cleaning up a Task in an AsyncDisposable (VSTHRD003)

I have a class which creates a Task which runs during its whole lifetime, that is, until Dispose() is called on it:
In the constructor I call:
_worker = Task.Run(() => ProcessQueueAsync(_workerCancellation.Token), _workerCancellation.Token);
The way I currently do it (which I am also not sure is the right way) is cancelling the CancellationToken, and waiting on the task.
public void Dispose()
{
if (_isDisposed)
{
return;
}
_workerCancellation.Cancel();
_worker.GetAwaiter().GetResult();
_isDisposed = true;
}
When I do the same in the AsyncDispose method like so:
public async ValueTask DisposeAsync()
{
await _worker;
}
I get this warning
How do I correctly dispose of such a worker? Thanks!
As requested, here is the full code of what I am trying to do:
public sealed class ActiveObjectWrapper<T, TS> : IAsyncDisposable
{
private bool _isDisposed = false;
private const int DefaultQueueCapacity = 1024;
private readonly Task _worker;
private readonly CancellationTokenSource _workerCancellation;
private readonly Channel<(T, TaskCompletionSource<TS>)> _taskQueue;
private readonly Func<T, TS> _onReceive;
public ActiveObjectWrapper(Func<T, TS> onReceive, int? queueCapacity = null)
{
_onReceive = onReceive;
_taskQueue = Channel.CreateBounded<(T, TaskCompletionSource<TS>)>(queueCapacity ?? DefaultQueueCapacity);
_workerCancellation = new CancellationTokenSource();
_worker = Task.Run(() => ProcessQueueAsync(_workerCancellation.Token), _workerCancellation.Token);
}
private async Task ProcessQueueAsync(CancellationToken cancellationToken)
{
await foreach (var (value, taskCompletionSource) in _taskQueue.Reader.ReadAllAsync(cancellationToken))
{
try
{
var result = _onReceive(value); // todo: do I need to propagate the cancellation token?
taskCompletionSource.SetResult(result);
}
catch (Exception exception)
{
taskCompletionSource.SetException(exception);
}
}
}
public async Task<TS> EnqueueAsync(T value)
{
// see: https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/
var completionSource = new TaskCompletionSource<TS>(TaskCreationOptions.RunContinuationsAsynchronously);
await _taskQueue.Writer.WriteAsync((value, completionSource));
return await completionSource.Task;
}
public async ValueTask DisposeAsync()
{
if (_isDisposed)
{
return;
}
_taskQueue.Writer.Complete();
_workerCancellation.Cancel();
await _worker;
_isDisposed = true;
}
}
This is the pattern that I use when implementing both IDisposabe and IDisposeableAsync. It isn't strictly compliant with the .Net recommendations. I found that implementing DisposeAsyncCore() was unnecessary as my classes are sealed.
public void Dispose()
{
Dispose(disposing: true);
//GC.SuppressFinalize(this);
Worker.GetAwaiter().GetResult();
}
public void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
if (disposing)
{
lock (isDisposing)
{
if (isDisposed)
{
return;
}
Cts.Cancel();
Cts.Dispose();
isDisposed = true;
}
}
}
public async ValueTask DisposeAsync()
{
Dispose(disposing: true);
//GC.SuppressFinalize(this);
await Worker;
}
This looks like an attempt to build a TransformBlock<TIn,TOut> on top of Channels. Using a Channel properly wouldn't generate such a warning. There's no reason to use a single processing task, or store it in a field.
Using Blocks
First, the equivalent code using a TransformBlock<Tin,TOut> would be:
var block=new TransformBlock<TIn,TOut>(msgIn=>func(msgIn));
foreach(....)
{
block.Post(someMessage);
}
To stop it, block.Complete() would be enough. Any pending messages would still be processed. A TransformBlock is meant to forward its output to other blocks, eg an ActionBlock or BufferBlock.
var finalBlock=new ActionBlock<TOut>(msgOut=>Console.WriteLine(msgOut));
block.LinkTo(finalBlock,new DataflowLinkOptions{PropagateCompletion = true});
...
block.Complete();
await finalBlock.Completion;
Using Channels
Doing something similar with channels doesn't need explicit classes, or Enqueue/Dequeue methods. Channels are build with separate ChannelReader, ChannelWriter interfaces to make it easier to control ownership, concurrency and completion.
A similar pipeline using channels would require only some methods:
ChannelReader<string> FolderToChannel(string path,CancellationToken token=default)
{
Channel<int> channel=Channel.CreateUnbounded();
var writer=channel.Writer;
_ = Task.Run(async ()=>{
foreach(var path in Directory.EnumerateFiles(path))
{
await _writer.SendAsync(path);
if (token.CancellationRequested)
{
return;
}
}
},token).ContinueWith(t=>_writer.TryComplete(t.Exception));
return channel;
}
This produces a reader that can be passed to a processing method. One that could generate another reader with the results:
static ChannelReader<MyClass> ParseFile(this ChannelReader<string> reader,CancellationToken token)
{
Channel<int> channel=Channel.CreateUnbounded();
var writer=channel.Writer;
_ = Task.Run(async ()=>{
await foreach(var path in reader.ReadAllAsync(token))
{
var json= await File.ReadAllTextAsync(path);
var dto= JsonSerializer.DeserializeObject<MyClass>(json);
await _writer.SendAsync(dto);
}
},token).ContineWith(t=>writer.TryComplete(t.Exception);
return channel;
}
And a final step that only consumes a channel:
static async Task LogIt(this ChannelReader<MyClass> reader,CancellationToken token)
{
await Task.Run(async ()=>{
await foreach(var dto in reader.ReadAllAsync(token))
{
Console.WriteLine(dto);
}
},token);
}
The three steps can be combined very easily:
var cts=new CancellationTokenSource();
var complete=FolderToChannel(somePath,cts.Token)
.ParseFile(cts.Token)
.LogIt(cts.Token);
await complete;
By encapsulating the channel itself and the processing in a method there's no ambiguity about who owns the channel, who is responsible for completion or cancellation

How to run multiple cron expression in c# background service

I am using BackgroundService for my tasks and i would like to run different tasks at different times for example i have a task which should run once a day and i have come up with this cron expression "#daily" which is ok for my first task. But for my second task which should run multiple times a day i need multiple cron expressions
for example
( 30 13 * * * ) daily at 13:30
( 10 17 * * * ) daily at 17:10
( 40 20 * * * ) daily at 20:40
( 15 22 * * * ) daily at 22:15
and the classes which i use looks like this
public abstract class BackgroundService : IHostedService
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_executingTask = ExecuteAsync(_stoppingCts.Token);
if (_executingTask.IsCompleted)
{
return _executingTask;
}
return Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
if (_executingTask == null)
{
return;
}
try
{
_stoppingCts.Cancel();
}
finally
{
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
protected virtual async Task ExecuteAsync(CancellationToken stoppingToken)
{
do
{
await Process();
await Task.Delay(5000, stoppingToken);
} while (!stoppingToken.IsCancellationRequested);
}
protected abstract Task Process();
}
public abstract class ScopedProcessor : BackgroundService
{
private IServiceScopeFactory _serviceScopeFactory;
public ScopedProcessor(IServiceScopeFactory serviceScopeFactory) : base()
{
_serviceScopeFactory = serviceScopeFactory;
}
protected override async Task Process()
{
using (var scope = _serviceScopeFactory.CreateScope())
{
await ProcessInScope(scope.ServiceProvider);
}
}
public abstract Task ProcessInScope(IServiceProvider scopeServiceProvider);
}
public abstract class ScheduledProcessor : ScopedProcessor
{
private CrontabSchedule _schedule;
private DateTime _nextRun;
protected abstract string Schedule { get; }
public ScheduledProcessor(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
{
_schedule = CrontabSchedule.Parse(Schedule);
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var now = DateTime.Now;
if (now > _nextRun)
{
await Process();
// for the first task which should run daily is ok but
// for my second task i want to run the multiple cron expressions after
// one another
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
await Task.Delay(5000, stoppingToken); // 5 seconds delay
};
}
}
and the actual class which contains the actual task.
public class MyTask : ScheduledProcessor
{
public MyTask(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
{
}
// cron expression
protected override string Schedule => "*/1 * * * *"; // every 1 min for testing purpose
// actual task
public override Task ProcessInScope(IServiceProvider scopeServiceProvider)
{
Console.WriteLine("MyTask Running " + DateTime.Now.ToShortTimeString());
// do other work
return Task.CompletedTask;
}
}
instead of executing a single cron expression i want to run multiple cron expressions after one another at a daily basis. Maybe CronTrigger can help but i dont know where and how can i use CronTrigger in my Classes.

SignalR Hub OnDisconnectedAsync Method Does Not Get Hit in Blazor Server

Here I am trying to remove ConnectionId from connected connectedId list UserHandler.ConnectedUser in OnDisconnectedAsync method of my hub.
But the problem is that when ever user gets logout from application or close that window the OnDisconnectedAsync method on hub does not get hit.
Any help with my code will be grate. Thank you.
Below is my hub
public class ChatHub : Hub
{
public List<string> SendingUserList = new List<string>();
public async Task SendMessage(string to, string message)
{
var senderConnectionId = Context.ConnectionId;
SendingUserList.Add(to);
SendingUserList.Add(senderConnectionId);
foreach (var connectionId in SendingUserList)
{
await Clients.Client(connectionId).SendAsync("ReceiveMessage", message);
}
}
public override Task OnConnectedAsync()
{
Clients.All.SendAsync("ReciveUser", UserHandler.UserName, Context.ConnectionId);
return base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception e)
{
UserHandler.ConnectedUser.Remove(Context.ConnectionId));
await base.OnDisconnectedAsync(e);
}
}
public static class UserHandler
{
public static HashSet<string> ConnectedUser = new HashSet<string>();
public static string UserName { get; set; }
}
Below is my remaining code
protected override async Task OnInitializedAsync()
{
UserHandler.UserName = httpContextAccessor.HttpContext.User.Identity.Name;
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.ServerTimeout = TimeSpan.FromMinutes(60);
hubConnection.On<string>("ReceiveMessage", BroadcastMessage);
hubConnection.On<string,string>("ReciveUser", RefreshUserList);
await hubConnection.StartAsync();
StateHasChanged();
}
private void RefreshUserList(string connectedUserId, string connectionId )
{
UserHandler.ConnectedUser.Add(connectionId);
connectedUserList = UserHandler.ConnectedUser;
StateHasChanged();
}
private void BroadcastMessage(string message)
{
var encodedMsg = $"{message}";
messages.Add(encodedMsg);
StateHasChanged();
}
public async ValueTask DisposeAsync()
{
await hubConnection.DisposeAsync();
}
You'll need to implement the IAsyncDisposable interface in the Hub's consumer (Razor Component), like this:
#implements IAsyncDisposable
Add these methods:
public void Dispose()
{
hubConnection.DisposeAsync();
}
public async ValueTask DisposeAsync()
{
await hubConnection.DisposeAsync();
}
Now, when you close the client's page the OnDisconnectedAsync method will be called.

ContinueConversationAsync() requires users to chat first after publishing bot

After publishing the bot, the user needs to chat with the bot again first in order send a proactive message. I followed this sample but instead of storing the conversation reference in the variable, I stored it in cosmosDB. However I still can't send proactive message if the user has not chatted with the bot after publishing. Is there a way to send proactive message even when the user has not chatted with the bot after publishing?
DialogBot
private async void AddConversationReference(ITurnContext turnContext)
{
var userstate = await _userProfileAccessor.GetAsync(turnContext, () => new BasicUserState());
userstate.SavedConversationReference = turnContext.Activity.GetConversationReference();
_conversationReferences.AddOrUpdate(userstate.SavedConversationReference.User.Id, userstate.SavedConversationReference, (key, newValue) => userstate.SavedConversationReference);
}
protected override Task OnConversationUpdateActivityAsync(ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
AddConversationReference(turnContext);
return base.OnConversationUpdateActivityAsync(turnContext, cancellationToken);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
Logger.LogInformation("Running dialog with Message Activity.");
AddConversationReference(turnContext);
await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
}
api/notify
namespace SabikoBotV2.Controllers
{
[Route("api/notify")]
public class NotifyController : ControllerBase
{
private readonly IBotFrameworkHttpAdapter _adapter;
private readonly string _appId;
private readonly ConcurrentDictionary<string, ConversationReference> _conversationReferences;
private readonly IStatePropertyAccessor<BasicUserState> _userProfileAccessor;
public NotifyController(UserState userState, IBotFrameworkHttpAdapter adapter, ICredentialProvider credentials, ConcurrentDictionary<string, ConversationReference> conversationReferences)
{
_userProfileAccessor = userState.CreateProperty<BasicUserState>("UserProfile");
_adapter = adapter;
_conversationReferences = conversationReferences;
_appId = ((SimpleCredentialProvider)credentials).AppId;
if (string.IsNullOrEmpty(_appId))
{
_appId = Guid.NewGuid().ToString(); //if no AppId, use a random Guid
}
}
public async Task<IActionResult> Get()
{
try
{
foreach (var conversationReference in _conversationReferences.Values)
{
await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, conversationReference, BotCallback, default(CancellationToken));
}
return new ContentResult()
{
Content = "<html><body><h1>Proactive messages have been sent.</h1></body></html>",
ContentType = "text/html",
StatusCode = (int)HttpStatusCode.OK,
};
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
private async Task BotCallback(ITurnContext turnContext, CancellationToken cancellationToken)
{
var userstate = await _userProfileAccessor.GetAsync(turnContext, () => new BasicUserState(), cancellationToken);
if (userstate.SavedConversationReference.ServiceUrl != null && userstate.SavedConversationReference.ServiceUrl != string.Empty)
{
MicrosoftAppCredentials.TrustServiceUrl(userstate.SavedConversationReference.ServiceUrl);
}
else if (turnContext.Activity.ServiceUrl != null && turnContext.Activity.ServiceUrl != string.Empty)
{
MicrosoftAppCredentials.TrustServiceUrl(turnContext.Activity.ServiceUrl);
}
else
{
MicrosoftAppCredentials.TrustServiceUrl("https://facebook.botframework.com/");
}
if(userstate.Reminders != null)
{
foreach (var reminder in userstate.Reminders)
{
if (reminder.DateAndTime.TrimMilliseconds() == DateTimeNowInGmt().TrimMilliseconds())
{
var timeProperty = new TimexProperty(reminder.DateAndTimeTimex);
var naturalDate = timeProperty.ToNaturalLanguage(DateTimeNowInGmt().TrimMilliseconds());
await turnContext.SendActivityAsync($"It's {DateTimeNowInGmt().ToLongDateString()}. \n\nReminding you to {reminder.Subject}.");
}
}
}
}
public static DateTime DateTimeNowInGmt()
{
TimeZoneInfo phTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Taipei Standard Time");
var t = DateTime.Now;
DateTime phTime = TimeZoneInfo.ConvertTime(t, phTimeZone);
return phTime;
}
}
}

Reopen TPL Dataflow input after marking it complete

I'm trying to make a processing pipeline service that users can place an item into and wait for the results to finish being processed. My idea is to use DI to have it inject able.
The problem I'm facing is that after processing the first set of data and marking the input block as complete, it remains closed when I try processing another set of data. Is there a way to reopen the pipeline to allow data processing again?
I'm also using a library ontop of TPL Dataflow called DataflowEx.
public interface IPipelineService
{
Task FillPipeline(object inputObj);
Task WaitForResults();
Task<List<object>> GetResults();
Task FlushPipeline();
Task Complete();
}
public class Pipeline : Dataflow<object>, IPipelineService
{
private TransformBlock<object, object> _inputBlock;
private ActionBlock<object> _resultBlock;
private List<object> _results { get; set; }
public Pipeline() : base(DataflowOptions.Default)
{
_results = new List<object>();
_inputBlock = new TransformBlock<object, object>(obj => Processing.Processing.ReceiveOrder(obj));
_resultBlock = new ActionBlock<object>(obj => _results.Add(Processing.Processing.ReturnProcessedOrder(obj)));
_inputBlock.LinkTo(_resultBlock, new DataflowLinkOptions() { PropagateCompletion = true });
RegisterChild(_inputBlock);
RegisterChild(_resultBlock);
}
public Task FillPipeline(object inputObj)
{
//InputBlock.Post(inputObj);
return Task.CompletedTask;
}
public async Task WaitForResults()
{
await this.CompletionTask;
}
public Task<List<object>> GetResults()
{
return Task.FromResult(_results);
}
public Task FlushPipeline()
{
_results = new List<object>();
return Task.CompletedTask;
}
Task IPipelineService.Complete()
{
InputBlock.Complete();
return Task.CompletedTask;
}
public override ITargetBlock<object> InputBlock { get { return _inputBlock; } }
public object Result { get { return _results; } }
}
This the basic example I'm working with at the moment to test this idea.
This is how I want to be able to use it and be able to have items be fed into it after it has finished processing the first set.
await _pipelineService.FillPipeline(new GenerateOrder(OrderType.HomeLoan).order);
await _pipelineService.FillPipeline(new GenerateOrder(OrderType.OtherLoan).order);
await _pipelineService.FillPipeline(new GenerateOrder(OrderType.PersonalLoan).order);
await _pipelineService.FillPipeline(new GenerateOrder(OrderType.CarLoan).order);
await _pipelineService.Complete();
await _pipelineService.WaitForResults();
You can't restart a completed dataflow set - I just reset my objects to start again (in this case I call ResetDataFlow in CompleteAsync())
public class DownloadConnector
{
public DownloadDataFlow DataFlow { get; set; }
public DownloadConnector(int maxDop)
{
DataFlow = new DownloadDataFlow(maxDop);
}
public async Task SendAsync(DownloadItem item)
{
await DataFlow.BufferBlock.SendAsync(item);
}
public async Task CompleteAsync()
{
DataFlow.BufferBlock.Complete();
await DataFlow.ActionBlock.Completion;
DataFlow.ResetDataFlow();
}
}
public class DownloadDataFlow
{
public BufferBlock<DownloadItem> BufferBlock { get; set; }
public TransformBlock<DownloadItem, DownloadItem> TransformBlock { get; set; }
public ActionBlock<DownloadItem> ActionBlock { get; set; }
public int MaxDop { get; set; }
public DownloadDataFlow(int maxDop)
{
MaxDop = maxDop;
ResetDataFlow();
}
public DownloadDataFlow ResetDataFlow()
{
BufferBlock = new BufferBlock<DownloadItem>();
TransformBlock = new TransformBlock<DownloadItem, DownloadItem>(DownloadAsync);
ActionBlock = new ActionBlock<DownloadItem>(OnCompletion, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = MaxDop });
BufferBlock.LinkTo(TransformBlock, new DataflowLinkOptions { PropagateCompletion = true });
TransformBlock.LinkTo(ActionBlock, new DataflowLinkOptions { PropagateCompletion = true });
return this;
}
public async Task DownloadAsync(DownloadItem item)
{
...
}
public async Task OnCompletion(DownloadItem item)
{
...
}
}
public class DownloadItem
{
...
}
And the code is run using:
var connector = new DownloadConnector(10);
await connector.SendAsync(new DownloadItem());
await connector.SendAsync(new DownloadItem());
await connector.SendAsync(new DownloadItem());
await connector.SendAsync(new DownloadItem());
await connector.CompleteAsync();
await connector.SendAsync(new DownloadItem());
await connector.SendAsync(new DownloadItem());
await connector.SendAsync(new DownloadItem());
await connector.SendAsync(new DownloadItem());
await connector.CompleteAsync();

Categories

Resources