65.9K
CodeProject 正在变化。 阅读更多。
Home

NET RESTful 中间件解决方案的比较分析

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (4投票s)

2016年4月20日

CPOL

12分钟阅读

viewsIcon

24169

downloadIcon

175

NET RESTful 中间件解决方案的比较分析

目录

引言

近年来,Web 前端技术发展迅速。这导致在 Web 应用程序的后端更频繁地使用 REST 范例。

当 NodeJS 进入市场后,全栈 JavaScript 开发人员有机会通过 Express 等工具最大限度地减少开发 REST 服务的精力。还有许多现成的、可直接使用的 RESTful 服务。

那么,.NET 世界提供了哪些用于创建 RESTful 服务的选项呢?

本文旨在...

我们将尝试分析基于 .NET 的 RESTful 服务创建解决方案,重点关注以下几个方面:

  • 创建项目的详细信息;
  • 服务扩展的复杂性(添加新实体);
  • 实现高级查询的复杂性;
  • 已识别的困难、问题及解决方法;
  • 部署到生产环境的流程。

本文将使用 Microsoft SQL Server 数据库作为后端。我们将从 MVCMusicStore 项目中获取演示数据库。

背景

您可以从其 Wiki 页面获取有关 RESTful 服务范例的一般信息。

除了 REST 概念的基础知识外,还有一个很好的扩展——OData 标准。它允许通过 HTTP 构建针对数据源的高级查询。

WCF OData (WCF 数据服务)

使用 WCF OData 技术入门的一个绝佳起点是这篇 CodeProject 文章

WCF OData 允许在几个步骤内使用 Visual Studio 创建现成的 REST 服务。

准备数据库供测试项目使用

首先,使用 SQL Server Management Studio 从附加的备份创建数据库。

Attach existed MVCMusicStore database file

Attach existed MVCMusicStore DB

因此,附加的数据库将包含如下表列表:

Structure of MVCMusicStore datABASE

创建模型项目

让我们实现一个模型项目,它将成为 WCF OData 和 Web API 项目的通用数据访问层。

首先,向我们的解决方案添加一个“类库”项目。

然后,基于 EntityFramework 创建 ADO.NET 实体数据模型。关于 EntityFramework 的工作方式,在这篇 文章中有很好的描述。

因此,出于本文的目的,我们将跳过创建数据库连接和创建 DBContext 的详细信息。

生成的 EDMX 方案将如下所示:

创建 WCF OData 项目

创建和初始配置 WCF OData 项目的详细步骤在 这篇文章中有描述。

我们将不重复。

然后添加对先前创建的模型项目的引用。

Add reference to Model project

然后,编辑 WCF 数据服务定义文件。

//------------------------------------------------------------------------------
// <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;
        }
    }
}

这样就完成了获取 OData REST 服务的全部工作!

服务使用

让我们运行项目,并获得如下所示的 OData 服务入口点列表。

WCF OData - get list of available entities collections

初始服务路由显示了可用的服务入口点列表。

在我们的例子中,它是 MVC Music Store 实体集合的列表:

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

让我们尝试以 JSON 格式获取某些实体的集合。对于此任务,请求字符串将是:https://:14040/WcfDataService1.svc/Album?$format=json

OData URI约定的详细信息在 OData.org 网站上定义。

要获取数据库架构更改后新实体的信息,我们需要

  1. 完成 edmx 容器;
  2. 构建项目;
  3. 部署到生产环境(IIS)。

部署过程并不困难,但有时可能会出现 IIS 配置方面的一些困难。

使用高级查询实体不需要构建和部署过程,因为它基于 OData 参数,如 $select, $expand, $filter 等。

ASP.NET Web API

关于 ASP.NET Web API 的一个很好的入门点是这篇 Codeproject 文章

向解决方案添加 Web API 项目

让我们像下面这样向解决方案添加 Web API 项目。

Add Web API project

Add Web API project 2

Add Web API project 3

创建项目的初始结构如下所示。

Web API project initial structure

定义控制器结构

出于本文的目的,我们需要一些可以执行相同操作的典型控制器:

  • 获取实体的完整集合;
  • 通过 ID 获取单个实体实例;
  • 获取分页集合;
  • 创建新的实体实例;
  • 更新现有实体;
  • 删除现有实体。

因此,这些操作对所有实体类型都执行得一样。

因此,建议创建一个通用控制器类。

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();
        }
    }
} 

目标实体控制器类的定义现在将如下所示。

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

将此应用到所有实体类后,我们得到以下控制器列表:

Web API controllers list

默认返回 JSON 响应

在 Web API 项目的初始配置中,如果请求直接由 Web 浏览器中的用户创建,API 服务将返回 XML 格式的数据。但是,为了我们的目的,JSON 格式将更方便。因此,我们需要在配置中进行适当的更改。这种配置技巧是在 SO 帖子上找到的。因此,我们将此应用到我们的 WebApiConfig.cs 文件。

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"));
        }
    }
}

