DNN Windows Client

This tutorial will walk you through the creation of a basic Windows Forms application to test remote authentication and authorization when communicating with a DNN CMS web portal using its web services framework / Web API. This is for development and educational purposes only running locally. You will at least want to make sure that you have SSL enabled for your DNN Web Services in a real world scenario.

Requirements

Create a New Windows Forms Project

DnnWinClient - Windows Forms Project
DnnWinClient – Windows Forms Project

While in the Form1.cs[Design] workspace, open the Toolbox and drag a TextBox and Label over to Form1 as shown below

Form1 - Add Controls
Form1 – Add Controls

Select the TextBox and then select F4 to bring up its Properties. Change the Name of the TextBox control to txtSite as shown below. Select the Label control, bring up its Properties, and Name it lblSite, set its Text to Site.

Form1 - TextBox Properties
Form1 – TextBox Properties

Repeat the steps adding these controls to the form and setting the respective Properties of each as needed:

    Labels

  • Name: lblUsername
    Text: Username
  • Name: lblPassword
    Text: Password
  • Name: lblRoute
    Text: Route
  • Name: lblResponse
    Text: Response
    Text Boxes

  • Name: txtUsername
  • Name: txtPassword
    UseSystemPasswordChar: True
  • Name: txtRoute
  • Name: txtResponseMultiline: True

For simplicity, we will just set the Text property of the Text Box controls to display our default values.

    Defaults

  • Name: txtSite
    Text: http://dnn7 (your DNN website Url)
  • Name: txtRoute
    Text: DesktopModules/Services/API/TestAuth

Add a Button control so we can make a connection to the DNN CMS from our Windows Client. Name: txtButton, Text: Connect. Almost done with the design, select the form, and set its Text to DNN Client. Position and resize the Form, Labels, Text Boxes and Button to your liking making sure you have given the txtResponse Text Box enough space to display the response from the DNN web service. Here is how my form looks after some dragging and sizing of the form and its controls.

DnnWinClient - Form1.cs[Design]
DnnWinClient – Form1.cs[Design]

Note the expanded Form1 node in Solution Explorer, the order of the controls is determined by their TabIndex property value.

If everything looks good, now is a good time to Save All (Ctrl+Shift+S) and Build (Ctrl+Shift+B) your Solution.

RestSharp

RestSharp is a Simple REST and HTTP API client for .NET that makes building the requests easy and efficient. To install RestSharp into the solution, I prefer NuGet. From the menu In Visual Studio, select Tools > Library Package Manager > Package Manager Console. Then run the following command:

PM> Install-Package RestSharp

Connect Code

Double click on the Connect button (btnConnect) to stub out a btnConnect_Click method in the Form1 class. Copy and Paste the following code into the method:

var domain = txtSite.Text;
var username = txtUsername.Text;
var password = txtPassword.Text;
var route = txtRoute.Text;

var apiUrl = string.Format("{0}/{1}", domain, route);

IRestRequest request = new RestRequest();
var client = new RestClient
{
    BaseUrl = apiUrl,
    Authenticator = new HttpBasicAuthenticator(username, password),
};
IRestResponse response = client.Execute(request);

txtResponse.Text = response.Content;

After removing the unused usings, adding RestSharp and a application/json request header, here is what the entire class should look like:

using System;
using System.Windows.Forms;
using RestSharp;

namespace DnnWinClient
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btnConnect_Click(object sender, EventArgs e)
        {
            var domain = txtSite.Text;
            var username = txtUsername.Text;
            var password = txtPassword.Text;
            var route = txtRoute.Text;

            var apiUrl = string.Format("{0}/{1}", domain, route);

            IRestRequest request = new RestRequest();
            //We want our response in Json format
            request.AddHeader("Content-type", "application/json; charset=utf-8");
            var client = new RestClient
            {
                BaseUrl = apiUrl,
                Authenticator = new HttpBasicAuthenticator(username, password),
            };
            IRestResponse response = client.Execute(request);

            txtResponse.Text = response.Content;
        }
    }
}

