Use ASP.NET Core SignalR with TypeScript and Webpack

By Sébastien Sougnez and Scott Addie

Webpack enables developers to bundle and build the client-side resources of a web app. This tutorial demonstrates using Webpack in an ASP.NET Core SignalR web app whose client is written in TypeScript.

In this tutorial, you learn how to:

  • Scaffold a starter ASP.NET Core SignalR app
  • Configure the SignalR TypeScript client
  • Configure a build pipeline using Webpack
  • Configure the SignalR server
  • Enable communication between client and server

View or download sample code (how to download)

Prerequisites

Create the ASP.NET Core web app

Configure Visual Studio to look for npm in the PATH environment variable. By default, Visual Studio uses the version of npm found in its installation directory. Follow these instructions in Visual Studio:

  1. Navigate to Tools > Options > Projects and Solutions > Web Package Management > External Web Tools.

  2. Select the $(PATH) entry from the list. Click the up arrow to move the entry to the second position in the list.

    Visual Studio Configuration

Visual Studio configuration is completed. It's time to create the project.

  1. Use the File > New > Project menu option and choose the ASP.NET Core Web Application template.
  2. Name the project SignalRWebPack, and select OK.
  3. Select .NET Core from the target framework drop-down, and select ASP.NET Core 2.2 from the framework selector drop-down. Select the Empty template, and select OK.

Configure Webpack and TypeScript

The following steps configure the conversion of TypeScript to JavaScript and the bundling of client-side resources.

  1. Execute the following command in the project root to create a package.json file:

    npm init -y
    
  2. Add the highlighted property to the package.json file:

    {
      "name": "SignalRWebPack",
      "version": "1.0.0",
      "private": true,
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
    

    Setting the private property to true prevents package installation warnings in the next step.

  3. Install the required npm packages. Execute the following command from the project root:

    npm install -D -E clean-webpack-plugin@0.1.19 css-loader@0.28.11 html-webpack-plugin@3.2.0 mini-css-extract-plugin@0.4.0 ts-loader@4.4.1 typescript@2.9.2 webpack@4.12.0 webpack-cli@3.0.6
    

    Some command details to note:

    • A version number follows the @ sign for each package name. npm installs those specific package versions.
    • The -E option disables npm's default behavior of writing semantic versioning range operators to package.json. For example, "webpack": "4.12.0" is used instead of "webpack": "^4.12.0". This option prevents unintended upgrades to newer package versions.

    See the official npm-install docs for more detail.

  4. Replace the scripts property of the package.json file with the following snippet:

    "scripts": {
      "build": "webpack --mode=development --watch",
      "release": "webpack --mode=production",
      "publish": "npm run release && dotnet publish -c Release"
    },
    

    Some explanation of the scripts:

    • build: Bundles your client-side resources in development mode and watches for file changes. The file watcher causes the bundle to regenerate each time a project file changes. The mode option disables production optimizations, such as tree shaking and minification. Only use build in development.
    • release: Bundles your client-side resources in production mode.
    • publish: Runs the release script to bundle the client-side resources in production mode. It calls the .NET Core CLI's publish command to publish the app.
  5. Create a file named webpack.config.js, in the project root, with the following content:

    const path = require("path");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const CleanWebpackPlugin = require("clean-webpack-plugin");
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    
    module.exports = {
        entry: "./src/index.ts",
        output: {
            path: path.resolve(__dirname, "wwwroot"),
            filename: "[name].[chunkhash].js",
            publicPath: "/"
        },
        resolve: {
            extensions: [".js", ".ts"]
        },
        module: {
            rules: [
                {
                    test: /\.ts$/,
                    use: "ts-loader"
                },
                {
                    test: /\.css$/,
                    use: [MiniCssExtractPlugin.loader, "css-loader"]
                }
            ]
        },
        plugins: [
            new CleanWebpackPlugin(["wwwroot/*"]),
            new HtmlWebpackPlugin({
                template: "./src/index.html"
            }),
            new MiniCssExtractPlugin({
                filename: "css/[name].[chunkhash].css"
            })
        ]
    };
    

    The preceding file configures the Webpack compilation. Some configuration details to note:

    • The output property overrides the default value of dist. The bundle is instead emitted in the wwwroot directory.
    • The resolve.extensions array includes .js to import the SignalR client JavaScript.
  6. Create a new src directory in the project root. Its purpose is to store the project's client-side assets.

  7. Create src/index.html with the following content.

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>ASP.NET Core SignalR</title>
    </head>
    <body>
        <div id="divMessages" class="messages">
        </div>
        <div class="input-zone">
            <label id="lblMessage" for="tbMessage">Message:</label>
            <input id="tbMessage" class="input-zone-input" type="text" />
            <button id="btnSend">Send</button>
        </div>
    </body>
    </html>
    

    The preceding HTML defines the homepage's boilerplate markup.

  8. Create a new src/css directory. Its purpose is to store the project's .css files.

  9. Create src/css/main.css with the following content:

    *, *::before, *::after {
        box-sizing: border-box;
    }
    
    html, body {
        margin: 0;
        padding: 0;
    }
    
    .input-zone {
        align-items: center;
        display: flex;
        flex-direction: row;
        margin: 10px;
    }
    
    .input-zone-input {
        flex: 1;
        margin-right: 10px;
    }
    
    .message-author {
        font-weight: bold;
    }
    
    .messages {
        border: 1px solid #000;
        margin: 10px;
        max-height: 300px;
        min-height: 300px;
        overflow-y: auto;
        padding: 5px;
    }
    

    The preceding main.css file styles the app.

  10. Create src/tsconfig.json with the following content:

    {
      "compilerOptions": {
        "target": "es5"
      }
    }
    

    The preceding code configures the TypeScript compiler to produce ECMAScript 5-compatible JavaScript.

  11. Create src/index.ts with the following content:

    import "./css/main.css";
    
    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();
    
    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.keyCode === 13) {
            send();
        }
    });
    
    btnSend.addEventListener("click", send);
    
    function send() {
    }
    

    The preceding TypeScript retrieves references to DOM elements and attaches two event handlers:

    • keyup: This event fires when the user types something in the textbox identified as tbMessage. The send function is called when the user presses the Enter key.
    • click: This event fires when the user clicks the Send button. The send function is called.

