Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET / ASP.NET4

Comparative analysis of .NET RESTful middleware solutions

4.71/5 (4 votes)
20 Apr 2016CPOL12 min read 23.4K   136  
Comparative analysis of .NET RESTful middleware solutions

Table of contents

Introduction

For the latest years web developers can see the rapid growth of web front-end technologies. It caused using more often the REST paradigm on the server side of web application.

When NodeJS entered the game, full stack JavaScript developers received a lot of opportunities to minimize efforts for developing REST services (with help of Express, first of all). There are also a lot of out-of-box and ready to use RESTful services.

But what does .NET world has to offer for creating RESTful services?

This article is aimed to ...

We'll try to analyse .NET based solutions to creating RESTful services with focus on next moments:

  • Details of creating project;
  • The complexity of service expansion (add new entities);
  • The complexity of implementing advanced queries;
  • Identified diffficulties, problems and ways to solve it;
  • Procedure of deployment to production environment.

For the purposes of this article we will use Microsoft SQL Server database as a backend. Our demo database will be from project MVCMusicStore.

Background

You can get general information about RESTful service paradigm from it's Wiki page.

In addition to baseline of REST concepts there is a good extension - OData standard. It allows to build advanced queries for data source over HTTP.

WCF OData (WCF Data Services)

The excelent starting point to work with WCF OData technology is this CodeProject article.

WCF OData allows to create ready to use REST service in a couple of steps with Visual Studio.

Prepare Database for testing projects

First, create a database from attached backup using SQL Server Management Studio.

Attach existed MVCMusicStore database file

Attach existed MVCMusicStore DB

So, attached database will have tables list like this:

Structure of MVCMusicStore datABASE

Create Model project

Let's implement Model project that will be common data access layer for WCF OData and Web API projects.

First, add project of "Class Library" to our solution.

Then, create ADO.NET Entity data model based on EntityFramework. Good description of working with EntityFramework are described in this article.

So, for the purpose of this article we'll skip detailes of create connection to DB and creating DBContext.

Resulting EDMX scheme will look something like this.

Image 4

Create WCF OData project

Detailed steps of creeating and initial configuration of WCF OData project described in this article.

We'll not duplicate it.

Then add reference to earlier created Model project.

Add reference to Model project

Then, edit file with WCF Data Service definition:

C#
//------------------------------------------------------------------------------
// <copyright company="Microsoft" file="WebDataService.svc.cs">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
using System.Data.Services;
using System.Data.Services.Common;

using WCFOdata.Utils;
using Model;

namespace WCFOdata 
{
    // This attribute allows to get JSON formatted results
    [JSONPSupportBehavior]
    public class WcfDataService1 : DataService<mvcmusicstoreentities>
    {
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*", EntitySetRights.All);
            config.DataServiceBehavior.MaxProtocolVersion =
                     DataServiceProtocolVersion.V2;

            // Output errors details
            config.UseVerboseErrors = true;
        }
    }
}

And it's all efforts to get working REST with OData!

Use of service

Let's run project and get OData service entry point list like shown below.

WCF OData - get list of available entities collections

Initial service route show list of available service entry points.

In our example it's a list of MVC Music Store entities collection:

  • Album;
  • Artist;
  • Cart;
  • Genre;
  • Order;
  • OrderDetail.

Let's try to get collection of some entities in  JSON. For this task request string will be: http://localhost:14040/WcfDataService1.svc/Album?$format=json.

Details of OData URI conventions are defined on site OData.org.

To get new entities after change of database  schema, we'll need to:

  1. complete edmx container;
  2. build project;
  3. delpoy to production environment (IIS).

Deploy procedure is not hard, but some difficulties with IIS configuration may appear of course.

Using advanced queries to entities doesn't require build and deploy process, cause it's based on OData parameters like $select, $expand, $filter and others.

ASP.NET Web API

Good starting point on topic of ASP.NET Web API is this Codeproject article.

Add Web API project to solution

Let's add Web API project to our solution like shown below.

Add Web API project

Add Web API project 2

