Click here to Skip to main content
15,886,562 members
Articles / Web Development / ASP.NET

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

Rate me:
Please Sign up or sign in to vote.
4.67/5 (3 votes)
8 Aug 2016CPOL11 min read 28.8K   11   7
Learn how to combine the power of ASP.NET Web API and React

In this post 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
  • How to use webpack and npm together with Visual Studio
  • How to easily make your applications 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:

... 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.

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:

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:
 

using System.ComponentModel.DataAnnotations;

namespace PusherRealtimeChat.WebAPI.Models
{
    public class ChatMessage
    {
        [Required]
        public string Text { get; set; }

        [Required]
        public string AuthorTwitterHandle { get; set; }
    }
}

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 <a href="http://www.asp.net/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api">Required</a> 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:

using PusherRealtimeChat.WebAPI.Models;
using PusherServer;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace PusherRealtimeChat.WebAPI.Controllers
{
    public class MessagesController : ApiController
    {
        private static List<ChatMessage> messages =
            new List<ChatMessage>()
            {
                new ChatMessage
                {
                    AuthorTwitterHandle = "Pusher",
                    Text = "Hi there! 😘"
                },
                new ChatMessage
                {
                    AuthorTwitterHandle = "Pusher",
                    Text = "Welcome to your chat app"
                }
            };

        public HttpResponseMessage Get()
        {
            return Request.CreateResponse(
                HttpStatusCode.OK, 
                messages);
        }

        public HttpResponseMessage Post(ChatMessage message)
        {
            if (message == null || !ModelState.IsValid)
            {
                return Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, 
                    "Invalid input");
            }
            messages.Add(message);
            return Request.CreateResponse(HttpStatusCode.Created);
        }
    }
}

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:


 

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 <a href="https://www.nuget.org/packages/PusherServer/">PusherServer</a> with the following command:

Install-Package PusherServer

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

public HttpResponseMessage Post(ChatMessage message)
{
    if (message == null || !ModelState.IsValid)
    {
        return Request.CreateErrorResponse(
            HttpStatusCode.BadRequest, 
            "Invalid input");
    }
    messages.Add(message);
    
    var pusher = new Pusher(
        "YOUR APP ID",
        "YOUR APP KEY", 
        "YOUR APP SECRET", 
           new PusherOptions
           {
               Cluster = "YOUR CLUSTER"
           });
      pusher.Trigger(
          channelName: "messages", 
          eventName: "new_message", 
          data: new
          {
              AuthorTwitterHandle = message.AuthorTwitterHandle,
              Text = message.Text
          });.

     return Request.CreateResponse(HttpStatusCode.Created);
}

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 <a href="https://www.nuget.org/packages/Microsoft.AspNet.WebApi.Cors">Microsoft.AspNet.WebApi.Cors</a> 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:

Install-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:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace PusherRealtimeChat.WebAPI
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services
            config.EnableCors();

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

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

using System.Web.Http.Cors;