Configure the ASP.NET Core app

  1. The code provided in the Startup.Configure method displays Hello World!. Replace the app.Run method call with calls to UseDefaultFiles and UseStaticFiles.

    app.UseDefaultFiles();
    app.UseStaticFiles();
    

    The preceding code allows the server to locate and serve the index.html file, whether the user enters its full URL or the root URL of the web app.

  2. Call AddSignalR in the Startup.ConfigureServices method. It adds the SignalR services to your project.

    services.AddSignalR();
    
  3. Map a /hub route to the ChatHub hub. Add the following lines at the end of the Startup.Configure method:

    app.UseSignalR(options =>
    {
        options.MapHub<ChatHub>("/hub");
    });
    
  4. Create a new directory, called Hubs, in the project root. Its purpose is to store the SignalR hub, which is created in the next step.

  5. Create hub Hubs/ChatHub.cs with the following code:

    using Microsoft.AspNetCore.SignalR;
    using System.Threading.Tasks;
    
    namespace SignalRWebPack.Hubs
    {
        public class ChatHub : Hub
        {
        }
    }
    
  6. Add the following code at the top of the Startup.cs file to resolve the ChatHub reference:

    using SignalRWebPack.Hubs;
    

Enable client and server communication

The app currently displays a simple form to send messages. Nothing happens when you try to do so. The server is listening to a specific route but does nothing with sent messages.

  1. Execute the following command at the project root:

    npm install @aspnet/signalr
    

    The preceding command installs the SignalR TypeScript client, which allows the client to send messages to the server.

  2. Add the highlighted code to the src/index.ts file:

    import "./css/main.css";
    import * as signalR from "@aspnet/signalr";
    
    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();
    
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/hub")
        .build();
    
    connection.start().catch(err => document.write(err));
    
    connection.on("messageReceived", (username: string, message: string) => {
        let m = document.createElement("div");
    
        m.innerHTML =
            `<div class="message-author">${username}</div><div>${message}</div>`;
    
        divMessages.appendChild(m);
        divMessages.scrollTop = divMessages.scrollHeight;
    });
    
    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.keyCode === 13) {
            send();
        }
    });
    
    btnSend.addEventListener("click", send);
    
    function send() {
    }
    

    The preceding code supports receiving messages from the server. The HubConnectionBuilder class creates a new builder for configuring the server connection. The withUrl function configures the hub URL.

    SignalR enables the exchange of messages between a client and a server. Each message has a specific name. For example, you can have messages with the name messageReceived that execute the logic responsible for displaying the new message in the messages zone. Listening to a specific message can be done via the on function. You can listen to any number of message names. It's also possible to pass parameters to the message, such as the author's name and the content of the message received. Once the client receives a message, a new div element is created with the author's name and the message content in its innerHTML attribute. It's added to the main div element displaying the messages.

  3. Now that the client can receive a message, configure it to send messages. Add the highlighted code to the src/index.ts file:

    import "./css/main.css";
    import * as signalR from "@aspnet/signalr";
    
    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();
    
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/hub")
        .build();
    
    connection.start().catch(err => document.write(err));
    
    connection.on("messageReceived", (username: string, message: string) => {
        let messageContainer = document.createElement("div");
    
        messageContainer.innerHTML =
            `<div class="message-author">${username}</div><div>${message}</div>`;
    
        divMessages.appendChild(messageContainer);
        divMessages.scrollTop = divMessages.scrollHeight;
    });
    
    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.keyCode === 13) {
            send();
        }
    });
    
    btnSend.addEventListener("click", send);
    
    function send() {
        connection.send("newMessage", username, tbMessage.value)
                  .then(() => tbMessage.value = "");
    }
    

    Sending a message through the WebSockets connection requires calling the send method. The method's first parameter is the message name. The message data inhabits the other parameters. In this example, a message identified as newMessage is sent to the server. The message consists of the username and the user input from a text box. If the send works, the text box value is cleared.

  4. Add the highlighted method to the ChatHub class:

    using Microsoft.AspNetCore.SignalR;
    using System.Threading.Tasks;
    
    namespace SignalRWebPack.Hubs
    {
        public class ChatHub : Hub
        {
            public async Task NewMessage(string username, string message)
            {
                await Clients.All.SendAsync("messageReceived", username, message);
            }
        }
    }
    

    The preceding code broadcasts received messages to all connected users once the server receives them. It's unnecessary to have a generic on method to receive all the messages. A method named after the message name suffices.

    In this example, the TypeScript client sends a message identified as newMessage. The C# NewMessage method expects the data sent by the client. A call is made to the SendAsync method on Clients.All. The received messages are sent to all clients connected to the hub.

