Laden...

Wie kann man den asynchronen Task eines SignalR Hubs parametriesieren?

Erstellt von andreasS vor einem Jahr Letzter Beitrag vor einem Jahr 827 Views
A
andreasS Themenstarter:in
5 Beiträge seit 2023
vor einem Jahr
Wie kann man den asynchronen Task eines SignalR Hubs parametriesieren?

Hallo Community,

wie kann man den asynchronen Task eines SignalR Hubs parametriesieren?

Die Parameter id, dis und dg sollen per URL z. B. https://domain.name/RennenLive/index/2977/3/1 übergeben und irgendwie an den asynchronen Task SendAllRennenLive() weitergeleitet werden.

Mein Hub sieht aus wie folgt:


    public class RennenLiveHub : Hub
    {
        RennenLiveRepository RennenLiveRepository;

        public RennenLiveHub(IConfiguration configuration)
        {
            var connectionString = configuration.GetConnectionString("DefaultConnection");           
            RennenLiveRepository = new RennenLiveRepository(connectionString);
        }

        public async Task SendAllRennenLive()
        {
            int id = 2977;
            int dis = 3;
            int dg = 1;            
            var allRennenLive = RennenLiveRepository.GetAllRennenLive(id, dis, dg);
            await Clients.All.SendAsync("ReceivedAllRennenLive", allRennenLive);
        }
    }

Der asynchrone Task SendAllRennenLive holt sich alle Datensätze über RennenLiveRepository.GetAllRennenLive.

SendAllRennenLive wird in der Javascript Datei der chtml-Datei aufgerufen:


function InvokeAllRennenLive() {	
	connection2.invoke("SendAllRennenLive").catch(function (err) {
		return console.error(err.toString());
	});
}

sowie in der Methode TableDependency_OnChanged der Klasse SubscribeRennenLiveTableDependency


public class SubscribeRennenLiveTableDependency : ISubscribeTableDependency
    {
        SqlTableDependency<RennenLive> tableDependency;
        RennenLiveHub rennenLiveHub;

        public SubscribeRennenLiveTableDependency(RennenLiveHub rennenLiveHub)
        {
            this.rennenLiveHub = rennenLiveHub;
        }

        public void SubscribeTableDependency(string connectionString)
        {
            tableDependency = new SqlTableDependency<RennenLive>(connectionString);
            tableDependency.OnChanged += TableDependency_OnChanged;
            tableDependency.OnError += TableDependency_OnError;
            tableDependency.Start();
        }

        private void TableDependency_OnChanged(object sender, TableDependency.SqlClient.Base.EventArgs.RecordChangedEventArgs<RennenLive> e)
        {
            if (e.ChangeType != TableDependency.SqlClient.Base.Enums.ChangeType.None)
            {
                rennenLiveHub.SendAllRennenLive();
            }
        }

        private void TableDependency_OnError(object sender, TableDependency.SqlClient.Base.EventArgs.ErrorEventArgs e)
        {
            Console.WriteLine($"{nameof(RennenLive)} SqlTableDependency error: {e.Error.Message}");
        }
    }

Die Übergabe der Variablen in der Javascript Datei bekomme ich noch hin, wie schafft man es aber, dem Task SendAllRennenLive an beiden Stellen die korrekten Variablen zu übergeben bzw. wo liegt mein Denkfehler?

(Microsoft.NETCore.App\6.0.13)

2.207 Beiträge seit 2011
vor einem Jahr

Hallo andreasS,

du kannst der Methode


rennenLiveHub.SendAllRennenLive(...);

die Parameter übergeben und im Javascript/Typescript die Parameter entsprechend entgegennehmen.

Gruss

Coffeebean

A
andreasS Themenstarter:in
5 Beiträge seit 2023
vor einem Jahr

Hallo Coffeebean,

vielen Dank für Deine Antwort. Ich hab das mittlerweile wie folgt lösen können:

Die Hub Klasse erhält die Variablen **rennenId **und durchgang, die in der Methode **SendAllRennenLive **übergeben werden und vom Javascript zu Beginn aufgerufen wird. (Der Parameter dis wird nicht mehr benötigt) Sie sind dann in der Instanz der Hub Klasse vorhanden, die ja weiter besteht. Zusätzlich gibt es nun eine Methode UpdateAllRennenLive, die innerhalb **TableDependency_OnChanged **ohne Parameter aufgerufen wird. Diese bedient sich dann der bereits vorhandenen Variablenwerte innerhalb der Hub Klasse und holt sich ihre Datensätze ebenfalls per RennenLiveRepository.GetRennenLive.


