Comparative analysis of .NET RESTful middleware solutions






4.71/5 (4 votes)
Comparative analysis of .NET RESTful middleware solutions
Table of contents
- Introduction
- Background
- WCF OData (WCF Data Services)
- ASP.NET Web API
- (RWAD Tech) OData Server
- Tests project
- Conclusion
- History
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.
So, attached database will have tables list like this:
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.
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.
Then, edit file with WCF Data Service definition:
//------------------------------------------------------------------------------
// <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.
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:
- complete edmx container;
- build project;
- 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.
Initial structure of created project will look like this:
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.
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.
namespace WebApiExample.Controllers
{
public class AlbumController : BaseAPIController<Album>
{
}
}
After apply this to all entity classes we have the following 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.
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.
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.
[{"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:
- complete edmx container;
- build project;
- 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:
Then, go to web browser and request service initial route (in our case - "http://localhost:8085/").
And we'll get available entry points.
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:
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.
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
// 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
// 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
// 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
// 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
// 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
// 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
$(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:
<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.
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.
<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.
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.