SignalR: Echtzeitkommunikation zwischen Backend und Frontend

In diesem Beitrag möchte ich gerne einen kleinen Chat-Client schreiben. Dabei ist es wichtig, dass die Daten nahezu in Echtzeit zwischen dem Backend und den verschiedenen Clients ausgetauscht werden können. Genau an dieser Stelle kommt nun SignalR ins Spiel. Bei SignalR handelt es sich um eine kostenlose Open-Source-Library von Microsoft, welche es ermöglicht mit Servercode asynchrone Benachrichtigungen an clientseitige Anwendungen zu senden. Das folgende Projekt besteht aus zwei Komponenten: Einmal dem Backend, welches eine ASP.NET Core Anwendung ist und dem Frontend, welches wir als Xamarin.Forms App für Android, iOS und UWP umsetzen wollen.

Beginnen wir zunächst mit dem Backend. Dazu öffnen wir Visual Studio und legen eine neue ASP.NET Core Web Application an. Ich habe mich für den Namen SignalRChat.Backend entschieden. Im Einrichtungsassistenten müsst ihr nun sicherstellen, dass ASP.NET Core 3.1 als Framework ausgewählt ist. Als Vorlage wählen wir Empty und die restlichen Einstellungen lassen wir auf den Default-Settings.

Im nächsten Schritt fügen wir einen neuen Ordner mit dem Namen Hubs dem Projekt hinzu. Innerhalb dieses Ordners legen wir nun eine Klasse mit dem Namen ChatHub an. Wir fügen Hub als Base-Klasse hinzu und schreiben die Methoden SendMessageAsync, welche zwei Parameter entgegennimmt und an alle anderen Clients eine Nachricht mit diesen Parametern schickt,

public class ChatHub : Hub
{
    public async Task SendMessageAsync(string user, string message)
    {
        await Clients.Others.SendAsync("ReceiveMessage", user, message);
    }
}

Nun öffnen wir die Datei Startup.cs und ergänzen den Aufruf AddSignalR in der Methode ConfigureServices.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR();
}

Anschließend müssen wir den UseEndpoints Aufruf in der Methode Configure anpassen. Weil wir hier unseren Hub noch auf einen Endpunkt mappen müssen.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/hubs/chat");
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

Damit ist unser Backend eigentlich schon fertig und wir können uns an die Umsetzung des Frontends machen. Dazu erstellen wir eine neue Xamarin.Forms App und nutzen die leere Vorlage als Grundlage. Anschließend aktualisieren wir die bereits installierten NuGet-Pakete und außerdem fügen wir noch die folgenden Pakete hinzu: Microsoft.AspNetCore.SignalR.Client, Unity.Container, Unity.ServiceLocation und Xamarin.Forms.Visual.Material.

Zunächst erstellen wir ein einfaches Datenmodell, welches unsere Chat-Nachricht repräsentieren soll. Dafür erstellen wir einen Ordner Models und darin eine Klasse ChatMessage mit folgendem Inhalt.

public class ChatMessage
{
    public string User { get; }
    public string Message { get; }
    public bool IsOwn { get; }

    public ChatMessage(string user, string message, bool isOwn = false)
    {
        User = user;
        Message = message;
        IsOwn = isOwn;
    }
}

Eine Chat-Nachricht besteht aus einem User, der eigentlichen Nachricht und einen Indikator, ob es sich um eine eigene Nachricht handelt. Diese Information wird später in der UI verwendet, um die eigenen Nachrichten in einer anderen Form darzustellen.

Das Herzstück unserer App wird der ChatService sein. Dafür wollen wir ein Interface für unseren ChatService mit dem Namen IChatService erstellen. Auch hierfür habe ich eine neuen Ordner Interfaces erstellt und darin dann das Interface IChatService mit dem folgenden Inhalt.

public interface IChatService
{
    event EventHandler<ChatMessage> MessagedReceived;

    Task<bool> ConnectAsync();

    Task<bool> DisconnectAsync();

    Task SendMessageAsync(string user, string message);
}

Wir bieten hier ein Event an, welches über neue Nachrichten informiert und darüber hinaus natürlich eine Connect– und eine Disconnect-Methode, um die Verbindung über SignalR herzustellen bzw. zu beenden. Die letzte Nachricht trägt den Namen SendMessageAsync und sendet die eigentliche Nachricht. Kümmern wir uns gleich um die Implementierung des Services. Dafür legen wir den neuen Ordner Services und die Klasse ChatService an.