public class RennenLiveHub : Hub
    {
        RennenLiveRepository RennenLiveRepository;
        int rennenId;        
        int durchgang;

        public RennenLiveHub(IConfiguration configuration)
        {
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            RennenLiveRepository = new RennenLiveRepository(connectionString);
        }


        public async Task SendAllRennenLive(int myRennenId, int myDurchgang)
        {  
            rennenId = myRennenId;            
            durchgang = myDurchgang;

            var results = RennenLiveRepository.GetAllRennenLive(rennenId, durchgang);
            await Clients.All.SendAsync("ReceivedAllRennenLive", results);
        }


        public async Task UpdateAllRennenLive()
        {           
            var results = RennenLiveRepository.GetRennenLive(rennenId, durchgang);
            await Clients.All.SendAsync("ReceivedRennenLive", results);
        }
}

Die Queryparameter der chtml Seite werden im Controller ausgelesen und in zwei hidden Textfelder auf der chtml Seite gespeichert. Das Javascript holt sich diese Werte dann aus den Textfeldern und startet den Hub:


function InvokeAllRennenLive() {	
	var rennen_ID = document.getElementById("Rennen_ID").value;
	var durchgang = document.getElementById("Durchgang").value;

	connection2.invoke("SendAllRennenLive", parseInt(rennen_ID), parseInt(durchgang)).catch(function (err) {
		return console.error(err.toString());
	});
}

16.835 Beiträge seit 2008
vor einem Jahr

Das is keine gute Idee. Damit hast Du Dir Race Conditions geschaffen, sofern Du mehrere Rennen und/oder Durchgänge parallel hast.
Das wird also in solchen Fällen hart nach hinten los gehen. So funktioniert im Endeffekt weder OOP noch SignalR (und dessen Hubs).

wie kann man den asynchronen Task eines SignalR Hubs parametriesieren?

Ist ehrlicherweise auch keine gute Fragestellung. Es ist schlauer zu beschreiben, was Du vor hast, sodass man eine Lösung zum Use Case anbieten kann.
Du bist aber schon mit der Parametriesierung der Methoden ins Rennen gegangen, und willst genau das umsetzen - völlig egal ob das der richtige Weg ist (was er hier wohl eher nicht ist).

Da Du leider nichts zum Use Case geschrieben hast, muss man raten: Du willst im Endeffekt, dass der User eine Connection hat, aber über SignalR alle Rennen parallel verarbeitet werden können.
Das geht aus den besagten Problemen mit Deiner Implementierung nicht.

Im SignalR ist es vorgesehen, dass Du Dir merkst, welche Connection welche Informationen haben will.
Früher musste man das selbst implementieren ( Mapping SignalR Users to Connections ) - mittlerweile macht man das mit SignalR Groups.
Manage users and groups in SignalR

Der Client gibt also bekannt, welches Rennen er abonniert, und Du fügst die Connection der Gruppe hinzu.
Willst Du also alle Clients eines Rennens zeitgleich informieren, dann informierst Du alle Connections der Gruppe des Rennens.