Save All (Ctrl+Shift+S) and Build (Ctrl+Shift+B) the Solution. We our now done with our simple DNN Windows Client. Now let’s move onto the web services class library.

Setup DNN Web Services

DotNetNuke (DNN) will need a Web API service endpoint created to allow the Windows Client to communicate with the CMS. See this blog post for info on how to build a DNN Web Services Framework class library.

RouteMapper.cs
using System;
using System.Web.Http;
using DotNetNuke.Web.Api;

namespace WebServices
{
    public class RouteMapper : IServiceRouteMapper
    {
        public void RegisterRoutes(IMapRoute mapRouteManager)
        {
            mapRouteManager.MapHttpRoute(
                moduleFolderName: "Services",
                routeName: "Default",
                url: "{controller}/{id}",
                defaults: new { id = RouteParameter.Optional },
                namespaces: new[] { "WebServices" }
            );
        }
    }
}

Create a model for the data we are returning to the client. In the Solution Explorer, right click on the WebServices project and select Add > New Folder and name it Models. Add this class to the Models folder:

Models.User.cs
using System;

namespace WebServices.Models
{
    class User
    {
        public string FirstName { get; set; }
        public string Email { get; set; }
        public bool IsSuperUser { get; set; }
        public string LastName { get; set; }
        public int PortalID { get; set; }
        public int UserID { get; set; }
    }
}

Create a DNN API controller for the endpoint that returns the model data to the client. In the Solution Explorer, right click on the WebServices project and select Add > Class and name it TestAuthController.

TestAuthController.cs
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using DotNetNuke.Web.Api;

namespace WebServices
{
    public class TestAuthController : DnnApiController
    {
        [DnnAuthorize]        
        public HttpResponseMessage Get()
        {
            //map UserInfo to User object we are returning
            Models.User user = new Models.User
            {
                Email = this.UserInfo.Email,
                IsSuperUser = this.UserInfo.IsSuperUser,
                FirstName = this.UserInfo.FirstName,
                LastName = this.UserInfo.LastName,
                PortalID = this.UserInfo.PortalID,
                UserID = this.UserInfo.UserID
            };
            return Request.CreateResponse(HttpStatusCode.OK, user);
        }

    }
}

Build the project making sure that the output is set to your DNN website root bin folder. For more information, refer to this blog post DotNetNuke 7. Open up your local DNN website in a browser to setup the DNN Web Services you just built.

Now for the moment of truth, Start the DnnWinClient and enter your DNN Username, Password and click Connect. The Response should contain the expected user info.

DnnWinClient - Running
DnnWinClient – Running
Troubleshooting

If the PortalID and UserID values are -1 and the other values are null, check your DNN website web.config modules node. You may need the runAllManagedModulesForAllRequests attribute set to true. In theory it has a performance impact, in practice, IIS should already be doing this. For more info, refer to this blog post by Rick Strahl: ASP.NET Routing not working on IIS 7.0

web.config – Replace
<modules>
web.config – With
<modules runAllManagedModulesForAllRequests="true">

This project is available for browsing and download at GitHub: https://github.com/jimfrenette/DnnWinClient

Resources

DotNetNuke 7 Web Services Framework Web API Routes


When building the DotNetNuke Web Services API controllers, I want to use this REST convention for the endpoints to access a resource such as the Chinook database. Note: My DotNetNuke website is running under IIS 7.5 with "dnn7" as the host name binding. Only the request method, url, protocol, content-type and content-body are included in request examples below.

Return all Tracks:

GET http://dnn7/DesktopModules/Services/API/Tracks HTTP/1.1

Return a Track where ID = 1:

GET http://dnn7/DesktopModules/Services/API/Tracks/1 HTTP/1.1
CREATE a Track:
POST http://dnn7/DesktopModules/Services/API/Tracks HTTP/1.1

Content-Type: application/json

