あけましたね。おめでとうございます。
今年も色々と技術や製品が出てくると思いますがおいてかれないようにしがみつけるよう頑張りたいと思います。
で、今年初めてのエントリなんですが大好きなLinqのエントリ。
巷にはいろんなWeb Serviceがありますよね。AmazonしかりGoogleしかり。楽天やらもあるのかな?しかしながらLinqからWeb Serviceってちょっと触りにくいんですよね。当然結果自体はXMLに流し込むなりでIEnumerable<XElement>にしちまえばオッケーなんですが一回のリクエストなりの件数ってそう多くは取れないしそもそもあまりLinqしてる感が少ないのが不満でした。(だって呼び出しでほとんどの苦労が集約されてるし。。。)
じゃーやってみんべかとおもって調べてみました。
ん~なんか色々クラスが分かれててややこしい。。。が、くじけずそのまま書いてみる。Web Service部分はXDocument.Loadでいっか。
まずはIQueryable or IOrderedQueryable<TData>があってIQueryProviderとExpressionを内包してると。でもってIQueryProviderからTerraServerQueryContextのstaticなメソッド類に委譲している感じ。肝としてはこいつらの実装をやればよさげな感じ。元記事でヘルパとなってるExpressionTreeHelpers.csやらEvaluator.csやらTypeSystem.csやらInvalidQueryException.csはひとまずそのまま持ってくるとしてTerraServerQueryContextから呼び出されているInnermostWhereFinder.csがWhere句を拾ってくるところ見たい。まぁWhereした内容でURLたたきたいのでひとまずこのまんま。実際にExpressionTreeから色々拾ってくるであろうLocationFinderは後で類似のものをこさえましょう。
次はExpressionTreeModifier.csですがガッツリと型定義が入ってますがやる内容は汎用的っぽいのでジェネリックで型を追い出しときました。
こんな感じで。
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 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;
}
}
}
名前空間にあるように今回はHotpepperのWeb Serviceに対して呼び出しをかけて見ようと思います。
でまずはIQueryableを実装してるQueryableTerraServerData<TData>ってのをクラス名を変えて実装。これ自体も基本的には汎用的なのでIQueryProviderを受け取るコンストラクタを追加。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Linq.Expressions;
namespace LinqToHotpepper
{
public class Query<TData> : IOrderedQueryable<TData>
{
#region Constructors
/// <summary>
/// This constructor is called by the client to create the data source.
/// </summary>
public Query(IQueryProvider provider)
{
Provider = provider;
Expression = Expression.Constant(this);
}
/// <summary>
/// This constructor is called by Provider.CreateQuery().
/// </summary>
/// <param name="expression"></param>
public Query(IQueryProvider provider, Expression expression)
{
if (provider == null)
{
throw new ArgumentNullException("provider");
}
if (expression == null)
{
throw new ArgumentNullException("expression");
}
if (!typeof(IQueryable<TData>).IsAssignableFrom(expression.Type))
{
throw new ArgumentOutOfRangeException("expression");
}
Provider = provider;
Expression = expression;
}
#endregion
#region Properties
public IQueryProvider Provider { get; private set; }
public Expression Expression { get; private set; }
public Type ElementType
{
get { return typeof(TData); }
}
#endregion
#region Enumerators
public IEnumerator<TData> GetEnumerator()
{
return (Provider.Execute<IEnumerable<TData>>(Expression)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return (Provider.Execute<IEnumerable>(Expression)).GetEnumerator();
}
#endregion
}
}
お次はIQueryProviderを実装しているところ。元の記事ではTerraServerQueryProviderにあたる部分で処理実態はTerraServerQueryContextに委譲してたやつです。さすがにサービスごとにこのペアを起こしてくのは面倒なので共通部分をベースクラスとしてくくりだします。共通っぽいところは念力で。。。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
namespace LinqToHotpepper
{
public abstract class QueryProvider<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.");
// Find the call to Where() and get the lambda expression predicate.
InnermostWhereFinder whereFinder = new InnermostWhereFinder();
MethodCallExpression whereExpression = whereFinder.GetInnermostWhere(expression);
Expression body = null;
if (whereExpression != null)
{
LambdaExpression lambdaExpression = (LambdaExpression)((UnaryExpression)(whereExpression.Arguments[1])).Operand;
// Send the lambda expression through the partial evaluator.
lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
body = lambdaExpression.Body;
}
IQueryable<T> result = GetResult(body).AsQueryable<T>();
// Copy the expression tree that was passed in, changing only the first
// argument of the innermost MethodCallExpression.
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);
}
}
やってることはジェネリックで型をくくり出した上で結果取得部分を抽象メソッドにしてやる感じ。こいつを継承しておいてからIEnumerable<T> GetResult(Expression expression); の中でサービス呼び出して結果を組み立てればOKですね。元記事では結果を完全に配列に仕立ててから送り出ししてますが気休め程度にイテレータ経由にしときます。(XDocumentなのでDOM構築しちゃうので。たくさんの件数が一度に取れるようなサービスの場合は@neueさん所のXStreamingReaderとか使うといいと思うよ!!)
で、実際に呼び出しをするやつを継承で作ります。ひとまずパラメータを要求しない呼び出し用に汎用的なものを一つこさえましょう。
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 MasterQueryProvider<T> : QueryProvider<T>
{
/// <summary>発行されたAPIキー</summary>
private string key;
/// <summary>リクエストURL</summary>
private string baseUrl;
/// <summary>抽出対象のXML要素名</summary>
private string name;
/// <summary>結果のファクトリ</summary>
private Func<XElement, T> builder;
/// <summary>
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="apiKey">発行されたAPIキー</param>
/// <param name="baseUrl">リクエストURL</param>
/// <param name="name">抽出対象のXML要素名</param>
/// <param name="builder">結果のファクトリ</param>
public MasterQueryProvider(string apiKey, string baseUrl, string name, Func<XElement, T> builder)
{
this.key = apiKey;
this.baseUrl = baseUrl;
this.name = name;
this.builder = builder;
}
/// <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);
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);
foreach (var item in element.Descendants("{http://webservice.recruit.co.jp/HotPepper/}" + name))
{
yield return builder(item);
}
}
}
}
ちょっとごちゃごちゃしてますがWeb Serviceを使うのに必要なAPIキーのみを渡している感じです。注意点としてレスポンスが名前空間付なのでDescendantsのところが("{http://webservice.recruit.co.jp/HotPepper/}" + nameのような名前空間付のXNameになっているとこですかね。
ここまでできればクエリ式経由で一応結果が取れます。(何も使ってないからLinqの意味はない)
var context = new Query<CodeName>(new MasterQueryProvider<CodeName>(
<APIキーをここに書く>,
"http://webservice.recruit.co.jp/hotpepper/budget/v1/",
"budget",
x => new CodeName(x)));
var query = from result in context
select result;
foreach (var item in query)
{
System.Diagnostics.Debug.WriteLine(item.Name);
}public class CodeName
{
public string Code { get; set; }
public string Name { get; set; }
public CodeName(XElement element)
{
this.Code = (string)element.Element(element.GetDefaultNamespace() + "code");
this.Name = (string)element.Element(element.GetDefaultNamespace() + "name");
}
}結果の型の組み立てのとこもGetDefaultNamespaceが必要なので注意。
ひとまずここまで+αのソースを置いておきます。
つづく