Okay, so we can call a method on the API of our ASP.NET site to send and receive messages to anybody that want’s to send and listen. Next we’ll cover SignalR as another way to get those messages to the client.
I’d recommend starting with reviewing Microsoft’s high-level coverage of SignalR. All we’re going to do here is set up a basic place for SignalR clients to register so they can begin getting SignalR data. Next week we’ll tackle integrating SignalR into WPF.
SignalR is an easy way for a web server to push information out to registered clients. Imagine there was User1 updating ticket information related to an invoice and User2 reviewing that invoice. As User1 was updating the ticket, the server can push updating invoice information out to User2 so they see changes in real-time as User1 was making them. It allows for a really nice, near-real time user experience.
Start by adding the Microsoft.AspNetCore.SignalR.Client package to the API. This will also bring in the rest of the needed dependent assemblies related to SignalR.
SignalR uses a “Hub” concept where clients register and where data is sent from. We’ll need to add our own hub so in the attached solution I’ve added a new folder called “Hubs” and put the code in there. Now, SignalR can be a bit finicky with making instances of the hub available. I’m not quite sure why Microsoft went this route but it’s tough to get an instance of the hub. What can be done is to use an IHubContext that provides access to the clients registered with a hub. To that end, I’ve set up an interface that is pretty basic.
public interface IChatClient
{
Task ReceiveMessage(WriteToChatMessage chatMessage);
}
This makes it really easy to define a strongly-typed hub where we know what type of clients to expect. I’ve done this so that we can define statics in the hub for sending data. Remember, we don’t directly access the instance of the hub, just the clients registered. So to minimize code duplication I define a static that takes the aforementioned clients.
public class ChatterChatHub : Hub<IChatClient>
{
public async static Task<WriteToChatMessage> SendMessage(IDbContextFactory<MessageDbContext> dbFactory, IChatClient clients, WriteToChatMessage chatMessage)
{
var db = await dbFactory.CreateDbContextAsync();
try
{
var message = new Message
{
Username = chatMessage.alias,
JsonMessage = JsonSerializer.Serialize(chatMessage),
};
db.Add(message);
await db.SaveChangesAsync();
chatMessage = chatMessage with { messageId = message.Id };
//For SignalR send to the hub
await clients.ReceiveMessage(chatMessage);
return chatMessage;
}
finally
{
await db.DisposeAsync();
}
}
}
So, to start with I extend Hub<IChatClient>. This means I expect that clients handle “ReceiveMessage” as defined in the interface. If I didn’t define that and just used Hub, I would have to manually tell SignalR to send to all clients that I’m responding to ReceiveMessage. In a lot of cases that is fine. But if I want to be able to allow for multiple places in my code to send data to clients, I’ll need that static. This might be a bit confusing on my justification, but I’ll clarify when we map the post in the API a bit later.
In the static is code I’ve covered before. I create a db connection from the factory, serialize the message and then save to the database. Pretty standard stuff. But following that is:
//For SignalR send to the hub
await clients.ReceiveMessage(chatMessage);
In the static method, I take the db factory, the clients to send to and the message to send. What’s nice about using the interface and making the hub strongly typed is I can just call “await clients.ReceiveMessage(chatMessage)” and it will go to any of the IChatClients that were passed in. The clients list can be controlled and filtered by the caller. My code doesn’t care and assumes that the caller knows which clients it wants to send to.
So how is this wired up in the API? We need to start by adding SignalR to our services.
builder.Services.AddSignalR();
Then, once the app is built we need to map the hub with the end point for routing and registering by clients.
app.MapHub<ChatterChatHub>("/chatterChatHub");
This provides the end-point clients will register with that want to receive chats.
And finally I’ll set up the Post for clients to send chats to.
app.MapPost("/sendMessage", async (IDbContextFactory<MessageDbContext> dbFactory, IHubContext<ChatterChatHub, IChatClient> hub, WriteToChatMessage chatMessage) =>
{
await ChatterChatHub.SendMessage(dbFactory, hub.Clients.All, chatMessage);
});
As you can see, when clients send to “sendMessage” the post is invoked with the db factory and hub context being passed in through dependency injection. By specifying the IChatClient it makes it easy to pass in the registered clients from the hub into the static. Remember, I can’t directly access the hub but the context allows me to access the clients registered with the hub. This means I can’t invoke methods from the hub and thus the reason I’ve implemented this with a static. It provides a nice, single entry point for other places in the code that might want to send messages to clients.
So what I’ve hidden here is another way to do this.
app.MapPost("/sendMessage2", async (IDbContextFactory<MessageDbContext> dbFactory, IHubContext<ChatterChatHub> hub, WriteToChatMessage chatMessage) =>
{
await hub.Clients.All.SendAsync("ReceiveMessage", chatMessage);
});
I’ve removed the interface from the hub being injected and just called the method directly on the clients. I don’t like this route, however, because I would need to implement the db save of the message here. Well, why shouldn’t I just do that and remove the static? Because it would mean that if anywhere else in code wanted to send the message, they would also need to save the message before directly calling the client.
Directly calling the clients to send does have it’s purposes. With constants for method names I’ve seen it used well. But I’ve also seen it lead to a bunch of cases where there is a lot of code duplication and violations of SOLID. I think the benefits, in this case, of using statics outweigh the detriments. It’s nice to have one place to manage all the code for a given hub. I would also recommend limiting the scope of hubs, if nothing else but for conforming to the single responsibility principle. But also for making it easier to manage scope and purpose of code.
As mentioned, next week will be integrating SignalR into the WPF client.
Thanks,
Brian