{
    "albumId": 1,
    "name": "For Those About To Rock (We Salute You)",
    "mediaTypeId": 1,
    "genreId": 1,
    "composer": "Angus Young, Malcolm Young, Brian Johnson",
    "milliseconds": 343719,
    "bytes": 11170334,
    "unitPrice": 0.99
}
UPDATE a Track:
PUT http://dnn7/DesktopModules/Services/API/Tracks/1 HTTP/1.1

Content-Type: application/json

{
    "albumId": 1,
    "name": "For Those About To Rock (We Salute You)",
    "mediaTypeId": 1,
    "genreId": 1,
    "composer": "Angus Young, Malcolm Young, Brian Johnson",
    "milliseconds": 343719,
    "bytes": 11170334,
    "unitPrice": 0.99
}
DELETE a Track where ID = 1:
DELETE http://dnn7/DesktopModules/Services/API/Tracks/1 HTTP/1.1
DELETE Tracks – body contains a json array of Track ID’s to delete:
DELETE http://dnn7/DesktopModules/Services/API/Tracks/Delete HTTP/1.1

Content-Type: application/json

{
    "ids": [
        6,
        7,
        8
    ]
}
RouteMapper.cs
using System;
using System.Web.Http;
using DotNetNuke.Web.Api;

namespace WebServices
{
    public class RouteMapper : IServiceRouteMapper
    {
        public void RegisterRoutes(IMapRoute mapRouteManager)
        {
            mapRouteManager.MapHttpRoute(
                moduleFolderName: "Services",
                routeName: "Default",
                url: "{controller}/{id}",
                defaults: new { id = RouteParameter.Optional },
                namespaces: new[] { "WebServices" }
            );
        }
    }
}

Json.NET

For the Delete method in the TracksController, we will use Json.NET to parse the track id’s from the json payload. To install Json.NET into the solution, I prefer NuGet. From the menu In Visual Studio, select Tools > Library Package Manager > Package Manager Console. Then run the following command:

PM> Install-Package Newtonsoft.Json
TracksController.cs
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using DotNetNuke.Web.Api;
using Newtonsoft.Json.Linq;

namespace WebServices
{
    [AllowAnonymous]
    public class TracksController : DnnApiController
    {
        public HttpResponseMessage Get()
        {
            //put your code here for call to handle the get
            //for example
            //tracks = TracksService.Get();

            return Request.CreateResponse(HttpStatusCode.OK, tracks);
        }

        public HttpResponseMessage Get(int id)
        {
            //put your code here for call to handle the get
            //for example
            //track = TracksService.Get(id);

            return Request.CreateResponse(HttpStatusCode.OK, track);
        }

        [DnnAuthorize]
        public HttpResponseMessage Post(Track track)
        {
            //put your code here for call to handle the create
            //for example
            //TracksService.Create(track);

            return Request.CreateResponse(HttpStatusCode.OK);
        }

        [DnnAuthorize]
        public HttpResponseMessage Put(Track track)
        {
            //put your code here for call to handle the update
            //for example
            //TracksService.Update(track);

            return Request.CreateResponse(HttpStatusCode.OK);
        }

        [DnnAuthorize]
        public HttpResponseMessage Delete(int id)
        {
            //put your code here for call to handle the delete
            //for example
            //TracksService.Delete(id);

            return Request.CreateResponse(HttpStatusCode.OK);
        }

        [DnnAuthorize]
        public HttpResponseMessage Delete(JObject jObject)
        {
            var ids = new JArray();
            if (jObject["ids"].Type == JTokenType.String)
            {
                ids.Add(jObject["ids"]);
            }
            else
            {
                ids = (JArray)jObject["ids"];
            }
            long[] assetIds = ids.Select(jv => Convert.ToInt64((string)jv)).ToArray();
            
            //put your code here for call to handle the delete
            //for example
            //TracksService.Delete(assetIds);

            return Request.CreateResponse(HttpStatusCode.OK);
        }

    }

}

Resources

Chinook Web API Project

For this project, you will need to have access to the Chinook SQL Server Database.

1. Create a New Project

New ASP.NET MVC 4 Web Application - ChinookWebApi
New ASP.NET MVC 4 Web Application – ChinookWebApi

2. Select the Web API Project Template

