SignalR is the canonical client-side async notification library for ASP.NET. With it, we can build clients that are ultra-responsive to changing conditions on our servers. But SignalR has always had one major flaw. To use it, you needed to use JavaScript. That's fair, right? We're writing web clients, which are always running in the browser, so of course, we need to use JavaScript. We'll suffer without type safety because of the functionality we're getting. It's always been a necessary evil, and we've dealt with it as such.
With the dawn of Blazor, this age of compromise is over. We can manage all of the data transfers between our servers and clients straight out of CLR types! That is what we're going to be demonstrating now.
Objectives
Let's concretize our objectives here. We're going to build a trivial Single-Paged-App(SPA) in Blazor wasm. That app will have a simple form to read inputs from users, and a table to see incoming messages.
Prerequisites
- Latest .NET Core 3.1 SDK
- Visual Studio Code or the latest version of Visual Studio. I will be using VS Code for this
Create Project
Navigate to your source directory in the console and run the following command.
dotnet new blazorwasm -ho -n SignalRClr
This will create a new directory SignalRClr
, in that directory it will create a solution called SignalRClr
and then projects:
SignalRClr.Shared.csproj
SignalRClr.Server.csproj
SignalRClr.Client.csproj
Those projects are what they say they are. The Shared
will be the shared models between the client and the server. The client
is going to be the compiled wasm that ends up in our client's browser. The Server
is our server-side code. Run cd SignalRClr
to navigate into the project's folder, then run code .
to open it in VS Code.
Add Dependencies
We need to add a Microsoft.AspNetCore.SignalR.Client
dependency to our SignalRClr.Client
project, and a Microsoft.AspNetCore.SignalR.Core
dependency to our SignalRClr.Server
project. We will also need the System.ComponentModel.Annotations
class for our Shared project. Cd into Client
and run the following:
dotnet add package Microsoft.AspNetCore.SignalR.Client
Then cd into the Server
directory parallel to the Client
directory and run the following:
dotnet add package Microsoft.AspNetCore.SignalR.Core
Then cd into the Shared
directory and run the following:
dotnet add package System.ComponentModel.Annotations
Build the Model
Add a file to our SignalRClr.Shared
project folder called Message.cs
. Here we'll add a simple message class that takes a UserName and Text. We'll annotate them to make both UserName and Text required.
using System.ComponentModel.DataAnnotations;
namespace SignalRClr.Shared
{
public class Message
{
[Required]
public string UserName { get; set; }
[Required]
public string Text { get; set; }
}
}
Add Message Hub
In the Server
Project, add a new folder called Hubs
. In that folder, add a MessageHub
class, we will add a new Hub class MessageHub
that will only have one method SendMessage
which will push the inbound message down to all of the HubConnection Client's using the SendAsync
method.
using Microsoft.AspNetCore.SignalR;
using SignalRClr.Shared;
using System.Threading.Tasks;
namespace SignalRClr.Server.Hubs
{
public class MessageHub : Hub
{
public async Task SendMessage(Message message)
{
await Clients.All.SendAsync("ReceiveMessage", message);
}
}
}
Configure Middleware
In the Startup.cs
file, we'll need to add a couple of lines of code to enable the MessageHub. In ConfigureServices
the line:
services.AddSignalR();
Then in the app.UseEndpoints
's delegate in the Configure
method, map the hub. Your UseEndpoints should look like:
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
endpoints.MapHub<Hubs.MessageHub>("/messageHub"); //Add this line
});
Build the Frontend
We're going use the Client/Pages/Index.razor
file for our frontend. Mind you, I'm not an expert on frontend development, so this will look simple.
So as you can see here, we have two sections, a Messages Table and a form that we'll use to send the messages.
Pull in Dependencies
We need to add a reference to Microsoft.AspNetCore.SignalR.Client
and SignalRClr.Shared
. We also need to inject a NavigationManager
, and to clean up the HubConnection we're going to use we'll need to implement IDisposable
. We will declare all this for our Index
component with the following:
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@using SignalRClr.Shared
@inject NavigationManager NavigationManager
@implements IDisposable
Add the Messages Table
We now need to add a Messages Table to our component. One of the cool things about razor/blazor is that we can do all of this with the CLR types that we want to use. In particular, our Message Model. We'll create a table; then, in the Table's body, we'll loop through a Messages Array that we will declare later, and add the UserName and the Text as table data.
<h2>Messages</h2>
<table class="table-active">
<thead>
<tr>
<th>User Name</th>
<th>Text</th>
</tr>
</thead>
<tbody>
@foreach(var message in _messages)
{
<tr>
<td>@message.UserName</td>
<td>@message.Text</td>
</tr>
}
</tbody>
</table>
Add Message Form
Next, we'll add an EditForm
to provide it our Message
model and add a ValidSubmit
method to run when the form has successfully validated.
<EditForm Model="@_message" OnValidSubmit="SendMessage">
<DataAnnotationsValidator />
<ValidationSummary />
<h3>User Name</h3>
<input @bind="@_message.UserName" placeholder="User Name" class="input-group-text" />
<h3>Text</h3>
<input @bind="@_message.Text" placeholder="User Name" class="input-group-text" />
<br />
<button class="btn btn-primary" type="submit">Send Message</button>
</EditForm>
Add Component Code
We will need to add a @code
block to our component now. This block is all the logic our page needs to execute. We will add a _hubConnection
property, which will manage the sending and receiving messages from our server over SignalR. We will add a _messages
list, which is simply the messages we've received from the server. And we'll add a _message
field, which will simply be the model we use for sending messages.
When initializing the Component, we will create the HubConnection, mapping it to the /messageHub
URI we declared in our middleware. When that HubConnection gets a Receive Message
signal, it will add the inbound message to our _messages
collection and notify the component the state has changed. Naturally, when we get a valid submission from our form, we will submit that new message to the server. Finally, when finalizing the component, we will dispose of the _hubConnection
. All the code to do this looks like:
@code {
private HubConnection _hubConnection;
private List<Message> _messages = new List<Message>();
private Message _message = new Message();
protected override async Task OnInitializedAsync(){
_hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/messageHub"))
.Build();
_hubConnection.On<Message>("ReceiveMessage",
(message)=>{
_messages.Add(message);
StateHasChanged();
});
await _hubConnection.StartAsync();
}
public async Task SendMessage()
{
await _hubConnection.SendAsync("SendMessage", _message);
_message = new Message();
}
public void Dispose()
{
_ = _hubConnection?.DisposeAsync();
}
}
That's It
And that's it! Everything we need to do to manage simple messages in real time between clients. To run this project, you can run the Server
project. Cd into the Server directory and run dotnet run
, and it will launch the project. Navigate to localhost:5000
(or wherever you configure it to run kestrel) in your browser, and you will see our little message program.
Wrapping Up
Naturally, this was a simple example that explicitly shows how simple it is to use these frameworks together. But these are the building blocks of how Blazor can be used to wipe out the need for javascript when interacting with our models.
Resources
- The source code from this demo can be found in GitHub
Top comments (3)
Nice! I'm told it's cleaner if you derive your hub from
IHub<T>
rather than justHub
. It made my code a little easier when I did that.Hi Katie, thank you for reading, that is a valid point. While they threw out
IHub<T>
for SignalR Core, they replaced it withHub<T>
. You could, as you suggested, use a strongly typed interface with the Hub and drive everything through that.I'm not entirely sold on that being cleaner, however. In the client, when you're building your HubConnection, you'd still need to tell it to register to an event with a stringified name of the called symbol.
So, if I had my Index component implement an interface
IMessageClient
and used a Hub extension I could call the clients like:But, I would still have to receive the message into my component like:
I found this, going from symbol => stringified Event Name versus stringified event name => stringified event name, more confusing. It's particularly odd as it doesn't enforce type-safety in the clients as there's no obligation for the client to actually implement the interface to receive the event. That's just a personal opinion, though; it could break either way.
It'd be nice if there were a
HubConnection<T>
in the Client library that would manage all that for you. This looks like it's something the aspnetcore team is at least considering: github.com/dotnet/aspnetcore/issue... I just upvoted this and I'd encourage you to as well :).That's a really good suggestion though and I'd encourage anyone reading this to take it under advisement, for all we know the aforementioned issue may be resolved for .NET 5!
Okay, I up voted too. And, I looked at my code again and you are right, I used
Hub<InterfaceType>
for by hub. Also, on the client side, I'm using the @microsoft/signalr package in TypeScript. It could use theHubConnection<T>
as you describe too.