有一个地方需要特别注意:这个块。

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

这段代码配置 WebAPI 路由器以正确处理分页请求。

服务使用

我们的 Web API 项目似乎已准备就绪。因此,让我们在 Chrome 浏览器中启动它。初始项目页面如下所示。

Web API project initial page

但是,我们需要检查 API 部分,而不是前端。因此,创建一个请求到以下路径:“https://:56958/api/Album”。API 服务响应的是专辑的 JSON 集合。

[{"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":[]}, ...

为了从 Web API 获取新实体,我们需要做什么?基本上,以下步骤:

  1. 完成 edmx 容器;
  2. 构建项目;
  3. 部署到生产环境(IIS)。

如何处理带有过滤、排序和投影的高级查询?

使用 Web API 项目,这些任务比使用 WCF 数据服务需要更多的精力。

执行此任务的一种方法是为每个特定任务构建显式的 API 入口点。例如,我们将创建一个单独的方法来按名称部分获取专辑。另一个方法将按名称部分返回艺术家集合。对于每项任务,我们还必须构建和部署项目。

另一种方法是创建通用方法,就像我们已经为 CRUD 操作和分页 GET 请求所做的那样。对于某些类型的查询,这将是一项非常棘手的任务。

但是,硬币的另一面(积极的一面)是,使用 Web API,您将获得对模型和请求处理流程的最大控制。

(RWAD Tech) OData 服务器

OData 服务器与上面分析的技术有很大不同。它是一个开箱即用的实用程序,可为现有关系数据库创建 REST 服务。我在 官方 OData 网站ecosystem/producers 部分中找到了对该项目的简短描述。

使用此实用程序时,我们不需要编写任何代码即可为我们的 MVC Music Store 数据库创建 RESTful 服务。那么,接下来是什么步骤呢?

准备和配置服务

首先,当然要下载产品分发版。

分发版是一个存档文件,建议以三种可用形式使用该实用程序:

  • Windows 控制台应用程序;
  • Windows 服务;
  • Microsoft IIS 节点。

出于本文的目的,我们将选择最省力的方式——控制台应用程序。

因此,在获取存档后,将“console”目录解压缩到我们项目的根路径。

此外,我们需要将服务连接到我们的 MVC Music Store 数据库。我们将通过编辑配置文件来完成此操作,该文件如下所示:

<?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="https://:9002" />
  </RWADOData>

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

这里需要注意的几点是:

  • connectionStrings - 首先,是到我们数据库的连接字符串;
  • RWADOData.commonSettings
    • connectionStringName="Default" - 定义使用先前声明的连接字符串;
    • port="8085" - 我们的 RESTful 服务将驻留的端口;
  • RWADOData.authSettings - 用于服务授权和身份验证的设置。这个话题超出了本文的范围。
  • RWADOData.corsSettings - CORS 解析的设置。我们将在本文后面讨论 CORS 问题。

我们的服务现在似乎已准备好启动。

服务使用

让我们运行控制台应用程序。我们将得到如下所示的结果:

Start  OData Server console app

然后,转到 Web 浏览器并请求服务初始路由(在我们的例子中是 - "https://:8085/")。

我们将获得可用的入口点。

OData Server initial route page

并且,就像我们之前做的那样,让我们以 JSON 格式获取专辑集合("https://:14040/WcfDataService1.svc/Album?$format=json")。

请求执行结果如下所示:

Get albums collection from OData Server

更改数据库模式后获取新实体集合怎么样?

使用 OData 服务器,除了重启服务外,我们无需执行任何操作即可处理模式更改!

对于高级查询,我们可以使用 OData 标准参数。

此外,它还支持调用存储过程。

如何通过文件操作等功能扩展服务?由于 OData 服务器是开箱即用的产品,因此不支持。

但是,还有一个用于用户、角色和权限管理的模块。因此,OData 服务器的功能似乎足以创建中小型应用程序。

此 RESTful 解决方案的另一个优点是可以使用它而无需 IIS。以 Windows 服务或控制台应用程序的形式。这在某些情况下可能是一个重要的方面。

测试项目

当我们的所有三个 .NET RESTful 服务都准备好使用时,让我们创建一个包含测试的项目。测试项目旨在执行请求并打印服务处理这些请求所花费的时间。

创建测试项目

首先,在我们的解决方案中创建一个测试项目。它将是一个简单的 ASP.NET MVC 应用程序。我们将使用相应的 Visual Studio 模板。

Add Tests project to solution

Create tests project 2

测试页面详情

测试页面是一个绝对最小化的典型页面,因此我们不详细介绍其创建。您可以在附加项目的代码中找到详细信息。

根据本文的目的,这里感兴趣的是客户端代码,它将向所有 3 个服务创建请求并输出服务花费的时间。

让我们尝试分析这段代码。

计算日期差的函数
// 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]);
}
一些辅助函数和操作函数
// 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 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 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 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 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;
}
整合
 $(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();
    });
});
定义测试参数