New ASP.NET MVC 4 Project - Web API Project Template
New ASP.NET MVC 4 Project – Web API Project Template

Data Models

In Solution Explorer, right-click the Models folder and select Add > New Item (Ctrl+Shift+A) > Select Class and save it as Tracks.cs. Then add the data type objects to the Tracks class.

Add New Item - Tracks.cs
Add New Item – Tracks.cs

namespace ChinookWebApi.Models
{
    public class Tracks
    {
        public int TrackId { get; set; }
        public string Name { get; set; }
        public int AlbumId { get; set; }
        public int MediaTypeId { get; set; }
        public int GenreId { get; set; }
        public string Composer { get; set; }
        public int Milliseconds { get; set; }
        public int Bytes { get; set; }
        public double UnitPrice { get; set; }
    }
}

Data Access

Create a Chinook class for database access. In the Solution Explorer, right click on the ChinookWebApi project and add a new class named Chinook.cs. To see the respective stored procedure SQL code, refer to Chinook SQL Server Database.

Chinook.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Web.Configuration;

namespace ChinookWebApi
{
    using Models;

    public class Chinook
    {
        public static void DeleteTrack(int trackId)
        {
            var conn = new SqlConnection(Config.ChinookConnection);
            conn.Open();
            var cmd = new SqlCommand("DeleteTrack", conn)
            {
                CommandType = CommandType.StoredProcedure
            };
            cmd.Parameters.AddWithValue("@Id", trackId);
            cmd.ExecuteNonQuery();
            conn.Close();
        }
        
        public static List Tracks(int trackId)
        {
            var trackList = new List();
            var conn = new SqlConnection(Config.ChinookConnection);
            conn.Open();
            var cmd = new SqlCommand("GetTracks", conn)
            {
                CommandType = CommandType.StoredProcedure
            };
            cmd.Parameters.AddWithValue("@Id", trackId);
            var dr = cmd.ExecuteReader();
            while (dr.Read())
            {
                trackList.Add(new Tracks
                {
                    TrackId = Convert.ToInt32(dr["TrackId"]),
                    Name = dr["Name"].ToString(),
                    AlbumId = Convert.ToInt32(dr["AlbumId"]),
                    MediaTypeId = Convert.ToInt32(dr["MediaTypeId"]),
                    GenreId = Convert.ToInt32(dr["GenreId"]),
                    Composer = dr["Composer"].ToString(),
                    Milliseconds = Convert.ToInt32(dr["Milliseconds"]),
                    Bytes = Convert.ToInt32(dr["Bytes"]),
                    UnitPrice = Convert.ToDouble(dr["UnitPrice"]),
                });
            }
            dr.Close();
            conn.Close();

            return trackList;
        }

        public static List TracksByAlbum(int albumId)
        {
            var trackList = new List();
            var conn = new SqlConnection(Config.ChinookConnection);
            conn.Open();
            var cmd = new SqlCommand("GetTracks", conn)
            {
                CommandType = CommandType.StoredProcedure
            };
            cmd.Parameters.AddWithValue("@Id", albumId);
            var dr = cmd.ExecuteReader();
            while (dr.Read())
            {
                trackList.Add(new Tracks
                {
                    TrackId = Convert.ToInt32(dr["TrackId"]),
                    Name = dr["Name"].ToString(),
                    AlbumId = Convert.ToInt32(dr["AlbumId"]),
                    MediaTypeId = Convert.ToInt32(dr["MediaTypeId"]),
                    GenreId = Convert.ToInt32(dr["GenreId"]),
                    Composer = dr["Composer"].ToString(),
                    Milliseconds = Convert.ToInt32(dr["Milliseconds"]),
                    Bytes = Convert.ToInt32(dr["Bytes"]),
                    UnitPrice = Convert.ToDouble(dr["UnitPrice"]),
                });
            }
            dr.Close();
            conn.Close();

            return trackList;
        }

