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.
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:
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:
… And upon clicking Join, taken to the chat where they can send and receive messages in realtime:
The Visual Studio solution will be comprised of two projects namely, PusherRealtimeChat.WebAPI and PusherRealtimeChat.UI:
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.
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:
When prompted to select a template, choose Empty and check Web API before clicking OK:
If you’re prompted by Visual Studio to configure Azure, click Cancel:
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:
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.
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:
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.
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.
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
When prompted again to choose a template, choose Empty before clicking OK:
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:
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:
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.
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:
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:
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:
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.
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:
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!
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:
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_messages
s 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.
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 ?!).