前回(Linq to 色々その3.汎用的な呼び出しのベースを作成する。)、ひとまずクエリを投げて取得する汎用的なものは作成できました。今回はページング対応のものを完成させることにします。ページングで汎用的なものを作ればグルメサーチも店名サーチも両方使えますね!
今回ページングするにあたってSkipとTakeは必要そうなのでInnermostWhereFinderをInnermostFinderとリネームして中身にSkip、Takeの取得を追加します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
namespace LinqToHotpepper
{
internal class InnermostFinder : ExpressionVisitor
{
private MethodCallExpression innermostWhereExpression;
private MethodCallExpression innermostSkipExpression;
private MethodCallExpression innermostTakeExpression;
public MethodCallExpression GetInnermostWhere(Expression expression)
{
Visit(expression);
return innermostWhereExpression;
}
public MethodCallExpression GetInnermostSkip(Expression expression)
{
Visit(expression);
return innermostSkipExpression;
}
public MethodCallExpression GetInnermostTake(Expression expression)
{
Visit(expression);
return innermostTakeExpression;
}
protected override Expression VisitMethodCall(MethodCallExpression expression)
{
if (expression.Method.Name == "Where")
innermostWhereExpression = expression;
if (expression.Method.Name == "Skip")
innermostSkipExpression = expression;
if (expression.Method.Name == "Take")
innermostTakeExpression = expression;
Visit(expression.Arguments[0]);
return expression;
}
}
}Modifierも併せて修正。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
namespace LinqToHotpepper
{
public static class ExpressionTreeModifier
{
public static ExpressionTreeModifier<T> Create<T>(IQueryable<T> query)
{
return new ExpressionTreeModifier<T>(query);
}
}
public class ExpressionTreeModifier<T> : ExpressionVisitor
{
private IQueryable<T> queryable;
internal ExpressionTreeModifier(IQueryable<T> query)
{
this.queryable = query;
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.Name == "Where")
return Visit(node.Arguments[0]);
if (node.Method.Name == "Skip")
return Visit(node.Arguments[0]);
if (node.Method.Name == "Take")
return Visit(node.Arguments[0]);
return base.VisitMethodCall(node);
}
protected override Expression VisitConstant(ConstantExpression c)
{
// Replace the constant Query<T> arg with the queryable Place collection.
if (c.Type == typeof(Query<T>))
return Expression.Constant(this.queryable);
else
return c;
}
}
}
Whereとおんなじ感じで追加。で、Whereの抽出部分を以前はQueryProviderBase<T>の中でやってたんですがSkipはページングしない場合は必要ないのでWhereの解釈部分も派生クラス側でやるように修正。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
namespace LinqToHotpepper
{
public abstract class QueryProviderBase<T> : IQueryProvider
{
public IQueryable CreateQuery(Expression expression)
{
Type elementType = TypeSystem.GetElementType(expression.Type);
try
{
return (IQueryable)Activator.CreateInstance(typeof(Query<>).MakeGenericType(elementType), new object[] { this, expression });
}
catch (System.Reflection.TargetInvocationException tie)
{
throw tie.InnerException;
}
}
// Queryable's collection-returning standard query operators call this method.
public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
{
return new Query<TResult>(this, expression);
}
public object Execute(Expression expression)
{
return this.Execute<object>(expression, false);
}
// Queryable's "single value" standard query operators call this method.
// It is also called from QueryableGourmetData.GetEnumerator().
public TResult Execute<TResult>(Expression expression)
{
bool IsEnumerable = (typeof(TResult).Name == "IEnumerable`1");
return this.Execute<TResult>(expression, IsEnumerable);
}
public TResult Execute<TResult>(Expression expression, bool IsEnumerable)
{
// The expression must represent a query over the data source.
if (!IsQueryOverDataSource(expression))
throw new InvalidProgramException("No query over the data source was specified.");
IQueryable<T> result = GetResult(expression).AsQueryable<T>();
var treeCopier = ExpressionTreeModifier.Create(result);
Expression newExpressionTree = treeCopier.Visit(expression);
// This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods.
if (IsEnumerable)
return (TResult)result.Provider.CreateQuery(newExpressionTree);
else
return (TResult)result.Provider.Execute(newExpressionTree);
}
private bool IsQueryOverDataSource(Expression expression)
{
// If expression represents an unqueried IQueryable data source instance,
// expression is of type ConstantExpression, not MethodCallExpression.
return (expression is MethodCallExpression);
}
protected abstract IEnumerable<T> GetResult(Expression expression);
}
}
あとはページング対応の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
{
/// <summary>
/// ファクトリクラス
/// </summary>
public static class PagingQueryProvider
{
/// <summary>
/// ファクトリメソッド
/// </summary>
/// <param name="apiKey">発行されたAPIキー</param>
/// <param name="baseUrl">リクエストURL</param>
/// <param name="name">抽出対象のXML要素名</param>
/// <param name="builder">結果のファクトリ</param>
public static PagingQueryProvider<T> Create<T>(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder)
{
return new PagingQueryProvider<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 PagingQueryProvider<T> Create<T>(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder, EqualsFinder<T> finder)
{
return new PagingQueryProvider<T>(apiKey, baseUrl, name, builder, finder);
}
}
public class PagingQueryProvider<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>
private int start;
/// <summary>取得中件数</summary>
private int index;
/// <summary>パラメータで指定された取得件数</summary>
private int? takeCount;
/// <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 PagingQueryProvider(string apiKey, string baseUrl, string name, Func<IEnumerable<XElement>, T> builder, EqualsFinder<T> finder)
{
this.start = 0;
this.key = apiKey;
this.baseUrl = baseUrl;
this.name = name;
this.resultBuilder = builder;
this.finder = finder;
}
protected override IEnumerable<T> GetResult(Expression expression)
{
InnermostFinder innerFinder = new InnermostFinder();
var sb = new StringBuilder(baseUrl);
sb.AppendFormat("?key={0}", key);
if (finder != null)
{
MethodCallExpression whereExpression = innerFinder.GetInnermostWhere(expression);
if (whereExpression != null)
{
LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
// Send the lambda expression through the partial evaluator.
lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
finder.Expression = lambdaExpression.Body;
foreach (var item in finder.Results)
{
sb.AppendFormat("&{0}={1}", item.Key, item.Value);
}
}
}
MethodCallExpression skipExpression = innerFinder.GetInnermostSkip(expression);
if (skipExpression != null)
{
this.start = int.Parse(ExpressionTreeHelpers.GetValueFromExpression(skipExpression.Arguments[1]));
}
MethodCallExpression takeExpression = innerFinder.GetInnermostTake(expression);
if (takeExpression != null)
{
this.takeCount = int.Parse(ExpressionTreeHelpers.GetValueFromExpression(takeExpression.Arguments[1]));
}
var url = sb.ToString();
var executeUrl = url + string.Format("&start={0}&count=30", start + 1);
var element = XDocument.Load(executeUrl);
//エラーチェック
var error = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}error").FirstOrDefault();
if (error != null) throw new InvalidQueryException(new Error(error).Message);
//クエリー条件にマッチする、検索結果の全件数
var total = int.Parse(element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_available").FirstOrDefault().Value);
//このXMLに含まれる検索結果の件数クエリー条件にマッチする、検索結果の全件数
var returned = int.Parse(element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_returned").FirstOrDefault().Value);
//検索結果の開始位置
var resultStart = int.Parse(element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}results_start").FirstOrDefault().Value);
var collection = element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}" + name);
var limit =total;
if (takeCount.HasValue) limit = takeCount.Value + start;
//残りがあるなら残りのリクエストをシーケンスにくっつけておく
if (resultStart + returned < limit)
{
//次の呼び出しのためにカウンタを進めておく
index = start + returned;
collection = collection.Concat(this.GetResultCore(url));
}
if (takeCount.HasValue)
{
collection.Take(takeCount.Value);
}
yield return resultBuilder(collection);
}
/// <summary>
/// 繰り返し部分の取得実体
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private IEnumerable<XElement> GetResultCore(string url)
{
//取得ループ開始
while (true)
{
var executeUrl = url + string.Format("&start={0}&count=30", index + 1);
var element = XDocument.Load(executeUrl);
//エラーチェック
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/}" + name);
var limit = int.Parse(total.Value);
if (takeCount.HasValue) limit = takeCount.Value + start;
foreach (var item in collection)
{
index++;
if(index > limit) yield break;
yield return item;
}
}
}
}
}
ひとまずこれで完成。
他のWebServiceもこれらをベースにすればLinq化するのも楽かもしれません。まぁWebはXML or JSONで十分って気がしないでもないですが^^;