在运行测试之前,我们需要编辑以下变量:

  • var testCategoryReplies = 20 - 每个测试类型的迭代次数;
  • var deletedFirstAlbumId = 854 - 要删除的第一个 Album 实体的 ID。我们可以通过 MS SSMS 查看“Album”表来获取此变量的值;
  • var topSkipPairs = [...] - 用于 top 和 skip 参数的值对数组。此数组的长度应等于或大于 testCategoryReplies 值。您可以添加一些随机化逻辑来生成此数组的元素。

CORS 问题及解决方法

让我们尝试运行测试,但首先只针对 WCF 数据服务。只需在调用其他服务请求的行中添加注释符号。

那么,我们将看到什么?不幸的是,什么都没发生,结果表是空的。让我们转到 Chrome 调试器查看发生了什么。并看到大量 CORS 问题错误,例如“XMLHttpRequest 无法加载 https://:14040/WcfDataService1.svc/Album。请求的资源上不存在 'Access-Control-Allow-Origin' 标头。因此,源 'https://:54835' 不允许访问。”

因此,我们将尝试定义 Access-Control-Allow-Origin 标头值并重新开始测试。编辑 WCF OData web.config 文件。

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

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

  </system.webServer>

添加 CORS 标头后,我们成功执行了 GET 请求。

但是 CREATE 请求引发了错误。

OPTIONS https://:14040/WcfDataService1.svc/Album 501 (未实现)

XMLHttpRequest 无法加载 https://:14040/WcfDataService1.svc/Album。预检响应的 HTTP 状态码无效 501".

搜索此类问题将给我们带来非常令人失望的结果。例如(StackOverflow 线程)

那么,在这种情况下,新的计划是什么?

我发现了一个解决方法,已经在其他实际项目中用过。它的想法是将所有 OPTIONS 请求视为一个简单的 POST/PUT 请求,而无需引用。为此,我们将创建新的服务器端请求,获取结果并返回给客户端。

让我们编写代码。

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;
        }
    }
}

注释已包含在代码中。

因此,通过编辑 web.config 文件来使用我们的模块。同时,添加一些 CORS 标头。

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

然后,再次尝试执行 WCF OData 测试。

最后,所有请求都成功执行了(正如 Chrome 调试器控制台所确认的)!

为了解决 Web API 项目中的 CORS 问题,我们也将使用 CORSProxyModule。

对结果进行简要分析

只需将表格内容复制到 Excel 文件以执行一些聚合操作。

下面显示的结果是为每个 RESTful 服务类型重复每个操作 20 次的样本计算的。

当您自己运行测试时,可以找出操作类型的时间分配。:)

提供者类型 总时间,毫秒
WCFOData 2683
WebApi 7889
ODataServer 2006

比较解决方案的可扩展性

在这里,我将尝试评估扩展我们服务功能的难度。

扩展难度
特性/任务 WCF OData ASP.NET Web API OData 服务器
需要编写代码(服务器端) 需要构建和部署(服务器端) 需要编写代码(服务器端) 需要构建和部署(服务器端) 需要编写代码(服务器端) 需要构建和部署(服务器端)
获取新实体集合 + + + + - -
获取带过滤器的集合 - - + + - -
获取带投影的集合 - - + + - -
获取带显式实体字段的集合 - - + + - -
调用存储过程 + + + + - -
解决 CORS 问题 + + + + - -
使用身份验证/授权 + + + + - -
使用文件 + + + + 目前不支持

结论

在本文中,我们处理了三个 .NET RESTful 服务。我们创建了 WCF OData 和 WebAPI 项目,并使用了自动 OData 服务器。然后,我们创建了一个包含 RESTful 服务一些测试的项目。

那么,考虑到测试结果和可扩展性方面,我个人会选择哪种 RESTful 服务类型?

我对这个问题的回答基于以下考虑:

- 如果我需要对 REST 基础功能进行完全控制,我将选择 ASP.NET Web API。这是功能需求不稳定且经常变化的场景。

但在此情况下,我将面临与编译/构建过程相关的所有问题以及随后的服务部署到生产环境的问题。

- 如果我需要一个开箱即用的 RESTful 服务解决方案,我一定会选择 OData 服务器。因为在这种情况下,我将获得出色的 OData 功能,而无需编写代码和进行构建/部署。

但是,如果服务的需求出乎意料且可能经常变化,那么这种替代方案就不是一个好的选择。在这种情况下,我们无法控制服务。

因此,此服务对于功能集稳定的中小型项目来说将是最佳选择。

我还没见过需要基于 WCF OData 的 RESTful 服务的场景,除非 OData 服务器因任何原因不可用。

历史

2016-04-18 - 初始状态。

© . All rights reserved.