前回(Linq to 色々)の続きです。
あれだけではLinqっぽさがまるで出てないので今回はExpressionの解釈も進めましょう。QueryProviderBaseの中でInnermostWhereFinderを使って最も内側のWhere句を拾うところまではすでに入っているのであとはWhereの解釈になります。元記事ではLocationFinderなるExpressionVisitorを継承したクラスがその役目を負っていました。これも型特化しているのでジェネリックに書き換えてみました。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
namespace LinqToHotpepper
{
public static class EqualsFinder
{
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="exp">走査対象のExpressionTree</param>
/// <param name="name">プロパティ名</param>
/// <returns></returns>
public static EqualsFinder<T> Creater<T>(Expression exp, params string[] name)
{
return new EqualsFinder<T>(exp, name);
}
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="P"></typeparam>
/// <param name="exp">走査対象のExpressionTree</param>
/// <param name="properties"></param>
/// <returns></returns>
public static EqualsFinder<T> Creater<T, P>(Expression exp, params Expression<Func<T, P>>[] properties)
{
string[] names = new string[properties.Length];
for (int i = 0; i < properties.Length; i++)
{
names[i] = ((MemberExpression)properties[i].Body).Member.Name;
}
return new EqualsFinder<T>(exp, names);
}
}
public class EqualsFinder<T> : ExpressionVisitor
{
private Expression expression;
private Dictionary<string,string> results;
private string[] name;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="exp">走査対象のExpressionTree</param>
/// <param name="name">プロパティ名</param>
public EqualsFinder(Expression exp, string[] name)
{
this.expression = exp;
this.name = name;
}
/// <summary>
/// 検索した結果
/// </summary>
public Dictionary<string, string> Results
{
get
{
if (results == null)
{
results = new Dictionary<string, string>();
this.Visit(this.expression);
}
return this.results;
}
}
protected override Expression VisitBinary(BinaryExpression be)
{
if (be.NodeType == ExpressionType.Equal)
{
foreach (var item in name)
{
if (ExpressionTreeHelpers.IsMemberEqualsValueExpression(be, typeof(T), item))
{
results[item] = ExpressionTreeHelpers.GetValueFromEqualsExpression(be, typeof(T), item);
return be;
}
}
return base.VisitBinary(be);
}
else
return base.VisitBinary(be);
}
}
}あとで使いやすいように特定の型のプロパティを使用してDictionaryに突っ込んでいく形としました。今回はちょっと複雑めのAPIとして店名サーチAPIを使用します。ホットペッパーAPIリファレンス
呼び出しで帰ってくる型やらを定義します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
namespace LinqToHotpepper
{
/// <summary>
/// WebService呼び出しに使用する結果型。(再内のWhereでの条件がリクエストに使用される)
/// </summary>
public class ShopResult
{
public string keyword { get; set; }
public string tel { get; set; }
public IEnumerable<ShopInfo> Results { get; set; }
}
public class ShopInfo
{
public string Id { get; set; }
public string Name { get; set; }
public string NameKana { get; set; }
public string Address { get; set; }
public Genre Genre { get; set; }
public Urls Urls { get; set; }
public string Desc { get; set; }
public ShopInfo(XElement element)
{
this.Id = (string)element.Element(element.GetDefaultNamespace() + "id");
this.Name = (string)element.Element(element.GetDefaultNamespace() + "name");
this.NameKana = (string)element.Element(element.GetDefaultNamespace() + "name_kana");
this.Address = (string)element.Element(element.GetDefaultNamespace() + "address");
this.Genre = new Genre(element.Element(element.GetDefaultNamespace() + "genre"));
this.Urls = new Urls(element.Element(element.GetDefaultNamespace() + "urls"));
this.Desc = (string)element.Element(element.GetDefaultNamespace() + "desc");
}
}
public class Genre
{
public string Name { get; set; }
public Genre(XElement element)
{
this.Name = (string)element.Element(element.GetDefaultNamespace() + "name");
}
}
public class Urls
{
public string Pc { get; set; }
public string Mobile { get; set; }
public string Qr { get; set; }
public string Sp { get; set; }
public Urls(XElement element)
{
this.Pc = (string)element.Element(element.GetDefaultNamespace() + "pc");
this.Mobile = (string)element.Element(element.GetDefaultNamespace() + "mobile");
this.Qr = (string)element.Element(element.GetDefaultNamespace() + "qr");
this.Sp = (string)element.Element(element.GetDefaultNamespace() + "sp");
}
}
}
APIのリファレンスでもわかるように<results>の中に件数やらがあった後に<shop>として繰り返し格納されます。ちょっと特殊なのがShopResult型。Web Serviceに投げるパラメータをクエリのWhereの条件として指定できるためには結果セットである必要があるのですが結果のなかに普通に結果と一体になっていると取得後の絞り込みとリクエストのパラメータと区別ができなくなるためIEnumerable<T>として結果セットを含む型を返すようにしました。取り出しはおなじみSelectManyです。
クエリ構文で書くと
var context = new HotpepperContext(<APIキー>);
var x =
from result in context.ShopInfo
from shop in result.Results
where result.keyword == "オーガニック 大阪"
select shop;
な感じですがfromが並ぶのってわかりにくいかもですね。。。
では実際に取得を行うProviderの中身を見ていきましょう。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Xml;
using System.Net;
namespace LinqToHotpepper
{
public class ShopInfoQueryProvider : QueryProvider<ShopResult>
{
/// <summary>発行されたAPIキー</summary>
private string key;
/// <summary>開始件数</summary>
private int start;
/// <summary>読み出し件数(最小1、最大30)</summary>
private int count;
/// <summary>条件抽出を行うFinder</summary>
private EqualsFinder<ShopResult> finder;
/// <summary>リクエストURL</summary>
private const string baseUrl = "http://webservice.recruit.co.jp/hotpepper/shop/v1/";
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="apiKey">発行されたAPIキー</param>
public ShopInfoQueryProvider(string apiKey)
{
key = apiKey;
}
/// <summary>
/// パラメータチェック
/// </summary>
/// <param name="finder"></param>
/// <returns></returns>
private bool IsValidParameter(EqualsFinder<ShopResult> finder)
{
return true;
}
protected override IEnumerable<ShopResult> GetResult(Expression expression)
{
// Get the place name(s) to query the Web service with.
this.finder = EqualsFinder.Creater(expression, (ShopResult o) => o.keyword);
//条件をチェック
if (!IsValidParameter(this.finder))
throw new InvalidQueryException("You must specify at least one place name in your query.");
this.start = 1;
this.count = 30;
var sb = new StringBuilder(baseUrl);
sb.AppendFormat("?key={0}", key);
if (finder.Results.ContainsKey("tel")) sb.AppendFormat("&tel={0}", finder.Results["tel"]);
if (finder.Results.ContainsKey("keyword")) sb.AppendFormat("&keyword={0}", finder.Results["keyword"]);
var url = sb.ToString();
var element = XDocument.Load(url + string.Format("&start={0}", start) + string.Format("&count={0}", count));
//エラーチェック
var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
if (error != null) throw new InvalidQueryException(new Error(error).Message);
//クエリー条件にマッチする、検索結果の全件数
var total = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_available").FirstOrDefault();
//このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
var returned = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_returned").FirstOrDefault();
//検索結果の開始位置
var resultStart = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_start").FirstOrDefault();
var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}shop")
.Select(x => new ShopInfo(x));
//残りがあるなら残りのリクエストをシーケンスにくっつけておく
if (int.Parse(resultStart.Value) + int.Parse(returned.Value) > int.Parse(total.Value))
{
//次の呼び出しのためにカウンタを進めておく
start += count;
collection = collection.Concat(this.GetResultCore(url));
}
//結果を返す
yield return new ShopResult()
{
tel = finder.Results.ContainsKey("tel") ? finder.Results["tel"] : string.Empty,
keyword = finder.Results.ContainsKey("keyword") ? finder.Results["keyword"] : string.Empty,
Results = collection,
};
}
/// <summary>
/// 繰り返し部分の取得実体
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private IEnumerable<ShopInfo> GetResultCore(string url)
{
//取得ループ開始
while (true)
{
var element = XDocument.Load(url + string.Format("&start={0}", start) + string.Format("&count={0}", count));
//エラーチェック
var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
if (error != null) throw new InvalidQueryException(new Error(error).Message);
//クエリー条件にマッチする、検索結果の全件数
var total = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_available").FirstOrDefault();
// このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
var returned = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_returned").FirstOrDefault();
//結果がなければ終了
if (total == null || total.Value == "0" || returned == null || returned.Value == "0") yield break;
var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}shop")
.Select(x => new ShopInfo(x));
foreach (var item in collection)
{
yield return item;
}
start += count;
}
}
}
}
リファレンスにあるように件数やらが返ったあとにshopが繰り返し来る形になります。また、パラメータにも取得開始行数、取得行数などがありページングしながら全部をIEnumerable越しに返すという部分を作りこみたいのでレスポンスを返し切ったら続きを取得というように書いてみます。(今回のAPIでは30件以上の問い合わせはそもそもエラーになってしまうのですが^^;;)
まずはリクエストを投げて件数などを見ながらShopInfoのイテレータに後続部分をConcatしながら返すという形で書いてみました。(今回はベタに1件目からとってますがこのあたりExpressionを解釈してSkip、Takeあたりを取れれば無駄のないリクエストができそうです)
で、実際の取得ですがこんな感じ
var shopInfo = new Query<ShopResult>(new ShopInfoQueryProvider(<APIキー>));
var query =
from result in shopInfo
from shop in result.Results
where result.keyword == "オーガニック 大阪"
select shop;
foreach (var item in query)
{
System.Diagnostics.Debug.WriteLine(item.Name);
}
ちょっとはLinqっぽくなってきましたね~。とはいえ今回のAPIはMAX30件という微妙な位置づけAPIなので本命はグルメサーチAPIなんですがパラメータが半端ないので次回に持ち越します。