LoginSignup
3
3

More than 3 years have passed since last update.

WebHooks.Customで実装したWebHookをLogic AppsのHTTP WebHookコネクタのプッシュトリガーに設定する

Last updated at Posted at 2017-03-23

はじめに

Azure FunctionsからAzure Logic Appsへのトリガーの方法として要求(request)を用いた場合、Azure FunctionsからPOSTする際にそのLogic apps固有のURLを記述する必要がありました。
a.png

汎用性を高めるため自前でWebHook(WebAPI向け)を用意し、Azure Logic AppsのトリガーとしてHttp WebHookを使用できたので、実装の一例としてまとめます。
b.png

HTTP Webhookの設定について

参考
https://docs.microsoft.com/ja-jp/azure/connectors/connectors-native-webhook#a-nameuse-the-webhook-triggerawebhook-トリガーの使用

c.png

WebHookの登録/解除を行うためのURLとメソッドを指定します。
後述しますが、Microsoft.AspNet.WebHooks.Custom.Apiでは登録や解除をするためのURLは固定です。(api/webhooks/registrations)
サブスクライブの本文はwebhookを満たすjsonを指定します。
WebHookUriだけが必須です。"@listCallbackUrl()"はHttp WebhookのコールバックURLを示す記載です。
Idは指定しなければGuidが割り当てられますが解除の際にIDを指定する必要があるため、固定に設定しています。
以下、Webhook コネクタの概要からの引用

サブスクライブ呼び出しは、ロジック アプリが新しい Webhook と共に保存されるか、無効から有効に切り替えられるたびに実行されます。 サブスクライブ解除呼び出しは、ロジック アプリの Webhook トリガーが削除されて保存されるか、有効から無効に切り替えられるたびに実行されます。

なおapi/webhooks/registrationsはAuthorize属性が指定されているため、App Serviceの認可を受ける必要があります。今回は動作確認のために認可をごまかしているので、セキュアに設定する際にはヘッダーにトークンを指定するなどが必要かと思われます。

WebHooks.Customの実装

ソースは以下に置きました。
github

そもそもWebHookとは

http://qiita.com/soarflat/items/ed970f6dc59b2ab76169
http://kiyokura.hateblo.jp/entry/2015/12/01/000136

WebHooks.Customの概要

ここから引用
2678.WebHooksSender_049B8B41.png

この図が一番しっくりきました。仕組みは用意されているのでところどころ注入してあげる感じです。

  • コールバックURL等を登録/解除する仕組みは用意されている:Register WebHooks
  • 情報を保存する場所を指定する必要がある(Azure StorageかDBかetc):IWebHookStore
  • クライアントの入り口を用意する(MVCかWebApiか両方か)
  • WebHookを管理するManagerを用意する:IWebHookManager
  • 送信方法を指定する必要がある(HTTP POSTするのかキューに貯めるのかetc):IWebHookSender

今回は入り口はWebApi、送信はコールバックURLにHTTP HOST、保存場所をAzure Storageにします。

使用したパッケージ

Microsoft.AspNet.WebHooks.Common
Microsoft.AspNet.WebHooks.Custom
Microsoft.AspNet.WebHooks.Custom.Api
Microsoft.AspNet.WebHooks.Custom.AzureStorage

IWebHookStoreについて

IWebHookStoreを継承したクラスが既に存在しています。
WebHooks.Custom.WebHookStore
WebHooks.Custom.MemoryWebHookStore

WebHooks.Custom.AzureStorage.AzureWebHookStore

WebHooks.Custom.SqlStorage.DbWebHookStore
WebHooks.Custom.SqlStorage.SqlWebHookStore

デフォルトはMemoryWebHookStoreでした。外部に格納せずコードに直接記載するならばこれを使用します。
AzureWebHookStoreはAzure Storageに特化したクラスでした。今回はこれを使用します。
Web.configに以下を追加する必要があります。

<configuration>
  <connectionStrings>
    <add name="MS_AzureStoreConnectionString" connectionString="~~~~~~~~~" />
  </connectionStrings>
</configuration>

connectionStringにはAzure Storageへの接続文字列を記載する必要があります。Azure ポータルから取得します。

DbWebHookStoreやSqlWebHookStoreは今回あんまり調査していませんが、DbWebHookStoreは抽象クラスなので自分でDB連携を実装する際に継承するのかな、と思います。

IWebHookSenderについて

IWebHookSenderを継承したクラスが既に存在しています。
WebHooks.Custom.WebHookSender
WebHooks.Custom.DataFlowWebHookSender

WebHooks.Custom.AzureStorage.AzureWebHookSender