namespace PusherRealtimeChat.WebAPI.Controllers
{
    [EnableCors("*", "*", "*")]
    public class MessagesController : ApiController
    {
        ...
    }

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:

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


 

Note: There's no need to check the <strong>Web API</strong> 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:

{
  "version": "1.0.0",
  "name": "ASP.NET",
  "private": true,
  "devDependencies": {
    "webpack": "1.13.1",
    "babel": "6.5.2",
    "babel-preset-es2015": "6.9.0",
    "babel-preset-react": "6.11.1",
    "babel-loader": "6.2.4"
  },
  "dependencies": {
    "react": "15.2.1",
    "react-dom": "15.2.1",
    "axios": "0.13.1",
    "pusher-js": "3.1.0"
  }
}

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. <a href="https://github.com/mzabriskie/axios">axios</a> 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:

"use strict";

module.exports = {
    entry: "./index.js",
    output: {
        filename: "bundle.js"
    },
    module: {
        loaders: [
            {
                test: /\.js$/,
                loader: "babel-loader",
                exclude: /node_modules/,
                query: {
                    presets: ["es2015", "react"]
                }
            }
        ]
    }
};

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:

{
  "version": "1.0.0",
  "name": "ASP.NET",
  "private": "true",
  "devDependencies": {
    ...
  },
  "dependencies": {
    ...
  },
  "scripts": {
    "build": "webpack --config webpack.config.js"
  }
}

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.

Setting up 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:


 

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 PusherRealtimeChat.UI project root:

<!DOCTYPE html>
<html>
<head>
    <title>Pusher Realtime Chat</title>
    <meta charset="utf-8" />
</head>
<body>
    <div class="container" id="app"></div>
    <script src="./bundle.js"></script>
</body>
</html>

Note: The <a href="https://github.com/pusher-community/pusher-dotnet-react-chat/blob/04e400e04ee28dca7218b080f4e26e2be106d6e5/PusherRealtimeChat.UI/index.html">index.html</a> 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:

import React from "react";
import ReactDOM from "react-dom";
import axios from "axios";
import Pusher from "pusher-js";

const baseUrl = 'http://localhost:50811';

const Welcome = ({ onSubmit }) => {
    let usernameInput;
    return (
        <div>
            <p>Enter your Twitter name and start chatting!</p>
            <form onSubmit={(e) => {
                e.preventDefault();
                onSubmit(usernameInput.value);
            }}>
                <input type="text" placeholder="Enter Twitter handle here" ref={node => {
                    usernameInput = node;
                }}/>
                <input type="submit" value="Join the chat" />
            </form>
        </div>
    );
};

const ChatInputForm = ({
    onSubmit
}) => {
    let messageInput;
    return (
        <form onSubmit = { e => {
            e.preventDefault();
            onSubmit(messageInput.value);
            messageInput.value = ""; 
        }}>
            <input type = "text" placeholder = "message" ref = { node => { 
                messageInput = node; 
            }}/> 
            <input type = "submit" value = "Send" / >
        </ form>
    );
};

const ChatMessage = ({ message, username }) => (
    <li className='chat-message-li'>
        <img src={`https://twitter.com/${username}/profile_image?size=original`} style={{
            width: 24,
            height: 24
        }}/>
        <strong>@{username}: </strong> {message}
    </li>
);

const ChatMessageList = ({ messages }) => (
    <ul>
        {messages.map((message, index) => 
            <ChatMessage 
                key={index} 
                message={message.Text} 
                username={message.AuthorTwitterHandle} /> 
        )}
    </ul>
);

const Chat = ({ onSubmit, messages }) => (
    <div> 
        <ChatMessageList messages={messages} />
        <ChatInputForm onSubmit={onSubmit}/>
    </div>
);

const App = React.createClass({
    getInitialState() {
        return {
            authorTwitterHandle: "",
            messages: []
        }
    },

    componentDidMount() {
        axios
            .get(`${baseUrl}/api/messages`)
            .then(response => {
                this.setState({
                    messages: response.data
                });
                var pusher = new Pusher('YOUR APP KEY', {
                    encrypted: true
                });
                var chatRoom = pusher.subscribe('messages');
                chatRoom.bind('new_message', message => {
                    this.setState({
                        messages: this.state.messages.concat(message)
                    });
                });
            });
    },

    sendMessage(messageText) {
        axios
            .post(`${baseUrl}/api/messages`, {
                text: messageText,
                authorTwitterHandle: this.state.authorTwitterHandle
            })
            .catch(() => alert('Something went wrong :('));
    },

    render() {
        if (this.state.authorTwitterHandle === '') {
            return (
                <Welcome onSubmit = { author => 
                    this.setState({
                        authorTwitterHandle: author
                    })
                }/>
            );
        } else {
            return <Chat messages={this.state.messages} onSubmit={this.sendMessage} />;
        }
    }
});

ReactDOM.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:

const App = React.createClass({
    getInitialState() {
        return {
            authorTwitterHandle: "",
            messages: []
        }
    },

    componentDidMount() {
        axios
            .get(`${baseUrl}/api/messages`)
            .then(response => {
                this.setState({
                    messages: response.data
                });
                var pusher = new Pusher('YOUR APP KEY', {
                    encrypted: true
                });
                var chatRoom = pusher.subscribe('messages');
                chatRoom.bind('new_message', message => {
                    this.setState({
                        messages: this.state.messages.concat(message)
                    });
                });
            });
    },

    sendMessage(messageText) {
        axios
            .post(`${baseUrl}/api/messages`, {
                text: messageText,
                authorTwitterHandle: this.state.authorTwitterHandle
            })
            .catch(() => alert('Something went wrong :('));
    },

    render() {
        if (this.state.authorTwitterHandle === '') {
            return (
                <Welcome onSubmit = { author => 
                    this.setState({
                        authorTwitterHandle: author
                    })
                }/>
            );
        } else {
            return <Chat messages={this.state.messages} onSubmit={this.sendMessage} />;
        }
    }
});

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

getInitialState () {
    return {
        authorTwitterHandle: "",
        messages: []
    }
}

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

render() {
    if (this.state.authorTwitterHandle === '') {
        return (
            <Welcome onSubmit = { author => 
                this.setState({
                     authorTwitterHandle: author
                })
            }/>
        );
    } else {
        return <Chat messages={this.state.messages} onSubmit={this.sendMessage} />;
    }
}

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:

componentDidMount() {
  axios
    .get(`${baseUrl}/api/messages`)
    .then(response => {
      this.setState({
        messages: response.data
      });
      var pusher = new Pusher('YOUR APP KEY', {
          encrypted: true
      });
      var chatRoom = pusher.subscribe('messages');
      chatRoom.bind('new_message', message => {
          this.setState({
              messages: this.state.messages.concat(message)
          });
      });
    });
},


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 😀!).


 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionIs this the original author? Pin
PHenry10-Aug-16 9:37
PHenry10-Aug-16 9:37 
GeneralSeems some pics are missing. Pin
YuKaikai9-Aug-16 20:41
YuKaikai9-Aug-16 20:41 
BugImages aren't showing up Pin
Mika8-Aug-16 22:17
Mika8-Aug-16 22:17 
GeneralRe: Images aren't showing up Pin
PHenry9-Aug-16 6:53
PHenry9-Aug-16 6:53 
I'm seeing.....or not seeing rather, the top six images. They aren't appearing for me neither.
GeneralRe: Images aren't showing up Pin
Mika9-Aug-16 9:42
Mika9-Aug-16 9:42 
GeneralRe: Images aren't showing up Pin
PHenry9-Aug-16 11:22
PHenry9-Aug-16 11:22 
GeneralRe: Images aren't showing up Pin
YuKaikai9-Aug-16 20:44
YuKaikai9-Aug-16 20:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.