await Clients.Group("MyRennenId").SendAsync("RennenUpdate" ...

Ich hoffe, dass das ist, was Du suchst. Ansonsten musst bitte die Tipps beachten und erzählen, was der Use Case ist...
Und der Hinweis, dass SignalR prinzipiell ein komplexes Konstrukt ist, das aber im Endeffekt sehr einfach an programmieren kann - aber man sollte mal die Docs gelesen haben, um die Konzepte (wie eben Gruppen) zu verstehen. Sonst wirds vermutlich eher was instabiles am Ende, wo Du kein Spaß dran haben wirst.

A
andreasS Themenstarter:in
5 Beiträge seit 2023
vor einem Jahr

Hallo Abt,

vielen Dank für Deine ausführliche Antwort und diesen wertvollen Hinweis!

Kurz zum Use Case, den Du schon vermutet hast:

Auf dem Server landen in einer Tabelle Resultatdatensätze. Diese stammen von Rennen und sind mit einer RennenId versehen. Es ist von mehreren Rennen
gleichzeitig auszugehen. Ein Client möchte EIN Rennen verfolgen und soll diese Resultatdatensätze "in Echtzeit" angezeigt bekommen. Das Rennen wählt er über
einen Parameter in der URL.

Anzahl der Rennen gleichzeitig bis ca. 10
Anzahl der Datensätze pro Rennen ca. 1-2 pro Minute, gesamt bis ca. 200
Anzahl der Clients pro Rennen bis ca. 500

Der Ansatz über EINE Tabelle zu gehen erschien mir durchaus möglich. Alternativ könnte man auch mit einer eigenen Tabelle pro Rennen arbeiten. Das ist aber wohl nicht entscheidend, denn parametrisieren muss ich trotzdem; der Hub muss ja wissen welche Datensätze aus der einen Tabelle bzw. aus welcher Tabelle er liefern soll. Oder gibt es dafür eine andere und bessere Möglichkeit? Ich muss das nicht genau so umsetzen sondern möchte eine stabile und performante Lösung ohne Stress 😉

Das Senden an ALLE Clients ist definitiv nicht erforderlich. Der Einsatz von Groups verringert die auszusendende Datenmenge erheblich.

Mein Hub sieht modifiziert mit den Groups nun wie folgt aus:


public class ResultHub : Hub
    {
        ResultRepository ResultRepository;

        int rennenId;        
        int durchgang;
        string clientGroup;

        public ResultHub(IConfiguration configuration)
        {
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            ResultRepository = new ResultRepository(connectionString);
        }


        public async Task JoinAndSendResults(int myRennenId, int myDurchgang)
        {  
            rennenId = myRennenId;            
            durchgang = myDurchgang;
            clientGroup = rennenId.ToString();

            string connectionId = Context.ConnectionId.ToString();
        
            await Groups.AddToGroupAsync(connectionId, clientGroup);
            var results = ResultRepository.GetResults(rennenId, durchgang);
            await Clients.Group(clientGroup).SendAsync("ReceivedResults", results);
        }


        public async Task UpdateResults()
        {
            var results = ResultRepository.GetResults(rennenId, durchgang);
            await Clients.Group(clientGroup).SendAsync("ReceivedResults", results);
        }

    }

Denkst Du, das kann so funktionieren?

16.835 Beiträge seit 2008
vor einem Jahr

Auch das wird nicht funktionieren, bitte les Dir die Basics von SignalR durch.

Bei ASP.NET Core SignalR existiert vom Hub nur eine einzige Instanz - es ist als Singleton registriert, sonst würde es nicht funktionieren.
Das heisst, dass alle Verbindungen sich diese Hub Instanz teilen.

Jede Verbindung bei Dir überschreibt damit Deine Felder, und es kommt zu Race Conditions.
Daher die Basics beachten: verwende niemals Felder zum Datenaustausch in SignalR Hubs.

Den Code kannst so nochmal umbauen 😉

A
andreasS Themenstarter:in
5 Beiträge seit 2023
vor einem Jahr

Hallo Benjamin,

ich denke, ich hab es jetzt verstanden 😉 Mein Problem war immer, woher weiß der Hub, welche Daten er aus der Tabelle selecten muss. Die Sache mit den Gruppen war das eine, das andere hab ich nun über die **TableDependency_OnChanged **gelöst.


private void TableDependency_OnChanged(object sender, TableDependency.SqlClient.Base.EventArgs.RecordChangedEventArgs<Result> e)
        {
            if (e.ChangeType != TableDependency.SqlClient.Base.Enums.ChangeType.None)
            {
                var changedEntity = e.Entity;
                int _rennen_Id = changedEntity.Rennen_ID;
                int _durchgang = changedEntity.Durchgang;
                resultHub.SendResults(_rennen_Id, _durchgang);
            }
        }

Und mein Hub ist wieder schlanker geworden.


public class ResultHub : Hub
    {
        ResultRepository ResultRepository;

        public ResultHub(IConfiguration configuration)
        {
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            ResultRepository = new ResultRepository(connectionString);
        }

        public async Task JoinResults(int RennenId)
        {        
            await Groups.AddToGroupAsync(Context.ConnectionId.ToString(), RennenId.ToString());            
        }

        public async Task SendResults(int RennenId, int Durchgang)
        {
            var results = ResultRepository.GetResults(RennenId, Durchgang);
            await Clients.Group(RennenId.ToString()).SendAsync("ReceivedResults", results);
        }

    }

Das Javascript führt nun nacheinander


JoinResults();
SendResults();

function JoinResults() {
	var rennen_ID = document.getElementById("Rennen_ID").value;	
	connection.invoke("JoinResults", parseInt(rennen_ID)).catch(function (err) {
		return console.error(err.toString());
	});
}

function SendResults() {
	var rennen_ID = document.getElementById("Rennen_ID").value;
	var durchgang = document.getElementById("Durchgang").value;
	connection.invoke("SendResults", parseInt(rennen_ID), parseInt(durchgang)).catch(function (err) {
		return console.error(err.toString());
	});
}

aus. Könnte nun funktionieren!?

16.835 Beiträge seit 2008
vor einem Jahr

Vorweg: weiß nicht genau wie Du auf Hallo Benjamin kommst, aber belass es bitte einfach bei "Abt". Danke.
Nebeneffekt: dann wissen auch andere potentielle Leser:innen, wen Du ansprichst. Nicknames gibts ja nicht umsonst.

Könnte nun funktionieren!?

Ja, sieht besser aus.

A
andreasS Themenstarter:in
5 Beiträge seit 2023
vor einem Jahr

Sorry - ich wollte Dir nicht zu nahe treten sondern höflich sein, werde aber Deiner Bitte in Zukunft gerne nachkommen.
Trotzdem recht herzlichen Dank für Deine kompetente Hilfe - es hat mir sehr geholfen.
andreas