How to Use React with Visual Studio and ASP.NET Web API

netreact.jpg

In this tutorial, you’ll learn how to structure a Visual Studio solution that uses React for the front-end and ASP.NET Web API for the backend.

Introduction

In this tutorial, you’ll learn how to structure a Visual Studio solution that uses React for the front-end and ASP.NET Web API for the back-end. Also, we will dive deep into how to use webpack and npm together with Visual Studi, and how to easily make your application realtime with Pusher.

Before getting started it might be helpful to have a basic understanding of:

  • React
  • Babel
  • webpack
  • ASP.NET Web API
  • NuGet
  • npm

You should also be using Visual Studio 2015 or greater.

In order to demonstrate how to combine the power of React, ASP.NET Web API, and Pusher, we’ll be building a realtime chat application. The chat application itself will be very simple:

Upon loading the application, the user will be prompted for their Twitter username:

chrome_2016-08-01_14-07-14

… And upon clicking Join, taken to the chat where they can send and receive messages in realtime:

chrome_2016-08-01_14-07-22

The Visual Studio solution will be comprised of two projects namely, PusherRealtimeChat.WebAPI and PusherRealtimeChat.UI:

devenv_2016-08-01_14-09-07

PusherRealtimeChat.WebAPI is where we’ll implement the ASP.NET Web API server. This simple server will revolve around a route called /api/messages to which clients can POST and GET chat messages. Upon receiving a valid chat message, the server will broadcast it to all connected clients, via Pusher.

PusherRealtimeChat.UI is where we’ll implement the React client. This client will subscribe to a Pusher channel for new chat messages and upon receiving one, immediately update the UI.

Implementing the Server

Separating the server and the client into separate projects gives us a clear separation of concerns. This is handy because it allows us to focus on the server and client in isolation.

In Visual Studio, create a new ASP.NET Web Application called PusherRealtimeChat.WebAPI:

devenv_2016-07-29_15-56-14

When prompted to select a template, choose Empty and check Web API before clicking OK:

devenv_2016-07-29_16-06-19

If you’re prompted by Visual Studio to configure Azure, click Cancel:

devenv_2016-07-29_16-09-07

Once the project has been created, in Solution Explorer, right-click the PusherRealtimeChat.WebAPI project, then click Properties. Under the Web tab, set Start Action to Don’t open a page. Wait for request from an external application:

devenv_2016-07-29_16-11-44

Setting this option does what you might expect – it tells Visual Studio to not open a web page in the default browser when you start the server. This is a lesser-known option that proves to be convenient when working with ASP.NET Web API projects, as ASP.NET Web API projects have no user interface.

Now that the PusherRealtimeChat.WebAPI project has been setup we can start to implement some code! A good place to start is by creating a ChatMessage.cs model inside the Models directory:

1using System.ComponentModel.DataAnnotations;
2
3namespace PusherRealtimeChat.WebAPI.Models
4{
5    public class ChatMessage
6    {
7        [Required]
8        public string Text { get; set; }
9
10        [Required]
11        public string AuthorTwitterHandle { get; set; }
12    }
13}

Note: If you’re following along and at any point you’re not sure where a code file belongs, check out the source code on GitHub.

The above model represents a chat message and we’ll be using it in the next step to define controller actions for the /api/messages/ route. The Required attributes make it easy to validate the model from said controller actions.

Next, we’ll define controller actions for the /api/messages route I mentioned. To do that, create a new controller called MessagesController.cs inside the Controllers directory:

1using PusherRealtimeChat.WebAPI.Models;
2using PusherServer;
3using System.Collections.Generic;
4using System.Net;
5using System.Net.Http;
6using System.Web.Http;
7
8namespace PusherRealtimeChat.WebAPI.Controllers
9{
10    public class MessagesController : ApiController
11    {
12        private static List<ChatMessage> messages =
13            new List<ChatMessage>()
14            {
15                new ChatMessage
16                {
17                    AuthorTwitterHandle = "Pusher",
18                    Text = "Hi there! ?"
19                },
20                new ChatMessage
21                {
22                    AuthorTwitterHandle = "Pusher",
23                    Text = "Welcome to your chat app"
24                }
25            };
26
27        public HttpResponseMessage Get()
28        {
29            return Request.CreateResponse(
30                HttpStatusCode.OK, 
31                messages);
32        }
33
34        public HttpResponseMessage Post(ChatMessage message)
35        {
36            if (message == null || !ModelState.IsValid)
37            {
38                return Request.CreateErrorResponse(
39                    HttpStatusCode.BadRequest, 
40                    "Invalid input");
41            }
42            messages.Add(message);
43            return Request.CreateResponse(HttpStatusCode.Created);
44        }
45    }
46}