デフォルトはDataflowWebHookSenderでした。こちらを使います。
AzureWebHookSenderはAzure Storageに特化したクラスでした。Azure Storage Queueに送信する仕組みかも?

IWebHookManagerについて

IWebHookManagerを継承したクラスが既に存在しています。
WebHooks.Custom.WebHookManager

あんまり意識しませんでした。

初期設定

空のプロジェクトにOWINのStartupクラスを追加しました。

Startup1

Startup1.cs
        public void Configuration(IAppBuilder app)
        {
            GlobalConfiguration.Configure(config =>
            {
                config.Filters.Add(new MyAuthorizeAttribute());

                config.MapHttpAttributeRoutes();

                config.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = RouteParameter.Optional });

                config.DependencyResolver = new MyResolver();

                CustomApiServices.SetIdValidator(new MyWebHookIdValidator());

                config.InitializeCustomWebHooks();
                config.InitializeCustomWebHooksAzureStorage(false);
                //config.InitializeCustomWebHooksAzureQueueSender();
                config.InitializeCustomWebHooksApis();
            });
        }
                config.Filters.Add(new MyAuthorizeAttribute());

は認可のごまかしです。

public class MyAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
    {
        public override async Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            await Task.Run(() =>
            {
                actionContext.RequestContext.Principal = new GenericPrincipal(new GenericIdentity("self"), new string[] { "Admin", "PowerUser" });
            });
        }
    }

いつものWebApiの初期設定が続きます。

                config.DependencyResolver = new MyResolver();

                CustomApiServices.SetIdValidator(new MyWebHookIdValidator());

この部分はいくつか問題があったため、独自に実装しました。(後述)

                config.InitializeCustomWebHooks();
                config.InitializeCustomWebHooksAzureStorage(false);
                //config.InitializeCustomWebHooksAzureQueueSender();
                config.InitializeCustomWebHooksApis();

各メソッドはHttpConfigurationの拡張メソッドで各パッケージに実装されています。

IWebHookFilterProviderの実装

参考
https://blogs.msdn.microsoft.com/webdev/2015/09/15/sending-webhooks-with-asp-net-webhooks-preview/

図には出てきませんでしたが、WebHookには通知先をフィルタリングするための仕組みが用意されていて、これを実装する必要があります。

CustomServices

CustomServices.cs

        /// <summary>
        /// Gets the set of <see cref="IWebHookFilterProvider"/> instances discovered by a default 
        /// discovery mechanism which is used if none are registered with the Dependency Injection engine.
        /// </summary>
        /// <returns>An <see cref="IEnumerable{T}"/> containing the discovered instances.</returns>
        public static IEnumerable<IWebHookFilterProvider> GetFilterProviders()
        {
            if (_filterProviders != null)
            {
                return _filterProviders;
            }

            IAssembliesResolver assembliesResolver = WebHooksConfig.Config.Services.GetAssembliesResolver();
            ICollection<Assembly> assemblies = assembliesResolver.GetAssemblies();
            IEnumerable<IWebHookFilterProvider> instances = TypeUtilities.GetInstances<IWebHookFilterProvider>(assemblies, t => TypeUtilities.IsType<IWebHookFilterProvider>(t));
            Interlocked.CompareExchange(ref _filterProviders, instances, null);
            return _filterProviders;
        }

の通りIWebHookFilterProviderを継承していれば名前は何でも良さそうです。

MyWebHookFilterProvider

MyWebHookFilterProvider.cs
    /// <summary>
    /// Use a IWebHookFilterProvider implementation to describe the events that users can 
    /// subscribe to. A wildcard is always registered meaning that users can register for 
    /// "all events". It is possible to have 0, 1, or more IWebHookFilterProvider 
    /// implementations.
    /// </summary>
        private readonly Collection<WebHookFilter> filters = new Collection<WebHookFilter>
        {
            new WebHookFilter { Name = "*", Description = "wildcard"}
        };

        public Task<Collection<WebHookFilter>> GetFiltersAsync()
        {
            return Task.FromResult(this.filters);
        }

コメントにも書いてあるのですが、ワイルドカードが使えます。今回はフィルタリングする気がないのでワイルドカードだけ指定します。

WebApiの実装

api/webhookに対してPOSTだけのメソッドを用意しました。引数として期待するオブジェクトは{ url: "https://~~~" }です。
Logic Appsに対して通知します。

WebhookController

WebhookController.cs
public class WebhookController : ApiController
    {
        public async Task<IHttpActionResult> Post(dynamic req)
        {
            await this.NotifyAsync("*", new { url = req.url });
            return Ok();
        }
    }