        public static Tracks UpsertTrack(Tracks tracks)
        {
            var conn = new SqlConnection(Config.ChinookConnection);
            conn.Open();
            var cmd = new SqlCommand("UpsertTrack", conn)
            {
                CommandType = CommandType.StoredProcedure
            };
            // Return value as parameter
            SqlParameter returnValue = new SqlParameter("returnVal", SqlDbType.Int);
            returnValue.Direction = ParameterDirection.ReturnValue;
            cmd.Parameters.Add(returnValue);

            cmd.Parameters.AddWithValue("@AlbumId", tracks.AlbumId);
            cmd.Parameters.AddWithValue("@Bytes", tracks.Bytes);
            cmd.Parameters.AddWithValue("@Composer", tracks.Composer);
            cmd.Parameters.AddWithValue("@GenreId", tracks.GenreId);
            cmd.Parameters.AddWithValue("@MediaTypeId", tracks.MediaTypeId);
            cmd.Parameters.AddWithValue("@Milliseconds", tracks.Milliseconds);
            cmd.Parameters.AddWithValue("@Name", tracks.Name);
            cmd.Parameters.AddWithValue("@TrackId", tracks.TrackId);
            cmd.Parameters.AddWithValue("@UnitPrice", tracks.UnitPrice);
            cmd.ExecuteNonQuery();

            int id = Convert.ToInt32(returnValue.Value);
            tracks.TrackId = id;

            conn.Close();

            return tracks;
        }

        private class Config
        {
            static public String ChinookConnection { get { return WebConfigurationManager.ConnectionStrings["ChinookConnection"].ConnectionString; } }
        }
    }
}

Register Routes

In order to follow Web API design best practices, we need to edit the routes so action names are not required. Open ~\App_Start\RouteConfig.cs and add a route mapping above the existing Default mapping so your RegisterRoutes method looks like this.

RouteConfig.cs
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "Custom1",
        url: "{controller}/{id}",
        defaults: new { id = UrlParameter.Optional }
    );

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

Tracks Controller

In Solution Explorer, right-click on the Controllers folder select Add New. Under Scaffolding options Select the API controller with empty read/write actions Template. Name it TracksController.

Add Controller - TracksController API controller with empty read/write actions
Add Controller – TracksController API controller with empty read/write actions

