At the time of the user's connection, everything is going well. When I have a product update (it becomes available for sale), the OnProductStartSales event is called.
But there is one problem, the message does not come to the client because the list of clients in the hub is disposed.
Here is the code of my hub.
public class SalesHub : Hub
{
private readonly ProductDatabaseListener _listener;
public SalesHub(ProductDatabaseListener listener)
{
_listener = listener ?? throw new ArgumentNullException(nameof(listener));
_listener.OnProductStartSales += (s, p) => ProductStartSales(p);
_listener.OnProductDataChanged += (s, p) => ProductDataChanged(p);
}
public async Task ListenProduct(string productId)
{
await this.Groups.AddToGroupAsync(Context.ConnectionId, productId);
}
private async Task ProductStartSales(Product product)
{
await this.Clients.Group(product.Id).SendAsync("StartSales", product.Id);
// await this.Clients.All.SendAsync("StartSales", product.Id);
}
private async Task ProductDataChanged(Product product)
{
await this.Clients.Group(product.Id).SendAsync("DataChanged", product);
}
}
Here is the code of listener.
public class ProductDatabaseListener
{
private readonly IRepository<Product> _repository;
private readonly object _locker = new object();
public ProductDatabaseListener(IServiceProvider serviceProvider)
{
using (var scope = serviceProvider.CreateScope())
{
_repository = scope.ServiceProvider.GetRequiredService<IRepository<Product>>() ?? throw new ArgumentNullException(nameof(_repository));
}
}
public event EventHandler<Product> OnProductStartSales;
public event EventHandler<Product> OnProductDataChanged;
// Need better performance...
public async Task ListenMongo()
{
while (true)
{
var products = await _repository.GetRange(0, int.MaxValue);
var date = DateTime.Now;
List<Task> tasks = new List<Task>();
foreach (var product in products)
{
if (product.IsSalesStart)
{
continue;
}
if (product.StartOfSales <= date)
{
product.IsSalesStart = true;
OnProductStartSales?.Invoke(this, product);
tasks.Add(_repository.Update(product));
}
}
Task.WaitAll(tasks.ToArray());
await Task.Delay(1000);
}
}
}
Here is the client code
"use strict";
var connection = new signalR.HubConnectionBuilder().withUrl("/salesHub").build();
connection.on("ReceiveMessage", function (id) {
var li = document.createElement("li")
document.getElementById("fromHub").appendChild(li)
li.textContent = id;
});
connection.on("startSales", function (id) {
var productId = document.getElementById("objectId").getAttribute("value");
if (productId == id) {
var button = document.getElementById("buy")
button.hidden = false
}
});
connection.logging = true;
connection.start().then(function () {
var productId = document.getElementById("objectId").getAttribute("value");
connection.invoke("ListenProduct", productId).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
}).catch(function (err) {
return console.error(err.toString());
});
Related
I have designed a simple JobProcessor using TPL Data flow (my first time using it). I want to be able to create jobs, and have them invoked and placed on a priority queue (the PriorityBufferBlock). My code structure is as follows
public interface IJob<TInput>
{
Task Execute(TInput input);
Priority Priority { get; }
}
where
public enum Priority
{
High,
Medium,
Low
}
and I have a custom version of the PriorityBufferBlock (taken from PriorityBufferBlock) with a custom refreshing cache implementation, to ensure clean up of the messages that are "reserved", this looks like
class PriorityBufferBlock<T> : ISourceBlock<T>, IReceivableSourceBlock<T>
{
private readonly BufferBlock<T> _highPriorityBuffer;
private readonly BufferBlock<T> _mediumPriorityBuffer;
private readonly BufferBlock<T> _lowPriorityBuffer;
private readonly RefreshingInMemoryCache<DataflowMessageHeader, ISourceBlock<T>> _messagesCache;
// ... More code here
}
The JobProcessor interface is
public interface IJobProcessor<TInput>
{
void RegisterHandler<TTask>(TInput input) where TTask : IJob<TInput>;
Task Enqueue(IJob<TInput> task);
}
with implementation as
public class JobProcessor<TInput> : IJobProcessor<TInput>
{
private readonly PriorityBufferBlock<IJob<TInput>> _priorityBufferBlock;
private readonly IOptions<AzureOptions> _options;
private readonly ILogger<IJobProcessor<TInput>> _logger;
private readonly CancellationToken _token;
public JobProcessor(
IClock clock,
IOptions<AzureOptions> options,
ILogger<IJobProcessor<TInput>> logger,
CancellationToken token)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger;
_token = token;
var dataflowBlockOptions = new DataflowBlockOptions { CancellationToken = token };
_priorityBufferBlock = new PriorityBufferBlock<IJob<TInput>>(dataflowBlockOptions, clock, options, logger);
_logger?.LogInformation($"{nameof(JobProcessor<TInput>)} initialized and configured successfully");
}
public void RegisterHandler<TJob>(TInput input) where TJob : IJob<TInput>
{
var actionBlock = new ActionBlock<IJob<TInput>>(
(job) => job.Execute(input),
new ExecutionDataflowBlockOptions
{
CancellationToken = _token,
MaxDegreeOfParallelism = _options.Value.TaskProcessorMaxDegreeOfParallelism
});
_priorityBufferBlock.LinkTo(
actionBlock,
new DataflowLinkOptions
{
PropagateCompletion = true
},
(task) => task is IJob<TInput>);
_logger?.LogInformation($"Handler for {typeof(TJob).Name} registered successfully");
}
public async Task Enqueue(IJob<TInput> task)
{
await _priorityBufferBlock.SendAsync(task, task.Priority);
_logger?.LogInformation($"Successfully enqueued {task.GetType().Name} for processing");
}
}
and this is proving to work well for everything except Exception handling. Because I am not awaiting the actionBlock, any exception from inside the running IJob are swallowed, as you would expect (here I am essentially doing Task.Run(() => throw new Exception());.
My question is, what is the best way to change the above so I can await and bubble the exceptions upwards to allow error responses from this API?
I have tried
public async Task RegisterHandler<TJob>(TInput input) where TJob : IJob<TInput>
{
var actionBlock = new ActionBlock<IJob<TInput>>(
(job) => job.Execute(input),
new ExecutionDataflowBlockOptions
{
CancellationToken = _token,
MaxDegreeOfParallelism = _options.Value.TaskProcessorMaxDegreeOfParallelism
});
await actionBlock.Completion;
_priorityBufferBlock.LinkTo(
actionBlock,
new DataflowLinkOptions
{
PropagateCompletion = true
},
(job) => job is IJob<TInput>);
_logger?.LogInformation($"Handler for {typeof(TJob).Name} registered successfully");
}
but this is not working and also makes no sense, I am a bit lost. Any help hugely appreciated. Thanks for your time.
NOTE:
An example of the working code via unit test is
[TestCase(1)]
[TestCase(3)]
[TestCase(6)]
public async Task EnsureConcurrencyLimitsAreNotExceeded(int maxDegreeOfParallelism)
{
var waitHandle = new ManualResetEventSlim(false);
var priorityBuffer = new ConcurrentQueue<Priority>();
_mockGeneralOptions.SetReturnsDefault(new AzureOptions()
{
PriorityBufferBlockExpiryMilliseconds = 10,
TaskProcessorMaxDegreeOfParallelism = maxDegreeOfParallelism
});
var jobProcessor = new JobProcessor<OptionalPair<ViewformTour, ViewformTour>>(
_mockClock.Object,
_mockGeneralOptions.Object,
_mockLogger.Object,
CancellationToken.None
);
var tour = new ViewformTour
{
Id = "_id"
};
jobProcessor.RegisterHandler<HighPriorityJob>(
new OptionalPair<ViewformTour, ViewformTour>(
Optional.From(tour),
Optional.None<ViewformTour>()
));
jobProcessor.RegisterHandler<LowPriorityJob>(
new OptionalPair<ViewformTour, ViewformTour>(
Optional.From(tour),
Optional.None<ViewformTour>()
));
var tasks = new List<Task>();
tasks.Add(jobProcessor.Enqueue(new LowPriorityJob(waitHandle, priorityBuffer)));
tasks.Add(jobProcessor.Enqueue(new LowPriorityJob(waitHandle, priorityBuffer)));
tasks.Add(jobProcessor.Enqueue(new LowPriorityJob(waitHandle, priorityBuffer)));
tasks.Add(jobProcessor.Enqueue(new LowPriorityJob(waitHandle, priorityBuffer)));
tasks.Add(jobProcessor.Enqueue(new LowPriorityJob(waitHandle, priorityBuffer)));
tasks.Add(jobProcessor.Enqueue(new LowPriorityJob(waitHandle, priorityBuffer)));
await Task.WhenAll(tasks.ToArray());
Thread.Sleep(150);
waitHandle.Set();
Assert.That(priorityBuffer, Is.Not.Null);
Assert.That(priorityBuffer.Count, Is.EqualTo(maxDegreeOfParallelism));
}
with
private class HighPriorityJob : IJob<OptionalPair<ViewformTour, ViewformTour>>
{
private ConcurrentQueue<Priority> _priorityBuffer;
public HighPriorityJob(ConcurrentQueue<Priority> priorityBuffer)
{
_priorityBuffer = priorityBuffer;
}
public Task Execute(OptionalPair<ViewformTour, ViewformTour> input)
{
_priorityBuffer.Enqueue(Priority);
return Task.CompletedTask;
}
public Priority Priority => Priority.High;
}
private class LowPriorityJob : IJob<OptionalPair<ViewformTour, ViewformTour>>
{
private ManualResetEventSlim _waitHandle;
private ConcurrentQueue<Priority> _priorityBuffer;
private int _delayMilliseconds;
private bool _setWaitHandle = false;
public LowPriorityJob(
ManualResetEventSlim waitHandle,
ConcurrentQueue<Priority> priorityBuffer,
bool setWaitHandle = false,
int delayMilliseconds = 100)
{
_waitHandle = waitHandle;
_priorityBuffer = priorityBuffer;
_delayMilliseconds = delayMilliseconds;
_setWaitHandle = setWaitHandle;
}
public async Task Execute(OptionalPair<ViewformTour, ViewformTour> input)
{
await Task.Delay(_delayMilliseconds);
_priorityBuffer.Enqueue(Priority);
if (_setWaitHandle)
{
_waitHandle.Set();
}
}
public Priority Priority => Priority.Low;
}
EDIT II:
Okay, so I have now tried the following - the RegisterHandler now returns the ActionBlock on registration, I then set up a continuation
public ActionBlock<IJob<TInput>> RegisterHandler<TJob>(TInput input) where TJob : IJob<TInput>
{
var actionBlock = new ActionBlock<IJob<TInput>>(
(job) => job.Execute(input),
new ExecutionDataflowBlockOptions
{
CancellationToken = _token,
MaxDegreeOfParallelism = _options.Value.TaskProcessorMaxDegreeOfParallelism
});
_priorityBufferBlock.LinkTo(
actionBlock,
new DataflowLinkOptions
{
PropagateCompletion = true
},
(job) => job is IJob<TInput>);
_logger?.LogInformation($"Handler for {typeof(TJob).Name} registered successfully");
return actionBlock;
}
Then in my test
[Test]
public async Task DoesHandleExceptionGracefully()
{
var priorityBuffer = new ConcurrentQueue<Priority>();
var jobProcessor = new JobProcessor<OptionalPair<ViewformTour, ViewformTour>>(
_mockClock.Object,
_mockGeneralOptions.Object,
_mockLogger.Object,
CancellationToken.None
);
var tour = new ViewformTour
{
Id = "_id"
};
try
{
var handler = jobProcessor.RegisterHandler<ThrowingHighPriorityJob>(
new OptionalPair<ViewformTour, ViewformTour>(
Optional.From(tour),
Optional.None<ViewformTour>()
));
await jobProcessor.Enqueue(new ThrowingHighPriorityJob());
await handler.Completion.ContinueWith(ant =>
{
throw new Exception("... From Continuation");
}, TaskContinuationOptions.OnlyOnFaulted);
}
catch (Exception ex)
{
Console.WriteLine($"External capture{ex.Message}");
}
}
This outputs "External capture... From continuation", we have externalized the exception handling. HOWEVER, the continuation setup is now blocking. In production, I want to enqueue jobs dynamically, and this prevents that. :'[
In the end, there was no way to do this cleanly without breaking the control flow and go against the "design principles" of the library. So, to do this, I merely embraced the pipeline/dataflow mantra and created a IPoisonQueue
public class PoisonQueue<TInput> : IPoisonQueue<TInput>
{
private readonly IPriorityBufferBlock<IJob<TInput>> _priorityBufferBlock;
public PoisonQueue(IPriorityBufferBlock<IJob<TInput>> priorityBufferBlock)
{
_priorityBufferBlock = priorityBufferBlock ?? throw new ArgumentNullException(nameof(priorityBufferBlock));
}
public async Task Enqueue(IJob<TInput> job)
{
// Push the PoisonJob onto the buffer block...
}
}
Then I have gone further with the DI for the JobProcessor and unit tested this to high heaven, the final implementation is
public class JobProcessor<TInput> : IJobProcessor<TInput>
{
private readonly IPriorityBufferBlock<IJob<TInput>> _priorityBufferBlock;
private readonly IPoisonQueue<TInput> _poisonQueue;
private readonly IOptions<GeneralOptions> _options;
private readonly ILogger<IJobProcessor<TInput>> _logger;
public JobProcessor(
IPriorityBufferBlock<IJob<TInput>> priorityBufferBlock,
IPoisonQueue<TInput> poisonQueue,
IClock clock,
IOptions<GeneralOptions> options,
ILogger<IJobProcessor<TInput>> logger)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger;
_priorityBufferBlock = priorityBufferBlock ?? throw new ArgumentNullException(nameof(priorityBufferBlock));
_priorityBufferBlock.Initialize();
_poisonQueue = poisonQueue ?? throw new ArgumentNullException(nameof(poisonQueue));
_logger?.LogInformation($"{nameof(JobProcessor<TInput>)} initialized and configured successfully");
}
public void RegisterHandler<TJob>(TInput input) where TJob : IJob<TInput>
{
var actionBlock = new ActionBlock<IJob<TInput>>(
async (job) =>
{
var jobType = job.GetType().Name;
var retryCount = _options.Value.JobProcessorRetryCount;
var retryIntervalMilliseconds = _options.Value.JobProcessorRetryIntervalMilliseconds;
var retryPolicy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(
retryCount,
retryAttempt => TimeSpan.FromMilliseconds(Math.Pow(retryIntervalMilliseconds, retryAttempt)));
try
{
await retryPolicy.ExecuteAsync(async () =>
{
_logger?.LogInformation("Starting execution of job {JobType}...", jobType);
await job.Execute(input);
});
}
catch (Exception ex)
{
_logger?.LogError(ex, $"{jobType} failed");
_logger?.LogWarning("Adding job {JobType} to poison queue...", jobType);
await _poisonQueue.Enqueue(job);
}
},
new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = _options.Value.JobProcessorMaxDegreeOfParallelism
});
_priorityBufferBlock.LinkTo(
actionBlock,
new DataflowLinkOptions
{
PropagateCompletion = true
},
(job) => job is IJob<TInput>);
_logger?.LogInformation($"Handler for {typeof(TJob).Name} registered successfully");
}
public async Task Enqueue(IJob<TInput> job)
{
await _priorityBufferBlock.SendAsync(job, job.Priority);
_logger?.LogInformation($"Successfully enqueued {job.GetType().Name} for processing");
}
}
Here I have use "Polly" for the retry and backoff, and once the retries are complete (and we still have failure), I push a PoisonJob on to the PriorityBufferBlock and the errors are handled in the same way other jobs are. It seems to work well, fingers crossed my test coverage is complete and correct.
I have a Microsoft Chatbot C# code,which has a TimerCallback function, which runs after some inactive time by user. This function gets hit when running code locally, but the same function does not get hit, when deployed on Azure environment.
Appologies for too much logging lines in the code, it was just to verify that function gets hit on deployed environment or not.
Here is the whole code:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Lebara.Crm.Bot.Core.Data;
using Lebara.Crm.Bot.Core.ServiceContracts;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.AspNetCore.Mvc;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.Extensions.Logging;
namespace Lebara.Crm.Bot.Services
{
public class ChatSessionTimeoutMiddleware : ActivityHandler, IMiddleware
{
private readonly IDistributedCache _distributedCache;
private readonly string _cachekey;
private readonly BotOptions _botOptions;
private readonly ISystemMessageSender _systemMessageSender;
private readonly CustomConversationStateAccessors _customConversationStateAccessors;
private readonly ITelemetryManager _telemetryManager;
private readonly ISessionHandler _sessionHandler;
private readonly IServiceProvider _serviceProvider;
private static ConcurrentDictionary<string, Timer> _sessionTimers = new ConcurrentDictionary<string, Timer>();
private readonly IBotFrameworkHttpAdapter _adapter;
private readonly string _appId;
private readonly ConcurrentDictionary<string, ConversationReference> _conversationReferences;
private string ObjectKey;
private readonly TelemetryClient telemetry = new TelemetryClient();
private readonly ILogger<ChatSessionTimeoutMiddleware> _logger;
// Timer related variables, made global, to avoid collected by Garbage Collector as they were created in memory previously.
private Timer _warningTimer = null;
private Timer _warningTimerGCIssue = null;
private Timer _timer = null;
private Timer _timerGCIssue = null;
public ChatSessionTimeoutMiddleware(
IDistributedCache distributedCache,
IConfiguration configuration,
IOptions<BotOptions> options,
ISystemMessageSender systemMessageSender,
CustomConversationStateAccessors customConversationStateAccessors,
ITelemetryManager telemetryManager,
ISessionHandler sessionHandler,
IServiceProvider serviceProvider,
IBotFrameworkHttpAdapter adapter,
ConcurrentDictionary<string, ConversationReference> conversationReferences,
ILogger<ChatSessionTimeoutMiddleware> logger)
{
_cachekey = $"{configuration["RedisCachingRoot"]}session-timeout:";
_distributedCache = distributedCache;
_botOptions = options.Value;
_systemMessageSender = systemMessageSender;
_customConversationStateAccessors = customConversationStateAccessors;
_telemetryManager = telemetryManager;
_sessionHandler = sessionHandler;
_serviceProvider = serviceProvider;
_adapter = adapter;
_conversationReferences = conversationReferences;
_appId = configuration["MicrosoftAppId"] ?? string.Empty;
_logger = logger;
}
private void AddConversationReference(Activity activity)
{
var conversationReference = activity.GetConversationReference();
_conversationReferences.AddOrUpdate(conversationReference.User.Id, conversationReference, (key, newValue) => conversationReference);
}
public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate nextTurn, CancellationToken cancellationToken)
{
_logger.LogDebug("chat session timeout middleware");
//telemetry.TrackEvent("ChatSessionMiddleware - OnTurnAsync");
//telemetry.TrackTrace("ChatSessionMiddleware - OnTurnAsync", SeverityLevel.Warning, null);
var customConversationState = await _customConversationStateAccessors.CustomConversationState.GetAsync(turnContext, () => new CustomConversationState());
var hasChatSessionRunning = await _sessionHandler.HasRunningSessionAsync(turnContext.Activity.Conversation.Id);
if (turnContext.Activity.Type == ActivityTypes.Message
&& !string.IsNullOrEmpty(turnContext.Activity.Text)
&& !hasChatSessionRunning)
{
_logger.LogDebug("chatsessiontimeout OnTurnAsync if statement");
var key = _cachekey + turnContext.Activity.Conversation.Id;
_logger.LogDebug($"Key {key}");
var warningKey = "warning_" + key;
_logger.LogDebug($"WarningKey {warningKey}");
var period = _botOptions.InactivityPeriod;
_logger.LogDebug($"chatsessiontimeout period {period}");
var warningPeriod = _botOptions.WarningInactivityPeriod;
_logger.LogDebug($"chatsessiontimeout warningPeriod {warningPeriod}");
var cacheOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = period.Add(period)
};
_logger.LogDebug($"cacheOptions {cacheOptions}");
AddConversationReference(turnContext.Activity as Activity);
await _distributedCache.SetStringAsync(key, JsonConvert.SerializeObject(DateTime.Now.Add(period)), cacheOptions);
await _distributedCache.SetStringAsync(warningKey, JsonConvert.SerializeObject(DateTime.Now.Add(warningPeriod)), cacheOptions);
var timerPeriod = period.Add(TimeSpan.FromSeconds(1));
var warningTimePeriod = warningPeriod.Add(TimeSpan.FromSeconds(1));
_warningTimer = null;
_warningTimerGCIssue = null;
_sessionTimers.TryGetValue(warningKey, out _warningTimer);
_logger.LogDebug($"warningTimer {_warningTimer}");
if (_warningTimer == null)
{
_logger.LogDebug("warningTimer is null");
_warningTimerGCIssue = new Timer(new TimerCallback(WarningCallback), (warningKey, turnContext), warningTimePeriod, warningTimePeriod);
_warningTimer = _sessionTimers.GetOrAdd(warningKey, _warningTimerGCIssue);
}
_timer = null;
_timerGCIssue = null;
_sessionTimers.TryGetValue(key, out _timer);
_logger.LogDebug($"timer {_timer}");
if (_timer == null)
{
_logger.LogDebug("timer is null");
_logger.LogDebug($"key {key}");
_timerGCIssue = new Timer(new TimerCallback(Callback), (key, turnContext), timerPeriod, timerPeriod);
_timer = _sessionTimers.GetOrAdd(key, _timerGCIssue);
}
_warningTimer.Change(warningTimePeriod, warningTimePeriod);
_logger.LogDebug("chatSessionTimeoutMiddleware timer change");
_timer.Change(timerPeriod, timerPeriod);
}
_logger.LogDebug("chatSessionTimeoutMiddleware nextturn");
await nextTurn(cancellationToken).ConfigureAwait(false);
}
private async void Callback(object target)
{
//telemetry.TrackEvent("ChatSessionMiddleware - InactivityCallback");
_logger.LogDebug("ChatSessionMiddleware InactivityCallback");
var tuple = ((string, ITurnContext))target;
ObjectKey = tuple.Item1;
var turnContext = tuple.Item2;
foreach (var conversationReference in _conversationReferences.Values)
{
_logger.LogDebug("ChatSessionMiddleware InactivityCallback for loop");
await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, conversationReference, EndOfChatCallback, default(CancellationToken));
}
}
private async void WarningCallback(object target)
{
//telemetry.TrackEvent("ChatSessionMiddleware - WarningCallback");
_logger.LogDebug("ChatSessionMiddleware WarningCallback");
var tuple = ((string, ITurnContext))target;
ObjectKey = tuple.Item1;
var turnContext = tuple.Item2;
foreach (var conversationReference in _conversationReferences.Values)
{
_logger.LogDebug("ChatSessionMiddleware WarningCallback for loop");
await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, conversationReference, WarningMessageCallback, default(CancellationToken));
}
}
private async Task WarningMessageCallback(ITurnContext turnContext, CancellationToken cancellationToken)
{
//telemetry.TrackEvent("ChatSessionMiddleware - WarningMessageCallback");
_logger.LogDebug("ChatSessionMiddleware WarningMessageCallback");
var customConversationState = await _customConversationStateAccessors.CustomConversationState.GetAsync(turnContext, () => new CustomConversationState());
void DisposeTimer()
{
bool found = _sessionTimers.TryRemove(ObjectKey, out var timer);
if (found)
{
timer.Dispose();
timer = null;
}
}
var json = await _distributedCache.GetStringAsync(ObjectKey);
var hasChatSessionRunning = await _sessionHandler.HasRunningSessionAsync(turnContext.Activity.Conversation.Id);
if (hasChatSessionRunning)
{
DisposeTimer();
return;
}
if (!string.IsNullOrEmpty(json))
{
var sessionEnd = JsonConvert.DeserializeObject<DateTime>(json);
if (DateTime.Now >= sessionEnd)
{
//telemetry.TrackEvent("ChatSessionMiddleware - SendingWarningMessage");
_logger.LogDebug("ChatSessionMiddleware SendingWarningMessage");
await _systemMessageSender.SendSystemMessage(turnContext, customConversationState, turnContext.Activity, ResourceIds.BotWarningEndOfChat);
}
}
DisposeTimer();
}
private async Task EndOfChatCallback(ITurnContext turnContext, CancellationToken cancellationToken)
{
//telemetry.TrackEvent("ChatSessionMiddleware - EndOfChatCallback");
_logger.LogDebug("ChatSessionMiddleware EndOfChatCallback");
var chatSdk = (IChatProvider)_serviceProvider.GetService(typeof(IChatProvider));
var customConversationState = await _customConversationStateAccessors.CustomConversationState.GetAsync(turnContext, () => new CustomConversationState());
void DisposeTimer()
{
bool found = _sessionTimers.TryRemove(ObjectKey, out var timer);
if (found)
{
timer.Dispose();
timer = null;
}
}
var json = await _distributedCache.GetStringAsync(ObjectKey);
var hasChatSessionRunning = await _sessionHandler.HasRunningSessionAsync(turnContext.Activity.Conversation.Id);
if (hasChatSessionRunning)
{
DisposeTimer();
return;
}
if (!string.IsNullOrEmpty(json))
{
var sessionEnd = JsonConvert.DeserializeObject<DateTime>(json);
if (DateTime.Now >= sessionEnd)
{
var parts = ObjectKey.Split(new char[] { ':' });
var dict = new Dictionary<string, string>
{
{"EndTime", json },
{"State", JsonConvert.SerializeObject(customConversationState) }
};
_telemetryManager.TrackEvent("AutomaticChatClosing", parts[parts.Length - 1], dict);
DisposeTimer();
//telemetry.TrackEvent("ChatSessionMiddleware - SendingEndOfChatMessage");
_logger.LogDebug("ChatSessionMiddleware SendingEndOfChatMessage");
await _systemMessageSender.SendSystemMessage(turnContext, customConversationState, turnContext.Activity, ResourceIds.BotAutomaticEndOfChat);
await Task.Delay(2000);
await chatSdk.EndChat(customConversationState.ChatContext, turnContext);
}
}
else
{
DisposeTimer();
}
}
}
}
The code creating issue/ or not getting hit:
if (_warningTimer == null)
{
_logger.LogDebug("warningTimer is null");
_warningTimerGCIssue = new Timer(new TimerCallback(WarningCallback), (warningKey, turnContext), warningTimePeriod, warningTimePeriod);
_warningTimer = _sessionTimers.GetOrAdd(warningKey, _warningTimerGCIssue);
}
the above mentioned part should call the WarningCallback function after a specific warning time, it does call it successfully while running the code locally, but does not call on deployed environment.
There is a JSON-RPC API, which I'm currently implementing. It can be tested here.
The problem is that if an incorrect DTO model is passed to SendAsync<TResponse>, JsonSerializer.Deserialize is going to throw a JsonException, which is not handled by my code. I know I've got to use SetException in some way, but I don't know how to do it, so here is the question. The exception message should be printed in the console as well.
public sealed class Client : IDisposable
{
...
private readonly ConcurrentDictionary<long, IResponseHandler> _handlers = new();
...
public Task StartAsync(CancellationToken cancellationToken)
{
_ = Task.Run(async () =>
{
await foreach (var message in _client.Start(cancellationToken))
{
using var response = JsonDocument.Parse(message);
try
{
var requestId = response.RootElement.GetProperty("id").GetInt32();
// TODO: Handle JsonException errors via `SetException`?
// TODO: Show error when incorrect input parameters are filled
if (_handlers.ContainsKey(requestId))
{
_handlers[requestId].SetResult(message);
_handlers.TryRemove(requestId, out _);
}
}
catch (KeyNotFoundException)
{
// My point is that a message should be processed only if it doesn't include `id`,
// because that means that the message is an actual web socket subscription.
_messageReceivedSubject.OnNext(message);
}
}
}, cancellationToken);
...
return Task.CompletedTask;
}
public Task<TResponse> SendAsync<TResponse>(string method, object #params)
{
var request = new JsonRpcRequest<object>
{
JsonRpc = "2.0",
Id = NextId(),
Method = method,
Params = #params
};
//var tcs = new TaskCompletionSource<TResponse>();
//_requestManager.Add(request.Id, request, tcs);
var handler = new ResponseHandlerBase<TResponse>();
_handlers[request.Id] = handler;
var message = JsonSerializer.Serialize(request);
_ = _client.SendAsync(message);
return handler.Task;
//return tcs.Task;
}
public async Task<AuthResponse?> AuthenticateAsync(string clientId, string clientSecret)
{
var #params = new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", clientId},
{"client_secret", clientSecret}
};
var response = await SendAsync<SocketResponse<AuthResponse>>("public/auth", #params).ConfigureAwait(false);
return response.Result;
}
...
private interface IResponseHandler
{
void SetResult(string payload);
}
private class ResponseHandlerBase<TRes> : IResponseHandler
{
private readonly TaskCompletionSource<TRes> _tcs = new();
public Task<TRes> Task => _tcs.Task;
public void SetResult(string payload)
{
var result = JsonSerializer.Deserialize(payload, typeof(TRes));
_tcs.SetResult((TRes) result);
}
}
}
Coincidentally, I did something very similar while live-coding a TCP/IP chat application last week.
Since in this case you already have an IAsyncEnumerable<string> - and since you can get messages other than responses - I recommend also exposing that IAsyncEnumerable<string>:
public sealed class Client : IDisposable
{
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
// TODO: parse and handle responses for our requests
yield return message;
}
}
}
You can change this to be Rx-based if you want (_messageReceivedSubject.OnNext), but I figure if you already have IAsyncEnumerable<T>, then you may as well keep the same abstraction.
Then, you can parse and detect responses, passing along all other messages:
public sealed class Client : IDisposable
{
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
var (requestId, response) = TryParseResponse(message);
if (requestId != null)
{
// TODO: handle
}
else
{
yield return message;
}
}
(long? RequestId, JsonDocument? Response) TryParseResponse(string message)
{
try
{
var document = JsonDocument.Parse(message);
var requestId = response.RootElement.GetProperty("id").GetInt32();
return (document, requestId);
}
catch
{
return (null, null);
}
}
}
}
Then, you can define your collection of outstanding requests and handle messages that are for those requests:
public sealed class Client : IDisposable
{
private readonly ConcurrentDictionary<int, TaskCompletionSource<JsonDocument>> _requests = new();
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
var (requestId, response) = TryParseResponse(message);
if (requestId != null && _requests.TryRemove(requestId.Value, out var tcs))
{
tcs.TrySetResult(response);
}
else
{
yield return message;
}
}
(long? RequestId, JsonDocument? Response) TryParseResponse(string message)
{
try
{
var document = JsonDocument.Parse(message);
var requestId = response.RootElement.GetProperty("id").GetInt32();
return (document, requestId);
}
catch
{
return (null, null);
}
}
}
}
Note the usage of ConcurrentDictionary.TryRemove, which is safer than accessing the value and then removing it.
Now you can write your general SendAsync. As I note in my video, I prefer to split up the code that runs synchronously in SendAsync and the code that awaits the response:
public sealed class Client : IDisposable
{
...
public Task<TResponse> SendAsync<TResponse>(string method, object #params)
{
var request = new JsonRpcRequest<object>
{
JsonRpc = "2.0",
Id = NextId(),
Method = method,
Params = #params,
};
var tcs = new TaskCompletionSource<JsonDocument>(TaskCreationOptions.RunContinuationsAsynchronously);
_requests.TryAdd(request.Id, tcs);
return SendRequestAndWaitForResponseAsync();
async Task<TResponse> SendRequestAndWaitForResponseAsync()
{
var message = JsonSerializer.Serialize(request);
await _client.SendAsync(message);
var response = await tcs.Task;
return JsonSerializer.Deserialize(response, typeof(TResponse));
}
}
}
I've removed the "handler" concept completely, since it was just providing the type for JsonSerializer.Deserialize. Also, by using a local async method, I can use the async state machine to propagate exceptions naturally.
Then, your higher-level methods can be built on this:
public sealed class Client : IDisposable
{
...
public async Task<AuthResponse?> AuthenticateAsync(string clientId, string clientSecret)
{
var #params = new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", clientId},
{"client_secret", clientSecret}
};
var response = await SendAsync<SocketResponse<AuthResponse>>("public/auth", #params);
return response.Result;
}
}
So the final code ends up being:
public sealed class Client : IDisposable
{
private readonly ConcurrentDictionary<int, TaskCompletionSource<JsonDocument>> _requests = new();
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
var (requestId, response) = TryParseResponse(message);
if (requestId != null && _requests.TryRemove(requestId.Value, out var tcs))
{
tcs.TrySetResult(response);
}
else
{
yield return message;
}
}
(long? RequestId, JsonDocument? Response) TryParseResponse(string message)
{
try
{
var document = JsonDocument.Parse(message);
var requestId = response.RootElement.GetProperty("id").GetInt32();
return (document, requestId);
}
catch
{
return (null, null);
}
}
}
public Task<TResponse> SendAsync<TResponse>(string method, object #params)
{
var request = new JsonRpcRequest<object>
{
JsonRpc = "2.0",
Id = NextId(),
Method = method,
Params = #params,
};
var tcs = new TaskCompletionSource<JsonDocument>(TaskCreationOptions.RunContinuationsAsynchronously);
_requests.TryAdd(request.Id, tcs);
return SendRequestAndWaitForResponseAsync();
async Task<TResponse> SendRequestAndWaitForResponseAsync()
{
var message = JsonSerializer.Serialize(request);
await _client.SendAsync(message);
var response = await tcs.Task;
return JsonSerializer.Deserialize(response, typeof(TResponse));
}
}
public async Task<AuthResponse?> AuthenticateAsync(string clientId, string clientSecret)
{
var #params = new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", clientId},
{"client_secret", clientSecret}
};
var response = await SendAsync<SocketResponse<AuthResponse>>("public/auth", #params);
return response.Result;
}
}
You may also want to check out David Fowler's Project Bedrock, which may simplify this code quite a bit.
I'm stuck and the docks for the lib are unhelpful. Given the below saga definition:
public class GetOrdersStateMachine : MassTransitStateMachine<GetOrdersState>
{
public State? FetchingOrdersAndItems { get; private set; }
public Event<GetOrders>? GetOrdersIntegrationEventReceived { get; private set; }
public GetOrdersStateMachine()
{
Initially(
When(GetOrdersIntegrationEventReceived)
.Activity(AddAccountIdToState)
.TransitionTo(FetchingOrdersAndItems));
}
private EventActivityBinder<GetOrdersState, GetOrders> AddAccountIdToState(
IStateMachineActivitySelector<GetOrdersState, GetOrders> sel) =>
sel.OfType<AddAccountIdToStateActivity>();
}
And the below activity definition:
public class AddAccountIdToStateActivity : Activity<GetOrdersState, GetOrders>
{
private readonly IPartnerService _partnerService;
public AddAccountIdToStateActivity(IPartnerService partnerService) => _partnerService = partnerService;
public void Probe(ProbeContext context) =>
context.CreateScope($"GetOrders{nameof(AddAccountIdToStateActivity)}");
public void Accept(StateMachineVisitor visitor) => visitor.Visit(this);
public async Task Execute(
BehaviorContext<GetOrdersState, GetOrders> context,
Behavior<GetOrdersState, GetOrders> next)
{
context.Instance.AccountId = await _partnerService.GetAccountId(context.Data.PartnerId);
await next.Execute(context);
}
public Task Faulted<TException>(
BehaviorExceptionContext<GetOrdersState, GetOrders, TException> context,
Behavior<GetOrdersState, GetOrders> next) where TException : Exception =>
next.Faulted(context);
}
And the below test definition:
var machine = new GetOrdersStateMachine();
var harness = new InMemoryTestHarness();
var sagaHarness = harness.StateMachineSaga<GetOrdersState, GetOrdersStateMachine>(machine);
var #event = new GetOrders("1", new[] {MarketplaceCode.De}, DateTime.UtcNow);
await harness.Start();
try
{
await harness.Bus.Publish(#event);
await harness.Bus.Publish<ListOrdersErrorResponseReceived>(new
{
#event.CorrelationId,
AmazonError = "test"
});
var errorMessages = sagaHarness.Consumed.Select<ListOrdersErrorResponseReceived>().ToList();
var sagaResult = harness.Published.Select<AmazonOrdersReceived>().ToList();
var state = sagaHarness.Sagas.Contains(#event.CorrelationId);
harness.Consumed.Select<GetOrders>().Any().Should().BeTrue();
sagaHarness.Consumed.Select<GetOrders>().Any().Should().BeTrue();
harness.Consumed.Select<ListOrdersErrorResponseReceived>().Any().Should().BeTrue();
errorMessages.Any().Should().BeTrue();
sagaResult.First().Context.Message.IsFaulted.Should().BeTrue();
errorMessages.First().Context.Message.CorrelationId.Should().Be(#event.CorrelationId);
errorMessages.First().Context.Message.AmazonError.Should().Be("test");
state.IsFaulted.Should().BeTrue();
}
finally
{
await harness.Stop();
}
As you can see, the AddAccountToStateActivity has a dependency on the IPartnerService. I can't figure a way to configure that dependency.There's nothing in the docs and neither can I find anything on the github. How do I do it?
Thanks to the help of one of the library's authors I ended up writing this code:
private static (InMemoryTestHarness harness, IStateMachineSagaTestHarness<GetOrdersState, GetOrdersStateMachine> sagaHarness) ConfigureAndGetHarnesses()
{
var provider = new ServiceCollection()
.AddMassTransitInMemoryTestHarness(cfg =>
{
cfg.AddSagaStateMachine<GetOrdersStateMachine, GetOrdersState>().InMemoryRepository();
cfg.AddSagaStateMachineTestHarness<GetOrdersStateMachine, GetOrdersState>();
})
.AddLogging()
.AddSingleton(Mock.Of<IPartnerService>())
.AddSingleton(Mock.Of<IStorage>())
.BuildServiceProvider(true);
var harness = provider.GetRequiredService<InMemoryTestHarness>();
var sagaHarness = provider
.GetRequiredService<IStateMachineSagaTestHarness<GetOrdersState, GetOrdersStateMachine>>();
return (harness, sagaHarness);
}
As you can see I'm registering my mocks with the ServiceProvider.
I'm trying to write an Unit Test for my ASP.Net Core application with XUnit framework and MOQ and am trying to test the below method(snippet given below):
public async Task<IActionResult> Save([FromBody] DTO.ContactUs contactUs)
{
contactUs.FirstName = _htmlEncoder.Encode(contactUs.FirstName);
contactUs.LastName = _htmlEncoder.Encode(contactUs.LastName);
contactUs.EmailAddress = _htmlEncoder.Encode(contactUs.EmailAddress);
contactUs.Phone = _htmlEncoder.Encode(contactUs.Phone);
if (HttpContext.User.CurrentClient() != null)
contactUs.ClientId = HttpContext.User.CurrentClient().ClientId;
contactUs.UserId = User.GetUserId();
string dbName = HttpContext.User.CurrentClient().ConnectionString;
var result = _clientService.AddNewContactUs(contactUs, dbName);
if (result)
{
try
{
int clientId = HttpContext.User.CurrentClient().ClientId;
var clientDetails = _clientService.GetClientDetailsByClientID(clientId);
// Lines of code...
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
return Json(result);
}
While I can mock all the other dependent services, I'm kind of stuck with the HttpContext part. I am not able to mock the HttpContext.User.CurrentClient() part where HttpContext.User is of type ClaimsPrincipal and CurrentClient is an user-defined function, defined as:
public static Client CurrentClient(this ClaimsPrincipal principal)
{
if (!string.IsNullOrEmpty(principal.Claims.Single(p => p.Type.Equals(AppClaimTypes.CurrentClient)).Value))
{
int clientId = Convert.ToInt32(principal.Claims.Single(p => p.Type.Equals(AppClaimTypes.CurrentClient)).Value);
return principal.GetClients().Where(c => c.ClientId == clientId).FirstOrDefault();
}
else
{
return null;
}
}
This is my UnitTest class that I have managed to write till now:
public class ContactUsControllerTests
{
private Mock<IClientService> clientServiceMock;
private Mock<IWebHostEnvironment> webHostEnvironmentMock;
private Mock<HtmlEncoder> htmlEncoderObjMock;
private Mock<IEmailNotification> emailNotificationMock;
private Mock<HttpContext> mockContext;
private Mock<HttpRequest> mockRequest;
private Mock<ClaimsPrincipal> mockClaimsPrincipal;
private ContactUs contactUsObj = new ContactUs()
{
FirstName = "TestFN",
LastName = "TestLN",
EmailAddress = "testemail#gmail.com",
Phone = "4564560000",
Comments = "This is just a test"
};
private ClaimsPrincipal principal = new ClaimsPrincipal();
public ContactUsControllerTests()
{
clientServiceMock = new Mock<IClientService>();
webHostEnvironmentMock = new Mock<IWebHostEnvironment>();
htmlEncoderObjMock = new Mock<HtmlEncoder>();
emailNotificationMock = new Mock<IEmailNotification>();
mockRequest = new Mock<HttpRequest>();
mockContext = new Mock<HttpContext>();
// set-up htmlEncoderMock
htmlEncoderObjMock.Setup(h => h.Encode(contactUsObj.FirstName)).Returns(contactUsObj.FirstName);
htmlEncoderObjMock.Setup(h => h.Encode(contactUsObj.LastName)).Returns(contactUsObj.LastName);
htmlEncoderObjMock.Setup(h => h.Encode(contactUsObj.EmailAddress)).Returns(contactUsObj.EmailAddress);
htmlEncoderObjMock.Setup(h => h.Encode(contactUsObj.Phone)).Returns(contactUsObj.Phone);
htmlEncoderObjMock.Setup(h => h.Encode(contactUsObj.Comments)).Returns(contactUsObj.Comments);
// set-up mockContext
mockContext.Setup(m => m.Request).Returns(mockRequest.Object);
mockContext.Object.User.CurrentClient().ClientId = 30; // this throws error
//other initialisations
}
[Fact]
public async void SaveMethodTest()
{
ContactUsController contactUsControllerObj = new ContactUsController(clientServiceMock.Object, webHostEnvironmentMock.Object, htmlEncoderObjMock.Object, emailNotificationMock.Object);
// Act
await contactUsControllerObj.Save(contactUsObj);
// Arrange
// Lines of code
}
}
Any help whatsoever on this would very helpful.