Note: Remember to import PusherServer.

As you can see, this controller is very simple and has just two principal members: **Post** and **Get**.

**Post** is called with an instance of the ChatMessage model whenever a POST request is sent to /api/messages. It validates the model using Model.IsValid (remember those Required attributes?) before storing the incoming message in the messages list.

**Get** is even simpler – it’s called whenever a GET request is sent to /api/messages and it returns the messages list as JSON.

Making the server realtime

As it stands, the server can accept and send messages via POST and GET requests respectively. This is a solid starting point but ideally, clients should be immediately updated when new messages become available (i.e. updated in realtime).

With the current implementation, one possible way we could achieve this is by periodically sending a GET request to /api/messages from the client. This is a technique known as short polling and whilst it’s simple, it’s also really inefficient. A much more efficient solution to this problem would be to use WebSockets and when you use Pusher, the code is equally simple.

If you haven’t already, head over to the Pusher dashboard and create a new Pusher application:

chrome_2016-07-29_16-40-27

Take a note of your Pusher application keys (or just keep the Pusher dashboard open in another window ?) and return to Visual Studio.

In Visual Studio, click Tools | NuGet Package Manager | Package Manager Console, then install PusherServer with the following command:

1Install-Package PusherServer

Once PusherServer has finished installing, head back to the MessagesController.cs controller we defined earlier and replace the Post method with:

1public HttpResponseMessage Post(ChatMessage message)
2{
3    if (message == null || !ModelState.IsValid)
4    {
5        return Request.CreateErrorResponse(
6            HttpStatusCode.BadRequest, 
7            "Invalid input");
8    }
9    messages.Add(message);
10
11    var pusher = new Pusher(
12        "YOUR APP ID",
13        "YOUR APP KEY", 
14        "YOUR APP SECRET", 
15           new PusherOptions
16           {
17               Cluster = "YOUR CLUSTER"
18           });
19      pusher.Trigger(
20          channelName: "messages", 
21          eventName: "new_message", 
22          data: new
23          {
24              AuthorTwitterHandle = message.AuthorTwitterHandle,
25              Text = message.Text
26          });.
27
28     return Request.CreateResponse(HttpStatusCode.Created);
29}

As you can see, when you use Pusher, you don’t have to do a whole lot to make the server realtime. All we had to do was instantiate Pusher with our application details before calling Pusher.Trigger to broadcast the inbound chat message. When the time comes to implement the React client, we’ll subscribe to the messages channel for new messages.

CORS

We’re almost ready to build the client but before we do, we must first enable cross-origin resource sharing (CORS) in ASP.NET Web API.

In a nutshell, the PusherRealtimeChat.WebAPI and PusherRealtimeChat.UI projects will run on separate port numbers and therefore have different origins. In order to make a request from PusherRealtimeChat.UI to PusherRealtimeChat.WebAPI, a cross-origin HTTP request must take place. This is noteworthy because web browsers disallow cross-origin requests unless CORS is enabled on the server.

To enable CORS in ASP.NET Web API, it’s recommended that you use the Microsoft.AspNet.WebApi.Cors NuGet package.

Just like we did with the PusherServer NuGet package, to install Microsoft.AspNet.WebApi.Cors, click Tools | NuGet Package Manager | Package Manager Console, then run:

1Install-Package Microsoft.AspNet.WebApi.Cors

Once Microsoft.AspNet.WebApi.Cors has finished installing, you’ll need to enable it by going to App_Start/WebApiConfig.cs and calling config.EnableCors() from the Register method, like this:

1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Web.Http;
5
6namespace PusherRealtimeChat.WebAPI
7{
8    public static class WebApiConfig
9    {
10        public static void Register(HttpConfiguration config)
11        {
12            // Web API configuration and services
13            config.EnableCors();
14
15            // Web API routes
16            config.MapHttpAttributeRoutes();
17
18            config.Routes.MapHttpRoute(
19                name: "DefaultApi",
20                routeTemplate: "api/{controller}/{id}",
21                defaults: new { id = RouteParameter.Optional }
22            );
23        }
24    }
25}

You’ll also need to decorate the MessagesController.cs controller with the EnableCors attribute (remember to import System.Web.Http.Cors!):

1using System.Web.Http.Cors;
2
3namespace PusherRealtimeChat.WebAPI.Controllers
4{
5    [EnableCors("*", "*", "*")]
6    public class MessagesController : ApiController
7    {
8        ...
9    }
10}