Add Web API project 3

Initial structure of created project will look like this:

Web API project initial structure

Define controllers structure

For the purpose of this article we need some typical controllers that can perform identical operations:

  • Get full collection of entities;
  • Get single entity instance by Id;
  • Get paginated collection;
  • Create new entity instance;
  • Update existed entity;
  • Delete existed entity.

So, these operations performed equally for all entity types.

That's why it's therefore advisable to create generic controller class.

C#
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Data;

using Model;
using WebApiExample.Utils;

namespace WebApiExample.Controllers
{
    /// <summary>
    /// Such a functionlaity will be enough to demonstration purposes.
    /// </summary>
    /// <typeparam name="T">Target entity type</typeparam>
    public class BaseAPIController<T> : ApiController where T:class
    {
        protected readonly MvcMusicStoreEntities _dbContext;

        public BaseAPIController()
        {
            _dbContext = new MvcMusicStoreEntities();
            _dbContext.Configuration.ProxyCreationEnabled = false;
        }       

        /// <summary>
        /// Get entity primary key name by entity class name
        /// </summary>
        /// <returns></returns>
        private string GetKeyFieldName()
        {
            // Key field by convention
            return string.Format("{0}{1}", typeof(T).Name, "Id");
        }

        /// <summary>
        /// Get entity by id
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        private T _Get(int id)
        {
            return _dbContext.Set<T>().Find(id);
        }

        /// <summary>
        /// Get full collection
        /// </summary>
        /// <returns></returns>
        public IEnumerable<T> Get()
        {
            return _dbContext.Set<T>();
        }

        /// <summary>
        /// Get single entity
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public T Get(int id)
        {
            return _Get(id);
        }

        /// <summary>
        /// Get collection's page
        /// </summary>
        /// <param name="top"></param>
        /// <param name="skip"></param>
        /// <returns></returns>
        public IEnumerable<T> Get(int top, int skip)
        {
            var res = _dbContext.Set<T>().OrderBy(GetKeyFieldName()).Skip(skip).Take(top).ToList();
            return res;
        }

        /// <summary>
        /// Create entity
        /// </summary>
        /// <param name="item"></param>
        public void Post([FromBody]T item)
        {
            _dbContext.Set<T>().Add(item);
            _dbContext.SaveChanges();
        }

        /// <summary>
        /// Update entity
        /// </summary>
        /// <param name="id"></param>
        /// <param name="item"></param>
        public void Put(int id, [FromBody]T item)
        {
            _dbContext.Entry(item).State = EntityState.Unchanged;

            var entry = _dbContext.Entry(item);

            foreach (var name in entry.CurrentValues.PropertyNames.Except(new[] { GetKeyFieldName() }))
            {
                entry.Property(name).IsModified = true;
            }

            _dbContext.SaveChanges();
        }

        /// <summary>
        /// Delete entity
        /// </summary>
        /// <param name="id"></param>
        public void Delete(int id)
        {
            var entry = _Get(id);
            _dbContext.Set<T>().Remove(entry);
            _dbContext.SaveChanges();
        }
    }
} 

And target entity controller class definition now will look like this.

C#
namespace WebApiExample.Controllers
{    
    public class AlbumController : BaseAPIController<Album>
    {
    }
}

After apply this to all entity classes we have the following controllers list:

Web API controllers list

Return JSON response by default

In initial configuration of Web API project, api service will return data in XML format if request was created directly by from user in web-browser. But, for our purpose the JSON format will be more comfortable. So, we need to apropriate changes in configuration. Such a configuration trick was found on SO thread. So, let's apply this to our WebApiConfig.cs file.

C#
namespace WebApiExample
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "PaginationApi",
                routeTemplate: "api/{controller}/{top}/{skip}"
            );

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

            // Configure to return JSON by default
            // http://stackoverflow.com/questions/9847564/how-do-i-get-asp-net-web-api-to-return-json-instead-of-xml-using-chrome
            config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
        }
    }
}

