I am using #aspnet/signalr to connect to a hub and from the hub i am calling the method to return me some data.
When i first initialize the server the connection is established normally, but if i refresh the window 3 or 4 times the client stop sending connection request to the server.
I tried logging the HubConnection, the connectionState at first is equal to 1 but after the problem accrues the connectionState is alwase equal 0
Here is how i am building the hubConnection:
buildConnection() {
this.hubConnection = new HubConnectionBuilder()
.withUrl(this.tradesService.getStockQuotationsHubUrl())
.build();
this.hubConnection.serverTimeoutInMilliseconds = 1000 * 10;
this.hubConnection.onclose(() => setTimeout(() => this.startSignalRConnection(), 2000));}
Here is how i am starting the hubConnection:
startConnection() {
this.hubConnection
.start()
.then(() => {
this.hubConnection.on('updateMethod', (data: any) => {
this.store.push([
{ type: 'update', key: data.stockID, data },
]);
});
this.dataSource = new DataSource({
store: this.store,
reshapeOnPush: true,
});
});}
Note: I am listing to 5 hubs at once in my page but the only one having problems is this one.
[Update]
The problem is from the server because when i restart the server the connection is reestablished between the client and the server, but if the client refresh or quit the page for multiple times the hub does not even try to connect to the client
public class StockQuotationsHub : Microsoft.AspNetCore.SignalR.Hub
{
private string SectorID { get; set; }
private int TradingSession { get; set; }
private int MarketID { get; set; }
public static StockQuotationExt stockQuotationExt { get; set; }
private readonly StockQuotationTicker _stockTicker;
public StockQuotationsHub(StockQuotationTicker stockTicker){
_stockTicker = stockTicker;
}
public override Task OnConnectedAsync(){
return base.OnConnectedAsync();
}
public IEnumerable<StockQuotation> GetAllStockQuotations(string[] stockID, string sectorID, int tradingSession, int marketType){
return _stockTicker.
GetAllStocks(Context.ConnectionId, stockID, sectorID, tradingSession, marketType);
}
public override async Task OnDisconnectedAsync(Exception exception){
await base.OnDisconnectedAsync(exception);
}
and here is my stock ticker class:
public IEnumerable<StockQuotation> GetAllStocks(string connectionId, string[] stockID, string sectorID, int tradingSession, int marketType)
{
_stocks = new List<StockQuotation>();
_stocks = Task.Run(async () => await GetStockQuotationModelAsync("", 0, 1, 0)).Result.ToList();
this.MaxTimeStamp = _stocks.Max(s => s.TStamp);
this.SectorID = sectorID;
this.TradingSession = tradingSession;
this.MarketID = marketType;
AddToGroups(connectionId, stockID);
if (_timer==null)
_timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
if (stockID.Length == 0)
{
return _stocks;
}
else
{
var stocksList = new List<StockQuotation>();
foreach (var stock in stockID)
{
stocksList.AddRange(_stocks.Where(s => s.StockID == stock).ToList());
}
return stocksList;
}
}
private void AddToGroups(string connectionId, string[] stockID)
{
if (_stocks.Count > 0)
{
if (stockID.Length == 0)
{
Hub.Groups.AddToGroupAsync(connectionId, "ALL");
}
else
{
foreach (var stock in stockID)
{
Hub.Groups.AddToGroupAsync(connectionId, stock);
var s = _stocks.FirstOrDefault(s => s.StockID == stock);
if(s != null)
{
s.Snapshots = new List<double>(GetStockQuotationSnapshots(stock));
}
}
}
}
}
private void AddToGroups(string connectionId, string[] stockID)
{
if (_stocks.Count > 0)
{
if (stockID.Length == 0)
{
Hub.Groups.AddToGroupAsync(connectionId, "ALL");
}
else
{
foreach (var stock in stockID)
{
Hub.Groups.AddToGroupAsync(connectionId, stock);
var s = _stocks.FirstOrDefault(s => s.StockID == stock);
if(s != null)
{
s.Snapshots = new List<double>(GetStockQuotationSnapshots(stock));
}
}
}
}
}
I really appreciated the help.
Eventually the problem was that the project type was MVC, so I changed it to be webApi
and everything worked successfully
Even if you fix it I would like to recommend you to add the following code in order to know exactly the root cause of disconnection.
First of all enable the extended debug info in the service configuration:
services.AddSignalR(o =>
{
o.EnableDetailedErrors = true;
});
Furthermore, you need to attach to the event Closed in HubConnection like that:
hubConnection.Closed += (exception) =>
{
if (exception == null)
{
Console.WriteLine("Connection closed without error.");
}
else
{
Console.WriteLine($"Connection closed due to an error: {exception}");
}
return null;
};
Related
This is a request-response model (like HTTP) but over sockets/websockets. We know which response corresponds to which request by comparing the request IDs.
The workflow is as following:
Subscribe to the observables Error, ContractDetails (_itemObservable) and ContractDetailsEnd (_itemEndObservable)
Match request id to response id and push the corresponding messages via .OnNext(...) to ContractDetails. When there is nothing else left to push, push a final message to ContractDetailsEnd which essentially does .OnCompleted.
Cleanup – unsubscribe, i.e. dispose the observables
In the first snippet below I use a List<TItemArgs>, CancellationTokenSource and a TaskCompletionSource<TItemArgs>. That's completely unnecessary if everything was Rx style. It would have definitely been less lines of code too.
The second snippet is my personal attempt to make it look more Rx like. It has some issues that I want to resolve:
I don't think the try/catch block is necessary since .Subscribe could handle errors
.Timeout didn't work out for me – if the request is not matched within a few seconds, it should return a Result<IEnumerable<TItemArgs>>.FromError(new TimeoutError(...))
In addition to the errors, a message pushed to _errorSubject should also return an error such as Result<IEnumerable<TItemArgs>>.FromError(new RemoteError(...)) but .Merge(errorMessages.Any(_ => false)) is not working.
In my attempt I use ReplaySubject in opposed to AsyncSubject because I believe AsyncSubject is only useful when I'm only interested in the last value of the sequence and want to avoid getting all previous values, which is not in my case. In my case I want to return all values, so ReplaySubject would be more suitable as it keeps track of all the previous values and wants all subscribers to receive the same values, regardless of when they subscribed.
public async ValueTask<Result<IEnumerable<TItemArgs>>> ExecuteAsync(Action<int> action)
{
var requestId = _client.GetNextRequestId();
var data = new List<TItemArgs>();
var cts = new CancellationTokenSource(_timeout);
var tcs = new TaskCompletionSource<IEnumerable<TItemArgs>>();
cts.Token.Register(() =>
{
tcs.TrySetCanceled();
}, false);
void OnError(ErrorData msg)
{
tcs.SetException(new IBClientException(msg.RequestId, msg.Code, msg.Message, msg.AdvancedOrderRejectJson));
}
void OnDetails(TItemArgs item)
{
data.Add(item);
}
void OnDetailsEnd(TItemListEndArgs item)
{
tcs.TrySetResult(data);
}
var disposable = new CompositeDisposable();
_client.Error
.Where(e => HasRequestId && e.RequestId == requestId)
.Subscribe(OnError)
.DisposeWith(disposable);
_itemObservable
.Where(item => MatchRequest(item, _itemRequestIdExtractor, requestId))
.Subscribe(OnDetails)
.DisposeWith(disposable);
_itemEndObservable
.Where(item => MatchRequest(item, _itemListEndRequestIdExtractor, requestId))
.Subscribe(OnDetailsEnd)
.DisposeWith(disposable);
action(requestId);
try
{
await tcs.Task.ContinueWith(x =>
{
disposable.Dispose();
cts.Dispose();
}, TaskContinuationOptions.RunContinuationsAsynchronously);
return Result<IEnumerable<TItemArgs>>.FromSuccess(tcs.Task.Result);
}
catch (Exception e)
{
return Result<IEnumerable<TItemArgs>>.FromError(new RemoteError(e.Message, null));
}
}
My attempt
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
var client = new IBClient();
var result = await client.GetContractDetailsAsync();
if (result.Success)
{
foreach (var item in result.Data!)
{
Console.WriteLine($"RequestId: {item.RequestId} | Data: {item.ContractDetails}");
}
}
public sealed class IBClient
{
private int _nextValidId;
private readonly Subject<ErrorData> _errorSubject = new();
public IObservable<ErrorData> Error => _errorSubject.AsObservable();
private readonly Subject<ContractDetailsData> _contractDetailsSubject = new();
public IObservable<ContractDetailsData> ContractDetails => _contractDetailsSubject.AsObservable();
private readonly Subject<RequestEndData> _contractDetailsEndSubject = new();
public IObservable<RequestEndData> ContractDetailsEnd => _contractDetailsEndSubject.AsObservable();
public ValueTask<Result<IEnumerable<ContractDetailsData>>> GetContractDetailsAsync()
{
return new PendingRequest<ContractDetailsData, RequestEndData>(
this,
ContractDetails,
ContractDetailsEnd,
e => e.RequestId,
e => e.RequestId)
.ExecuteAsync(reqId => TestCall(reqId));
}
private void TestCall(int requestId)
{
_contractDetailsSubject.OnNext(new ContractDetailsData(requestId, "hey from test call"));
_contractDetailsSubject.OnNext(new ContractDetailsData(requestId, "hey two"));
// TODO: Errors doesn't seem to work
// _errorSubject.OnNext(new ErrorData(requestId, 123, "Error happened", ""));
_contractDetailsEndSubject.OnNext(new RequestEndData(requestId));
// There shouldn't be matched.
_contractDetailsSubject.OnNext(new ContractDetailsData(123, "fake ones, so we know it works"));
_contractDetailsEndSubject.OnNext(new RequestEndData(123));
}
public int GetNextRequestId()
{
return Interlocked.Increment(ref _nextValidId);
}
}
public sealed class PendingRequest<TItemArgs, TItemListEndArgs>
{
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(2);
private readonly IBClient _client;
private readonly IObservable<TItemArgs> _itemObservable;
private readonly IObservable<TItemListEndArgs> _itemEndObservable;
private readonly Func<TItemArgs, int>? _itemRequestIdExtractor;
private readonly Func<TItemListEndArgs, int>? _itemListEndRequestIdExtractor;
public PendingRequest(
IBClient client,
IObservable<TItemArgs> itemObservable,
IObservable<TItemListEndArgs> itemEndObservable,
Func<TItemArgs, int>? itemRequestIdExtractor = null,
Func<TItemListEndArgs, int>? itemListEndRequestIdExtractor = null)
{
_client = client;
_itemObservable = itemObservable;
_itemEndObservable = itemEndObservable;
_itemRequestIdExtractor = itemRequestIdExtractor;
_itemListEndRequestIdExtractor = itemListEndRequestIdExtractor;
}
private bool HasRequestId => _itemRequestIdExtractor != null && _itemListEndRequestIdExtractor != null;
public async ValueTask<Result<IEnumerable<TItemArgs>>> ExecuteAsync(Action<int> action, IScheduler? scheduler = null)
{
scheduler ??= ImmediateScheduler.Instance;
var requestId = _client.GetNextRequestId();
var results = new ReplaySubject<TItemArgs>();
try
{
var errorMessages = _client.Error
.Where(e => HasRequestId && e.RequestId == requestId);
using (_itemObservable
.Where(item => MatchRequest(item, _itemRequestIdExtractor, requestId))
.ObserveOn(scheduler)
.Subscribe(results))
using (_itemEndObservable
.Any(item => MatchRequest(item, _itemListEndRequestIdExtractor, requestId))
.Merge(errorMessages.Any(_ => false)) // TODO: ???
.ObserveOn(scheduler)
.Subscribe(_ => results.OnCompleted()))
{
action(requestId);
// Don't want an Exception thrown if there result list is empty
await results.DefaultIfEmpty();
return Result<IEnumerable<TItemArgs>>.FromSuccess(results.ToEnumerable());
}
}
catch (Exception ex)
{
return Result<IEnumerable<TItemArgs>>.FromError(new RemoteError(ex.Message, null));
}
}
private bool MatchRequest<T>(T item, Func<T, int>? idExtractor, int id)
{
return !HasRequestId || (idExtractor != null && idExtractor(item) == id);
}
}
public sealed class ContractDetailsData
{
public ContractDetailsData(int requestId, string contractDetails)
{
RequestId = requestId;
ContractDetails = contractDetails;
}
public int RequestId { get; }
public string ContractDetails { get; }
}
public sealed class ErrorData
{
public ErrorData(int requestId, int code, string message, string advancedOrderRejectJson)
{
RequestId = requestId;
Code = code;
Message = message;
AdvancedOrderRejectJson = advancedOrderRejectJson;
}
public int RequestId { get; }
public int Code { get; }
public string Message { get; }
public string AdvancedOrderRejectJson { get; }
}
public sealed class RequestEndData
{
public RequestEndData(int requestId)
{
RequestId = requestId;
}
public int RequestId { get; }
}
public class IBClientException : Exception
{
public IBClientException(int requestId, int errorCode, string message, string advancedOrderRejectJson)
: base(message)
{
RequestId = requestId;
ErrorCode = errorCode;
AdvancedOrderRejectJson = advancedOrderRejectJson;
}
public IBClientException(string err)
: base(err)
{
}
public IBClientException(Exception e)
{
Exception = e;
}
public int RequestId { get; }
public int ErrorCode { get; }
public string? AdvancedOrderRejectJson { get; }
public Exception? Exception { get; }
}
public abstract record Error(int? Code, string Message, object? Data);
public record RemoteError : Error
{
public RemoteError(string message, object? data) : base(null, message, data)
{
}
public RemoteError(int? code, string message, object? data) : base(code, message, data)
{
}
}
public record Result<T>(bool Success, T? Data, Error? Error)
{
public Result(T data) : this(true, data, default)
{
}
public Result(Error error) : this(false, default, error)
{
}
public static Result<T> FromSuccess(T data)
{
return new Result<T>(data);
}
public static Result<T> FromError<TError>(TError error) where TError : Error
{
return new Result<T>(error);
}
}
public static class DisposableExtensions
{
public static T DisposeWith<T>(this T disposable, ICollection<IDisposable> collection)
where T : IDisposable
{
ArgumentNullException.ThrowIfNull(disposable);
ArgumentNullException.ThrowIfNull(collection);
collection.Add(disposable);
return disposable;
}
}
Here's a good start:
IObservable<TItemArgs> observable =
Observable
.Merge(
_itemObservable
.Where(item => MatchRequest(item, _itemRequestIdExtractor, requestId))
.Select(item => Notification.CreateOnNext(item))
.Take(1),
_itemEndObservable
.Any(item => MatchRequest(item, _itemListEndRequestIdExtractor, requestId))
.Select(item => Notification.CreateOnCompleted<TItemArgs>()))
.Dematerialize();
And here's a final observable without the ReplaySubject.
private static async Task<Result<IEnumerable<Details>>> Test(IScheduler scheduler) =>
await
Observable
.Defer(() =>
{
var client = new IBClient();
var requestId = 1;
return Observable.Create<Result<IEnumerable<Details>>>(o =>
{
IDisposable subscription =
Observable
.Merge(
client.Details.Where(x => x.RequestId == requestId).Select(x => Notification.CreateOnNext(x)),
client.DetailsEnd.Any(x => x.RequestId == requestId).Select(x => Notification.CreateOnCompleted<Details>()),
client.Error.Where(x => x.RequestId == requestId).Select(x => Notification.CreateOnError<Details>(new Exception($"Code: {x.Code}, Message: {x.Message}"))))
.Dematerialize()
.Synchronize()
.ToArray()
.Select(items => Result<IEnumerable<Details>>.FromSuccess(items))
.Catch<Result<IEnumerable<Details>>, Exception>(ex => Observable.Return(Result<IEnumerable<Details>>.FromError(new Error(null, ex.Message, null))))
.Timeout(TimeSpan.FromSeconds(2))
.ObserveOn(scheduler)
.Subscribe(o);
client.Emit(2, 4);
client.Emit(requestId, 42);
client.EmitEnd(2);
client.Emit(requestId, 62);
client.Emit(requestId, 123);
//client.EmitError(requestId, 123, "Something bad happened");
client.EmitEnd(requestId);
client.Emit(requestId, 1);
return subscription;
});
});
I have a somewhat unusual problem.
I wrote a damage meter for a game and have the problem, if I attack 2 opponents at the same time in the game, I am shown several times in the list.
To get the data from the game, I use PhotonParser, which reads out the network data.
(First of all, it's nothing illegal.)
public class AlbionPackageParser : PhotonParser
{
private readonly HealthUpdateEventHandler HealthUpdateEventHandler;
public AlbionPackageParser(TrackingController trackingController, MainWindowViewModel mainWindowViewModel)
{
HealthUpdateEventHandler = new HealthUpdateEventHandler(trackingController);
}
protected override void OnEvent(byte code, Dictionary<byte, object> parameters)
{
var eventCode = ParseEventCode(parameters);
if (eventCode == EventCodes.Unused)
{
return;
}
Task.Run(async () =>
{
switch (eventCode)
{
case EventCodes.HealthUpdate:
await HealthUpdateEventHandlerAsync(parameters).ConfigureAwait(false);
return;
}
});
}
private static EventCodes ParseEventCode(IReadOnlyDictionary<byte, object> parameters)
{
if (!parameters.TryGetValue(252, out var value))
{
return EventCodes.Unused;
}
return (EventCodes)Enum.ToObject(typeof(EventCodes), value);
}
private async Task HealthUpdateEventHandlerAsync(Dictionary<byte, object> parameters)
{
var value = new HealthUpdateEvent(parameters);
await HealthUpdateEventHandler.OnActionAsync(value);
}
}
public class HealthUpdateEventHandler
{
private readonly TrackingController _trackingController;
public HealthUpdateEventHandler(TrackingController trackingController)
{
_trackingController = trackingController;
}
public async Task OnActionAsync(HealthUpdateEvent value)
{
await _trackingController.CombatController.AddDamageAsync(value.ObjectId, value.CauserId, value.HealthChange, value.NewHealthValue);
}
}
public class HealthUpdateEvent
{
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod()?.DeclaringType);
public long CauserId;
public int CausingSpellType;
public EffectOrigin EffectOrigin;
public EffectType EffectType;
public double HealthChange;
public double NewHealthValue;
public long ObjectId;
public GameTimeStamp TimeStamp;
public HealthUpdateEvent(Dictionary<byte, object> parameters)
{
ConsoleManager.WriteLineForNetworkHandler(GetType().Name, parameters);
try
{
if (parameters.ContainsKey(0))
{
ObjectId = parameters[0].ObjectToLong() ?? throw new ArgumentNullException();
}
if (parameters.ContainsKey(1))
{
TimeStamp = new GameTimeStamp(parameters[1].ObjectToLong() ?? 0);
}
if (parameters.ContainsKey(2))
{
HealthChange = parameters[2].ObjectToDouble();
}
if (parameters.ContainsKey(3))
{
NewHealthValue = parameters[3].ObjectToDouble();
}
if (parameters.ContainsKey(4))
{
EffectType = (EffectType) (parameters[4] as byte? ?? 0);
}
if (parameters.ContainsKey(5))
{
EffectOrigin = (EffectOrigin) (parameters[5] as byte? ?? 0);
}
if (parameters.ContainsKey(6))
{
CauserId = parameters[6].ObjectToLong() ?? throw new ArgumentNullException();
}
if (parameters.ContainsKey(7))
{
CausingSpellType = parameters[7].ObjectToShort();
}
}
catch (ArgumentNullException ex)
{
ConsoleManager.WriteLineForError(MethodBase.GetCurrentMethod()?.DeclaringType, ex);
Log.Error(MethodBase.GetCurrentMethod()?.DeclaringType, ex);
}
catch (Exception e)
{
ConsoleManager.WriteLineForError(MethodBase.GetCurrentMethod()?.DeclaringType, e);
Log.Error(MethodBase.GetCurrentMethod()?.DeclaringType, e);
}
}
}
Most important file:
public class CombatController
{
public async Task AddDamageAsync(long objectId, long causerId, double healthChange, double newHealthValue)
{
if (!IsDamageMeterActive || objectId == causerId)
{
return;
}
var gameObject = _trackingController?.EntityController?.GetEntity(causerId);
if (gameObject?.Value == null
|| gameObject.Value.Value?.ObjectType != GameObjectType.Player
|| !_trackingController.EntityController.IsUserInParty(gameObject.Value.Value.Name)
)
{
return;
}
if (GetHealthChangeType(healthChange) == HealthChangeType.Damage)
{
var damageChangeValue = (int)Math.Round(healthChange.ToPositiveFromNegativeOrZero(), MidpointRounding.AwayFromZero);
if (damageChangeValue <= 0)
{
return;
}
gameObject.Value.Value.Damage += damageChangeValue;
}
if (GetHealthChangeType(healthChange) == HealthChangeType.Heal)
{
var healChangeValue = healthChange;
if (healChangeValue <= 0)
{
return;
}
if (IsMaxHealthReached(objectId, newHealthValue))
{
return;
}
gameObject.Value.Value.Heal += (int)Math.Round(healChangeValue, MidpointRounding.AwayFromZero);
}
if (gameObject.Value.Value?.CombatStart == null)
{
gameObject.Value.Value.CombatStart = DateTime.UtcNow;
}
if (await IsUiUpdateAllowedAsync() && !IsUiUpdateActive)
{
await UpdateDamageMeterUiAsync(_mainWindowViewModel.DamageMeter, _trackingController.EntityController.GetAllEntities());
}
}
private static bool IsUiUpdateActive;
public async Task UpdateDamageMeterUiAsync(AsyncObservableCollection<DamageMeterFragment> damageMeter, List<KeyValuePair<Guid, PlayerGameObject>> entities)
{
IsUiUpdateActive = true;
var highestDamage = entities.GetHighestDamage();
var highestHeal = entities.GetHighestHeal();
_trackingController.EntityController.DetectUsedWeapon();
foreach (var healthChangeObject in entities)
{
if (_mainWindow?.Dispatcher == null || healthChangeObject.Value?.UserGuid == null)
{
continue;
}
var fragment = damageMeter.ToList().FirstOrDefault(x => x.CauserGuid == healthChangeObject.Value.UserGuid);
if (fragment != null)
{
await UpdateDamageMeterFragmentAsync(fragment, healthChangeObject, entities, highestDamage, highestHeal).ConfigureAwait(true);
}
else
{
await AddDamageMeterFragmentAsync(damageMeter, healthChangeObject, entities, highestDamage, highestHeal).ConfigureAwait(true);
}
Application.Current.Dispatcher.Invoke(() => _mainWindowViewModel.SetDamageMeterSort());
}
if (HasDamageMeterDupes(_mainWindowViewModel.DamageMeter))
{
await RemoveDuplicatesAsync(_mainWindowViewModel.DamageMeter);
}
IsUiUpdateActive = false;
}
private async Task UpdateDamageMeterFragmentAsync(DamageMeterFragment fragment, KeyValuePair<Guid, PlayerGameObject> healthChangeObject, List<KeyValuePair<Guid, PlayerGameObject>> entities, long highestDamage, long highestHeal)
{
var itemInfo = await SetItemInfoIfSlotTypeMainHandAsync(fragment.CauserMainHand, healthChangeObject.Value?.CharacterEquipment?.MainHand).ConfigureAwait(false);
fragment.CauserMainHand = itemInfo;
// Damage
if (healthChangeObject.Value?.Damage > 0)
{
fragment.DamageInPercent = (double)healthChangeObject.Value.Damage / highestDamage * 100;
fragment.Damage = (long)healthChangeObject.Value?.Damage;
}
if (healthChangeObject.Value?.Dps != null)
{
fragment.Dps = healthChangeObject.Value.Dps;
}
// Heal
if (healthChangeObject.Value?.Heal > 0)
{
fragment.HealInPercent = (double)healthChangeObject.Value.Heal / highestHeal * 100;
fragment.Heal = (long)healthChangeObject.Value?.Heal;
}
if (healthChangeObject.Value?.Hps != null)
{
fragment.Hps = healthChangeObject.Value.Hps;
}
// Generally
if (healthChangeObject.Value != null)
{
fragment.DamagePercentage = entities.GetDamagePercentage(healthChangeObject.Value.Damage);
fragment.HealPercentage = entities.GetHealPercentage(healthChangeObject.Value.Heal);
_trackingController.EntityController.SetPartyCircleColor(healthChangeObject.Value.UserGuid, itemInfo?.FullItemInformation?.CategoryId);
}
}
private async Task AddDamageMeterFragmentAsync(AsyncObservableCollection<DamageMeterFragment> damageMeter, KeyValuePair<Guid, PlayerGameObject> healthChangeObject,
List<KeyValuePair<Guid, PlayerGameObject>> entities, long highestDamage, long highestHeal)
{
if (healthChangeObject.Value == null
|| (double.IsNaN(healthChangeObject.Value.Damage) && double.IsNaN(healthChangeObject.Value.Heal))
|| (healthChangeObject.Value.Damage <= 0 && healthChangeObject.Value.Heal <= 0))
{
return;
}
var mainHandItem = ItemController.GetItemByIndex(healthChangeObject.Value?.CharacterEquipment?.MainHand ?? 0);
var itemInfo = await SetItemInfoIfSlotTypeMainHandAsync(mainHandItem, healthChangeObject.Value?.CharacterEquipment?.MainHand);
var damageMeterFragment = new DamageMeterFragment
{
CauserGuid = healthChangeObject.Value.UserGuid,
Damage = healthChangeObject.Value.Damage,
Dps = healthChangeObject.Value.Dps,
DamageInPercent = (double)healthChangeObject.Value.Damage / highestDamage * 100,
DamagePercentage = entities.GetDamagePercentage(healthChangeObject.Value.Damage),
Heal = healthChangeObject.Value.Heal,
Hps = healthChangeObject.Value.Hps,
HealInPercent = (double)healthChangeObject.Value.Heal / highestHeal * 100,
HealPercentage = entities.GetHealPercentage(healthChangeObject.Value.Heal),
Name = healthChangeObject.Value.Name,
CauserMainHand = itemInfo
};
damageMeter.Init(Application.Current.Dispatcher.Invoke);
damageMeter.Add(damageMeterFragment);
_trackingController.EntityController.SetPartyCircleColor(healthChangeObject.Value.UserGuid, itemInfo?.FullItemInformation?.CategoryId);
}
// Try 1
private bool IsUiUpdateAllowed(int waitTimeInSeconds = 1)
{
var currentDateTime = DateTime.UtcNow;
var difference = currentDateTime.Subtract(_lastDamageUiUpdate);
if (difference.Seconds >= waitTimeInSeconds && !IsUiUpdateActive)
{
_lastDamageUiUpdate = currentDateTime;
return true;
}
return false;
}
// Try 2
private async Task<bool> IsUiUpdateAllowedAsync()
{
await Task.Delay(100).ConfigureAwait(true);
if (_updateReadyCounter++ < 2)
{
return false;
}
_updateReadyCounter = 0;
return true;
}
}
In the last class "CombatController" I am currently trying to get the problem under control.
The "UpdateDamageMeterUiAsync ()" method should only be triggered once if IsUiUpdateAllowedAsync () is true.
Nevertheless, "UpdateDamageMeterUiAsync ()" is always triggered 2-3 times at the same time, which is why multiple entries are created. All other lists are fine, only the UI object "AsyncObservableCollection _mainWindowViewModel.DamageMeter" has the problem.
Does anyone have any idea how I can tackle the problem?
I'm building an app which will scrape some data from a website and shows a notification when some criteria are met.
Everything works well without problems when the app is open (because the WebView is being rendered) but when I close the app the WebView is disabled so I cannot use it to scrape data anymore.
The scraping code is inside a class called from a ForegroundService.
I've already looked on the internet but I'm unable to find a solution or a substitute to WebView, do you have any ideas?
I'm sorry if this question looks stupid to you, I've started to develop for mobile just one week ago
Below the JDMonitoring class which is called from the AlarmTask class
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace CGSJDSportsNotification {
public class JDMonitoring {
class Ticket {
string owner;
string title;
string store;
string lastUpdated;
string link;
public string ID { get; set; }
public string Owner {
get {
return owner == null ? "Nobody" : owner;
} set {
owner = value.Remove(0, value.IndexOf('(') + 1).Replace(")", "");
}
}
public string Title {
get {
return title;
} set {
if (value.StartsWith("(P"))
title = value.Remove(0, value.IndexOf(')') + 2);
}
}
public string Status { get; set; }
public string Store {
get {
return store;
} set {
store = value.Replace(#"\u003C", "").Replace(">", "");
}
}
public string LastUpdated {
get {
return lastUpdated;
} set {
string v;
int time = Convert.ToInt32(System.Text.RegularExpressions.Regex.Replace(value, #"[^\d]+", ""));
// Convert to minutes
if (value.Contains("hours"))
time *= 60;
v = time.ToString();
if (value.Contains("seconds"))
v = v.Insert(v.Length, " sec. ago");
else
v = v.Insert(v.Length, " min. ago");
lastUpdated = v;
}
}
public string Link {
get {
return link;
} set {
link = "https://support.jdplc.com/" + value;
}
}
}
public JDMonitoring() {
WB.Source = JDQueueMainUrl;
WB.Navigated += new EventHandler<WebNavigatedEventArgs>(OnNavigate);
}
IForegroundService FgService { get { return DependencyService.Get<IForegroundService>(); } }
WebView WB { get; } = MainPage.UI.MonitoringWebView;
string JDQueueMainUrl { get; } = "https://support.jdplc.com/rt4/Search/Results.html?Format=%27%3Cb%3E%3Ca%20href%3D%22__WebPath__%2FTicket%2FDisplay.html%3Fid%3D__id__%22%3E__id__%3C%2Fa%3E%3C%2Fb%3E%2FTITLE%3A%23%27%2C%0A%27%3Cb%3E%3Ca%20href%3D%22__WebPath__%2FTicket%2FDisplay.html%3Fid%3D__id__%22%3E__Subject__%3C%2Fa%3E%3C%2Fb%3E%2FTITLE%3ASubject%27%2C%0AStatus%2C%0AQueueName%2C%0AOwner%2C%0APriority%2C%0A%27__NEWLINE__%27%2C%0A%27__NBSP__%27%2C%0A%27%3Csmall%3E__Requestors__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__CreatedRelative__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__ToldRelative__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__LastUpdatedRelative__%3C%2Fsmall%3E%27%2C%0A%27%3Csmall%3E__TimeLeft__%3C%2Fsmall%3E%27&Order=DESC%7CASC%7CASC%7CASC&OrderBy=LastUpdated%7C%7C%7C&Query=Queue%20%3D%20%27Service%20Desk%20-%20CGS%27%20AND%20(%20%20Status%20%3D%20%27new%27%20OR%20Status%20%3D%20%27open%27%20OR%20Status%20%3D%20%27stalled%27%20OR%20Status%20%3D%20%27deferred%27%20OR%20Status%20%3D%20%27open%20-%20awaiting%20requestor%27%20OR%20Status%20%3D%20%27open%20-%20awaiting%20third%20party%27%20)&RowsPerPage=0&SavedChartSearchId=new&SavedSearchId=new";
bool MonitoringIsInProgress { get; set; } = false;
public bool IsConnectionAvailable {
get {
try {
using (new WebClient().OpenRead("http://google.com/generate_204"))
return true;
} catch {
return false;
}
}
}
async Task<bool> IsOnLoginPage() {
if (await WB.EvaluateJavaScriptAsync("document.getElementsByClassName('left')[0].innerText") != null)
return true;
return false;
}
async Task<bool> Login() {
await WB.EvaluateJavaScriptAsync($"document.getElementsByName('user')[0].value = '{UserSettings.SecureEntries.Get("rtUser")}'");
await WB.EvaluateJavaScriptAsync($"document.getElementsByName('pass')[0].value = '{UserSettings.SecureEntries.Get("rtPass")}'");
await WB.EvaluateJavaScriptAsync("document.getElementsByClassName('button')[0].click()");
await Task.Delay(1000);
// Checks for wrong credentials error
if (await WB.EvaluateJavaScriptAsync("document.getElementsByClassName('action-results')[0].innerText") == null)
return true;
return false;
}
async Task<List<Ticket>> GetTickets() {
List<Ticket> tkts = new List<Ticket>();
// Queue tkts index (multiple of 2)
int index = 2;
// Iterates all the queue
while (await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].innerText") != null) {
Ticket tkt = new Ticket();
tkt.LastUpdated = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index + 1}].getElementsByTagName('td')[4].innerText");
// Gets only the tkts which are not older than the value selected by the user
if (Convert.ToInt32(System.Text.RegularExpressions.Regex.Replace(tkt.LastUpdated, #"[^\d]+", "")) > Convert.ToInt32(UserSettings.Entries.Get("searchTimeframe")))
break;
tkt.ID = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[0].innerText");
tkt.Owner = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[4].innerText");
tkt.Title = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[1].innerText");
tkt.Status = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[2].innerText");
tkt.Store = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index + 1}].getElementsByTagName('td')[1].innerText");
tkt.Link = await WB.EvaluateJavaScriptAsync($"document.getElementsByClassName('ticket-list collection-as-table')[0].getElementsByTagName('tbody')[0].getElementsByTagName('tr')[{index}].getElementsByTagName('td')[1].getElementsByTagName('a')[0].getAttribute('href')");
tkts.Add(tkt);
index += 2;
}
return tkts;
}
//async Task<string> QueueGetTkt
async void OnNavigate(object sender, WebNavigatedEventArgs args) {
if (MonitoringIsInProgress)
return;
if (IsConnectionAvailable) {
if (await IsOnLoginPage()) {
if (await Login() == false) {
// If the log-in failed we can't proceed
MonitoringIsInProgress = false;
FgService.NotificationNewTicket("Log-in failed!", "Please check your credentials");
// Used to avoid an infinite loop of OnNavigate method calls
WB.Source = "about:blank";
return;
}
}
// Main core of the monitoring
List<Ticket> tkts = await GetTickets();
if (tkts.Count > 0) {
foreach(Ticket t in tkts) {
// Looks only after the tkts with the country selected by the user (and if it was selected by the user, also for the tkts without a visible country)
// Firstly we look in the title
if (t.Title.Contains(MainPage.UI.CountryPicker.SelectedItem.ToString())) {
FgService.NotificationNewTicket($"[{t.ID}] {t.LastUpdated}",
$"{t.Title}\r\n\r\n" +
$"Status: {t.Status}\r\n" +
$"Owner: {t.Owner}\r\n" +
$"Last updated: {t.LastUpdated}");
break;
}
}
}
}
MonitoringIsInProgress = false;
}
}
}
AlarmTask class
using Android.App;
using Android.Content;
using Android.Support.V4.App;
namespace CGSJDSportsNotification.Droid {
[BroadcastReceiver(Enabled = true, Exported = true, DirectBootAware = true)]
[IntentFilter(new string[] { Intent.ActionBootCompleted, Intent.ActionLockedBootCompleted, "android.intent.action.QUICKBOOT_POWERON", "com.htc.intent.action.QUICKBOOT_POWERON" }, Priority = (int)IntentFilterPriority.HighPriority)]
public class AlarmTask : BroadcastReceiver {
IAlarm _MainActivity { get { return Xamarin.Forms.DependencyService.Get<IAlarm>(); } }
public override void OnReceive(Context context, Intent intent) {
if (intent.Action != null) {
if (intent.Action.Equals(Intent.ActionBootCompleted)) {
// Starts the app after reboot
var serviceIntent = new Intent(context, typeof(MainActivity));
serviceIntent.AddFlags(ActivityFlags.NewTask);
context.StartActivity(serviceIntent);
Intent main = new Intent(Intent.ActionMain);
main.AddCategory(Intent.CategoryHome);
context.StartActivity(main);
// Does not work, app crashes on boot received
/*if (UserSettings.Entries.Exists("monitoringIsRunning")) {
if ((bool)UserSettings.Entries.Get("monitoringIsRunning"))
FgService.Start();
}*/
}
} else
// Checks for new tkts on a new thread
new JDMonitoring();
// Restarts the alarm
_MainActivity.AlarmStart();
}
// Called from JDMonitoring class
public static void NotificationNewTicket(string title, string message, bool icoUnknownCountry = false) {
new AlarmTask().NotificationShow(title, message, icoUnknownCountry);
}
void NotificationShow(string title, string message, bool icoUnknownCountry) {
int countryFlag = Resource.Drawable.newTktUnknownCountry;
if (icoUnknownCountry == false) {
switch (MainPage.UI.CountryPicker.SelectedItem.ToString()) {
case "Italy":
countryFlag = Resource.Drawable.newTktItaly;
break;
case "Spain":
countryFlag = Resource.Drawable.newTktSpain;
break;
case "Germany":
countryFlag = Resource.Drawable.newTktGermany;
break;
case "Portugal":
countryFlag = Resource.Drawable.newTktPortugal;
break;
}
}
var _intent = new Intent(Application.Context, typeof(MainActivity));
_intent.AddFlags(ActivityFlags.ClearTop);
_intent.PutExtra("jdqueue_notification", "extra");
var pendingIntent = PendingIntent.GetActivity(Application.Context, 0, _intent, PendingIntentFlags.OneShot);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(Application.Context, "newTktNotification_channel")
.SetVisibility((int)NotificationVisibility.Public)
.SetPriority((int)NotificationPriority.High)
.SetDefaults((int)NotificationDefaults.Sound | (int)NotificationDefaults.Vibrate | (int)NotificationDefaults.Lights)
.SetSmallIcon(Resource.Drawable.newTktNotification)
.SetLargeIcon(Android.Graphics.BitmapFactory.DecodeResource(Application.Context.Resources, countryFlag))
.SetSubText("Click to check the queue")
.SetStyle(new NotificationCompat.BigTextStyle()
.SetBigContentTitle("New ticket available!")
.BigText(message))
.SetContentText(title)
.SetAutoCancel(true)
.SetContentIntent(pendingIntent);
NotificationManagerCompat.From(Application.Context).Notify(0, notificationBuilder.Build());
}
}
}
And the ForegroundService class which is responsible to trigger for the first time the alarm
using Android.App;
using Android.Content;
using Android.OS;
namespace CGSJDSportsNotification.Droid {
[Service]
class ForegroundService : Service {
IAlarm _MainActivity { get { return Xamarin.Forms.DependencyService.Get<IAlarm>(); } }
public override IBinder OnBind(Intent intent) { return null; }
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId) {
// Starts the Foreground Service and the notification channel
StartForeground(9869, new ForegroundServiceNotification().ReturnNotif());
Android.Widget.Toast.MakeText(Application.Context, "JD Queue - Monitoring started!", Android.Widget.ToastLength.Long).Show();
_MainActivity.AlarmStart();
return StartCommandResult.Sticky;
}
public override void OnDestroy() {
Android.Widget.Toast.MakeText(Application.Context, "JD Queue - Monitoring stopped!", Android.Widget.ToastLength.Long).Show();
_MainActivity.AlarmStop();
UserSettings.Entries.AddOrEdit("monitoringIsRunning", false);
UserSettings.Entries.AddOrEdit("monitoringStopPending", false, false);
base.OnDestroy();
}
public override bool StopService(Intent name) {
return base.StopService(name);
}
}
}
Thank you!
[BETTER-FINAL-SOLUTION]
After several hours I've discovered Android WebView which does exactly what I need (I'm developing this app only for Android)
I've written this Browser helper class
class Browser {
public Android.Webkit.WebView WB;
static string JSResult;
public class CustomWebViewClient : WebViewClient {
public event EventHandler<bool> OnPageLoaded;
public override void OnPageFinished(Android.Webkit.WebView view, string url) {
OnPageLoaded?.Invoke(this, true);
}
}
public Browser(CustomWebViewClient wc, string url = "") {
WB = new Android.Webkit.WebView(Android.App.Application.Context);
WB.Settings.JavaScriptEnabled = true;
WB.SetWebViewClient(wc);
WB.LoadUrl(url);
}
public string EvalJS(string js) {
JSInterface jsi = new JSInterface();
WB.EvaluateJavascript($"javascript:(function() {{ return {js}; }})()", jsi);
return JSResult;
}
class JSInterface : Java.Lang.Object, IValueCallback {
public void OnReceiveValue(Java.Lang.Object value) {
JSResult = value.ToString();
}
}
}
[EDIT]
Improved the JS returning function with async callbacks (so the JS return value will be always delivered).
Credits to ChristineZuckerman
class Browser {
public Android.Webkit.WebView WB;
public class CustomWebViewClient : WebViewClient {
public event EventHandler<bool> OnPageLoaded;
public override void OnPageFinished(Android.Webkit.WebView view, string url) {
OnPageLoaded?.Invoke(this, true);
}
}
public Browser(CustomWebViewClient wc, string url = "") {
WB = new Android.Webkit.WebView(Android.App.Application.Context);
WB.ClearCache(true);
WB.Settings.JavaScriptEnabled = true;
WB.Settings.CacheMode = CacheModes.NoCache;
WB.Settings.DomStorageEnabled = true;
WB.Settings.SetAppCacheEnabled(false);
WB.Settings.UserAgentString = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10";
WB.LoadUrl(url);
WB.SetWebViewClient(wc);
}
public async Task<string> EvalJS(string js, bool returnNullObjectWhenNull = true) {
string JSResult = "";
ManualResetEvent reset = new ManualResetEvent(false);
Device.BeginInvokeOnMainThread(() => {
WB?.EvaluateJavascript($"javascript:(function() {{ return {js}; }})()", new JSInterface((r) => {
JSResult = r;
reset.Set();
}));
});
await Task.Run(() => { reset.WaitOne(); });
return JSResult == "null" ? returnNullObjectWhenNull ? null : "null" : JSResult;
}
class JSInterface : Java.Lang.Object, IValueCallback {
private Action<string> _callback;
public JSInterface(Action<string> callback) {
_callback = callback;
}
public void OnReceiveValue(Java.Lang.Object value) {
string v = value.ToString();
if (v.StartsWith('"') && v.EndsWith('"'))
v = v.Remove(0, 1).Remove(v.Length - 2, 1);
_callback?.Invoke(v);
}
}
}
Example:
Browser.CustomWebViewClient wc = new Browser.CustomWebViewClient();
wc.OnPageLoaded += BrowserOnPageLoad;
Browser browser = new Browser(wc, "https://www.google.com/");
void BrowserOnPageLoad(object sender, bool e) {
string test = browser.EvalJS("document.getElementsByClassName('Q8LRLc')[0].innerText");
// 'test' will contain the value returned from the JS script
// You can acces the real WebView object by using
// browser.WB
}
// OR WITH THE NEW RETURNING FUNCTION
async void BrowserOnPageLoad(object sender, bool e) {
string test = await browser.EvalJS("document.getElementsByClassName('Q8LRLc')[0].innerText");
// 'test' will contain the value returned from the JS script
// You can acces the real WebView object by using
// browser.WB
}
[FINAL-SOLUTION]
Finally I've found an easy and efficient alternative to WebView.
Now I'm using SimpleBroswer and works great!
[SEMI-SOLUTION]
Alright, I've written a workaround but I don't really like this idea, so please, if you know a better method let me know.
Below my workaround:
In my ForegroundServiceHelper interface I've added a method to check if the MainActivity (where the WebView it's rendered) is visible or not, if isn't visible the MainActivity will be shown and immediately will be hidden back.
And the app will be removed from the last used applications
Method inside my ForegroundServiceHelper Interface
public void InitBackgroundWebView() {
if ((bool)SharedSettings.Entries.Get("MainPage.IsVisible") == false) {
// Shows the activity
Intent serviceIntent = new Intent(context, typeof(MainActivity));
serviceIntent.AddFlags(ActivityFlags.NewTask);
context.StartActivity(serviceIntent);
// And immediately hides it back
Intent main = new Intent(Intent.ActionMain);
main.AddFlags(ActivityFlags.NewTask);
main.AddCategory(Intent.CategoryHome);
context.StartActivity(main);
// Removes from the last app used
ActivityManager am = (new ContextWrapper(Android.App.Application.Context)).GetSystemService(Context.ActivityService).JavaCast<ActivityManager>();
if (am != null) {
System.Collections.Generic.IList<ActivityManager.AppTask> tasks = am.AppTasks;
if (tasks != null && tasks.Count > 0) {
tasks[0].SetExcludeFromRecents(true);
}
}
}
}
The SharedSettings class is an helper class wrapped around the App.Current.Properties Dictionary
And in the OnAppearing and OnDisappearing callbacks I set the shared values to true/false
[EDIT]
This workaround works only if the user is on the homepage, so I need to find an another solution...
I am trying to implement a simple example/demo for a state machine using Automatonymous with RabbitMQ. Unfortunately I could not find one to rebuild / learn from (I found the ShoppingWeb, but in my eyes it's anything but simple). Also in my opinion the documentation is lacking information.
This is the state machine example I thought of (sorry, it's pretty ugly):
Please note that this example is completely made up and it's not important if it makes sense or not. This project's purpose is to get "warm" with Automatonymous.
What I want to do / to have is:
Four applications running:
The state machine itself
The "requester" sending requests to be interpreted
The "validator" or "parser" checking if the provided request is valid
The "interpreter" interpreting the given request
An example of this could be:
Requester sends "x=5"
Validator checks if a "=" is contained
Intepreter says "5"
My implementation of the state machine looks like this:
public class InterpreterStateMachine : MassTransitStateMachine<InterpreterInstance>
{
public InterpreterStateMachine()
{
InstanceState(x => x.CurrentState);
Event(() => Requesting, x => x.CorrelateBy(request => request.Request.RequestString, context => context.Message.Request.RequestString)
.SelectId(context => Guid.NewGuid()));
Event(() => Validating, x => x.CorrelateBy(request => request.Request.RequestString, context => context.Message.Request.RequestString));
Event(() => Interpreting, x => x.CorrelateBy(request => request.Request.RequestString, context => context.Message.Request.RequestString));
Initially(
When(Requesting)
.Then(context =>
{
context.Instance.Request = new Request(context.Data.Request.RequestString);
})
.ThenAsync(context => Console.Out.WriteLineAsync($"Request received: {context.Data.Request.RequestString}"))
.Publish(context => new ValidationNeededEvent(context.Instance))
.TransitionTo(Requested)
);
During(Requested,
When(Validating)
.Then(context =>
{
context.Instance.Request.IsValid = context.Data.Request.IsValid;
if (!context.Data.Request.IsValid)
{
this.TransitionToState(context.Instance, Error);
}
else
{
this.TransitionToState(context.Instance, RequestValid);
}
})
.ThenAsync(context => Console.Out.WriteLineAsync($"Request '{context.Data.Request.RequestString}' validated with {context.Instance.Request.IsValid}"))
.Publish(context => new InterpretationNeededEvent(context.Instance))
,
Ignore(Requesting),
Ignore(Interpreting)
);
During(RequestValid,
When(Interpreting)
.Then((context) =>
{
//do something
})
.ThenAsync(context => Console.Out.WriteLineAsync($"Request '{context.Data.Request.RequestString}' interpreted with {context.Data.Answer}"))
.Publish(context => new AnswerReadyEvent(context.Instance))
.TransitionTo(AnswerReady)
.Finalize(),
Ignore(Requesting),
Ignore(Validating)
);
SetCompletedWhenFinalized();
}
public State Requested { get; private set; }
public State RequestValid { get; private set; }
public State AnswerReady { get; private set; }
public State Error { get; private set; }
//Someone is sending a request to interprete
public Event<IRequesting> Requesting { get; private set; }
//Request is validated
public Event<IValidating> Validating { get; private set; }
//Request is interpreted
public Event<IInterpreting> Interpreting { get; private set; }
class ValidationNeededEvent : IValidationNeeded
{
readonly InterpreterInstance _instance;
public ValidationNeededEvent(InterpreterInstance instance)
{
_instance = instance;
}
public Guid RequestId => _instance.CorrelationId;
public Request Request => _instance.Request;
}
class InterpretationNeededEvent : IInterpretationNeeded
{
readonly InterpreterInstance _instance;
public InterpretationNeededEvent(InterpreterInstance instance)
{
_instance = instance;
}
public Guid RequestId => _instance.CorrelationId;
}
class AnswerReadyEvent : IAnswerReady
{
readonly InterpreterInstance _instance;
public AnswerReadyEvent(InterpreterInstance instance)
{
_instance = instance;
}
public Guid RequestId => _instance.CorrelationId;
}
}
Then I have services like this:
public class RequestService : ServiceControl
{
readonly IScheduler scheduler;
IBusControl busControl;
BusHandle busHandle;
InterpreterStateMachine machine;
InMemorySagaRepository<InterpreterInstance> repository;
public RequestService()
{
scheduler = CreateScheduler();
}
public bool Start(HostControl hostControl)
{
Console.WriteLine("Creating bus...");
machine = new InterpreterStateMachine();
repository = new InMemorySagaRepository<InterpreterInstance>();
busControl = Bus.Factory.CreateUsingRabbitMq(x =>
{
IRabbitMqHost host = x.Host(new Uri(/*rabbitMQ server*/), h =>
{
/*credentials*/
});
x.UseInMemoryScheduler();
x.ReceiveEndpoint(host, "interpreting_answer", e =>
{
e.PrefetchCount = 5; //?
e.StateMachineSaga(machine, repository);
});
x.ReceiveEndpoint(host, "2", e =>
{
e.PrefetchCount = 1;
x.UseMessageScheduler(e.InputAddress);
//Scheduling !?
e.Consumer(() => new ScheduleMessageConsumer(scheduler));
e.Consumer(() => new CancelScheduledMessageConsumer(scheduler));
});
});
Console.WriteLine("Starting bus...");
try
{
busHandle = MassTransit.Util.TaskUtil.Await<BusHandle>(() => busControl.StartAsync());
scheduler.JobFactory = new MassTransitJobFactory(busControl);
scheduler.Start();
}
catch (Exception)
{
scheduler.Shutdown();
throw;
}
return true;
}
public bool Stop(HostControl hostControl)
{
Console.WriteLine("Stopping bus...");
scheduler.Standby();
if (busHandle != null) busHandle.Stop();
scheduler.Shutdown();
return true;
}
static IScheduler CreateScheduler()
{
ISchedulerFactory schedulerFactory = new StdSchedulerFactory();
IScheduler scheduler = MassTransit.Util.TaskUtil.Await<IScheduler>(() => schedulerFactory.GetScheduler()); ;
return scheduler;
}
}
My questions are:
How do I send the "intial" request, so that the state machine will transition to my initial state
How do I "react" within the consumers to check the data that were sent and then send new data like in 1?
Okay I figured it out. I probably had problems because I'm not only new to Masstransit/Automatonymous and RabbitMQ, but also don't have much experience with C# yet.
So if anyone ever will have the same problem, here is what you need:
Given the above example there are three different types plus some small interfaces needed:
A sender (in this case the "requester") including a specific consumer
A service that consumes specific message types (the "validator" and "interpreter")
A service that holds the state machine without a specific consumer
Some "contracts", which are interfaces defining the type of message that's sent/consumed
1) This is the sender:
using InterpreterStateMachine.Contracts;
using MassTransit;
using System;
using System.Threading.Tasks;
namespace InterpreterStateMachine.Requester
{
class Program
{
private static IBusControl _busControl;
static void Main(string[] args)
{
var busControl = ConfigureBus();
busControl.Start();
Console.WriteLine("Enter request or quit to exit: ");
while (true)
{
Console.Write("> ");
String value = Console.ReadLine();
if ("quit".Equals(value,StringComparison.OrdinalIgnoreCase))
break;
if (value != null)
{
String[] values = value.Split(';');
foreach (String v in values)
{
busControl.Publish<IRequesting>(new
{
Request = new Request(v),
TimeStamp = DateTime.UtcNow
});
}
}
}
busControl.Stop();
}
static IBusControl ConfigureBus()
{
if (null == _busControl)
{
_busControl = Bus.Factory.CreateUsingRabbitMq(cfg =>
{
var host = cfg.Host(new Uri(/*rabbitMQ server*/), h =>
{
/*credentials*/
});
cfg.ReceiveEndpoint(host, "answer_ready", e =>
{
e.Durable = true;
//here the consumer is registered
e.Consumer<AnswerConsumer>();
});
});
_busControl.Start();
}
return _busControl;
}
//here comes the actual logic of the consumer, which consumes a "contract"
class AnswerConsumer : IConsumer<IAnswerReady>
{
public async Task Consume(ConsumeContext<IAnswerReady> context)
{
await Console.Out.WriteLineAsync($"\nReceived Answer for \"{context.Message.Request.RequestString}\": {context.Message.Answer}.");
await Console.Out.WriteAsync(">");
}
}
}
}
2) This is the service (here it is the validation sercive)
using InterpreterStateMachine.Contracts;
using MassTransit;
using MassTransit.QuartzIntegration;
using MassTransit.RabbitMqTransport;
using Quartz;
using Quartz.Impl;
using System;
using System.Threading.Tasks;
using Topshelf;
namespace InterpreterStateMachine.Validator
{
public class ValidationService : ServiceControl
{
readonly IScheduler _scheduler;
static IBusControl _busControl;
BusHandle _busHandle;
public static IBus Bus => _busControl;
public ValidationService()
{
_scheduler = CreateScheduler();
}
public bool Start(HostControl hostControl)
{
Console.WriteLine("Creating bus...");
_busControl = MassTransit.Bus.Factory.CreateUsingRabbitMq(x =>
{
IRabbitMqHost host = x.Host(new Uri(/*rabbitMQ server*/), h =>
{
/*credentials*/
});
x.UseInMemoryScheduler();
x.UseMessageScheduler(new Uri(RabbitMqServerAddress));
x.ReceiveEndpoint(host, "validation_needed", e =>
{
e.PrefetchCount = 1;
e.Durable = true;
//again this is how the consumer is registered
e.Consumer<RequestConsumer>();
});
});
Console.WriteLine("Starting bus...");
try
{
_busHandle = MassTransit.Util.TaskUtil.Await<BusHandle>(() => _busControl.StartAsync());
_scheduler.JobFactory = new MassTransitJobFactory(_busControl);
_scheduler.Start();
}
catch (Exception)
{
_scheduler.Shutdown();
throw;
}
return true;
}
public bool Stop(HostControl hostControl)
{
Console.WriteLine("Stopping bus...");
_scheduler.Standby();
_busHandle?.Stop();
_scheduler.Shutdown();
return true;
}
static IScheduler CreateScheduler()
{
ISchedulerFactory schedulerFactory = new StdSchedulerFactory();
IScheduler scheduler = MassTransit.Util.TaskUtil.Await<IScheduler>(() => schedulerFactory.GetScheduler());
return scheduler;
}
}
//again here comes the actual consumer logic, look how the message is re-published after it was checked
class RequestConsumer : IConsumer<IValidationNeeded>
{
public async Task Consume(ConsumeContext<IValidationNeeded> context)
{
await Console.Out.WriteLineAsync($"(c) Received {context.Message.Request.RequestString} for validation (Id: {context.Message.RequestId}).");
context.Message.Request.IsValid = context.Message.Request.RequestString.Contains("=");
//send the new message on the "old" context
await context.Publish<IValidating>(new
{
Request = context.Message.Request,
IsValid = context.Message.Request.IsValid,
TimeStamp = DateTime.UtcNow,
RequestId = context.Message.RequestId
});
}
}
}
The validator consumes the contract "IValidationNeeded" and then publishes the contract "IValidating", which then will be consumed by the state machine itself (the "Validating" event).
3) The difference between a consumer service and the sate machine service lies withing the "ReceiveEndpoint". Here is no consumer registered, but the state machine is set:
...
InterpreterStateMachine _machine = new InterpreterStateMachine();
InMemorySagaRepository<InterpreterInstance> _repository = new InMemorySagaRepository<InterpreterInstance>();
...
x.ReceiveEndpoint(host, "state_machine", e =>
{
e.PrefetchCount = 1;
//here the state machine is set
e.StateMachineSaga(_machine, _repository);
e.Durable = false;
});
4) Last but not least, the contracts are pretty small and look like this:
using System;
namespace InterpreterStateMachine.Contracts
{
public interface IValidationNeeded
{
Guid RequestId { get; }
Request Request { get; }
}
}
So overall it's pretty straightforward, I just had to use my brain :D
I hope this will help someone.
I have a WinForms application that gets Car data from a Sqlite database file which is generated by a CSV file from a WebService, and the related Parts for each Car. When instanced, the Car class populates all properties from the database and gets a lot of data from a WebService that takes about 30 seconds to finish so I just call an async Task after populating the database properties and let it run asyncronously. After the web service returns all data and all work is done, the instance is saved in a List that works like an internal cache.
All works well until the user makes an operation on a Part which requires the property ParentCar to be returned while the WebService method is still running, causing a new Car to be instantiated and re polling the web service as many times the property is requested while the Car doesn't exist on the cache list. This stops as soon as the first instance finishes processing but all changes will be overwritten every time the next instances finish.
I'm struggling to find a way to ensure that a Car is only instanced one time without blocking the UI during the application life time, any ideas?
This is the code that I currently have:
public class Car
{
private List<Part> _parts = new List<Part>();
public string Id { get; private set; }
public int DbIndex
{
get { return DbClass.GetCarIndexById(Id); }
}
public ReadOnlyCollection<Part> Parts { get => _parts.AsReadOnly(); }
public Car(System.Data.SQLite.SQLiteDataReader sQLiteDataReader)
{
// Code to Load all properties from sQLiteDataReader
Task.Run(() => LongRuningWebServiceTask());
}
private async Task LongRuningWebServiceTask
{
// Long running code that will populate even more properties
SaveToInternalDb();
}
private void SaveToInternalDb()
{
if (DbIndex > -1)
DbClass.UpdateCarData(this);
else
DbClass.AddCar(this);
}
public void RelateParts(IEnumerable<Part> parts)
{
_parts.AddRange(parts);
}
~Car()
{
SaveToInternalDb();
}
}
public class Part
{
public string ParentCarId { get; private set; }
public Car ParentCar
{
get
{
Task getCar = DbClass.GetCarById(ParentCarId);
getCar.Wait();
return getCar.Result;
}
}
}
public static class DbClass
{
private static SQLiteConnection sqlConn;
private readonly string sqlFile = "pathToDbFile";
private static List<Car> CarCache = new List<Car>();
public static async Task<Car> GetCarById(string> carId, bool ignoreCache = false)
{
Car foundCar = null;
if (!ignoreCache)
foundCar = CarCache.Find(s => s.Id == carId);
if (foundCar == null)
{
try
{
string sql = string.Format("SELECT * FROM all_Cars WHERE Car = '{0}';", carId);
using (SQLiteCommand command = new SQLiteCommand(sql, sqlConn))
{
using (SQLiteDataReader reader = (SQLiteDataReader)await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
foundCar = new Car(reader));
}
}
}
catch (Exception e)
{
string m = e.Message;
}
if (foundCar != null)
{
var partsList = await GetPartsByCarId(carId);
if (partsList.Count > 0)
Car.RelateParts(partsList);
if (!ignoreCache)
{
if (foundCar.DbIndex == -1)
CarCache.Add(foundCar);
}
}
}
return foundCar;
}
public static async Task<List<Part>> GetPartsByCarId(string carId)
{
List<Part> foundParts = new List<Part>();
int index = GeCarIndexById(carId);
Car foundCar = index > -1 ? CarCache[index] : null;
if (foundCar != null)
foundParts = await GetPartsByCarId(carId);
return foundParts;
}
public static void InitiateSqlConnection()
{
if (sqlConn == null)
{
sqlConn = new SQLiteConnection("Data Source=" + sqlFile.FullName + ";Version=3;");
try
{
sqlConn.Open();
}
catch (Exception e)
{
var m = e.Message;
}
}
else
{
if(sqlConn.State == System.Data.ConnectionState.Broken || sqlConn.State == System.Data.ConnectionState.Closed)
sqlConn.Open();
}
}
public static int GetCarIndexById(string carId)
{
int index = -1;
for(int c = 0; c < CarCache.Count;c++)
{
if(CarCache[c].Id == carId)
{
index = c;
break;
}
}
return index;
}
public static void AddCar(Car car)
{
CarCache.Add(car);
}
public static void UpdateCarData(Car car)
{
if(car != null)
{
int index = car.DbIndex;
if(index > -1)
CarCache[index] = car;
}
}
}