前回(Linq to 色々その2)、前々回(Linq to 色々)に引き続きIQueryableの残りを作っていきます。
Where句を抽出してWeb Serviceのパラメータとするところまでは概ねうまくいっていたのですが気になる現象がみられました。
それはWhere指定した条件が最終的な列挙にも使用されているという現象です。(なので結果セットを作るところでkeywordとかを詰めなおしていた)
これはExpressionTreeからWhereを評価後に該当するWhere句を取っ払ってしまえばよく、そのためのExpressionTreeModifier<T>というクラスが存在していますので中身にWhere句部分を取っ払うようにしてみましょう。
Where句というのはWhereの抽出で行っていた中身にあるように実質はWhereメソッドの呼び出しになります。(Argument[0]が後続のSelect等のクエリ、Argument[1]がWhereの条件部分)なのでここをWhereの抽出時同様にVisitMethodCallをオーバライドするようにしましょう。こうすれば結果セットに対してのWhereはかからなくなるので期待通り結果のみつめればよくなります。
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.Name == "Where")
return Visit(node.Arguments[0]);
return base.VisitMethodCall(node);
}
で、いよいよ大物のグルメサーチAPIと行きたいところですがマスタ類も結局は必要になるので先にかたずけるとしましょう。前回ページング対応のものは作ったのですが同じ形でAPI毎に作るというのも面倒なのでほとんどのマスタ系呼び出しに共通するページングなしのQueryProviderを作成しましょう。
やりたいこととしては
- URLを切り替えれるようにする
- 結果のXMLから抽出するタグ名を指定できるように
- 検索条件があれば指定できるように
あたりの事が出来ればひとまずよさそうです。というわけで。。。
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
{
/// <summary>
/// ファクトリクラス
/// </summary>
public static class SimpleQueryProvider
{
/// <summary>
/// ファクトリメソッド
/// </summary>
/// <param name="apiKey">発行されたAPIキー</param>
/// <param name="baseUrl">リクエストURL</param>
/// <param name="name">抽出対象のXML要素名</param>
/// <param name="builder">結果のファクトリ</param>
public static SimpleQueryProvider<T> Create<T>(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder)
{
return new SimpleQueryProvider<T>(apiKey, baseUrl, name, builder, null);
}
/// <summary>
/// ファクトリメソッド
/// </summary>
/// <param name="apiKey">発行されたAPIキー</param>
/// <param name="baseUrl">リクエストURL</param>
/// <param name="name">抽出対象のXML要素名</param>
/// <param name="builder">結果のファクトリ</param>
/// <param name="finder">要素から抽出を行うFinder</param>
public static SimpleQueryProvider<T> Create<T>(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder, EqualsFinder<T> finder)
{
return new SimpleQueryProvider<T>(apiKey, baseUrl, name, builder, finder);
}
}
public class SimpleQueryProvider<T> : QueryProviderBase<T>
{
/// <summary>発行されたAPIキー</summary>
private string key;
/// <summary>リクエストURL</summary>
private string baseUrl;
/// <summary>要素から抽出を行うFinder</summary>
private EqualsFinder<T> finder;
/// <summary>抽出対象のXML要素名</summary>
private string name;
/// <summary>結果セットのファクトリ</summary>
private Func<IEnumerable<XElement>, T> resultBuilder;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="apiKey">発行されたAPIキー</param>
/// <param name="baseUrl">リクエストURL</param>
/// <param name="name">抽出対象のXML要素名</param>
/// <param name="builder">結果のファクトリ</param>
/// <param name="finder">要素から抽出を行うFinder</param>
public SimpleQueryProvider(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder, EqualsFinder<T> finder)
{
this.key = apiKey;
this.baseUrl = baseUrl;
this.name = name;
this.resultBuilder = builder;
this.finder = finder;
}
/// <summary>
/// パラメータチェック
/// </summary>
/// <param name="finder"></param>
/// <returns></returns>
private bool IsValidParameter(EqualsFinder<ShopResult> finder)
{
return true;
}
protected override IEnumerable<T> GetResult(Expression expression)
{
var sb = new StringBuilder(baseUrl);
sb.AppendFormat("?key={0}", key);
if (finder != null)
{
finder.Expression = expression;
foreach (var item in finder.Results)
{
sb.AppendFormat("&{0}={1}", item.Key, item.Value);
}
}
var element = XDocument.Load(sb.ToString());
//エラーチェック
var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
if (error != null) throw new InvalidQueryException(new Error(error).Message);
var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}" + name);
yield return resultBuilder(collection);
}
}
}
SimpleQueryProvider<T>ですがQueryProviderBase<T> (abstractだったのでQueryProviderからQueryProviderBaseに名称変えました)をシンプルに実装しただけですね。同名のSimpleQueryProviderクラスはSimpleQueryProvider<T>に対するファクトリです。ジェネリッククラスのコンストラクタはいちいち<T>を書かないとダメなんですがジェネリックメソッドは引数からの型推論が効くのでジェネリックメソッドにはだいたい作っちゃうことが多いです。
こうすることでnew Query<ResultBase<CodeName>>(new SimpleQueryProvider<ResultBase<CodeName>>(...
なんて書き方がQuery.Create(SimpleQueryProvider.Create(...ですんじゃうので。で、呼び出し方はそのまんまですが
var foodQuery = Query.Create(SimpleQueryProvider.Create(
<APIキー>,
"http://webservice.recruit.co.jp/hotpepper/food/v1/",
"food",
x => new FoodResult()
{
Results = x.Select(_ => new Food(_))
},
EqualsFinder.Creater((FoodResult _) => _.code
, _ => _.keyword,
_ => _.food_category)));
var query = from result in foodQuery
from food in result.Results
where result.keyword == "料理"
select food;
foreach (var item in query)
{
System.Diagnostics.Debug.WriteLine(item.Name);
}
みたいな感じにすることでOK。ページング対応の部分も必要なので前回のShopInfoQueryProviderをもとに今回のような汎用的な検索のページングバージョンを作成しましょう。
長くなったので続く。