AsyncOAuth - C#用の全プラットフォーム対応の非同期OAuthライブラリ

待ち望まれていたHttpClientがPortable Class Library化しました、まだBetaだけどね!というわけで、早速PCL版のHttpClientをベースにしたOAuthライブラリを仕上げてみました。ポータブルクラスライブラリなので、.NET 4.5は勿論、Windows Phone 7.5, 8, Windows Store Apps, Silverlight, それと.NET 4.0にも対応です。

前身のReactiveOAuthがTwitterでしかロクにテストしてなくてHatenaでズタボロだったことを反省し、今回はSampleにTwitterとHatenaを入れておきました&どっちでもちゃんと正常に動きます。なお、完全に上位互換なので、ReactiveOAuthはObsoleteです。それと、ライブラリのインストールはNuGet経由でのみの提供です。

PM> Install-Package AsyncOAuth -Pre

もしくはPreReleaseを表示に含めてGUIから検索してください。

AsyncOAuth is not a new library

AsyncOAuthの実態はOAuthMessageHandlerというDelegatingHandlerです。

var client = new HttpClient(new OAuthMessageHandler("consumerKey", "consumerSecret", new AccessToken("accessToken", "accessTokenSecret")));

// 上のだとnewの入れ子が面倒なので短縮形、戻り値は上のと同じ
var client = OAuthUtility.CreateOAuthClient("consumerKey", "consumerSecret", new AccessToken("accessToken", "accessTokenSecret"));

こうなっていると何がいいか、というと、全ての操作がHttpClient標準通りなのです。

// Get
var json = await client.GetStringAsync("http://api.twitter.com/1.1/statuses/home_timeline.json?count=" + count + "&page=" + page);

// Post
var content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("status", status) });
var response = await client.PostAsync("http://api.twitter.com/1.1/statuses/update.json", content);
var json = await response.Content.ReadAsStringAsync();

// Multi Post
var content = new MultipartFormDataContent();
content.Add(new StringContent(status), "\"status\"");
content.Add(new ByteArrayContent(media), "media[]", "\"" + fileName + "\"");

var response = await client.PostAsync("https://upload.twitter.com/1/statuses/update_with_media.json", content);
var json = await response.Content.ReadAsStringAsync();

もうおれおれクライアントのAPIを覚える必要はありません。これからの標準クライアントであるHttpClientの操作だけを覚えればいいのです。

コンセプトはHttpClientチームから掲示されているサンプルコードExtending HttpClient with OAuth to Access Twitterどおりですが、このサンプルコードは本当にただのコンセプトレベルなサンプルで、そのまんまじゃ使えないので、ちゃんと実用的なOAuthライブラリとして叩き直したのがAsyncOAuthになります。DelegatingHandlerというのは、リクエストを投げる直前をフックするものなので、そこでOAuth用の認証を作っているわけです。

イニシャライズ

使う場合は、必ず最初にHMAC-SHA1の計算関数をセットしなければなりません。何故か、というと、ポータブルクラスライブラリには現状、暗号系のライブラリが含まれていなくて、その部分は含むことができないからです。外部から差し込んでもらうことでしか対処できない、という。ご不便おかけしますが、的な何か。そのうち含まれてくれるといいなあ、って感じですねえ。それまでは、以下のコードをApp.xaml.csとかApplication_Startとか、初回の本当に最初の最初に呼ばれるところに、コピペってください。

// WinRT以外(Silverlight, Windows Phone, Consoleなどなど)
OAuthUtility.ComputeHash = (key, buffer) => { using (var hmac = new HMACSHA1(key)) { return hmac.ComputeHash(buffer); } };

// Windows Store App(めんどうくせえええええ)
AsyncOAuth.OAuthUtility.ComputeHash = (key, buffer) =>
{
    var crypt = Windows.Security.Cryptography.Core.MacAlgorithmProvider.OpenAlgorithm("HMAC_SHA1");
    var keyBuffer = Windows.Security.Cryptography.CryptographicBuffer.CreateFromByteArray(key);
    var cryptKey = crypt.CreateKey(keyBuffer);

    var dataBuffer = Windows.Security.Cryptography.CryptographicBuffer.CreateFromByteArray(buffer);
    var signBuffer = Windows.Security.Cryptography.Core.CryptographicEngine.Sign(cryptKey, dataBuffer);

    byte[] value;
    Windows.Security.Cryptography.CryptographicBuffer.CopyToByteArray(signBuffer, out value);
    return value;
};