One moment to pay explicit attention is this block:

            config.Routes.MapHttpRoute(
                 name: "PaginationApi",
                 routeTemplate: "api/{controller}/{top}/{skip}"
             );

This block of code configures WebAPI router to correctly handle paginated requests.

Use of service

Our Web API project seems ready to use. So, let's start it in Chrome browser. Initial project page look like shown below.

Web API project initial page

But, we need to check API part, not frontend. So create a request to the next path: "http://localhost:56958/api/Album". And api service answer is a JSON collection of albums.

JavaScript
[{"AlbumId":386,"GenreId":1,"ArtistId":1,"Title":"DmZubr album","Price":100.99,"AlbumArtUrl":"/Content/Images/placeholder.gif","Artist":null,"Genre":null,"Cart":[],"OrderDetail":[]},
{"AlbumId":387,"GenreId":1,"ArtistId":1,"Title":"Let There Be Rock","Price":8.99,"AlbumArtUrl":"/Content/Images/placeholder.gif","Artist":null,"Genre":null,"Cart":[],"OrderDetail":[]}, ...

What will we have to do to get new entity from Web API. Basically, next steps:

  1. complete edmx container;
  2. build project;
  3. delpoy to production environment (IIS).

What about advanced queries with filtering, ordering and projections?

With Web API project this tasks requires more efforts than with WCF Data Services.

One approach to perform this task is building explicit API entry point for every specifical task. For example, we'll create individual method to get Albums by name part. Other method will return Artists collection by name part. For every task we will also have to build and deploy project.

Another approach is creating generic methods, like we've already done for CRUD operations and paginated GET request. For some types of queries it will be the very untrivial task.

But, the other (positive) side of the coin is that with Web API you will have the maximal extent of control over model and flow of requests handling.

(RWAD Tech) OData Server

OData Server have a big difference from the technologies analysed above. It's an out-of-box utility, that create REST service for existed relational database. I've founded short description of this project on official OData site in ecosystem/producers section.

We  don't need  to write any code to create RESTful service for our MVC Music Store DB when using this  utility. But, what are the next steps then?

Prepare and configure service

First, download product distribute, of course.

Distribute is an archive file that suggest to use utility in three available forms:

  • Windows console application;
  • Windows service;
  • Microsoft IIS node.

For the purpose of this article we'll choose a way with minimal efforts - console application.

So, after getting an archive - unzip catalogue "console" to our project root path.

Further, we need  to connect service to out MVC Music Store database. We'll do this by editing configuration file, that look like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="RWADOData" type="ODataServer.HttpLevel.Configuration.RWADODataSettingsSection, ODataServer.HttpLevel" requirePermission="false" />
  </configSections>
  
  <startup> 
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  
  <connectionStrings>
      <add name="Default" connectionString="Data Source=localhost\SQLExpress;Database=MVCMusicStore; User ID=******; Password=******" providerName="System.Data.SqlClient"/>
  </connectionStrings>
  
  <appSettings>
    <add key="log4net.Config" value="log4net.config" />
  </appSettings>

  <RWADOData>
    <commonSettings connectionStringName="Default" port="8085" rootServiceHost="http://definedhost" />
    <authSettings authenticationMethod="None" tokenStoreMethod="InCookie" tokenCookieName="RWADTechODataAuthToken" />
    <corsSettings useCORS="true" allowedOriginHosts="http://localhost:9002" />
  </RWADOData>

  <system.web>
    <machineKey
      validationKey="C50B3C89CB21F4F1422FF158A5B42D0E8DB8CB5CDA1742572A487D9401E3400267682B202B746511891C1BAF47F8D25C07F6C39A104696DB51F17C529AD3CABE"
      decryptionKey="8A9BE8FD67AF6979E7D20198CFEA50DD3D3799C77AF2B72F"
      validation="SHA1" />
  </system.web>
</configuration> 

Moments to note here are:

  • connectionStrings - first of all, connection string to our DB;
  • RWADOData.commonSettings:
    • connectionStringName="Default" - define using previously declared connection string;
    • port="8085" - port where our RESTful service will live;
  • RWADOData.authSettings - settings to use service authorization and authentication. This topic is out of scope of this article.
  • RWADOData.corsSettings - setting of CORS resolving. We'll discuss CORS problem later in this article.

Our service seems ready to start now.

Use of service

Let's run console application. We'll get result like this one:

Start  OData Server console app

Then, go to web browser and request service initial route (in our case - "http://localhost:8085/").

And we'll get available entry points.

OData Server initial route page

And, as we did earlier, let's get an albums collection in JSON format ("http://localhost:14040/WcfDataService1.svc/Album?$format=json").

Result of request executing seems like this:

Get albums collection from OData Server

What about getting collections of new entities after database schema change?

With OData Server we don't need to do anything except of restart service after schema change!

For advanced queries we can use OData standard parameters,

Also, there is a support of invocation of stored procedures.

What about functional expasion of service by features like working with file? It's not supported because OData Server is an out-of-box product.

But there is also a module for user, roles and permissions management. So, OData Server's functionality seems enough for creating small and medium applications.

Another advantage of this RESTful solution is a possibility to use it without IIS. In a form of Windows service or console application. It may be significant aspect for some cases.

Tests project

When all of our three .NET RESTful services are ready to use, let's create a project with tests. Tests project is aimed to execute requests and print time spent by service to handle it.

Create tests project

First, create a project for tests in our solution. It will be simple ASP.NET MVC application. We'll use appropriate Visual Studio template.

Add Tests project to solution

Create tests project 2

Testing page details

Testing page is a absolutely minimal typical page, so we'll not cover details of creating it. You can know details in code of attached project.

According to purpose of this article, the point of interest here is a client-side code that will create requests to all 3 services and output time that services will spend.

Let's try to analyse this code.

Function to get date difference
JavaScript
// datepart: 'y', 'm', 'w', 'd', 'h', 'n', 's',  'ms'
Date.DateDiff = function(datepart, fromdate, todate) {    
   datepart = datepart.toLowerCase();    
   var diff = todate - fromdate;    
   var divideBy = { w:604800000, 
      d:86400000, 
      h:3600000, 
      n:60000, 
      s:1000,
      ms: 1};
   return Math.floor( diff/divideBy[datepart]);
}
Little helpers and action functions
JavaScript
// Append row to results table
function AppendResRow(providerType, action, route, time) {
   var row = '<tr> \
      <td>' + providerType + '</td> \
      <td>' + action + '</td> \
      <td>' + route + '</td> \
      <td>' + time + '</td> \
   </tr>';
   $('#results tbody').append(row);
}

// Get provider target host by type
function GetHostByProviderType(providerType) {
   var host = '';
   switch (providerType) {
      case 'WCFOData':
         res = $('#WCFODataHost').val();
         break;
      case 'WebApi':
         res = $('#WebApiHost').val();
         break;
      case 'ODataServer':
         res = $('#ODataServerHost').val();
         break;
   }

   return res;
}
Perform simple entities collections requests
JavaScript
// Perform tests of getting entities collection
function RunGetCollectionTests(providerType) {
   var targetHost = GetHostByProviderType(providerType);

   var timings = {};

   $.each(entitiesList, function (index) {
      var item = entitiesList[index];
      var targetUrl = targetHost + item;
      if (providerType == 'ODataServer')
         targetUrl += '?$format=json';
      timings[targetUrl] = new Date();

      // Not using standard $.get, cause we need to have control over 'Accept' header
      $.ajax({
         url: targetUrl,
         headers: {
            'Accept': 'application/json'
         },
         async: false
      })
      .then(function (res) {
         var timeSpan = Date.DateDiff('ms', timings[targetUrl], new Date());
         AppendResRow(providerType, 'Get full collection', targetUrl, timeSpan);
      });
   });
}
Perform paginated entities collections requests
JavaScript
// Perform tests of getting entities collection
   function RunGetCollectionWithPaginationTests(providerType) {
      var targetHost = GetHostByProviderType(providerType);

      var timings = {};

      for (var i = 0; i < testCategoryReplies; i++) {
         var top = topSkipPairs[i].top;
         var skip = topSkipPairs[i].skip;

         var targetUrl = targetHost + 'Album';

         if (providerType == 'WebApi')
            targetUrl += '/' + top + '/' + skip;
         else
            targetUrl += '?$top=' + top + '&skip=' + skip + '&$format=json';

         timings[targetUrl] = new Date();

         // Not using standard $.get, cause we need to have control over 'Accept' header
         $.ajax({
            url: targetUrl,
            headers: {
               'Accept': 'application/json'
            },
            async: false
         })
         .then(function (res) {
            var timeSpan = Date.DateDiff('ms', timings[targetUrl], new Date());
            AppendResRow(providerType, 'Get collection with pagination', targetUrl, timeSpan);
         });
    }
}
Perform "create entity" requests
JavaScript
// Perform tests of create operation
function RunCreateEntityTests(providerType) {
   var targetHost = GetHostByProviderType(providerType);

   // Let's create Album entity
   var contentType = 'application/json';
   var album = {
      GenreId: 1,
      ArtistId: 1,
      Title: "Album created from " + providerType,
      Price: "20.99",
      AlbumArtUrl: "/Content/Images/placeholder.gif"
   };
   var data = JSON.stringify(album);
             
   var timings = {};
   var targetUrl = targetHost + 'Album';

   for (var i = 0; i < testCategoryReplies; i++) {
      var timingsKey = targetUrl + i;
      timings[timingsKey] = new Date();
      $.ajax({
         type: 'POST',
         url: targetUrl,
         data: data,
         headers: {
            'Accept': 'application/json',
            'Content-Type': contentType
         },
         async: false
      })
      .then(function (res) {
         var timeSpan = Date.DateDiff('ms', timings[timingsKey], new Date());
         AppendResRow(providerType, 'Create entity (album)', targetUrl, timeSpan);
      });
   }
}
Perform "delete entity" requests
JavaScript
// Perform tests of Delete operation
function RunDeleteEntityTests(providerType, initialAlbumId) {
   var targetHost = GetHostByProviderType(providerType);
   var timings = {};
   var targetUrlBase = targetHost + 'Album';
            
   for (var i = 0; i < testCategoryReplies; i++) {
      targetUrl = targetUrlBase;
      if (providerType == 'WebApi')
         targetUrl += '/' + initialAlbumId;
      else
         targetUrl += '(' + initialAlbumId + ')';

      var timingsKey = targetUrl + i;
      timings[timingsKey] = new Date();
      $.ajax({
         type: 'DELETE',
         url: targetUrl,
         headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
         },
         async: false
      })
      .then(function (res) {
         var timeSpan = Date.DateDiff('ms', timings[timingsKey], new Date());
         AppendResRow(providerType, 'Delete entity (album)', targetUrl, timeSpan);
       }, function (err) {
          console.log(err);
       });

       initialAlbumId++;
   }

   deletedFirstAlbumId += testCategoryReplies;
}
Pull things together
JavaScript
 $(function () {            
    $('#start-tests').click(function () {
       RunGetCollectionTests('WCFOData');
       RunGetCollectionTests('WebApi');
       RunGetCollectionTests('ODataServer');

       RunGetCollectionWithPaginationTests('WCFOData');
       RunGetCollectionWithPaginationTests('WebApi');
       RunGetCollectionWithPaginationTests('ODataServer');

       RunCreateEntityTests('WCFOData');
       RunCreateEntityTests('WebApi');                
       RunCreateEntityTests('ODataServer');
                
       RunDeleteEntityTests('WCFOData', deletedFirstAlbumId);
       RunDeleteEntityTests('WebApi', deletedFirstAlbumId);
       RunDeleteEntityTests('ODataServer', deletedFirstAlbumId);
    });

    $('#clear-table').click(function () {
       $('#results tbody').children().remove();
    });
});
Define tests parameters