Test the app

Confirm that the app works with the following steps.

  1. Run Webpack in release mode. Using the Package Manager Console window, execute the following command in the project root. If you are not in the project root, enter cd SignalRWebPack before entering the command.

    npm run release
    

    This command yields the client-side assets to be served when running the app. The assets are placed in the wwwroot folder.

    Webpack completed the following tasks:

    • Purged the contents of the wwwroot directory.
    • Converted the TypeScript to JavaScript—a process known as transpilation.
    • Mangled the generated JavaScript to reduce file size—a process known as minification.
    • Copied the processed JavaScript, CSS, and HTML files from src to the wwwroot directory.
    • Injected the following elements into the wwwroot/index.html file:
      • A <link> tag, referencing the wwwroot/main.<hash>.css file. This tag is placed immediately before the closing </head> tag.
      • A <script> tag, referencing the minified wwwroot/main.<hash>.js file. This tag is placed immediately before the closing </body> tag.
  2. Select Debug > Start without debugging to launch the app in a browser without attaching the debugger. The wwwroot/index.html file is served at http://localhost:<port_number>.

  3. Open another browser instance (any browser). Paste the URL in the address bar.

  4. Choose either browser, type something in the Message text box, and click the Send button. The unique user name and message are displayed on both pages instantly.

message displayed in both browser windows

Additional resources