And that’s it! You won’t be able to observe the impact of this change right now, but know that it’ll save us from cross-origin errors later down the road.

Implementing the Client

As I mentioned in the overview, the client code will reside in it’s own project called PusherRealtimeChat.UI. Let’s create that project now.

In Solution Explorer, right-click the PusherRealtimeChat solution, then go to Add | New Project. You should be presented with the Add New Project window. Choose ASP.NET Web Application and call it PusherRealtimeChat.UI

devenv_2016-07-29_17-03-23

When prompted again to choose a template, choose Empty before clicking OK:

devenv_2016-07-29_17-04-05

Note: There’s no need to check the Web API check box this time.

Again, if you’re prompted by Visual Studio to configure Azure, click Cancel:

devenv_2016-07-29_16-09-07

Once the PusherRealtimeChat.UI project has been created, the first thing we’ll want to do is declare all the front-end dependencies and devDependencies we anticipate needing. To do that, create an npm configuration file called package.json in the root of the PusherRealtimeChat.UI project:

1{
2  "version": "1.0.0",
3  "name": "ASP.NET",
4  "private": true,
5  "devDependencies": {
6    "webpack": "1.13.1",
7    "babel": "6.5.2",
8    "babel-preset-es2015": "6.9.0",
9    "babel-preset-react": "6.11.1",
10    "babel-loader": "6.2.4"
11  },
12  "dependencies": {
13    "react": "15.2.1",
14    "react-dom": "15.2.1",
15    "axios": "0.13.1",
16    "pusher-js": "3.1.0"
17  }
18}

Upon saving the above package.json file, Visual Studio will automatically download the dependencies into a local node_modules directory, via npm:

devenv_2016-07-29_17-14-23

I expect that the react, react-dom, webpack, and babel-* dependencies are already familiar to you, as they’re commonly used with React. axios is a modern HTTP client and pusher-js is the Pusher client library we’ll be using to subscribe for new messages.

Once the aforementioned modules have finished installing, we can setup Babel and WebPack to transpile our source code.

Transpilation

Because modern web browsers don’t yet understand JavaScript modules or JSX, we must first transpile our source code before distributing it. To do that, we’ll use WebPack in conjunction with the babel-loader WebPack loader.

At the core of any WebPack build is a webpack.config.js file. We’ll puts ours alongside package.json in the root of the RealtimeChat.UI project:

1"use strict";
2
3module.exports = {
4    entry: "./index.js",
5    output: {
6        filename: "bundle.js"
7    },
8    module: {
9        loaders: [
10            {
11                test: /\.js$/,
12                loader: "babel-loader",
13                exclude: /node_modules/,
14                query: {
15                    presets: ["es2015", "react"]
16                }
17            }
18        ]
19    }
20};

I shan’t belabour the webpack.config.js configuration file but suffice to say, it directs WebPack to look at the index.js file and to transpile its contents using babel-loader, and to output the result to a file called bundle.js.

This is all very good and well but how do we run WebPack from Visual Studio?

First of all, you’ll want to define an npm script in package.json that runs webpack with the webpack.config.js configuration file we just created:

1{
2  "version": "1.0.0",
3  "name": "ASP.NET",
4  "private": "true",
5  "devDependencies": {
6    ...
7  },
8  "dependencies": {
9    ...
10  },
11  "scripts": {
12    "build": "webpack --config webpack.config.js"
13  }
14}

Then, to actually run the above script from within Visual Studio, I recommend using the npm Task Runner Visual Studio extension by @mkristensen:

chrome_2016-08-01_09-57-28

If you haven’t already, install the extension, then go to Tools | Task Runner Explorer to open it.

Note: You can also load the extension by searching for “Task Runner Explorer” in Quick Launch. Also Note: You’ll need to restart Visual Studio before npm scripts will appear in Task Runner Explorer.

Inside Task Runner Explorer, you should see the custom build script we added:

2016-08-01_10-04-51

There isn’t much use in running the build script quite yet, as there’s nothing to build. That being said, for future reference, to run the script you just need to double click it.

Rather than running the script manually every time we update the client code, it would be better to automatically run the script whenever we run the Visual Studio solution. To make that happen, right-click the build script, then go to Bindings and check After Build:

devenv_2016-08-01_10-13-49

Now, whenever we run the PusherRealtimeChat.UI project, the build script will be run automatically – nice!

One more thing we could do to make development easier going forward is to treat both the PusherRealtimeChat.WebAPI and PusherRealtimeChat.UI projects as one thus that when we press Run, both projects start.