また、使いかたの詳しいサンプルは、GitHub上のソースコードからAsyncOAuth.ConsoleAppの中にTwitter.csとHatena.csがあるので、それを見てもらえればと思います。AccessToken取得までの、認証系の説明はここには書きませんが(OAuthAuthorizerという特別に用意してあるものを使う)、その具体的な書き方が乗っています。特にHatenaの認証はTwitterに比べるとかなりメンドーくさいので、メンドーくさい系のOAuthが対象の場合は参考になるかと思います。

ストリーミング、Single vs Multiple、或いはRxの再来

勿論、TwitterのストリーミングAPIにも対応できます。以下のようなコードを書けばOK。

public async Task GetStream(Action<string> fetchAction)
{
    var client = OAuthUtility.CreateOAuthClient(consumerKey, consumerSecret, accessToken);
    client.Timeout = System.Threading.Timeout.InfiniteTimeSpan; // ストリーミングなのでTimeoutで切られないよう設定しておくこと

    using (var stream = await client.GetStreamAsync("https://userstream.twitter.com/1.1/user.json"))
    using (var sr = new StreamReader(stream))
    {
        while (!sr.EndOfStream)
        {
            var s = await sr.ReadLineAsync();
            fetchAction(s);
        }
    }
}

ほぅ、Actionですか、コールバックですか……。ダサい。使い勝手悪い。最悪。しかし、じゃあ何返せばいいんだよ!ということになる。Taskは一つしか返せない、でもストリーミングは複数。うーん、うーん、と、そこでIObservable<T>の出番です。Reactive Extensionsを参照して、以下のように書き換えましょう。

public IObservable<string> GetStream()
{
    return Observable.Create<string>(async (observer, ct) =>
    {
        try
        {
            var client = OAuthUtility.CreateOAuthClient(consumerKey, consumerSecret, accessToken);
            client.Timeout = System.Threading.Timeout.InfiniteTimeSpan; // ストリーミングなのでTimeoutで切られないよう設定しておくこと

            using (var stream = await client.GetStreamAsync("https://userstream.twitter.com/1.1/user.json"))
            using (var sr = new StreamReader(stream))
            {
                while (!sr.EndOfStream && !ct.IsCancellationRequested)
                {
                    var s = await sr.ReadLineAsync();
                    observer.OnNext(s);
                }
            }
        }
        catch (Exception ex)
        {
            observer.OnError(ex);
            return;
        }
        if (!ct.IsCancellationRequested)
        {
            observer.OnCompleted();
        }
    });
}
var client = new TwitterClient(consumerKey, consumerSecret, new AccessToken(accessTokenKey, accessTokenSecret));

// subscribe async stream
var cancel = client.GetStream()
    .Skip(1)
    .Subscribe(x => Console.WriteLine(x));

Console.ReadLine();
cancel.Dispose(); // キャンセルはDisposeで行う

といったように、自然にRxと繋げられます。コールバックのObservable化はObservable.Createで、そんなに難しくはない(ただしOnNext以外にちゃんとOnError, OnCompletedも記述してあげること)です。キャンセル対応に関しては、ちゃんとCancelleationToken付きのオーバーロードで行いましょう。そうしないと、Subscribeの解除はされていても、内部ではループが延々と動いている、といったような状態になってしまいますので。

ともあれ、asyncやCancellationTokenとRxがスムースに結合されていることは良くわかるかと思います。完璧!

こういった、単発の非同期はTaskで、複数の非同期はIObservable<T>で行う、というガイドはTPLチームからも示されています。先日のpfxteamからのスライドから引用すると(ちなみにこのスライドはTask系の落とし穴などが超丁寧に書かれているので必読!)

といった感じです。んねー。

まとめ

ReactiveOAuthはオワコン。HttpClient始まってる。Reactive Extensions自体は終わってない、むしろ始まってる。というわけで、色々と使いこなしていきましょう。

追記:リリースから一晩開けて、POST周りを中心にバグが発見されていてお恥ずかしい限りです。あらかた修正したとは思うのですが(NuGetのバージョンは随時上げています)、怪しい挙動見つけたら報告下さると嬉しいです。勿論、GitHubなのでPull Requestでも!

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive