Entity Framework では 変更追跡と遅延読み込みを可能とするために Model クラスに対して自動的にプロキシクラスを作るのが既定の動きです。ですが、WCF ではこのプロキシオブジェクト をシリアライズすることができません。いや、正確にいうと、 DataContractSeirializer ではシリアライズできません。
MSDN にあるチュートリアルでは IOperationBehavior と Attribute を実装し、 WCF のメソッドに属性で適用する例が説明されています。ですが、該当するメソッドにいちいち属性を付けるのは実際のアーキテクチャでは採用されないでしょう。面倒だし、属性を忘れたら訳のわからないエラー(基礎になる接続が閉じられました、とかなんとか)で苦しむことになります。
データアクセスに EntityFramework を使用する、と決めたのならば WCF からの戻り値は POCO またはそのコレクション、またはそれらを含むクラスになるでしょう。ならば、あらかじめどこかで設定しておけば新しくメソッドを追加しても、戻り値がちゃんとシリアライズされるようになるのがアーキテクチャとして求められていることになるかと思います。
ではそのようにしてみたいと思います。Contract を用意します。
using System.ServiceModel; using MyWcf.Models; namespace MyWcf { [ServiceContract] public interface IService1 { [OperationContract] Customer GetData(); } }
戻り値の Customer は EntityFramework で取得した結果、プロキシオブジェクトとなります。次のように定義しました。
using System; using System.Collections.Generic; using System.Runtime.Serialization; namespace MyWcf.Models { [DataContract] public class Customer { public Customer() { this.CustomerAddresses = new List<CustomerAddress>(); this.SalesOrderHeaders = new List<SalesOrderHeader>(); } [DataMember] public int CustomerID { get; set; } [DataMember] public bool NameStyle { get; set; } [DataMember] public string Title { get; set; } [DataMember] public string FirstName { get; set; } [DataMember] public string MiddleName { get; set; } [DataMember] public string LastName { get; set; } [DataMember] public string Suffix { get; set; } [DataMember] public string CompanyName { get; set; } [DataMember] public string SalesPerson { get; set; } [DataMember] public string EmailAddress { get; set; } [DataMember] public string Phone { get; set; } [DataMember] public string PasswordHash { get; set; } [DataMember] public string PasswordSalt { get; set; } [DataMember] public System.Guid rowguid { get; set; } [DataMember] public System.DateTime ModifiedDate { get; set; } [DataMember] public virtual ICollection<CustomerAddress> CustomerAddresses { get; set; } [DataMember] public virtual ICollection<SalesOrderHeader> SalesOrderHeaders { get; set; } } }
この Customer クラス、AdventureWorksというサンプルデータベース内のクラスです。DataConract, DataMember 属性は自分で付けました。
次に Service です。
using System.Linq; using MyWcf.Models; namespace MyWcf { public class Service1 : IService1 { public Customer GetData() { using (var context = new AdventureWorksLTContext()) { context.Configuration.LazyLoadingEnabled = false; var data = context.Customers.FirstOrDefault(); return data; } } } }
EntityFramework のコードファーストによる例です。さりげなく重要なのが、LazyLoadingEnabled を false にしている点です。遅延読み込みが有効になっていると、他 Model との参照関係を virtual プロパティで実装している部分が原因でうまくシリアライズできないようです。
さて、このままではエラーになります。戻り値である Customer オブジェクトがプロキシオブジェクトだからです。
ではいよいよ実装していきましょう。目標はメソッドを追加しても問題なくシリアライズできるようになる、ということなので、拡張は Contract、または Service のどちらかのレベルで実装することになります。
どっちでやっても大差ないのですが、今回は両方やってみたいと思います。まずは IContractBehavior の実装です。
using System; using System.ServiceModel; using System.ServiceModel.Description; using System.ServiceModel.Channels; using System.Data.Objects; namespace MyWcf { public class ApplyDataContractResolverAttribute : Attribute, IContractBehavior { public ApplyDataContractResolverAttribute() { } #region IContractBehavior void IContractBehavior.AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } void IContractBehavior.ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { } void IContractBehavior.ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.DispatchRuntime dispatchRuntime) { foreach (var operation in contractDescription.Operations) { var dataContractSerializerOperationBehavior = operation.Behaviors.Find<DataContractSerializerOperationBehavior>(); dataContractSerializerOperationBehavior.DataContractResolver = new ProxyDataContractResolver(); } } void IContractBehavior.Validate(ContractDescription contractDescription, ServiceEndpoint endpoint) { } #endregion } }
ApplyDispatchBehavior メソッドに注目です。OperationBehavior の中に、DataContractSerializerOeprationBehavior といういかにもな ビヘイビアが既定で登録されているので、それを探し出しているんですね。こいつの DataContractResolver プロパティに、これまた用意されている ProxyDataContractResolver を設定するだけです。
ただ、IContractBehavior の実装=Interface に対する振る舞いの実装なわけですから、 Interface に複数実装されているであろう Operation(メソッド) 全てに対して ProxyDataContractResolver を設定しなければならないので、 foreach でぐるぐる回しています。
インターフェースの明示的な実装をしているのはわざとです。IServiceBehavior の実装もこのクラスにする予定だからです。実際のアーキテクチャでは、 IContractBehavior, IServiceBehabior のどちらかしか実装しないでしょうから、明示的に実装する必要はありません。
さて、Contract = Interface なわけですから、Interface に対して属性を適用します。次の[ApplyDataContractResolver] という箇所です。
using System.ServiceModel; using MyWcf.Models; namespace MyWcf { [ApplyDataContractResolver] [ServiceContract] public interface IService1 { [OperationContract] Customer GetData(); } }
ContractBehavior は構成ファイルで設定することができません。これで実装終了です。動かせば、ちゃんとシリアライズされるはずです。
次は IServiceBehavior の実装です。さて、宣言どおり先ほどと同じクラスに実装しちゃいます。
using System; using System.ServiceModel; using System.ServiceModel.Description; using System.ServiceModel.Channels; using System.Data.Objects; namespace MyWcf { public class ApplyDataContractResolverAttribute : Attribute, IContractBehavior, IServiceBehavior { public ApplyDataContractResolverAttribute() { } #region IServiceBehavior void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase host) { foreach (var endpoint in description.Endpoints) { foreach (var operation in endpoint.Contract.Operations) { var dataContractSerializerOperationBehavior = operation.Behaviors.Find<DataContractSerializerOperationBehavior>(); dataContractSerializerOperationBehavior.DataContractResolver = new ProxyDataContractResolver(); } } } void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { } void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { } #endregion #region IContractBehavior void IContractBehavior.AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } void IContractBehavior.ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { } void IContractBehavior.ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.DispatchRuntime dispatchRuntime) { foreach (var operation in contractDescription.Operations) { var dataContractSerializerOperationBehavior = operation.Behaviors.Find<DataContractSerializerOperationBehavior>(); dataContractSerializerOperationBehavior.DataContractResolver = new ProxyDataContractResolver(); } } void IContractBehavior.Validate(ContractDescription contractDescription, ServiceEndpoint endpoint) { } #endregion } }
今度は Service レベルですから、 Service → Endpoint → Contract → Operation と下がるだけですね。
Contract = Interface ならば、Service とは Interface をImplements(実装・実現)したクラスのことですから、今回の例でいうと Service1 クラスのことです。属性を適用します。さきほど Interface に設定した属性は削除しておきましょう。
using System.Linq; using MyWcf.Models; namespace MyWcf { [ApplyDataContractResolver] public class Service1 : IService1 { public Customer GetData() { using (var context = new AdventureWorksLTContext()) { context.Configuration.LazyLoadingEnabled = false; var data = context.Customers.FirstOrDefault(); return data; } } } }
動かしてみると、シリアライズされることがわかるはずです。
構成ファイルでこの IServiceBehavior を設定できるようにするためには、 BehaviorExtensionElement を実装し、構成ファイル内で登録する必要があります。
using System; using System.ServiceModel.Configuration; namespace MyWcf { public class ApplyDataContractResolverElement : BehaviorExtensionElement { public override Type BehaviorType { get { return typeof(ApplyDataContractResolverAttribute); } } protected override object CreateBehavior() { return new ApplyDataContractResolverAttribute(); } } }
構成ファイルでの登録です。extension 要素内で ApplyDataContractResolverElement を追加することで拡張機能を登録しています。
Service に対しては、MyServiceBehavior という名前のビヘイビア内に新しく ApplyDataContractResolver という要素を追加することで登録しています。
<system.serviceModel> <services> <service behaviorConfiguration="MyServiceBehavior" name="MyWcf.Service1"> <endpoint address="" binding="basicHttpBinding" contract="MyWcf.IService1" /> </service> </services> <behaviors> <serviceBehaviors> <behavior name="MyServiceBehavior"> <serviceMetadata httpGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="true" /> <ApplyDataContractResolver /> </behavior> </serviceBehaviors> </behaviors> <extensions> <behaviorExtensions> <add name="ApplyDataContractResolver" type="MyWcf.ApplyDataContractResolverElement, MyWcf"/> </behaviorExtensions> </extensions> <serviceHostingEnvironment multipleSiteBindingsEnabled="true" /> </system.serviceModel>
さきほど、Service1 に付けた属性を削除して動かしてみると、シリアライズされるはずです。
・・・さて、余談ですが、上記の config ファイルで設定した ApplyDataContractResolver 要素って、エラー扱いになりませんか?
これについて言及している人がほとんどいない、というかいない!なぜでしょうか・・・。
このエラーは非常に気持ちが悪いですね。ちゃんと動くのにエラー扱いなんですから。やり方に賛否両論あるでしょうが、消す方法はあります。
config ファイルを開いた状態で XML → スキーマの作成 とすると、現在の config を XSD スキーマにしてくれます。このスキーマをプロジェクト内に
保存してしまいます。
すると、 Web.config のスキーマとして作成した xsd ファイルが自動的に含まれます。 Web.config 上で右クリック → プロパティでプロパティウィンドウを開き、スキーマの部分を見てみると、一番最後に 追加されているのが確認できるでしょう。これでエラーが表示されなくなります。