Setting Multiple Start Up Projects

To setup multiple startup project, in Solution Explorer, right-click the PusherRealtimeChat solution, then click Properties. In the Properties window, go to Common Properties | Startup Projects, then click the Multiple startup projects radio button. Finally, set the Action for both PusherRealtimeChat.UI and PusherRealtimeChat.WebAPI to Start:

2016-08-01_10-15-04

Now, when you press Run, both projects will start. This makes perfect sense for this project because it’s rare that you would want to run the server but not the client and vice versa.

That is more or less it in terms of setting up our build tools, let’s move on and implement some code… at last!

Implementing the Client

To begin with, create an index.html file in the PusherRealtimeUI.UI project root:

1<!DOCTYPE html>
2<html>
3<head>
4    <title>Pusher Realtime Chat</title>
5    <meta charset="utf-8" />
6</head>
7<body>
8    <div class="container" id="app"></div>
9    <script src="./bundle.js"></script>
10</body>
11</html>

Note: The index.html file on GitHub will look a bit different due to the fact that I applied styles to the final code but do not mention styles in this post.

There isn’t much to note here except that we reference bundle.js, which is the file output by WebPack.

bundle.js won’t exist at the moment because there’s no code to build. We’ll implement some code in just a moment but first, let’s take a step back and try to get a feeling for the structure of the client application.

React popularized the idea of breaking your UI into a hierarchy of components. This approach has many benefits, one of which is that it makes it easy to see an overview of the application, here’s ours:

Untitled Diagram

Notice how I make a distinction between containers and presentational components. You can read more about the distinction here in an article by @dan_abromav but in a nutshell, container components fetch data and store state whereas presentational components only concern themselves with presentation. I won’t be explaining the presentational components in this article, as they simply render content – all the noteworthy stuff happens inside the App container!

For production applications, it’s recommended that you separate your components into separate files. For the purposes of this tutorial, however, I’m going to present the code in a single file called index.js:

1import React from "react";
2import ReactDOM from "react-dom";
3import axios from "axios";
4import Pusher from "pusher-js";
5
6const baseUrl = 'http://localhost:50811';
7
8const Welcome = ({ onSubmit }) => {
9    let usernameInput;
10    return (
11        <div>
12            <p>Enter your Twitter name and start chatting!</p>
13            <form onSubmit={(e) => {
14                e.preventDefault();
15                onSubmit(usernameInput.value);
16            }}>
17                <input type="text" placeholder="Enter Twitter handle here" ref={node => {
18                    usernameInput = node;
19                }}/>
20                <input type="submit" value="Join the chat" />
21            </form>
22        </div>
23    );
24};
25
26const ChatInputForm = ({
27    onSubmit
28}) => {
29    let messageInput;
30    return (
31        <form onSubmit = { e => {
32            e.preventDefault();
33            onSubmit(messageInput.value);
34            messageInput.value = ""; 
35        }}>
36            <input type = "text" placeholder = "message" ref = { node => { 
37                messageInput = node; 
38            }}/> 
39            <input type = "submit" value = "Send" / >
40        </ form>
41    );
42};
43
44const ChatMessage = ({ message, username }) => (
45    <li className='chat-message-li'>
46        <img src={`https://twitter.com/${username}/profile_image?size=original`} style={{
47            width: 24,
48            height: 24
49        }}/>
50        <strong>@{username}: </strong> {message}
51    </li>
52);
53
54const ChatMessageList = ({ messages }) => (
55    <ul>
56        {messages.map((message, index) => 
57            <ChatMessage 
58                key={index} 
59                message={message.Text} 
60                username={message.AuthorTwitterHandle} /> 
61        )}
62    </ul>
63);
64
65
66const Chat = ({ onSubmit, messages }) => (
67    <div> 
68        <ChatMessageList messages={messages} />
69        <ChatInputForm onSubmit={onSubmit}/>
70    </div>
71);
72
73const App = React.createClass({
74    getInitialState() {
75        return {
76            authorTwitterHandle: "",
77            messages: []
78        }
79    },
80
81    componentDidMount() {
82        axios
83            .get(`${baseUrl}/api/messages`)
84            .then(response => {
85                this.setState({
86                    messages: response.data
87                });
88                var pusher = new Pusher('YOUR APP KEY', {
89                    encrypted: true
90                });
91                var chatRoom = pusher.subscribe('messages');
92                chatRoom.bind('new_message', message => {
93                    this.setState({
94                        messages: this.state.messages.concat(message)
95                    });
96                });
97            });
98    },
99
100    sendMessage(messageText) {
101        axios
102            .post(`${baseUrl}/api/messages`, {
103                text: messageText,
104                authorTwitterHandle: this.state.authorTwitterHandle
105            })
106            .catch(() => alert('Something went wrong :('));
107    },
108
109    render() {
110        if (this.state.authorTwitterHandle === '') {
111            return (
112                <Welcome onSubmit = { author => 
113                    this.setState({
114                        authorTwitterHandle: author
115                    })
116                }/>
117            );
118        } else {
119            return <Chat messages={this.state.messages} onSubmit={this.sendMessage} />;
120        }
121    }
122});
123
124ReactDOM.render(<App />, document.getElementById("app"));

Note: Remember to update YOUR APP KEY and baseUrl. baseUrl should point to your server’s address.

Like I mentioned previously, I won’t be explaining the presentational components in this post but I will be explaining the App container component. Here it is again for reference:

1const App = React.createClass({
2    getInitialState() {
3        return {
4            authorTwitterHandle: "",
5            messages: []
6        }
7    },
8
9    componentDidMount() {
10        axios
11            .get(`${baseUrl}/api/messages`)
12            .then(response => {
13                this.setState({
14                    messages: response.data
15                });
16                var pusher = new Pusher('YOUR APP KEY', {
17                    encrypted: true
18                });
19                var chatRoom = pusher.subscribe('messages');
20                chatRoom.bind('new_message', message => {
21                    this.setState({
22                        messages: this.state.messages.concat(message)
23                    });
24                });
25            });
26    },
27
28    sendMessage(messageText) {
29        axios
30            .post(`${baseUrl}/api/messages`, {
31                text: messageText,
32                authorTwitterHandle: this.state.authorTwitterHandle
33            })
34            .catch(() => alert('Something went wrong :('));
35    },
36
37    render() {
38        if (this.state.authorTwitterHandle === '') {
39            return (
40                <Welcome onSubmit = { author => 
41                    this.setState({
42                        authorTwitterHandle: author
43                    })
44                }/>
45            );
46        } else {
47            return <Chat messages={this.state.messages} onSubmit={this.sendMessage} />;
48        }
49    }
50});

When the App container is first loaded, the getInitialState lifecycle method is called:

1getInitialState () {
2    return {
3        authorTwitterHandle: "",
4        messages: []
5    }
6}

getInitialState quickly returns an object that describes the initial state of the application – the render method is run almost immediately afterwards:

1render() {
2    if (this.state.authorTwitterHandle === '') {
3        return (
4            <Welcome onSubmit = { author => 
5                this.setState({
6                     authorTwitterHandle: author
7                })
8            }/>
9        );
10    } else {
11        return <Chat messages={this.state.messages} onSubmit={this.sendMessage} />;
12    }
13}

The render function first looks at this.state.authorTwitterHandle to determine whether or not the user has provided their Twitter handle yet. If they have not, the Welcome component is rendered; otherwise, the Chat component is rendered.

Notice how we pass an onClick property to the Welcome component. This allows us to update the state and re-render the App container when the Welcome component’s form is submitted. Similarly, we pass this.state.messages and another onClick function to the Chat component. These properties allow the Chat component to render and submit messages respectively.

After render, componentDidMount is called:

1componentDidMount() {
2  axios
3    .get(`${baseUrl}/api/messages`)
4    .then(response => {
5      this.setState({
6        messages: response.data
7      });
8      var pusher = new Pusher('YOUR APP KEY', {
9          encrypted: true
10      });
11      var chatRoom = pusher.subscribe('messages');
12      chatRoom.bind('new_message', message => {
13          this.setState({
14              messages: this.state.messages.concat(message)
15          });
16      });
17    });
18},

componentDidMount first makes an asynchronous GET request to /api/messages.

When the asynchronous GET request to /api/messages has finished, we update the container’s state using this.setState before initializing Pusher and subscribing for new new_messagess on the messages channel. Remember how we programmed the server to broadcast messages via the messages channel? This is where we subscribe to them. As new messages trickle in, the state, and by extension, the UI is updated in realtime.

Conclusion

ASP.NET Web API is a tremendously powerful server-side technology but historically, it has been hard to integrate with modern front-end technologies. With the advent of handy extensions like npm Task Runner and native support for npm in Visual Studio, building full-fledged web applications with ASP.NET Web API and React is not only possible, it’s easy!

I appreciate a tutorial with so many moving parts might be hard to follow so I took the liberty of putting the code on GitHub.

If you have any comments of questions, please feel free to leave a comment below or message me on Twitter (I’m @bookercodes ?!).