public class ChatService : IChatService
{
    private readonly HubConnection _hubConnection;

    public event EventHandler<ChatMessage> MessagedReceived;

    public ChatService()
    {
        _hubConnection = new HubConnectionBuilder()
            .WithUrl($"{Statics.BaseUrl}/hubs/chat")
            .Build();
    }

    public async Task<bool> ConnectAsync()
    {
        if (_hubConnection.State != HubConnectionState.Connected)
        {
            await _hubConnection.StartAsync();
        }

        _hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            MessagedReceived?.Invoke(this, new ChatMessage(user, message));
        });

        return _hubConnection.State == HubConnectionState.Connected;
    }

    public async Task<bool> DisconnectAsync()
    {
        if (_hubConnection.State == HubConnectionState.Connected)
        {
            await _hubConnection.StopAsync();
        }

        return _hubConnection.State == HubConnectionState.Disconnected;
    }

    public async Task SendMessageAsync(string user, string message)
    {
        if (_hubConnection.State != HubConnectionState.Connected)
            await ConnectAsync();

        await _hubConnection.SendAsync("SendMessageAsync", user, message);
    }
}

Intern nutzt unser ChatService eine HubConnection, welche durch das SignalR-Package bereitgestellt werden. Im Konstruktor des Services bauen wir uns die HubConnection zusammen, indem wir die passende URL zu unserem Backend übergeben. Ihr erinnert euch sicherlich, dass wir im Backend innerhalb der Startup.cs-Datei den Aufruf MapHubs ergänzt haben.

Innerhalb der ConnectAsync-Methode stellen wir nun eine Verbindung über SignalR her und registrieren uns für das Erhalten der ReceiveMessage-Nachrichten. Hier nutzen wir dann unser eigenes Event, welches die Daten dann noch aufbereitet. Um eine Nachricht über SignalR zu senden, rufen wir SendAsync auf unserer HubConnection mit den passenden Parametern auf.

Ansonsten habe ich für die App noch ein ChatViewModel geschrieben, welches die Informationen, wie den Namen und die Nachricht des Nutzers aus der UI übernimmt und verschiedene Commands bereitstellt, um entsprechend eine Verbindung über SignalR herzustellen oder auch die Nachricht zu senden. Darüber hinaus wird innerhalb des ViewModels auch eine ObservableCollection verwaltet, welche die ganzen Chat-Nachrichten verwaltet. An dieser Stelle stelle ich euch nicht den gesamten Code hier im Blog zur Verfügung, aber ihr findet den vollständigen Code am Ende des Beitrags.

Nun ist es an der Zeit einmal unser Backend zusammen mit dem Frontend auszuprobieren. Dazu öffnen wir die Properties von unserer Solution und wählen bei Startup Project nun Multiple startup projects aus und starten das Backend, die Android- und die UWP-Version unserer App.

Um den Zugriff von einem Android-Gerät auf das Backend zu gewährleisten, nutze ich ngrok, um den localhost entsprechend über eine ngrok.io-Adresse verfügbar zu machen. Wie genau ngrok funktioniert, habe ich euch hier bereits gezeigt.

Wenn wir nun auf Start innerhalb von Visual Studio klicken, dann starten neben dem Backend auch der UWP- und der Android-Client. Der folgende Screenshot zeigt einmal die Konversation unter UWP.

Der folgende Screenshot zeigt den passenden Android-Client zu der Konversation. Ihr seht nun auch, warum wir die IsOwn-Property innerhalb unserer ChatMessage haben, denn so können wir entsprechend die eigenen Nachrichten auf der rechten Seite und die Nachrichten der anderen Teilnehmer auf der linken Seite anzeigen und auch die Hintergrundfarbe entsprechend anpassen.

Damit haben wir jetzt in wenigen Schritten ein Backend erstellt, welches uns eine Echtzeitkommunikation über SignalR zur Verfügung stellt, welchen wir nutzen können, um diese kleine Chat-App zu schreiben.

Wie versprochen findet ihr den gesamten Code unter dem folgenden Download-Link:

Xamarin.Forms: Welche NuGet Pakete verwende ich? Deutschlands Kennzeichen: Meine erste iOS-App Azure OpenAI verwenden, um einen Copiloten für eigenen Daten zu erstellen – Teil 1