はじめに
Azure FunctionsからAzure Logic Appsへのトリガーの方法として要求(request)を用いた場合、Azure FunctionsからPOSTする際にそのLogic apps固有のURLを記述する必要がありました。
汎用性を高めるため自前でWebHook(WebAPI向け)を用意し、Azure Logic AppsのトリガーとしてHttp WebHookを使用できたので、実装の一例としてまとめます。
HTTP Webhookの設定について
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の概要
ここから引用
この図が一番しっくりきました。仕組みは用意されているのでところどころ注入してあげる感じです。
- コールバック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クラスを追加しました。
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には通知先をフィルタリングするための仕組みが用意されていて、これを実装する必要があります。
/// <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を継承していれば名前は何でも良さそうです。
/// <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に対して通知します。
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が用意されているので助かります。