Before run tests we need to edit next variables:

  • var testCategoryReplies = 20 - count of iterations of  each test type;
  • var deletedFirstAlbumId = 854 - id of first Album entity to delete. We can get value of this variable by viewing table "Album" via MS SSMS;
  • var topSkipPairs = [...] - array of pairs of values for top and skip parameters. Length of this array should be equal to or greater than testCategoryReplies value. You can add some randomization logic to generate elements of this array.

CORS Problem and ways to solve it

Let's try to run tests, but for start point - only for WCF Data service. Just put comments symbols in lines where other services requests are called.

So, what will we see then? Unfortunately, nothing happens and results table is free. Let's go to Chrome debugger to see what's happen. And get a lot of CORS problem errors like this one "XMLHttpRequest cannot load http://localhost:14040/WcfDataService1.svc/Album. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:54835' is therefore not allowed access."

So, we'll try to define Access-Control-Allow-Origin header value and start tests again. Edit WCF OData web.config file:

XML
 <system.webServer>
    <directoryBrowse enabled="true" />

    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
      </customHeaders>
    </httpProtocol>

  </system.webServer>

After adding CORS header we have succesfully performed GET requests.

But CREATE requests have thrown an error:

"OPTIONS http://localhost:14040/WcfDataService1.svc/Album 501 (Not Implemented)