TracksController.cs
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace ChinookWebApi.Controllers
{
    using ChinookWebApi.Models;

    public class TracksController : ApiController
    {
        // GET api/tracks
        public HttpResponseMessage Get()
        {
            var value = Chinook.Tracks(0);
            return Request.CreateResponse(HttpStatusCode.OK, value);
        }

        public HttpResponseMessage Get(int id)
        {
            var value = Chinook.Tracks(id);
            return Request.CreateResponse(HttpStatusCode.OK, value);
        }

        public HttpResponseMessage Get(string albumId)
        {
            int id = Convert.ToInt32(albumId);
            var value = Chinook.TracksByAlbum(id);
            return Request.CreateResponse(HttpStatusCode.OK, value);
        }

        // POST api/tracks
        public HttpResponseMessage Post([FromBody]Tracks tracks)
        {
            var value = Chinook.UpsertTrack(tracks);
            return Request.CreateResponse(HttpStatusCode.OK, tracks);
        }

        // PUT api/tracks/5
        public HttpResponseMessage Put(int id, [FromBody]Tracks tracks)
        {
            tracks.TrackId = id;
            var value = Chinook.UpsertTrack(tracks);
            return Request.CreateResponse(HttpStatusCode.OK, tracks);
        }

        // DELETE api/tracks
        public HttpResponseMessage Delete([FromBody]int[] value)
        {
            foreach (int id in value)
            {
                Chinook.DeleteTrack(id);
            }
            return Request.CreateResponse(HttpStatusCode.OK);
        }

        // DELETE api/tracks/5
        public HttpResponseMessage Delete(int id)
        {
            Chinook.DeleteTrack(id);
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}

Now would be a good time to select F5 and debug the app. Load the url to request a track and see what the method we added returns, for example http://localhost:65374/api/tracks/1. If you want to return json, you could use the Advanced Rest Client Chrome App with the Content-type header set to application/json.

Advanced Rest Client - PUT Tracks Json Test
Advanced Rest Client – PUT Tracks Json Test
PUT Json Payload Example

Copy this example json payload into the Advanced Rest Client and set the request type to PUT to update a Track record. The key is making sure the HTTP request type is set for the type of CRUD operation you wish to perform.
C – POST create/insert
R – GET  read
U – PUT  update
D – DELETE

{
    "TrackId": 2918,
    "Name": "\"?\"",
    "AlbumId": 231,
    "MediaTypeId": 3,
    "GenreId": 19,
    "Composer": "",
    "Milliseconds": 2782333,
    "Bytes": 528227089,
    "UnitPrice": 1.99
}

You could also use a GET request: ~/api/tracks/2918 to retrieve Json to use.

Copy this example json payload into the Advanced Rest Client and set the request type to POST to create a new Track record. The new tracks object will be returned including the new TrackId.

POST Json Payload Example
{
    "Name": "\"_New Track\"",
    "AlbumId": 231,
    "MediaTypeId": 3,
    "GenreId": 19,
    "Composer": "",
    "Milliseconds": 3003222,
    "Bytes": 540772000,
    "UnitPrice": 0.99
}

With the HTTP request type set to DELETE, you can remove a single Track record with a request like:
~/api/tracks/2918
or to Delete multiple records, a request like this will work:
~/api/tracks
with a payload of TrackId’s to remove:

[ 2918, 2919, 2920 ]

Cross-Origin Resource Sharing (CORS)

To enable CORS in the Web API and allow JavaScript XMLHttpRequests from a browser in another domain, Carlos Figueira’s MSDN blog shows us how to create a global message handler for all controllers and actions in the application. Right-click the ChinookWebApi project and add a new folder named Handlers with a new CorsHandler class in it:

CorsHandler.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Net.Http;
using System.Threading.Tasks;
using System.Threading;
using System.Net;

namespace ChinookWebApi.Handlers
{
    public class CorsHandler : DelegatingHandler
    {
        const string Origin = "Origin";
        const string AccessControlRequestMethod = "Access-Control-Request-Method";
        const string AccessControlRequestHeaders = "Access-Control-Request-Headers";
        const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
        const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
        const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";

        protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            bool isCorsRequest = request.Headers.Contains(Origin);
            bool isPreflightRequest = request.Method == HttpMethod.Options;
            if (isCorsRequest)
            {
                if (isPreflightRequest)
                {
                    HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
                    response.Headers.Add(AccessControlAllowOrigin, request.Headers.GetValues(Origin).First());

                    string accessControlRequestMethod = request.Headers.GetValues(AccessControlRequestMethod).FirstOrDefault();
                    if (accessControlRequestMethod != null)
                    {
                        response.Headers.Add(AccessControlAllowMethods, accessControlRequestMethod);
                    }

                    string requestedHeaders = string.Join(", ", request.Headers.GetValues(AccessControlRequestHeaders));
                    if (!string.IsNullOrEmpty(requestedHeaders))
                    {
                        response.Headers.Add(AccessControlAllowHeaders, requestedHeaders);
                    }

                    TaskCompletionSource tcs = new TaskCompletionSource();
                    tcs.SetResult(response);
                    return tcs.Task;
                }
                else
                {
                    return base.SendAsync(request, cancellationToken).ContinueWith(t =>
                    {
                        HttpResponseMessage resp = t.Result;
                        resp.Headers.Add(AccessControlAllowOrigin, request.Headers.GetValues(Origin).First());
                        return resp;
                    });
                }
            }
            else
            {
                return base.SendAsync(request, cancellationToken);
            }
        }
    }
}

Register the CorsHandler in the Application_Start() method.

Global.asax.cs
using System;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace ChinookWebApi
{
    using Handlers;

    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            GlobalConfiguration.Configuration.MessageHandlers.Add(new CorsHandler()); 
        }
    }
}

To be continued … Validation and Error Handling

This project is available for browsing and download at GitHub:
https://github.com/jimfrenette/ChinookWebApi

Resources