かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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!!」と表示されます。めんどくさいけどメデタシメデタシ。