[00:02] SignalR, this video is the answer you were looking for. I'm going to show you how we can scale SignalR using Redis as a backplane, and I'm also going to walk you through everything that happens behind the scenes to make this work [00:16] seamlessly inside of your .NET applications. Let me first explain why you even need a backplane when you're working with SignalR. So, let's say you've got an application server, and this application server uses SignalR to [00:30] send messages in real time to your application users. So, we have our users here, and our application server is sitting somewhere in the back, and then we've got our real-time messages flowing in both directions using SignalR on the [00:44] backend. Now, what's interesting about SignalR is first how awesome it is, right? We've got an abstraction that takes care of WebSockets. It also works with server-sent events as a fallback and polling as a last resort. It also [00:57] manages the connection lifetimes for all of the connected clients. And this part is the key problem when you want to scale out. So, let's say you introduce another application server, and what ends up happening is you now have [01:11] another SignalR server running on your second instance. And because we are still using the in-memory storage for connection management, this means that connection management, this means that you basically have two different SignalR [01:24] applications on the backend, and they don't know about each other, they can't communicate with each other. And worse yet, if you don't have session affinity configured where client applications, then you might get the same client [01:36] servers for different requests. So, this is why we introduce a backplane, and Redis is actually a great option here. So, let me explain how this works when we want to scale out our SignalR [01:49] deployment. We're going to have a backplane, in this case it's going to be Redis. You can also use the Azure SignalR service, but many of the same principles still apply. So, what happens is now all of our SignalR messages flow [02:04] practically means for you is that you're still going to have connection management within your single application servers. So, this part doesn't change. What does change is whenever you publish a message that's [02:17] intended as a broadcast to all clients or some group, it also gets sent to the backplane. And the backplane, in this case Redis, takes care of publishing this message to all connected servers. So, now when the first application [02:32] server wants to send some message as a broadcast to all of the clients, it can first take care of the connections that it has stored locally. So, we can send those to the connected clients. Then we're going to publish a message to the [02:46] backplane, and the backplane takes this message and routes it to all of the connected servers. So, now this server is going to pick up the message, and it's going to see what are all of the clients and connections matching the [02:59] message that it's supposed to send, and it's going to broadcast the message to its connected clients. So, the backplane is effectively there to act as a bridge between multiple SignalR servers running on your backend. A couple of [03:12] implications that this introduces that you'll have to think about, and the first one is session affinity. It's also known as sticky sessions, as you really want your client to connect to one application server and stay connected to [03:27] remainder of the session. Another thing you want to think about is network latency with regards to the backplane, as this is going to be your limiting factor to make this work in real time. If your application servers aren't close [03:44] to the backplane, ideally on the same machine, but if not at least in the same data center, you're going to get an increase in latency between your SignalR messages, and this may even defeat the purpose of using SignalR for real-time [03:58] communication. Now, as I said, this applies when you want to scale out your application servers and your application servers are also acting as your SignalR servers at the same time. So, this is all fine in theory, but how do we [04:11] actually apply this inside of our .NET applications that are using SignalR? So, let me first walk you through my baseline setup. I've got a .NET 10 Web baseline setup. I've got a .NET 10 Web API that has SignalR installed. It [04:23] we enable it by saying builder services.addsignalr. This gives us access to the SignalR hub context, and once we define our hub, we can start communicating with our client applications. In this case, I'm also [04:37] making the hub authorized. I'm also ensuring that the hub is protected by decorating it with the authorize attribute. I have a strongly-typed client interface that I'm using to send messages from the backend. And then I've [04:50] got a couple of endpoints that are responsible for creating an order, storing those inside of our cache. So, this application is already using Redis for caching. And we've got this endpoint here where we update the order status, [05:03] and near the end you're going to see a hub context, which is actually the IHubContext interface, and we can inject this with dependency injection to use it to send a message to our clients or just a single client from the backend [05:17] whenever we want to. Remember that SignalR takes care of maintaining the to send a message to a specific client anytime from our backend. So, the hub context is our abstraction of choice, and what we do is say clients.user, [05:32] and in this case I want to send a message to a specific user, so I can pass in the user ID and then invoke the client method that I want to call. What's going to happen here is I'm going to send over the updated order, and I [05:45] also have this instance ID value, which I'm pulling from an OpenTelemetry attribute, and the value I'm looking for is service instance ID. This will just allow me to uniquely identify the instance ID that is responsible for the [05:59] current request. And how do I have multiple instances? Well, I'm just using the Aspire capability to replicate my application by calling web replicas. In this case, I have just two, but we can always add more, and we're using this to [06:13] test out if SignalR will work correctly with a backplane. By default, Aspire is going to round-robin incoming requests between the available API instances, so enable this, we would need to introduce a reverse proxy, for example, YARP, [06:29] But you will see that this will work even without session affinity because we're going to have a backplane. I'm going to use Redis as a backplane. That's one of the available options, and I'm going to leave some documentation [06:42] for you in the description of this video if you want to explore more about how scaling Redis works and what your options are when it comes to deployment. So, we're going to need an additional NuGet package, and I'm going to look for [06:56] it. I'll go over to browse, and then I'll say SignalR, and then let's type in is called Microsoft.AspNetCore.SignalR.StackExchange.Redis. [07:11] version, and all I need to do is chain an additional call after addsignalr, and addstackexchangerredis. I need to pass in a connection string, [07:23] and I can say builder.configuration. Get connection string, and I've got one called cache, which will be provided by Aspire at runtime. And with this one line of code, we have enabled our backplane. Now, I want to actually show [07:38] you what's going on when we call this method, and you will see this RedisHubLifetimeManager being added to dependency injection, and this is what replaces the default hub lifetime manager. Now, what happens here [07:52] is this takes care of publishing the respective message that you want to send out through a Redis channel, which is the mechanism that Redis offers for implementing publish-subscribe messaging. This is a very performant and [08:05] a lightweight solution for implementing a backplane. However, it does have some limitations. For example, when you send a message to a channel that you are also where we have, let's say, three servers. Server one publishes a message to a [08:20] channel, and you would think it only reaches servers two and three, but the first server that was the one sending the message. also have to take care of these edge cases, but luckily the backplane implementation takes care [08:33] of that for us. Other than adding this line of code, nothing really changes inside of our application. So, I'm going to run the backend with Aspire, and you can see in the resources view that we have two instances of our order [08:48] notifications API. Now, these are the instance IDs, which is how we will be able to tell which instance was the one that served the current request. And in my client application, I can go ahead and create a new order, and in the UI, I [09:03] should be able to see which client was the one that served the request. So, I'm also going to open this up from one more browser window. And here, I can look up the order. And now I've got two connections to our backend that both [09:16] established the SignalR connection, and they are waiting any real-time messages that come in from the SignalR server. So, then I can jump into my console and send a PUT request to update the order status. And if I jump into the UI, we [09:30] can see a new status appear on the timeline. We can also see which API message. So, now I'm going to change this status a couple of times between the different order status types that I have available. And if you go back to [09:44] our client application, you can see that our timeline keeps getting updated, but the instance serving the response can change. This is going to change based on which of our APIs was the one that sent the SignalR message to our client [09:58] messaging is going through our backplane. If we take a look at the distributed traces, you can see that every time we update the order, when we send a pull request, there are also some commands sent to our Redis instance. If [10:13] I go into DataGrip to take a look at what I have inside of my Redis instance, you can see we've got our one order that we are storing in the cache, but to be able to see the channels, I have to execute a custom command. So, we're [10:25] going to say pubsub channels star, and in the response, you can see all of the configured through our backplane. You can see there's a dedicated channel for the specific user. There's two channels for the two connections that we have, [10:38] who are two browser tabs, and then there are some internal channels that SignalR creates. So, you can imagine in a production environment, this would contain a lot more channels, basically one for every connection and user that [10:50] connects to your application. And this is how we're able to use SignalR as a backplane using the channel feature. One more final reminder to think about session affinity, so you want to have a proxy in front of your multiple [11:02] to keep your application servers and your backplane close together to reduce network latency. Let me know in the comments what you think about of scaling SignalR using Redis as a backplane. Would you rather use the Azure SignalR [11:16] service? If you want to learn more about working with SignalR, I recommend taking a look at this video next. Thanks a lot for watching, and until next time, stay for watching, and until next time, stay awesome.