読者です 読者をやめる 読者になる 読者になる

LINQ to Trac

C# trac

先日、TracのチケットをLINQで集計するプログラムを書いてみたのですが、いったんチケットを全部取得してから集計するというかっこ悪いものでした。(LINQでTracのチケット集計 - ZOETROPEの日記)

そのため全取得ではなく、whereで渡されたラムダ式からTracのクエリを生成するようなことをしたいと思っていたのですが、ラムダ式から文字列に変換する方法が分からずに放置していました。

そしたらコメント欄で、id:matarilloさんから「ラムダ式は関数としてではなく、式ツリーとして扱うことができるよ」と教えていただきました。

式ツリー(Expression)なるものは全然知らなかったのですが、教えていただいたリンク(d:id:okazuki:20070506:1178416670d:id:coma2n:20080121:1200917603)を参考にしつつ、LINQ to Tracを作ってみました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Linq.Expressions;

namespace LinqToTrac
{
    public class TicketTable : IQueryable<TicketInfo>, IQueryProvider
    {
        private TicketManager m_manager;
        private Expression m_expression;

        protected internal TicketTable(String uri, String user, String pass)
        {
            m_manager = new TicketManager();
            m_manager.Connect(uri, user, pass);
        }

        protected TicketTable(TicketManager mgr, Expression exp)
        {
            m_manager = mgr;
            m_expression = exp;
        }

        public IEnumerator<TicketInfo> GetEnumerator()
        {
            return Provider.Execute<IEnumerator<TicketInfo>>(Expression);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        public Type ElementType
        {
            get { return typeof(TicketInfo); }
        }

        public Expression Expression
        {
            get { return m_expression; }
        }

        public IQueryProvider Provider
        {
            get { return this; }
        }

        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            return (IQueryable<TElement>)CreateQuery(expression);
        }

        public IQueryable CreateQuery(Expression expression)
        {
            return new TicketTable(m_manager, expression);
        }

        public TResult Execute<TResult>(Expression expression)
        {
            return (TResult)Execute(expression);
        }

        public object Execute(Expression expression)
        {
            if (expression == null)
            {
                return GetTicketList(@"order=id").GetEnumerator();
            }
            else
            {
                String result = ParseExpression(expression, String.Empty);
                return GetTicketList(result).GetEnumerator();
            }
        }

        private String ParseExpression(Expression expr, String query)
        {
            BinaryExpression be;
            switch (expr.NodeType)
            {
                case ExpressionType.Lambda:
                    LambdaExpression le = (LambdaExpression)expr;
                    query = ParseExpression(le.Body, query);
                    break;
                case ExpressionType.Equal:
                    be = (BinaryExpression)expr;
                    query = ParseExpression(be.Left, query);
                    query += @"=";
                    query = ParseExpression(be.Right, query);
                    break;
                case ExpressionType.AndAlso:
                    be = (BinaryExpression)expr;
                    query = ParseExpression(be.Left, query);
                    query += @"&";
                    query = ParseExpression(be.Right, query);
                    break;
                case ExpressionType.MemberAccess:
                    MemberExpression me = (MemberExpression)expr;
                    query += me.Member.Name;
                    break;
                case ExpressionType.Constant:
                    ConstantExpression ce = (ConstantExpression)expr;
                    query += ce.Value.ToString();
                    break;
            }
            return query;
        }

        private IEnumerable<TicketInfo> GetTicketList(String query)
        {
            int[] ticketlist = m_manager.FindTicketIndex(query);

            foreach (int id in ticketlist)
            {
                yield return m_manager.GetTicket(id);
            }
        }
    }

    public static class TicketExtension
    {
        public static IQueryable<TicketInfo> Where(
            this IQueryable<TicketInfo> collection,
            Expression<Predicate<TicketInfo>> expression)
        {
            return collection.Provider.CreateQuery<TicketInfo>(expression);
        }
    }
}

ParseExpressionというメソッドで、式ツリーからTracのクエリ文字列に変換しています。

実装がかなりテキトーなので、大したことはできませんが、以下のようにしてチケットを取得することができます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LinqToTrac
{
    class Program
    {
        static void Main(string[] args)
        {
            TicketTable table = 
                new TicketTable("http://localhost/trac/SampleProject/login/xmlrpc", "admin", "admin");

            var result = from ticket in table
                         where ticket.status == "new" && ticket.owner == "zoetrope"
                         select ticket;

            foreach (var i in result)
            {
                Console.WriteLine("id = " + i.id + " name = " + i.summary);
            }

            Console.ReadKey();
        }
    }
}

いやー、LINQはホントに面白いですね。ここまで自由に作れると、いろんなところに応用できそうだなぁ。


あと、需要はないかもしれませんが、一応ソースを置いておきます。