Echtzeit-Anwendungen mit SignalR in .NET

12 Min. Lesezeit9. März 2026
SignalR .NETReal-time .NETWebSocket .NETSignalR HubSignalR RedisSignalR scalingReal-time dashboardSignalR authentication

# Echtzeit-Anwendungen mit SignalR in .NET

Live-Dashboards, Chat-Systeme, kollaborative Editoren, Benachrichtigungs-Feeds -- all das erfordert ein grundlegend anderes Kommunikationsmodell als klassische Request-Response-Architekturen. SignalR abstrahiert die Komplexitaet persistenter Verbindungen, Transport-Verhandlung und Wiederverbindungslogik in ein sauberes Programmiermodell. Bei Echtzeit-Dashboards, die ich fuer Monitoring-Plattformen entwickelt habe, war SignalR durchgehend der schnellste Weg von der Idee zur produktionsreifen bidirektionalen Kommunikation.

Warum SignalR statt roher WebSockets

SignalR ist mehr als ein WebSocket-Wrapper. Es uebernimmt Transport-Fallback (WebSockets, Server-Sent Events, Long Polling), automatische Wiederverbindung, Verbindungs-Lifecycle-Management und Nachrichten-Serialisierung. All das von Grund auf mit rohen WebSockets zu implementieren ist moeglich, aber fuer Geschaeftsanwendungen selten lohnend.

Die wichtigsten Vorteile:

  • Automatische Transport-Verhandlung basierend auf Client- und Server-Faehigkeiten
  • Eingebaute Gruppen- und Benutzerverwaltung
  • Strongly-typed Hubs fuer Compile-Time-Sicherheit
  • Native Integration mit ASP.NET Core Authentifizierung und Autorisierung
  • Skalierung ueber Redis, Azure SignalR Service oder eigene Backplanes
  • SignalR Hub einrichten

    Der Hub ist der serverseitige Einstiegspunkt fuer jede Client-Kommunikation. Jede oeffentliche Methode auf dem Hub kann von verbundenen Clients aufgerufen werden.

    Einfacher Hub

    csharp
    public class BenachrichtigungHub : Hub
    {
        private readonly ILogger<BenachrichtigungHub> _logger;
    
        public BenachrichtigungHub(ILogger<BenachrichtigungHub> logger)
        {
            _logger = logger;
        }
    
        public async Task BenachrichtigungSenden(string benutzer, string nachricht)
        {
            _logger.LogInformation(
                class="code-string">"Benachrichtigung von {ConnectionId} an {Benutzer}",
                Context.ConnectionId, benutzer);
    
            await Clients.User(benutzer).SendAsync(class="code-string">"BenachrichtigungEmpfangen", nachricht);
        }
    
        public async Task NachrichtAnAlle(string nachricht)
        {
            await Clients.All.SendAsync(class="code-string">"NachrichtEmpfangen", Context.User?.Identity?.Name, nachricht);
        }
    
        public override async Task OnConnectedAsync()
        {
            _logger.LogInformation(class="code-string">"Client verbunden: {ConnectionId}", Context.ConnectionId);
            await base.OnConnectedAsync();
        }
    
        public override async Task OnDisconnectedAsync(Exception? exception)
        {
            _logger.LogInformation(
                class="code-string">"Client getrennt: {ConnectionId}, Grund: {Grund}",
                Context.ConnectionId, exception?.Message ?? class="code-string">"saubere Trennung");
            await base.OnDisconnectedAsync(exception);
        }
    }

    Registrierung in Program.cs

    csharp
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddSignalR(options =>
    {
        options.EnableDetailedErrors = builder.Environment.IsDevelopment();
        options.KeepAliveInterval = TimeSpan.FromSeconds(class="code-number">15);
        options.ClientTimeoutInterval = TimeSpan.FromSeconds(class="code-number">30);
        options.MaximumReceiveMessageSize = class="code-number">64 * class="code-number">1024; class=class="code-string">"code-comment">// class="code-number">64 KB
    });
    
    var app = builder.Build();
    
    app.MapHub<BenachrichtigungHub>(class="code-string">"/hubs/benachrichtigungen");
    app.Run();

    Strongly-Typed Hubs

    String-basierte Methodennamen wie `SendAsync("NachrichtEmpfangen", ...)` sind fragil. Ein Tippfehler kompiliert problemlos, schlaegt aber zur Laufzeit stillschweigend fehl. Strongly-typed Hubs beseitigen dieses Problem vollstaendig.

    Client-Interface definieren

    csharp
    public interface IBenachrichtigungClient
    {
        Task BenachrichtigungEmpfangen(string nachricht);
        Task NachrichtEmpfangen(string benutzer, string nachricht);
        Task BenutzerGruppeBeigetreten(string benutzer, string gruppe);
        Task BenutzerGruppeVerlassen(string benutzer, string gruppe);
        Task DashboardUpdateEmpfangen(DashboardDaten daten);
    }

    Strongly-Typed Hub implementieren

    csharp
    public class BenachrichtigungHub : Hub<IBenachrichtigungClient>
    {
        public async Task BenachrichtigungSenden(string benutzer, string nachricht)
        {
            class=class="code-string">"code-comment">// Zur Compile-Zeit geprueft -- keine Magic Strings mehr
            await Clients.User(benutzer).BenachrichtigungEmpfangen(nachricht);
        }
    
        public async Task NachrichtAnAlle(string nachricht)
        {
            var absender = Context.User?.Identity?.Name ?? class="code-string">"Anonym";
            await Clients.All.NachrichtEmpfangen(absender, nachricht);
        }
    
        public async Task DashboardUpdateSenden(DashboardDaten daten)
        {
            await Clients.Group(class="code-string">"dashboard-betrachter").DashboardUpdateEmpfangen(daten);
        }
    }

    Bei Echtzeit-Dashboards, die ich entwickelt habe, haben Strongly-typed Hubs mehrere Fehler beim Kompilieren abgefangen, die sonst stille Laufzeitfehler gewesen waeren. Der geringe Anfangsaufwand fuer die Interface-Definition zahlt sich sofort aus.

    Client-Server-Kommunikationsmuster

    SignalR unterstuetzt verschiedene Kommunikationsmuster, jeweils fuer unterschiedliche Szenarien geeignet.

    Server an Client

    Das haeufigste Muster. Der Server pusht Daten an verbundene Clients, ohne dass diese sie anfordern.

    csharp
    class=class="code-string">"code-comment">// Innerhalb einer Hub-Methode
    await Clients.All.NachrichtEmpfangen(benutzer, nachricht);           class=class="code-string">"code-comment">// Alle Clients
    await Clients.Caller.BenachrichtigungEmpfangen(class="code-string">"Bestaetigt");        class=class="code-string">"code-comment">// Nur der Aufrufer
    await Clients.Others.NachrichtEmpfangen(benutzer, nachricht);        class=class="code-string">"code-comment">// Alle ausser Aufrufer
    await Clients.User(userId).BenachrichtigungEmpfangen(msg);           class=class="code-string">"code-comment">// Bestimmter Benutzer
    await Clients.Group(class="code-string">"admins").BenachrichtigungEmpfangen(msg);        class=class="code-string">"code-comment">// Gruppenmitglieder

    Client an Server

    Clients rufen Hub-Methoden direkt auf. Hier ein JavaScript-Client-Beispiel:

    javascript
    const connection = new signalR.HubConnectionBuilder()
        .withUrl(class="code-string">"/hubs/benachrichtigungen", {
            accessTokenFactory: () => getAccessToken()
        })
        .withAutomaticReconnect([class="code-number">0, class="code-number">2000, class="code-number">5000, class="code-number">10000, class="code-number">30000])
        .configureLogging(signalR.LogLevel.Information)
        .build();
    
    connection.on(class="code-string">"NachrichtEmpfangen", (benutzer, nachricht) => {
        console.log(`${benutzer}: ${nachricht}`);
        nachrichtAnUIAnhaengen(benutzer, nachricht);
    });
    
    connection.on(class="code-string">"DashboardUpdateEmpfangen", (daten) => {
        dashboardAktualisieren(daten);
    });
    
    async function nachrichtSenden(nachricht) {
        await connection.invoke(class="code-string">"NachrichtAnAlle", nachricht);
    }
    
    await connection.start();

    Hub-Methoden von ausserhalb des Hubs aufrufen

    In Produktionssystemen muessen Nachrichten oft aus Hintergrunddiensten, API-Controllern oder Event-Handlern gesendet werden -- nicht nur aus Hub-Methoden heraus.

    csharp
    public class BestellverarbeitungService : BackgroundService
    {
        private readonly IHubContext<BenachrichtigungHub, IBenachrichtigungClient> _hubContext;
    
        public BestellverarbeitungService(
            IHubContext<BenachrichtigungHub, IBenachrichtigungClient> hubContext)
        {
            _hubContext = hubContext;
        }
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var updates = await GetAusstehendeBenachrichtigungen();
    
                foreach (var update in updates)
                {
                    await _hubContext.Clients.User(update.BenutzerId)
                        .BenachrichtigungEmpfangen(
                            $class="code-string">"Bestellung {update.BestellungId} Status: {update.Status}");
                }
    
                await Task.Delay(TimeSpan.FromSeconds(class="code-number">5), stoppingToken);
            }
        }
    }

    Gruppen- und Benutzerverwaltung

    Gruppen sind der primaere Mechanismus, um festzulegen, welche Clients welche Nachrichten erhalten. Sie sind leichtgewichtig, dynamisch und erfordern keine Vorregistrierung.

    Gruppenoperationen

    csharp
    public class ZusammenarbeitHub : Hub<IZusammenarbeitClient>
    {
        private readonly IGruppenTracker _gruppenTracker;
    
        public ZusammenarbeitHub(IGruppenTracker gruppenTracker)
        {
            _gruppenTracker = gruppenTracker;
        }
    
        public async Task ProjektBeitreten(string projektId)
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, $class="code-string">"projekt-{projektId}");
    
            var benutzername = Context.User?.Identity?.Name ?? class="code-string">"Anonym";
            await _gruppenTracker.BenutzerZuGruppeHinzufuegen(
                projektId, benutzername, Context.ConnectionId);
    
            await Clients.Group($class="code-string">"projekt-{projektId}")
                .BenutzerGruppeBeigetreten(benutzername, projektId);
        }
    
        public async Task ProjektVerlassen(string projektId)
        {
            await Groups.RemoveFromGroupAsync(Context.ConnectionId, $class="code-string">"projekt-{projektId}");
    
            var benutzername = Context.User?.Identity?.Name ?? class="code-string">"Anonym";
            await _gruppenTracker.BenutzerAusGruppeEntfernen(projektId, Context.ConnectionId);
    
            await Clients.Group($class="code-string">"projekt-{projektId}")
                .BenutzerGruppeVerlassen(benutzername, projektId);
        }
    
        public override async Task OnDisconnectedAsync(Exception? exception)
        {
            var gruppen = await _gruppenTracker.GruppenFuerVerbindung(Context.ConnectionId);
            foreach (var gruppe in gruppen)
            {
                await _gruppenTracker.BenutzerAusGruppeEntfernen(gruppe, Context.ConnectionId);
                await Clients.Group($class="code-string">"projekt-{gruppe}")
                    .BenutzerGruppeVerlassen(
                        Context.User?.Identity?.Name ?? class="code-string">"Anonym", gruppe);
            }
    
            await base.OnDisconnectedAsync(exception);
        }
    }

    Ein entscheidender Punkt: SignalR-Gruppen sind nicht persistent. Bei einem Serverneustart gehen alle Gruppenmitgliedschaften verloren. Wenn Ihre Anwendung dauerhafte Gruppenzugehoerigkeit benoetigt, verwalten Sie diese in Ihrem eigenen Datenspeicher und treten Sie bei Wiederverbindung erneut den Gruppen bei.

    Authentifizierung mit SignalR

    SignalR integriert sich direkt in die ASP.NET Core Authentifizierung. Dieselbe JWT- oder Cookie-Authentifizierung, die Ihre API-Endpunkte schuetzt, schuetzt auch Ihre Hubs.

    JWT-Authentifizierung fuer SignalR

    WebSocket-Verbindungen koennen nach dem initialen Handshake keine benutzerdefinierten HTTP-Header senden. SignalR loest dies, indem das Token waehrend der Verhandlungsphase als Query-String-Parameter akzeptiert wird.

    csharp
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = builder.Configuration[class="code-string">"Jwt:Issuer"],
                ValidAudience = builder.Configuration[class="code-string">"Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(builder.Configuration[class="code-string">"Jwt:Key"]!))
            };
    
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var accessToken = context.Request.Query[class="code-string">"access_token"];
                    var path = context.HttpContext.Request.Path;
    
                    if (!string.IsNullOrEmpty(accessToken) &&
                        path.StartsWithSegments(class="code-string">"/hubs"))
                    {
                        context.Token = accessToken;
                    }
    
                    return Task.CompletedTask;
                }
            };
        });

    Hub-Methoden absichern

    csharp
    [Authorize]
    public class SichererHub : Hub<IBenachrichtigungClient>
    {
        [Authorize(Roles = class="code-string">"Admin")]
        public async Task AdminNachricht(string nachricht)
        {
            await Clients.All.BenachrichtigungEmpfangen(nachricht);
        }
    
        [Authorize(Policy = class="code-string">"PremiumBenutzer")]
        public async Task PremiumFunktion(string daten)
        {
            await Clients.Caller.BenachrichtigungEmpfangen($class="code-string">"Premium-Ergebnis: {daten}");
        }
    }

    SignalR mit Redis Backplane skalieren

    Ein einzelner Server verwaltet alle seine Verbindungen im Speicher. Bei der Skalierung auf mehrere Server hinter einem Load Balancer kann ein Client auf Server A keine Nachrichten von Server B empfangen. Das Redis Backplane loest dieses Problem, indem Nachrichten ueber alle Server hinweg verbreitet werden.

    Konfiguration

    csharp
    builder.Services.AddSignalR()
        .AddStackExchangeRedis(builder.Configuration.GetConnectionString(class="code-string">"Redis")!, options =>
        {
            options.Configuration.ChannelPrefix = RedisChannel.Literal(class="code-string">"SignalR");
        });

    Funktionsweise

  • Client verbindet sich mit Server A und tritt der Gruppe "dashboard" bei
  • Ein Hintergrunddienst auf Server B sendet eine Nachricht an die Gruppe "dashboard"
  • Server B veroeffentlicht die Nachricht in Redis
  • Server A ist auf den Redis-Kanal abonniert, empfaengt die Nachricht und leitet sie an den Client weiter
  • Wann stattdessen Azure SignalR Service

    Fuer Produktionslasten mit mehr als einigen tausend gleichzeitigen Verbindungen sollten Sie Azure SignalR Service in Betracht ziehen. Er lagert die gesamte Verbindungsverwaltung aus und eliminiert den Bedarf an Sticky Sessions, Redis-Infrastruktur und Kapazitaetsplanung fuer Verbindungszahlen.

    csharp
    builder.Services.AddSignalR()
        .AddAzureSignalR(builder.Configuration[class="code-string">"Azure:SignalR:ConnectionString"]);

    Fehlerbehandlung und Wiederverbindungsstrategie

    Echtzeit-Verbindungen sind von Natur aus fragil. Netzwerke fallen aus, Server starten neu, Clients gehen in den Schlafmodus. Eine robuste Wiederverbindungsstrategie ist fuer Produktionssysteme unverzichtbar.

    Serverseitige Fehlerbehandlung

    csharp
    public class RobusterHub : Hub<IBenachrichtigungClient>
    {
        private readonly ILogger<RobusterHub> _logger;
    
        public RobusterHub(ILogger<RobusterHub> logger)
        {
            _logger = logger;
        }
    
        public async Task DatenVerarbeiten(DatenAnfrage anfrage)
        {
            try
            {
                var ergebnis = await ValidierenUndVerarbeiten(anfrage);
                await Clients.Caller.BenachrichtigungEmpfangen($class="code-string">"Erfolg: {ergebnis}");
            }
            catch (ValidationException ex)
            {
                _logger.LogWarning(ex,
                    class="code-string">"Validierung fehlgeschlagen fuer {ConnectionId}", Context.ConnectionId);
                throw new HubException($class="code-string">"Validierungsfehler: {ex.Message}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex,
                    class="code-string">"Unerwarteter Fehler fuer {ConnectionId}", Context.ConnectionId);
                throw new HubException(
                    class="code-string">"Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.");
            }
        }
    }

    Das Werfen einer `HubException` sendet die Nachricht an den Client. Jeder andere Exception-Typ fuehrt zu einer generischen Fehlermeldung, es sei denn `EnableDetailedErrors` ist aktiviert. Aktivieren Sie detaillierte Fehler niemals in Produktion -- sie legen Stack-Traces und internen Zustand offen.

    Clientseitige Wiederverbindung

    javascript
    const connection = new signalR.HubConnectionBuilder()
        .withUrl(class="code-string">"/hubs/benachrichtigungen")
        .withAutomaticReconnect({
            nextRetryDelayInMilliseconds: (retryContext) => {
                if (retryContext.elapsedMilliseconds < class="code-number">60000) {
                    return Math.random() * class="code-number">10000;
                }
                return null; class=class="code-string">"code-comment">// Nach class="code-number">60 Sekunden aufhoeren
            }
        })
        .build();
    
    connection.onreconnecting((error) => {
        verbindungsstatusAnzeigen(class="code-string">"Verbindung wird wiederhergestellt...");
    });
    
    connection.onreconnected((connectionId) => {
        verbindungsstatusAnzeigen(class="code-string">"Verbunden");
        class=class="code-string">"code-comment">// Nach Wiederverbindung Gruppen erneut beitreten
        gruppenNeuBeitreten();
    });
    
    connection.onclose((error) => {
        verbindungsstatusAnzeigen(class="code-string">"Getrennt");
        setTimeout(() => verbindungStarten(), class="code-number">5000);
    });

    Das entscheidende Detail, das viele Teams uebersehen: Nach einer Wiederverbindung erhaelt der Client eine neue ConnectionId. Alle Gruppenmitgliedschaften der vorherigen Verbindung sind weg. Sie muessen Gruppen explizit erneut beitreten. Bei Echtzeit-Dashboards, die ich gebaut habe, fuehre ich clientseitig eine Liste aktiver Abonnements und spiele sie bei jeder Wiederverbindung erneut ab.

    SignalR vs WebSockets vs Server-Sent Events

    Jede Technologie bedient unterschiedliche Anwendungsfaelle. Die falsche Wahl fuehrt zu unnoetieger Komplexitaet oder fehlenden Faehigkeiten.

    | Aspekt | SignalR | Rohe WebSockets | Server-Sent Events (SSE) |

    |---|---|---|---|

    | Richtung | Bidirektional | Bidirektional | Nur Server an Client |

    | Transport | Automatisch | Nur WebSocket | HTTP-Streaming |

    | Wiederverbindung | Eingebaut | Manuelle Implementierung | Eingebaut (EventSource) |

    | Browser-Support | Universal (mit Fallback) | Moderne Browser | Moderne Browser |

    | Nachrichtenformat | JSON/MessagePack | Rohe Bytes/Text | Nur Text |

    | Gruppen/Benutzer | Eingebaut | Manuelle Implementierung | Manuelle Implementierung |

    | Skalierung | Redis/Azure Backplane | Eigene Loesung | Eigene Loesung |

    | Auth-Integration | ASP.NET Core nativ | Manuell | Standard-HTTP |

    | Komplexitaet | Niedrig | Hoch | Niedrig |

    | Ideal fuer | .NET Full-Stack-Apps | Eigene Protokolle, Spiele | Einfache Benachrichtigungen |

    Waehlen Sie SignalR, wenn Sie im .NET-Oekosystem arbeiten und bidirektionale Kommunikation mit minimalem Boilerplate benoetigen. Waehlen Sie rohe WebSockets, wenn Sie ein eigenes Binaerprotokoll oder eine Multiplayer-Game-Engine bauen. Waehlen Sie SSE, wenn Sie nur Server-zu-Client-Streaming brauchen und die einfachste Implementierung wuenschen.

    Haeufige SignalR-Fehler

    1. Zustand in Hub-Instanzen speichern

    Hubs sind transient. Fuer jeden Methodenaufruf wird eine neue Instanz erstellt. Jeder als Feld auf der Hub-Klasse gespeicherte Zustand geht zwischen Aufrufen verloren.

    csharp
    class=class="code-string">"code-comment">// FALSCH -- dieser Zaehler setzt sich bei jedem Aufruf zurueck
    public class SchlechterHub : Hub
    {
        private int _nachrichtenZaehler = class="code-number">0;
    
        public async Task NachrichtSenden(string msg)
        {
            _nachrichtenZaehler++; class=class="code-string">"code-comment">// Immer class="code-number">1
            await Clients.All.SendAsync(class="code-string">"Zaehler", _nachrichtenZaehler);
        }
    }
    
    class=class="code-string">"code-comment">// RICHTIG -- injizierten Singleton-Service verwenden
    public class GuterHub : Hub
    {
        private readonly INachrichtenZaehler _zaehler;
    
        public GuterHub(INachrichtenZaehler zaehler) => _zaehler = zaehler;
    
        public async Task NachrichtSenden(string msg)
        {
            var anzahl = _zaehler.Erhoehen();
            await Clients.All.SendAsync(class="code-string">"Zaehler", anzahl);
        }
    }

    2. Hub mit lang laufenden Operationen blockieren

    Hub-Methoden laufen auf dem Thread der SignalR-Verbindung. Blockieren verhindert, dass der Client andere Nachrichten empfaengt.

    3. Gruppen-Neubeitritt nach Wiederverbindung vergessen

    Wie oben erwaehnt: Wiederverbindung erstellt eine neue Verbindung. Gruppen werden nicht automatisch wiederhergestellt. Dies ist die haeufigste Ursache fuer Fehler der Art "funktioniert in der Entwicklung, aber nicht in Produktion".

    4. CORS nicht korrekt konfigurieren

    SignalR nutzt sowohl HTTP (fuer die Verhandlung) als auch WebSockets. CORS muss beides erlauben.

    csharp
    builder.Services.AddCors(options =>
    {
        options.AddPolicy(class="code-string">"SignalRPolicy", policy =>
        {
            policy.WithOrigins(class="code-string">"https:class="code-commentclass="code-string">">//app.beispiel.de")
                  .AllowAnyHeader()
                  .AllowAnyMethod()
                  .AllowCredentials(); class=class="code-string">"code-comment">// Erforderlich fuer SignalR
        });
    });

    5. Nachrichtengroessen-Limits ignorieren

    Die standardmaessige maximale Nachrichtengroesse betraegt 32 KB. Das Senden grosser Payloads ohne Anpassung dieses Limits verursacht stille Verbindungsabbrueche.

    6. MessagePack bei High-Throughput-Szenarien nicht verwenden

    JSON ist das Standardprotokoll, aber fuer hochfrequente Updates reduziert die binaere MessagePack-Serialisierung die Payload-Groesse um 30-50% und verbessert die Serialisierungsgeschwindigkeit erheblich.

    csharp
    builder.Services.AddSignalR()
        .AddMessagePackProtocol();

    Fazit

    SignalR verwandelt Echtzeit-Kommunikation von einer systemnahen Herausforderung in ein unkompliziertes Feature auf Anwendungsebene. Strongly-typed Hub-Muster, eingebaute Gruppenverwaltung und nahtlose Authentifizierungsintegration machen es zur natuerlichen Wahl fuer .NET-Anwendungen, die Live-Updates benoetigen. Die kritischen Entscheidungen drehen sich um die Skalierungsstrategie (Redis vs Azure SignalR Service), Wiederverbindungsbehandlung und das Verstaendnis, dass Hubs transient sind. Wenn diese Punkte stimmen, verschwindet SignalR im Hintergrund -- und genau das sollte gute Infrastruktur tun.

    Gerne unterstuetze ich bei der Architektur Ihrer Echtzeit-Features und der Ueberpruefung Ihrer SignalR-Skalierungsstrategie fuer die Produktion.

    Verwandte Artikel

    Haben Sie ein Flutter-Projekt?

    Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

    Kontakt aufnehmen