WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

WCF サービスの非同期呼び出し

by WebSurfer 2018年2月28日 16:47

Visual Studio 2015 Community で WCF のサービス参照を追加して生成されるサービスプロキシには非同期版のメソッドも含まれ、それを利用すれば async / await キーワードを付与することで簡単に非同期呼び出しができるという話を書きます。

WCF のクライアント

何を今さらと言われるかもしれませんが、自分としては新発見で、これは備忘録として残しておかねばと思いましたので。(笑)

上の画像は Microsoft のチュートリアル「10 行でズバリ!! [C#] WCF サービスの作成と利用」をベースに作った WPF クライアントアプリです。string 型のデータを応答として返す WCF サービスを呼び出して、その応答を表示しています。

上のチュートリアルにある通り、クライアントアプリから WCF サービスを呼び出すのに利用するサービスプロキシを生成するため、サービス参照の追加を行います。

その際、「サービス参照の追加」ダイアログで[詳細設定(V)...]をクリックすると表示される「サービス参照設定」ダイアログを見ると、Visual Studio 2015 Community ではデフォルトで以下の画像の設定になっています。

サービス参照の追加

この設定画面で[非同期操作の生成を許可する(P)]にチェックが入っていて、[タスクベースの操作を生成する(T)]が選択されているのがポイントのようです。自信度はあまり高くないですが・・・(汗)

ダイアログ右上の ? マークをクリックすると表示されるヘルプを読んでも何のことだかよく分かりませんでしたが、Microsoft のドキュメント「同期操作と非同期操作」を読むと、その中の「タスク ベースの非同期パターン」が相当するのではないかと思いました。

(Visual Studio 2010 Professional では[非同期操作の生成(G)]というのがありますが、デフォルトではチェックは入っていません。未確認ですが、.NET 4.5 以降でしか使用できない async / await キーワードを利用して使うものではなさそうです)

サービス参照の追加が完了すると、以下の画像の通り、同期版の GetData メソッドに加えて非同期版の GetDataAsync メソッドも生成されているのがインテリセンスの表示から分かります。

インテリセンスの表示

クライアントアプリからは、async / await キーワードを使って非同期版メソッドを呼び出せば、時間がかかる WCF サービスから応答が返ってくるまで UI がブロックされるということはなくなります。

それを試すには以下のようにします。

WCF サービス

まず、WCF のメソッドに 3 秒の待ち時間を入れます。これにより、同期の場合は呼び出し側の WPF アプリは 3 秒ブロックされますが、非同期の場合はブロックされないことを確認できます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;

// このサービスをホストする ASP.NET アプリの web.config
// で aspNetCompatibilityEnabled="true" と設定されている
// 場合は以下の属性が必要
[AspNetCompatibilityRequirements(RequirementsMode = 
             AspNetCompatibilityRequirementsMode.Allowed)]
public class Service : IService
{
  public string GetData(int value)
  {
    // 非同期実行を確認するため追加
    System.Threading.Thread.Sleep(3000);

    return string.Format("You entered: {0}", value);
  }

  // CompositeType は IService.cs に自動生成されたクラス
  public CompositeType 
      GetDataUsingDataContract(CompositeType composite)
  {
    // 非同期実行を確認するため追加        
    System.Threading.Thread.Sleep(3000);
        
    if (composite == null)
    {
      throw new ArgumentNullException("composite");
    }

    if (composite.BoolValue)
    {
      composite.StringValue += "Suffix";
    }

    return composite;
  }
}

WPF クライアントアプリ

呼び出し側のクライアントアプリでは以下のようにします。一番上の画像で、[同期呼び出し]ボタンのクリックイベントのハンドラが button_Click で、[非同期呼び出し]の方が button1_Click です。

// 同期
private void button_Click(object sender, RoutedEventArgs e)
{
  var client = new TestService.ServiceClient();
  var composite = new TestService.CompositeType();
  label.Content = client.GetData(12345);
  composite.BoolValue = checkBox.IsChecked == true;
  composite.StringValue = textBox.Text;
  composite = client.GetDataUsingDataContract(composite);
  label1.Content = composite.StringValue;
}

// 非同期
// async void を async Task に変更すると、メソッドとデリゲート
// 型との間に互換性が無いというエラーになる
private async void button1_Click(object sender, 
                                         RoutedEventArgs e)
{
  var client = new TestService.ServiceClient();
  var composite = new TestService.CompositeType();
  label.Content = await client.GetDataAsync(12345);
  composite.BoolValue = checkBox.IsChecked == true;
  composite.StringValue = textBox.Text;
  composite = 
    await client.GetDataUsingDataContractAsync(composite);
  label1.Content = composite.StringValue;
}

[同期呼び出し]ボタンをクリックすると上の画像のアプリは WCF サービスから応答が返ってくるまで操作できませんが、[非同期呼び出し]ボタンクリックなら操作可能で応答も期待通り返ってくることが確認できました。こんな簡単なことでホントにいいのか・・・という不安はありますが。(汗)

あと、Microsoft の文書「非同期プログラミングのベストプラクティス」には「async void メソッドよりも async Task メソッドを利用する」ということが書いてありますが、それはできなかったです(上のコードのコメントに書いたようにエラーになります)。解決方法があるのかどうかわかりませんが、分かったら追記することにします。

Tags: , ,

.NET Framework

文字列連結とインターンプール

by WebSurfer 2018年2月26日 15:37

文字列を複数に分けて連結する場合、インターンプールとの関係で注意した方がよさそうと思ったことを書きます。(元の話は MSDN Forum のスレッド「ASP.NET MVC で SQL文の塊を静的クラスで宣言することについて」です)

長い文字列を一行で書くと可読性が落ちるので、複数の行に分けて書くいうことがあると思います。

例えば、以下のコードのように、SQL Server に発行するクエリを複数の文字列に分けて、改行して、+ 演算子で連結するというケースを考えてみます。

その際、以下のコードの s2, s3 のようにするのは止めた方がよさそうです。その理由を以下に書きます。

string s0 = 
    "SELECT aaa, bbb, ccc FROM Table1 WHERE ddd = @ddd";

string s1 = "SELECT aaa, bbb, ccc " +
            "FROM Table1 " +
            "WHERE ddd = @ddd";

string s2 = "SELECT aaa, bbb, ccc ";
      s2 += "FROM Table1 ";
      s2 += "WHERE eee = @eee";

// StringBuilder で連結した結果はインターンされない。
// ただし Asspnd の引数の個々の文字列はインターンされる
string s3 = new StringBuilder().
                Append("SELECT aaa, bbb, ccc ").
                Append("FROM Table1 ").
                Append("WHERE fff = @fff").
                ToString();

// t1 は "WHERE ddd = @ddd"
string t1 = new StringBuilder().
                Append("WHERE ddd "). Append("= @ddd").
                ToString();

// t2 は "WHERE eee = @eee"
string t2 = new StringBuilder().
                Append("WHERE eee ").Append("= @eee").
                ToString();

// t3 は "WHERE fff = @fff"
string t3 = new StringBuilder().
                Append("WHERE fff ").Append("= @fff").
                ToString();

Console.WriteLine("s0 と s1 は同一オブジェクトを指す: {0}", 
                (object)s1 == (object)s0);

Console.WriteLine("t1 はインターンされている: {0}", 
                (string.IsInterned(t1) == null));

Console.WriteLine("t2 はインターンされている: {0}", 
                (string.IsInterned(t2) == null));

Console.WriteLine("t3 はインターンされている: {0}", 
                (string.IsInterned(t3) == null));


// 結果は:
// s0 と s1 は同一オブジェクトを指す: True
// t1 はインターンされている: True
// t2 はインターンされている: False
// t3 はインターンされている: False

s0 のコードで文字列 "SELECT aaa, bbb, ccc FROM Table1 WHERE ddd = @ddd" はインターンプールに格納さます。

s1 のコードでは、"SELECT aaa, bbb, ccc ", "FROM Table1 ", "WHERE ddd = @ddd" という複数の文字列に分けて、それらを + 演算子で連結しています。しかしながら、個々の文字列がインターンされることはなく、コンパイラは連結後の結果だけを見て、s0 のコードで生成されインターンされた文字列の参照を s1 に代入するようです。

s0 と s1 は同じオブジェクトを指すこと(即ち、(object)s1 == (object)s0 は true になります)と、StringBuilder で生成した t1("WHERE ddd = @ddd" という文字列)がインターンされてないことがそれを裏付けていると思います。

しかし、s2, s3 の場合、t2, t3 がインターンされているという結果からみると、右辺の個々の文字列のオブジェクトも作られ、それらがインターンプールに格納されるという無駄なことが行われるようです。

ちなみに、インターンプールというのは、文字列を key に、ヒープ上の String オブジェクトのアドレスを value に持つハッシュテーブルのようなものだそうです。詳しくは MSDN ライブラリの記事「String.Intern メソッド」の記事の解説を見てください。

寿命についてもインターンプール特有のものがあるそうです。詳しくは上の記事の「パフォーマンスに関する考慮事項」のセクションを見てください。

Tags:

.NET Framework

HttpClient で WCF サービスを呼出

by WebSurfer 2018年2月24日 23:49

先の記事「HttpWebRequest で WCF サービスを呼出」では、JSON 文字列をデータとしてやり取りする WCF サービスのメソッドを HttpWebRequest / HttpWebResponse を利用して呼び出して JSON 文字列のデータを取得し、それを逆シリアル化して C# のオブジェクトに変換する方法を書きました。

HttpWebRequest / HttpWebResponse に代えて HttpClient を利用し、同じ WCF サービスのメソッドを呼び出してデータを取得するサンプルを書きます。 (呼び出し先の WCF サービスは「WCF と jQuery AJAX」を参照)

参考にしたのは Call a Web API From a .NET Client (C#) という記事ですので合わせて見ていただくと良いと思います。

まず、先の HttpWebRequest / HttpWebResponse を利用した記事と同様に、以下のクラス / データコントラクト定義を行います。

[DataContract]
public class RootObject
{
    // GetCarsByDoorsResult は WCF サービスメソッドに付与した
    // BodyStyle = WebMessageBodyStyle.WrappedRequest による
    // ラップの名前(ラップするのはセキュリティ対策)
    [DataMember]
    public List<Car> GetCarsByDoorsResult { get; set; }
}

[DataContract]
public class Car
{
    [DataMember]
    public string Make { get; set; }

    [DataMember]
    public string Model { get; set; }

    [DataMember]
    public int Year { get; set; }

    [DataMember]
    public int Doors { get; set; }

    [DataMember]
    public string Colour { get; set; }

    [DataMember]
    public float Price { get; set; }
}

参考にした Microsoft の記事ではデシリアライズに HttpContentExtensions.ReadAsAsync<T> メソッド (HttpContent) を使っていますが、今回の例のような入れ子になったオブジェクトはデシリアライズできないようです。

なので、先の記事と同様、DataContractJsonSerializer クラスを利用して応答ストリームに含まれる JSON 文字列を C# のオブジェクトにデシリアライズすることにしました。

HttpClient.SendAsync メソッドを利用して、JSON 文字列を WCF サービスの GetCarsByDoors(int doors) メソッドに POST 送信します。以下のコードの例では 5 ドア車を要求しています。

応答の HttpContent が HttpClient.SendAsync メソッドの戻り値として返されますので、DataContractJsonSerializer クラスを利用して応答ストリームに含まれる JSON 文字列を C# のオブジェクトにデシリアライズします。

詳しくは以下のサンプルコードとそれに付与したコメントを見てください。

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Threading.Tasks;
using System.Net.Http;
using System.IO;

namespace ConsoleAppHttpClientWCF
{
  class Program
  {
    static void Main(string[] args)
    {
      // GetCarsAsync メソッドの引数で 5 ドアを要求
      RootObject rootObject =
          GetCarsAsync(5).GetAwaiter().GetResult();
      ShowCars(rootObject);
    }

    static async Task<RootObject> GetCarsAsync(int doors)
    {
      using (HttpClient httpClient = new HttpClient())
      {
        string url = "http://.../GetCarsByDoors";

        // POST 送信を指定
        HttpRequestMessage request =
          new HttpRequestMessage(HttpMethod.Post, url);

        // POST 送信する JSON 文字列
        string postData = 
            "{\"doors\":" + doors.ToString() + "}";

        // Content-Type: application/json; charset=utf-8 が
        // 要求ヘッダに必要。それを POST 送信する JSON 文字
        // 列と共にここで設定
        request.Content = new StringContent(postData, 
                                          Encoding.UTF8, 
                                          "application/json");

        var response = await httpClient.SendAsync(request);

        // 応答のコンテンツを Stream として取得
        using (Stream responseStream = 
                await response.Content.ReadAsStreamAsync())
        {
          // JSON シリアライザの初期化
          DataContractJsonSerializer ser =
            new DataContractJsonSerializer(typeof(RootObject));

          // 応答のコンテンツを逆シリアル化して C# の
          // オブジェクトを取得
          RootObject rootObject =
              (RootObject)ser.ReadObject(responseStream);

          return rootObject;
        }                
      }
    }

    // コンソールに取得結果を書き出すためのヘルパメソッド
    static void ShowCars(RootObject rootObject)
    {
      foreach (Car car in rootObject.GetCarsByDoorsResult)
      {
        Console.WriteLine("Make:{0}, Model:{1}, Doors:{2}",
                          car.Make, car.Model, car.Doors);
      }
    }
  }

  /*
  結果は:
  Make:Audi, Model:A4, Doors:5
  Make:Ford, Model:Focus, Doors:5
  Make:Renault, Model:Laguna, Doors:5
  Make:Toyota, Model:Previa, Doors:5
  */
}

DataContractJsonSerializer クラスを利用したシリアル化 / 逆シリアル化については、MSDN ライブラリの記事「方法 : JSON データをシリアル化および逆シリアル化する」が参考になると思います。

なお、本題とは直接関係ないことですが、You're using HttpClient wrong and it is destabilizing your software によると、HttpClient の初期化と Dispose を繰り返すようなことをすると、socket が浪費されるという問題があるそうです。これは覚えておいた方がよさそうです。

Tags: ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2018年6月  >>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar