かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

ServiceActionのハローワールド

データの公開は簡単に出来ることがわかりました。では、こいつに対して何かしらアクションを実行したい!という時はどうすればいいのでしょうか?実は、WCF Data Services 5.0からServiceActionというやつが追加されてます。こいつを使うと、WCF Data Servicesに任意のアクションを追加することが出来ます。アクションを追加できる単位は、オブジェクトだったりオブジェクトの集合だったり、そういうのに紐づかないServiceActionも作成できます。WCF Data Services 5.0以前でもServiceOperationというものが提供されていましたが、こいつは引数にプリミティブ型しかダメだとか制約があったので、実際に使うには厳しい感じでした。ServiceActionは、シリアライズ可能なら複合型もいけるみたいなので期待大です!

サービスアクションの定義方法

ServiceActionをWCF Data Servicesで公開するには、IDataServiceActionProviderインターフェースと、IDataServiceUpdateProvider2インターフェースを実装したクラスを、IServiceProviderインターフェースを通じて公開しないといけません。一言でいうとメンドクサイです。ということで、簡単にハローワールド的なServiceActionを作ってみようと思います。たった1つのServiceActionを公開するのに、これだけのコードが必要です・・・!

namespace WebApplication1
{
    using System;
    using System.Collections.Generic;
    using System.Data.Services;
    using System.Data.Services.Providers;
    using System.Linq;

    public class ServiceActions : 
        // サービスアクションを公開するためのProvider
        IDataServiceActionProvider, 
        // サービス更新 + サービスアクション実行のためのProvider
        IDataServiceUpdateProvider2
    {
        // 実行するアクションをためておく
        private List<Action> actions = new List<Action>();

        // 公開するサービスアクション
        private ServiceAction action;

        public ServiceActions()
        {
            this.action = new ServiceAction(
                // アクション名はGreet
                "Greet",
                // 戻り値はstring
                ResourceType.GetPrimitiveResourceType(typeof(string)),
                // 何か特定のエンテティとは紐づかないのでnull
                null,
                // 何か特定のエンテティとは紐づかないのでNever
                OperationParameterBindingKind.Never,
                // アクションのパラメータ
                new[]
                {
                    // string型のmessage
                    new ServiceActionParameter("message", ResourceType.GetPrimitiveResourceType(typeof(string)))
                });
            // アクションは設定が終わったらSetReadOnlyを呼ぶこと
            this.action.SetReadOnly();
        }

        public bool AdvertiseServiceAction(DataServiceOperationContext operationContext, ServiceAction serviceAction, object resourceInstance, bool resourceInstanceInFeed, ref Microsoft.Data.OData.ODataAction actionToSerialize)
        {
            return true;
        }

        // サービスアクションを呼ぶクラスを作成
        public IDataServiceInvokable CreateInvokable(DataServiceOperationContext operationContext, ServiceAction serviceAction, object[] parameterTokens)
        {
            // Greet決め打ち
            return new GreetInvokable((string)parameterTokens.First());
        }

        // すべてのアクションを返す
        public IEnumerable<ServiceAction> GetServiceActions(DataServiceOperationContext operationContext)
        {
            yield return this.action;
        }

        // 特定のエンテティに紐づいたアクションを返す
        public IEnumerable<ServiceAction> GetServiceActionsByBindingParameterType(DataServiceOperationContext operationContext, ResourceType bindingParameterType)
        {
            yield break;
        }

        // アクションを取得する
        public bool TryResolveServiceAction(DataServiceOperationContext operationContext, string serviceActionName, out ServiceAction serviceAction)
        {
            // Greetしか無いことを想定した簡単実装
            if (serviceActionName == "Greet")
            {
                serviceAction = this.action;
                return true;
            }

            serviceAction = null;
            return false;
        }

        // invokableを呼ぶアクションをリストに追加
        void IDataServiceUpdateProvider2.ScheduleInvokable(IDataServiceInvokable invokable)
        {
            this.actions.Add(invokable.Invoke);
        }

        // リストに登録しておいたアクションを全実行
        void IUpdatable.SaveChanges()
        {
            try
            {
                foreach (var action in this.actions)
                {
                    action();
                }
            }
            catch (Exception ex)
            {
                throw new DataServiceException(
                    500,
                    ex.Message);
            }
        }

        // キャンセル
        void IUpdatable.ClearChanges()
        {
            this.actions.Clear();
        }

        // 以下はエンテティの更新のときに必要なメソッドなのでServiceActionでは使わない
        void IDataServiceUpdateProvider.SetConcurrencyValues(object resourceCookie, bool? checkForEquality, IEnumerable<KeyValuePair<string, object>> concurrencyValues)
        {
            throw new NotImplementedException();
        }

        void IUpdatable.AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)
        {
            throw new NotImplementedException();
        }