XMLHttpRequest cannot load http://localhost:14040/WcfDataService1.svc/Album. Response for preflight has invalid HTTP status code 501".

And googling for such a trouble will give us very disappointing results. Like this one (StackOverflow thread).

So, what will be the new plan in this case?

I've founded one workaround, that have already used in other real project. It's idea is to make all OPTIONS requests as a simple POST/PUT request with no reffering. For this purpose we'll create new request on the server-side, get results and return it to the client.

Let's code it.

C#
using System;
using System.IO;
using System.Web;
using System.Net;
using System.Text;

namespace Shared.HttpModules
{
    /// <summary>
    /// This module will resolve CORS OPTIONS requests problem
    /// </summary>
    public class CORSProxyModule : IHttpModule
    {
        public CORSProxyModule()
        {
        }

        // In the Init function, register for HttpApplication 
        // events by adding your handlers.
        public void Init(HttpApplication application)
        {
            application.BeginRequest += (new EventHandler(this.Application_BeginRequest));
        }

        private void Application_BeginRequest(Object sender, EventArgs e)
        {
            try
            {
                HttpApplication app = (HttpApplication)sender;
                HttpContext ctx = app.Context;

                // We need strange request flow only in case of requests with "OPTIONS" verb
                // For other requests our custom headers section in web config will be enough
                if (ctx.Request.HttpMethod  == "OPTIONS")
                {
                    // Create holder for new HTTP response object
                    HttpWebResponse resp = null;

                    var res = WebServiceRedirect(app, ctx.Request.Url.ToString(), out resp);

                    // Define content encodding and type accordding to received answer
                    ctx.Response.ContentEncoding = Encoding.UTF8;
                    ctx.Response.ContentType = resp.ContentType;
                    ctx.Response.Write(res);
                    ctx.Response.End();
                }
            }
            catch (Exception) { }
        }

