Building a Real-time Typing Indicator in ASP.NET
Real-time features like typing indicators, presence detection, and live updates have become essential to modern chat and collaboration apps. Users increasingly expect interfaces to be responsive and dynamic, updating automatically without the need for manual refreshes.
One study found that adding real-time functionality increases user engagement by an average of 30%, with some apps seeing as much as a 3x improvement in key metrics like time spent in the app and messages sent (source: Pusher case studies).
In this article, we‘ll walk through how to add a typing indicator to an ASP.NET chat application, providing users with a visual cue that another person is actively writing a message. We‘ll use Pusher to easily integrate real-time updates without the complexity of managing raw WebSocket connections.
Why Pusher?
Pusher is a hosted service that provides a complete real-time API, including managed WebSocket infrastructure, client libraries, and developer tooling. It allows developers to quickly add features like live updates, pub/sub messaging, and presence channels to their applications.
While it‘s certainly possible to implement real-time functionality from scratch using raw WebSockets, this requires significant development effort and ongoing maintenance. Challenges include:
- Scaling and load balancing socket connections
- Ensuring secure authorization and authentication
- Enabling communication between different application instances
- Supporting fallback transports for older browsers
- Monitoring and debugging live connections
Pusher handles all of these concerns out of the box, allowing developers to focus on their application logic rather than low-level infrastructure. It‘s used by companies like GitHub, Lyft, and Mailchimp to power real-time experiences at significant scale (source: Pusher customers).
Application Architecture
Here‘s an overview of how the typing indicator feature will work:
- When a user starts typing a message, the client sends a POST request to a /typing endpoint with the user‘s name
- The server receives the request and broadcasts a typing event to all other clients subscribed to the "chat" channel
- Clients receive the typing event over their WebSocket connection and display "{user} is typing…" in the UI
- If no further typing events are received after a short timeout (e.g. 5 seconds), the typing indicator is automatically hidden
We‘ll use ASP.NET Core for the backend API and JavaScript with jQuery on the frontend. Pusher will manage the real-time WebSocket connections between the clients and server.
Setting Up the Backend
First, create a new ASP.NET Core Web Application in Visual Studio and choose the Web API template. Install the Pusher server library from NuGet:
Install-Package PusherServer
Next, define a route for receiving typing events by adding a new controller action:
[HttpPost]
[Route("/typing")]
public async Task<IActionResult> Typing(string user)
{
var options = new PusherOptions
{
Cluster = "PUSHER_APP_CLUSTER",
Encrypted = true
};
var pusher = new Pusher(
"PUSHER_APP_ID",
"PUSHER_APP_KEY",
"PUSHER_APP_SECRET",
options);
await pusher.TriggerAsync(
"presence-chat",
"typing",
new { user },
new TriggerOptions
{
SocketId = Request.Headers["X-Pusher-Socket-ID"]
});
return new OkResult();
}
This action accepts a user parameter indicating who is typing, and triggers a typing event on the presence-chat channel. When a user starts typing, we‘ll send a POST request to this endpoint from the client.
The SocketId option specifies the ID of the socket that triggered the typing event, which tells Pusher not to send the event back to that same client. This will prevent users from seeing their own typing indicator.
The presence-chat channel is a special type of Pusher channel that includes presence awareness. This will allow us to display more granular typing indicators like "Alice and Bob are typing…", rather than a single generic message.
Note that the PUSHER_ variables should be replaced with your actual app credentials from the Pusher dashboard.
Implementing the Client
Now let‘s set up the frontend to send typing events to the server and display the indicator in the UI.
First, add references to the Pusher and jQuery libraries in your view:
<script src="https://js.pusher.com/7.0/pusher.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
Then initialize the Pusher client with your app key:
const pusher = new Pusher("PUSHER_APP_KEY", {
cluster: "PUSHER_APP_CLUSTER",
encrypted: true,
authEndpoint: "/pusher/auth"
});
Note that we‘re specifying an authEndpoint, which is a server-side route that will authenticate the client and grant it access to the presence channel. Let‘s add that route to the backend now:
public class PusherAuthController : Controller
{
[HttpPost]
[Route("/pusher/auth")]
public IActionResult AuthForChannel()
{
var socketId = Request.Form["socket_id"];
var channelName = Request.Form["channel_name"];
var options = new PusherOptions
{
Cluster = "PUSHER_APP_CLUSTER",
Encrypted = true
};
var pusher = new Pusher(
"PUSHER_APP_ID",
"PUSHER_APP_KEY",
"PUSHER_APP_SECRET",
options);
if (!channelName.StartsWith("presence-"))
{
return new BadRequestResult();
}
var user = new PresenceChannelMember
{
UserId = User.Identity.Name, // Or any unique user ID
UserInfo = new {} // Any additional user data
};
var auth = pusher.Authenticate(socketId, channelName, user);
return Json(auth);
}
}
The /pusher/auth
endpoint is automatically called by the Pusher client when attempting to connect to a private or presence channel. Our method uses the Pusher server library to authenticate the request, granting read and write access to the channel.
We perform a check to ensure the client is only trying to access presence channels, and return a 400 error otherwise. The User.Identity.Name property is used as a unique identifier for the current user, which will display in the typing indicator.
With the auth endpoint in place, the client can now subscribe to the presence-chat channel:
const channel = pusher.subscribe("presence-chat");
When the user starts typing, send a POST request to the /typing
endpoint we defined earlier:
let timer;
$("#message-input").on("keydown", e => {
clearTimeout(timer);
const user = "@User.Identity.Name";
const socketId = pusher.connection.socket_id;
$.post("/typing", { user, socketId });
timer = setTimeout(() => {
$.post("/typing", { user: "_reset_" });
}, 2000);
});
We use a timer to debounce the requests, ensuring we don‘t flood the server with typing events on every keystroke. If no keys are pressed for 2 seconds, we send a special reset event to indicate the user has stopped typing.
Finally, subscribe to the typing event and display the indicator in the UI:
let users = {};
channel.bind("pusher:subscription_succeeded", () => {
users = channel.members.members;
});
channel.bind("pusher:member_added", member => {
users[member.id] = member.info;
});
channel.bind("pusher:member_removed", member => {
delete users[member.id];
});
channel.bind("typing", ({ user }) => {
if (user === "_reset_") {
users = channel.members.members;
updateTypingIndicator();
} else if (user !== "@User.Identity.Name") {
users[user] = true;
updateTypingIndicator();
}
});
function updateTypingIndicator() {
const typingUsers = Object.keys(users).filter(id => id !== "@User.Identity.Name");
if (typingUsers.length === 0) {
$("#typing-indicator").hide();
} else if (typingUsers.length === 1) {
$("#typing-indicator").text(`${typingUsers[0]} is typing...`).show();
} else {
$("#typing-indicator").text(`${typingUsers.length} people are typing...`).show();
}
}
Here‘s what‘s happening:
-
We maintain a users object that tracks the current members of the presence channel and whether they‘re typing.
-
When the subscription succeeds, we initialize users with the current member list. We also handle pusher:member_added and pusher:member_removed events to keep the user list up to date as people join and leave the chat.
-
When a typing event is received, we add that user to the users object. If the special reset event is received, we remove that user instead.
-
The updateTypingIndicator function converts the users object to an array of IDs, filters out the current user, and renders the typing message to the UI. If no users are typing, the indicator is hidden.
Performance and Scalability
In a high-traffic chat application with thousands or millions of concurrent users, it‘s important to ensure the typing indicator remains responsive without degrading the overall performance of the app.
Luckily, Pusher handles most of the heavy lifting by distributing incoming WebSocket connections across multiple servers and broadcasting events efficiently to all subscribed clients. Each chat room can be assigned its own presence channel to segment the typing events and keep the client-side overhead manageable.
On the server side, the /typing
endpoint should be kept as lightweight as possible. Avoid doing any expensive database lookups or other blocking operations. If additional data needs to be fetched for each typing event, consider caching it in memory or using a fast key-value store like Redis.
To prevent malicious clients from spamming the /typing
endpoint and overwhelming your server, you can implement rate limiting based on IP address or user ID. For example, restrict each client to no more than 5 typing events per second.
Security Considerations
When implementing any real-time feature, it‘s critical to validate and authorize all incoming events from clients. Never trust that a WebSocket connection is coming from your own app — always assume clients may be compromised or sending malicious payloads.
Pusher secures incoming connections with HTTPS encryption and performs auth checks on all private/presence channel subscriptions. However, you should still validate the user parameter on the /typing
endpoint to ensure one user can‘t spoof typing events for someone else.
[HttpPost]
[Route("/typing")]
public async Task<IActionResult> Typing(string user)
{
if (user != User.Identity.Name) {
return BadRequest("Mismatched typing user");
}
// ...
}
You can also configure the Pusher client to only communicate with your own server by setting the wsHost option, preventing it from connecting to third-party WS servers that could intercept events:
const pusher = new Pusher(‘app_key‘, {
wsHost: "ws.yourdomain.com",
enabledTransports: ["ws", "wss"]
});
By taking these precautions and leveraging Pusher‘s existing security features, you can add a typing indicator to your app without introducing any significant vulnerabilities.
Taking It Further
This article demonstrated a simple but effective implementation of a typing indicator in ASP.NET and Pusher. There are many ways to extend this functionality and make it more valuable for your users:
- Show typing indicators in a user list next to each person‘s name, rather than a single indicator at the bottom of the chat
- Customize the styling to match your app‘s UI kit, perhaps with a flashing ellipsis animation (…) to simulate typing
- Support typing events from multiple users in a group chat, displaying a summary like "Bob, Alice, and 2 others are typing"
- Combine typing events with other real-time features like online presence indicators, desktop notifications, or even live collaborative editing
- Implement more granular permissions on presence channels, so users can only see typing indicators for a subset of people they‘re allowed to chat with
The possibilities are endless! Real-time features like typing indicators are becoming table stakes for modern apps, and with tools like Pusher it‘s never been easier to get started.
Conclusion
Typing indicators provide a more dynamic and engaging experience for users of chat and collaboration apps, but implementing them from scratch can be a daunting task. By leveraging a real-time platform like Pusher and following the steps in this article, you can easily add professional, scalable typing indicators to your ASP.NET application.
The key takeaways are:
- Use Pusher Channels for real-time WebSocket communication between clients and server
- Debounce typing events on the client side to reduce unnecessary server requests
- Implement secure authentication on presence channels to ensure only authorized users can access typing events
- Validate and authorize all WebSocket payloads on the server to prevent spoofing or tampering
- Keep performance in mind by avoiding blocking operations and heavy computations in the
/typing
endpoint - Extend the basic typing indicator with additional real-time features to further enhance the user experience
I encourage you to take the sample code from this article and adapt it to your own application. Experiment with different UX patterns, combine typing events with other real-time data streams, and gather feedback from your users on what works best.
With a solid technical foundation and a focus on delivering value to your users, you‘ll be well on your way to building an amazing real-time experience. Happy coding!