NotifyAsyncはAPIControllerに対しての拡張メソッドです。
第一引数にアクションを指定します。IWebHookFilterProviderで指定したフィルターと関係あります。今回はワイルドカードです。
第二引数に通知したい内容を記載します。{ url: "https://~~~" }というjsonを期待します。

他にNotifyAllAsyncもあります。違いはApiController.User(IPrincipal)で絞るかどうかだと思うんですけどIdで絞るのか役割(Role)で絞るのか、あんまり調査しませんでした。

デフォルトで提供されているWebApiについて

RESTな感じです。

メソッド URL 説明
GET api/webhooks/filters 設定されているフィルターが全て返ってきます。
GET api/webhooks/registrations 全ての情報が返ってきます。
GET api/webhooks/registrations/{id} 特定の情報が返ってきます。
POST api/webhooks/registrations 登録します。bodyにwebhook情報が必要です。
PUT api/webhooks/registrations/{id} 特定の情報を更新します。bodyにwebhook情報が必要です。
DELETE api/webhooks/registrations/ 全ての登録情報を削除します。
DELETE api/webhooks/registrations/{id} 特定の情報を削除します。

POST及びPUTで指定できるjsonについて

webhookを満たしている必要があります。必須項目はWebHookUriだけです。

{
  Id: "webhook",
  Secret: "abcdefghijklmnopqrstuvwxyzabcdef",
  WebHookUri:"http(s)://~~~~~~~~~",
  Description:"Description",
  IsPaused: true,
  Headers:{"key":"value"},
  Properties:{"key":{"abc":"def"}}
}

POSTで失敗する!

Logic Appsで設定されるコールバックURLをPOSTすると怒られました。

WebHookという仕組みの元々の話なのかわかりませんが、登録するURLにたいしてechoというクエリを付与してGetメソッドを送信して正しく返ってくることを確認しているようです。(VerifyEchoAsync)
これを回避するためにはnoechoというクエリをURLにつけなさい、とのことなんですがLogic Appsで設定されるコールバックURLに付与すると認証エラーになってしまいます。
幸いVerifyEchoAsyncはvirtualだったので、WebHookManagerを継承してoverrideして回避することにしました。
WebHookManagerはSystem.Web.Http.Dependencies.IDependencyResolver.GetServiceメソッドから取得されていたので、
HttpConfigurationのDependencyResolverを独自に設定し、IWebHookManager取得時だけ独自のWebHookManagerを与えることにしました。

config.DependencyResolver = new MyResolver();

  public class MyResolver : System.Web.Http.Dependencies.IDependencyResolver
    {
        public IDependencyScope BeginScope() => this;

        public void Dispose()
        {
        }

        public object GetService(Type serviceType)
        {
            if (typeof(IWebHookManager).Equals(serviceType))
            {
                IWebHookStore store = this.GetStore();
                IWebHookSender sender = this.GetSender();
                ILogger logger = this.GetLogger();
                IWebHookManager instance = new MyWebHookManager(store, sender, logger);

                return instance;
            }

            return null;
        }

        public IEnumerable<object> GetServices(Type serviceType) => Enumerable.Empty<object>();
    }

  public class MyWebHookManager : WebHookManager
    {
        public MyWebHookManager(IWebHookStore webHookStore, IWebHookSender webHookSender, ILogger logger)
            : base(webHookStore, webHookSender, logger)
        {
        }

        protected override async Task VerifyEchoAsync(WebHook webHook) => await Task.FromResult<object>(null);
    }

POSTでIdを固定にできない!

いろいろPOSTしているとIdだけが設定できませんでした。調査したところ、IWebHookIdValidatorなる仕組みがあり、デフォルトではIdをnullに上書きされていました。
HTTP Webhookでのサブスクライブ解除のために固有のIDが必要だったため、初期設定にて上書きしました。

CustomApiServices.SetIdValidator(new MyWebHookIdValidator());

public class MyWebHookIdValidator : IWebHookIdValidator
    {
        public async Task ValidateIdAsync(HttpRequestMessage request, WebHook webHook) => await Task.FromResult<object>(null);
    }

まとめ

デフォルトで用意されている仕組みに乗っかると簡単に実装できました。
DBや送信方法をカスタマイズする場合にもインターフェースに乗っかって実装すれば良いので迷うことは少なそうです。
SenderやStoreを差し替える場合にはWebHooks.Custom.CustomServicesのSetStoreやSetSenderを使用します。

MVCのほうにもMicrosoft.AspNet.WebHooks.Custom.Mvcが用意されているので助かります。

3
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3