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.
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.
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.
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
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; }
}
}
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);
}
}
}
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
});
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.
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
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>
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>
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();
}
}
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.
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.