        public void Dispose() { }

        /// <summary>
        /// Create new request and return received results
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="url"></param>
        /// <param name="response"></param>
        /// <returns></returns>
        private string WebServiceRedirect(HttpApplication ctx, string url, out HttpWebResponse response)
        {
            // Write request body
            byte[] bytes = ctx.Request.BinaryRead(ctx.Request.TotalBytes);

            char[] reqBody = Encoding.UTF8.GetChars(bytes, 0, bytes.Length);

            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
            req.AllowAutoRedirect = false;
            req.ContentLength = ctx.Request.ContentLength;
            req.ContentType = ctx.Request.ContentType;
            req.UseDefaultCredentials = true;
            //req.UserAgent = ".NET Web Proxy";
            req.Referer = url;
            req.Method = ctx.Request.RequestType; // "POST";

            if (ctx.Request.AcceptTypes.Length > 0)
                req.MediaType = ctx.Request.AcceptTypes[0];

            foreach (string str in ctx.Request.Headers.Keys)
            {
                // It's not possible to set some headers value by accessing through Headers collection
                // So, we need to handle such a situations
                try { req.Headers.Add(str, ctx.Request.Headers[str]); }
                catch { }
            }

            // Duplicate initial request body to just created request
            using (StreamWriter sw = new StreamWriter((req.GetRequestStream())))
            {
                sw.Write(reqBody);
                sw.Flush();
                sw.Close();
            }

            // We'll store service answer in string form here
            string temp = "";
            try
            {
                response = (HttpWebResponse)req.GetResponse();
                using (StreamReader sw = new StreamReader((response.GetResponseStream())))
                {
                    temp = sw.ReadToEnd();
                    sw.Close();
                }
            }
            catch (WebException exc)
            {
                // Handle received exception
                using (StreamReader sw = new StreamReader((exc.Response.GetResponseStream())))
                {
                    response = (HttpWebResponse)exc.Response;
                    temp = sw.ReadToEnd();
                    sw.Close();
                }
            }

            return temp;
        }
    }
}