        object IUpdatable.CreateResource(string containerName, string fullTypeName)
        {
            throw new NotImplementedException();
        }

        void IUpdatable.DeleteResource(object targetResource)
        {
            throw new NotImplementedException();
        }

        object IUpdatable.GetResource(IQueryable query, string fullTypeName)
        {
            throw new NotImplementedException();
        }

        object IUpdatable.GetValue(object targetResource, string propertyName)
        {
            throw new NotImplementedException();
        }

        void IUpdatable.RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
        {
            throw new NotImplementedException();
        }

        object IUpdatable.ResetResource(object resource)
        {
            throw new NotImplementedException();
        }

        object IUpdatable.ResolveResource(object resource)
        {
            throw new NotImplementedException();
        }

        void IUpdatable.SetReference(object targetResource, string propertyName, object propertyValue)
        {
            throw new NotImplementedException();
        }

        void IUpdatable.SetValue(object targetResource, string propertyName, object propertyValue)
        {
            throw new NotImplementedException();
        }
    }

    public class GreetInvokable : IDataServiceInvokable
    {
        private string message;
        private string result;

        public GreetInvokable(string message)
        {
            this.message = message;
        }

        public object GetResult()
        {
            return result;
        }

        public void Invoke()
        {
            result = message + " hello!!";
        }
    }

}

正直やってられません。DataServiceを継承したクラスで、上で作ったクラスを公開してやるコードも必要です。

[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class WcfDataService : DataService<SampleContext>, IServiceProvider
{
    private readonly ServiceActions ActionProvider = new ServiceActions();

    public static void InitializeService(DataServiceConfiguration config)
    {
        config.UseVerboseErrors = true;
        config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
        config.SetServiceActionAccessRule("*", ServiceActionRights.Invoke);
        config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
    }

    object IServiceProvider.GetService(Type serviceType)
    {
        if (serviceType == typeof(IDataServiceActionProvider))
        {
            return this.ActionProvider;
        }

        if (serviceType == typeof(IDataServiceUpdateProvider2))
        {
            return this.ActionProvider;
        }

        return null;
    }
}

これは、IServiceProviderを実装するだけなので、簡単ですね。SetServiceActionAccessRuleでServiceActionを呼び出せるようにすることも忘れずに。

呼び出し方法

コンソールアプリケーションを作成して、そこにサービス参照を追加します。そして、以下のようなコードでサービスアクションを呼び出せます。残念ながら、サービスアクションは、呼び出すためのコードを作ってくれません…。

namespace ConsoleApplication4
{
    using ConsoleApplication4.ServiceReference1;
    using System;
    using System.Data.Services.Client;
    using System.Linq;

    class Program
    {
        static void Main(string[] args)
        {
            var client = new SampleContext(
                new Uri("http://localhost:49533/WcfDataService.svc"));

            var result = client.Execute<string>(
                new Uri("Greet", UriKind.Relative),
                "POST",
                true,
                new BodyOperationParameter("message", "たなか"))
                .First();
            Console.WriteLine(result);
        }
    }
}

実行すると「たなか hello!!」と表示されます。めんどくさいけどメデタシメデタシ。