Web サービスは "Legacy Technology" なので Windows Communication Foundation (WCF) を使うようにと言われています。なので、今さらながらですが、先の記事 jQuery AJAX と Web サービスで作ったサンプルと同様なものを WCF で実装してみました。
作成に当たって参考にしたのは以下の 2 つの記事です。しかし、読んだだけではほとんど意味が分かりません。(汗)
自分の手を動かして作ってみれば少しは理解が深まるであろうということで、自分の環境(Vista SP2 32-bit の IIS7、ASP.NET 4, Visual Studio 2010 Professional、ASP.NET Web Forms アプリ)で実際にやってみました。やってみていろいろ分かったことがありましたので備忘録として書いておきます。
まず、開発マシンの IIS7 上で動く既存の ASP.NET Web Forms アプリに、上の画像のように Visual Studio の[新しい項目の追加(W)...]メニューからテンプレートを使って[AJAX 対応 WCF サービス]を CarService.svc という名前で追加します。([WCF サービス]の方は従来の SOAP を使う WCF サービス)
CarService.svc を追加すると以下のコードが自動生成されます。今回の記事では Web アプリケーションプロジェクトを使っています。Web サイトプロジェクトの場合は、コードビハンド CarService.svc.cs が CarService.cs という名前になって App_Code フォルダに置かれる、アセンブリ名 / 名前空間名が付与されないという違いがありますので注意してください。
CarService.svc(xxx は名前空間名)
<%@ ServiceHost
Language="C#"
Debug="true"
Service="xxx.CarService"
CodeBehind="CarService.svc.cs"
%>
CarService.svc.cs(xxx は名前空間名)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;
namespace xxx
{
[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
public class CarService
{
// HTTP GET を使用するために [WebGet] 属性を追加します
// (既定の ResponseFormat は WebMessageFormat.Json)。
// XML を返す操作を作成するには、
// [WebGet(ResponseFormat=WebMessageFormat.Xml)] を追加し、
// 操作本文に次の行を含めます。
// WebOperationContext.Current.OutgoingResponse.ContentType =
// "text/xml";
[OperationContract]
public void DoWork()
{
// 操作の実装をここに追加してください
return;
}
// 追加の操作をここに追加して、[OperationContract] とマーク
// してください
}
}
web.config への追加(xxx は名前空間名)
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="xxx.CarServiceAspNetAjaxBehavior">
<enableWebScript />
</behavior>
</endpointBehaviors>
</behaviors>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="true" />
<services>
<service name="xxx.CarService">
<endpoint address=""
behaviorConfiguration="xxx.CarServiceAspNetAjaxBehavior"
binding="webHttpBinding"
contract="xxx.CarService" />
</service>
</services>
</system.serviceModel>
上記は、web.config に <enableWebScript /> とあることから分かるように、ASP.NET AJAX の ScriptManager を使用する Web ページからサービスを使用するための WCF AJAX サービスのコードです。
これらのコードを、上の 2 つの記事を参考に、JavaScript / jQuery を使用する Web ページからアクセスできるように書き換えてみました。
上に紹介した ASP.NET を使用せずに WCF AJAX サービスを作成する方法 に書いてあるように、以下の 3 つの手順に分けて説明します。
(1) ブラウザーからアクセスできる AJAX エンドポイントの作成
(2) AJAX 互換サービス コントラクトの作成
(3) WCF AJAX サービスへのアクセス
(1) ブラウザーからアクセスできる AJAX エンドポイントの作成
AJAX エンドポイントを作成するためには、
-
.svc ファイルの @ServiceHost の Factory 属性に WebServiceHostFactory を設定する、または
-
web.config の <system.serviceModel> 要素で WebHttpBinding と WebHttpBehavior を使用してエンドポイントを構成する
のいずれかの方法を取るのだそうです。具体例は上の記事(前者)のサンプルコードを参照してください。
1 の方が簡単そうに思えますが、実は 2 の方は web.config に自動生成されて追加されたコード(上のコードを見てください)の中の <enableWebScript /> を <webHttp /> に書き換えるだけで済みますので、手間的には大差はないです。
今回は 2 の方法(.svc ファイルには手を加えないで、web.config に自動生成されたコードの中の <enableWebScript /> を <webHttp /> に書き換え)を取りました。
エンドポイントアドレス名を指定してサービスを呼び出したい場合は、自動生成された address="" を修正してください。例えば、address="ajaxEndpoint" とすると、carservice.svc/ajaxEndpoint/GetAllCars でアクセスできます。(ちなみに、そのように address を設定すると、carservice.svc/GetAllCars では HTTP 404 Not Found になります)
1 の方法を取る場合、web.config に自動生成されたコードは削除した方がよさそうです。<enableWebScript /> を <webHttp /> に書き換えれば WebScriptEnablingBehavior では BodyStyle の Wrapped がサポートされてない等の問題は回避できますが、既存のサービスなどと競合してエラーになることがありましたので。
(2) AJAX 互換サービス コントラクトの作成
先の記事 ASP.NET AJAX と Web サービスの Web サービス(.asmx ファイル)のコードを CarService.svc.cs に移植します。
GetAllCars は GET 要求で呼び出せるようにしました。(JSON サービスを GET で要求するのは潜在的なセキュリティ上のリスクがあるそうです。詳しくはこのセクションの下の方で紹介した記事を見てください)
GetCarsByDoors は先の例と同様に POST 要求で呼び出すようにしました。サンプルコードは以下のようになります。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;
namespace WebApplication1
{
// web.config で aspNetCompatibilityEnabled="true" と設定され
// ているので [AspNetCompatibilityRequirements(...)] が必要
[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
public class CarService
{
List<Car> cars = new List<Car> {
new Car {Make="Audi",Model="A4",Year=1995,
Doors=5,Colour="Red",Price=2995f},
new Car {Make="Ford",Model="Focus",Year=2002,
Doors=5,Colour="Black",Price=3250f},
new Car {Make="BMW",Model="5 Series",Year=2006,
Doors=4,Colour="Grey",Price=24950f},
new Car {Make="Renault",Model="Laguna",Year=2000,
Doors=5,Colour="Red",Price=3995f},
new Car {Make="Toyota",Model="Previa",Year=1998,
Doors=5,Colour="Green",Price=2695f},
new Car {Make="Mini",Model="Cooper",Year=2005,
Doors=2,Colour="Grey",Price=9850f},
new Car {Make="Mazda",Model="MX 5",Year=2003,
Doors=2,Colour="Silver",Price=6995f},
new Car {Make="Ford",Model="Fiesta",Year=2004,
Doors=3,Colour="Red",Price=3759f},
new Car {Make="Honda",Model="Accord",Year=1997,
Doors=4,Colour="Silver",Price=1995f}
};
[OperationContract]
[WebGet(ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Wrapped)]
public List<Car> GetAllCars()
{
return cars;
}
[OperationContract]
[WebInvoke(ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Wrapped)]
public List<Car> GetCarsByDoors(int doors)
{
var query = from c in cars
where c.Doors == doors
select c;
return query.ToList();
}
}
[DataContract]
public class Car
{
[DataMember]
public string Make { get; set; }
[DataMember]
public string Model { get; set; }
[DataMember]
public int Year { get; set; }
[DataMember]
public int Doors { get; set; }
[DataMember]
public string Colour { get; set; }
[DataMember]
public float Price { get; set; }
}
}
ポイントは WebGetAttribute 属性と WebInvokeAttribute 属性の設定で、具体的には以下の通りです。
-
ResponseFormat = WebMessageFormat.Json を設定して、応答に XML ではなく JSON を明示的に指定します。
-
上に紹介した記事(後者)には "WebGetAttribute または WebInvokeAttribute 属性に Wrapped の本文スタイルが必要です" とありますが、その通りにすると応答もラップされて {"GetAllCarsResult":[{"Colour":"Red", ... ,"Year":1997}]} というような形になります。
-
BodyStyle = WebMessageBodyStyle.Wrapped を削除すると "GetAllCarsResult" というラップはなくなりますが、今度は POST 要求で HTTP 400 Bad Request となってしまいます。なので、少なくとも POST 要求する方は BodyStyle = WebMessageBodyStyle.WrappedRequest とする必要があります。
今回は記事に従って BodyStyle = WebMessageBodyStyle.Wrapped とし、ラップを外す処置をクライアントスクリプトにコーディングしました。具体的には、下の「(3) WCF AJAX サービスへのアクセス」のセクションを見てください。
ラップすると、Web サービスの JSON 応答を "d" でラップするのと同様に、セキュリティ上のリスクの軽減に効果があるはずです。
Web サービスの JSON 応答は .NET 3.5 から "d" でラップされるようになりましたが、それは JSON サービスの脆弱性に起因する XSS 対策だそうです。そのメカニズムは以下の記事に詳しく書いてあります。
WCF では "<メソッド名>Result" でラップされますが、Web サービスで "d" でラップするのと同様に JSON 配列を返さないという効果があります。(JSON 配列は JavaScript として有効というところが問題だそうです)
あと、上に紹介した記事に書いてありますが、GET 要求を使うことにもリスクがあるそうです。
いろいろ条件が重ならないとリスクにはならないですが、「君子危うきに近寄らず」ということで、(1) ラップする、(2) POST 要求を使う、という 2 つのことは実施した方がよさそうです。(ちなみに、ASP.NET AJAX を利用する場合はデフォルトでそうなります)
(3) WCF AJAX サービスへのアクセス
これは先の記事 jQuery AJAX と Web サービス とほぼ同様な jQuery.ajax のコードを利用して可能です。
上記 (2) の手順で、GetAllCars は GET 要求で、GetCarsByDoors は POST 要求で取得するようにしたので、それぞれ以下のようなコードで要求をかけ、JSON 文字列の応答を得てそれを処置することができます。
function getAllCars() {
$.ajax({
type: "GET",
url: "carservice.svc/GetAllCars",
success: function (cars) {
if (cars.hasOwnProperty('GetAllCarsResult')) {
cars = cars.GetAllCarsResult;
// ・・・cars の処置(省略)・・・
}
},
error: function (jqXHR, textStatus, errorThrown) {
// ・・・省略・・・
}
});
}
function getCarsByDoors() {
$.ajax({
type: "POST",
url: "carservice.svc/GetCarsByDoors",
data: '{"doors":' + $("#ddlDoors").val() + '}',
contentType: "application/json; charset=utf-8",
success: function (cars) {
if (cars.hasOwnProperty('GetCarsByDoorsResult')) {
cars = cars.GetCarsByDoorsResult;
// ・・・cars の処置(省略)・・・
}
},
error: function (jqXHR, textStatus, errorThrown) {
// ・・・省略・・・
}
});
}
POST の方には contentType: "application/json; charset=utf-8" の設定は必須です。それがないと HTTP 400 Bad Request となって失敗します。
success オプションに設定したコールバックの引数 cars にはパース済みの JavaScript オブジェクトが渡されます。上の「(2) AJAX 互換サービス コントラクトの作成」のセクションで述べたように "<メソッド名>Result" でラップされていますので、ラップを外す処置が必要です。具体的には上のコードを見てください。(命名規則が不明なので、ラップを外す処置をハードコーディングするのはちょっと不安なのですが)