Comments are presented in code.

So, use our module by edit web.config file. At the same time, add some CORS headers.

XML
  <system.webServer>
    <directoryBrowse enabled="true" />
    
    <modules runAllManagedModulesForAllRequests="true">
      <add name="CORSProxyModule" type="Shared.HttpModules.CORSProxyModule" />
    </modules>
    
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Expose-Headers" value="Authorization,Origin,Content-type,Accept" />
        <add name="Access-Control-Allow-Credentials" value="True" />
        <add name="Access-Control-Allow-Headers" value="Authorization,Origin,Content-type,Accept" />
        <add name="Access-Control-Allow-Methods" value="GET,POST,PUT,DELETE,OPTIONS,HEAD" />
        <add name="Access-Control-Max-Age" value="3600" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>

Then, try to perform WCF OData tests again

And, finally, all the requests performed successfully (as Chrome debugger console confirms)!

To solve CORS problem in Web API project we'll also use the CORSProxyModule.

Little analysis of results

Just copy table content to Excel file to perform some aggregating operations.

Results shown below was calculated for the sample of 20 repeats of each operation for each RESTful service type.

You can find out the allocation of ellapsed time to operation types when run test yourself. :)

Provider type Total time, ms
WCFOData 2683
WebApi 7889
ODataServer 2006

Compare solutions extensibility

Here I will try to estimate difficulty of expanding our services functionality.

Extensibility difficulty
Feature/Task WCF OData ASP.NET Web API OData Server
Need to code (server side) Need to build and deploy (server side) Need to code (server side) Need to build and deploy (server side) Need to code (server side) Need to build and deploy (server side)
Get a collection of new entity + + + + - -
Get a collection with filters - - + + - -
Get a collection with projections - - + + - -
Get a collection with explicit entity fields - - + + - -
Invoke stored procedure + + + + - -
Resolve CORS problem + + + + - -
Use authentication/authorization + + + + - -
Working with files + + + + Is not supported at the moment

Conclusion

In this article we've worked with three .NET RESTful services. We've created a WCF OData and WebAPI projects and use automatical OData Server. Then, we've  created the project with some tests for RESTful services.

So, what RESTful service type will be my personal choice, if taking into account the results of tests and aspect of extensibility?

My answer for this question is based on the next considerations:

- If I will need full control over REST baseline functionality - I will choose ASP.NET Web API. This is a case when the functional requirements to the service are not stable and are changing often.

But in this case I will have all the problems related to compile/build process and the following service deploy to production environment.

- If I will need an out-of-box RESTful service solution - I will definetly prefer OData Server. Because in this case I will have great OData opportunities with no efforts to code and build/deploy procedure.

But this alternative is not a good  choice if requirements to service are unexpected and can change often. We don't have control over service in this scenario.

So, this service will be optimal for small and some middle size projects with the stable set of service features.

I haven't seen scenarios where I definetly would need RESTful service based on WCF OData except of case when OData Server is not available for any reasons.

History

18-04-2016 - initial state. 

License

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