File: System\Data\Services\Client\ALinq\ExpressionWriter.cs
Project: ndp\fx\src\DataWeb\Client\System.Data.Services.Client.csproj (System.Data.Services.Client)
//---------------------------------------------------------------------
// <copyright file="ExpressionWriter.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Serializes sub-expressions as query options in the URI.
// </summary>
//
// @owner  Microsoft
//---------------------------------------------------------------------
 
namespace System.Data.Services.Client
{
    #region Namespaces.
 
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.Text;
 
    #endregion Namespaces.
 
    /// <summary>
    /// Special visitor to serialize supported expression as query parameters
    /// in the generated URI.
    /// </summary>
    internal class ExpressionWriter : DataServiceALinqExpressionVisitor
    {
        #region Private fields.
 
        /// <summary>Internal buffer.</summary>
        private readonly StringBuilder builder;
 
        /// <summary>Data context used to generate type names for types.</summary>
        private readonly DataServiceContext context;
 
        /// <summary>Stack of expressions being visited.</summary>
        private readonly Stack<Expression> expressionStack;
 
        /// <summary>set if can't translate expression</summary>
        private bool cantTranslateExpression;
 
        /// <summary>Parent expression of the current expression (expression.Peek()); possibly null.</summary>
        private Expression parent;
 
        #endregion Private fields.
 
        /// <summary>
        /// Creates an ExpressionWriter for the specified <paramref name="context"/>.
        /// </summary>
        /// <param name='context'>Data context used to generate type names for types.</param>
        private ExpressionWriter(DataServiceContext context)
        {
            Debug.Assert(context != null, "context != null");
            this.context = context;
            this.builder = new StringBuilder();
            this.expressionStack = new Stack<Expression>();
            this.expressionStack.Push(null);
        }
 
        /// <summary>
        /// Serializes an expression to a string
        /// </summary>
        /// <param name='context'>Data context used to generate type names for types.</param>
        /// <param name="e">Expression to serialize</param>
        /// <returns>serialized expression</returns>
        internal static string ExpressionToString(DataServiceContext context, Expression e)
        {
            ExpressionWriter ew = new ExpressionWriter(context);
            string serialized = ew.Translate(e);
            if (ew.cantTranslateExpression)
            {
                throw new NotSupportedException(Strings.ALinq_CantTranslateExpression(e.ToString()));
            }
 
            return serialized;
        }
 
        /// <summary>Main visit method.</summary>
        /// <param name="exp">Expression to visit</param>
        /// <returns>Visited expression</returns>
        internal override Expression Visit(Expression exp)
        {
            this.parent = this.expressionStack.Peek();
            this.expressionStack.Push(exp);
            Expression result = base.Visit(exp);
            this.expressionStack.Pop();
            return result;
        }
 
        /// <summary>
        /// ConditionalExpression visit method
        /// </summary>
        /// <param name="c">The ConditionalExpression expression to visit</param>
        /// <returns>The visited ConditionalExpression expression </returns>
        internal override Expression VisitConditional(ConditionalExpression c)
        {
            this.cantTranslateExpression = true;
            return c;
        }
 
        /// <summary>
        /// LambdaExpression visit method
        /// </summary>
        /// <param name="lambda">The LambdaExpression to visit</param>
        /// <returns>The visited LambdaExpression</returns>
        internal override Expression VisitLambda(LambdaExpression lambda)
        {
            this.cantTranslateExpression = true;
            return lambda;
        }
 
        /// <summary>
        /// NewExpression visit method
        /// </summary>
        /// <param name="nex">The NewExpression to visit</param>
        /// <returns>The visited NewExpression</returns>
        internal override NewExpression VisitNew(NewExpression nex)
        {
            this.cantTranslateExpression = true;
            return nex;
        }
 
        /// <summary>
        /// MemberInitExpression visit method
        /// </summary>
        /// <param name="init">The MemberInitExpression to visit</param>
        /// <returns>The visited MemberInitExpression</returns>
        internal override Expression VisitMemberInit(MemberInitExpression init)
        {
            this.cantTranslateExpression = true;
            return init;
        }
 
        /// <summary>
        /// ListInitExpression visit method
        /// </summary>
        /// <param name="init">The ListInitExpression to visit</param>
        /// <returns>The visited ListInitExpression</returns>
        internal override Expression VisitListInit(ListInitExpression init)
        {
            this.cantTranslateExpression = true;
            return init;
        }
 
        /// <summary>
        /// NewArrayExpression visit method
        /// </summary>
        /// <param name="na">The NewArrayExpression to visit</param>
        /// <returns>The visited NewArrayExpression</returns>
        internal override Expression VisitNewArray(NewArrayExpression na)
        {
            this.cantTranslateExpression = true;
            return na;
        }
 
        /// <summary>
        /// InvocationExpression visit method
        /// </summary>
        /// <param name="iv">The InvocationExpression to visit</param>
        /// <returns>The visited InvocationExpression</returns>
        internal override Expression VisitInvocation(InvocationExpression iv)
        {
            this.cantTranslateExpression = true;
            return iv;
        }
 
        /// <summary>
        /// Input resource set references are intentionally omitted from the URL string. 
        /// </summary>
        /// <param name="ire">The input reference</param>
        /// <returns>The same input reference expression</returns>
        internal override Expression VisitInputReferenceExpression(InputReferenceExpression ire)
        {
            // This method intentionally does not write anything to the URI.
            // This is how 'Where(<input>.Id == 5)' becomes '$filter=Id eq 5'.
            Debug.Assert(ire != null, "ire != null");
            if (this.parent == null || this.parent.NodeType != ExpressionType.MemberAccess)
            {
                // Ideally we refer to the parent expression as the un-translatable one,
                // because we cannot reference 'this' as a standalone expression; however
                // if the parent is null for any reasonn, we fall back to the expression itself.
                string expressionText = (this.parent != null) ? this.parent.ToString() : ire.ToString();
                throw new NotSupportedException(Strings.ALinq_CantTranslateExpression(expressionText));
            }
 
            return ire;
        }
 
        /// <summary>
        /// MethodCallExpression visit method
        /// </summary>
        /// <param name="m">The MethodCallExpression expression to visit</param>
        /// <returns>The visited MethodCallExpression expression </returns>
        internal override Expression VisitMethodCall(MethodCallExpression m)
        {
            string methodName;
            if (TypeSystem.TryGetQueryOptionMethod(m.Method, out methodName))
            {
                this.builder.Append(methodName);
                this.builder.Append(UriHelper.LEFTPAREN);
 
                // There is a single function, 'substringof', which reorders its argument with 
                // respect to the CLR method. Thus handling it as a special case rather than
                // using a more general argument reordering mechanism.
                if (methodName == "substringof")
                {
                    Debug.Assert(m.Method.Name == "Contains", "m.Method.Name == 'Contains'");
                    Debug.Assert(m.Object != null, "m.Object != null");
                    Debug.Assert(m.Arguments.Count == 1, "m.Arguments.Count == 1");
                    this.Visit(m.Arguments[0]);
                    this.builder.Append(UriHelper.COMMA);
                    this.Visit(m.Object);
                }
                else
                {
                    if (m.Object != null)
                    {
                        this.Visit(m.Object);
                    }
 
                    if (m.Arguments.Count > 0)
                    {
                        if (m.Object != null)
                        {
                            this.builder.Append(UriHelper.COMMA);
                        }
 
                        for (int ii = 0; ii < m.Arguments.Count; ii++)
                        {
                            this.Visit(m.Arguments[ii]);
                            if (ii < m.Arguments.Count - 1)
                            {
                                this.builder.Append(UriHelper.COMMA);
                            }
                        }
                    }
                }
 
                this.builder.Append(UriHelper.RIGHTPAREN);
            }
            else
            {
                this.cantTranslateExpression = true;
            }
 
            return m;
        }
 
        /// <summary>
        /// Serializes an MemberExpression to a string
        /// </summary>
        /// <param name="m">Expression to serialize</param>
        /// <returns>MemberExpression</returns>
        internal override Expression VisitMemberAccess(MemberExpression m)
        {
            if (m.Member is FieldInfo)
            {
                throw new NotSupportedException(Strings.ALinq_CantReferToPublicField(m.Member.Name));
            }
 
            Expression e = this.Visit(m.Expression);
 
            // if this is a Nullable<T> instance, don't write out /Value since not supported by server
            if (m.Member.Name == "Value" && m.Member.DeclaringType.IsGenericType
                && m.Member.DeclaringType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                return m;
            }
 
            if (!IsInputReference(e))
            {
                this.builder.Append(UriHelper.FORWARDSLASH);
            }
 
            this.builder.Append(m.Member.Name);
 
            return m;
        }
 
        /// <summary>
        /// ConstantExpression visit method
        /// </summary>
        /// <param name="c">The ConstantExpression expression to visit</param>
        /// <returns>The visited ConstantExpression expression </returns>
        internal override Expression VisitConstant(ConstantExpression c)
        {
            string result = null;
            if (c.Value == null)
            {
                this.builder.Append(UriHelper.NULL);
                return c;
            }
            else if (!ClientConvert.TryKeyPrimitiveToString(c.Value, out result))
            {
                throw new InvalidOperationException(Strings.ALinq_CouldNotConvert(c.Value));
            }
 
            Debug.Assert(result != null, "result != null");
            this.builder.Append(result);
            return c;
        }
 
        /// <summary>
        /// Serializes an UnaryExpression to a string
        /// </summary>
        /// <param name="u">Expression to serialize</param>
        /// <returns>UnaryExpression</returns>
        internal override Expression VisitUnary(UnaryExpression u)
        {
            switch (u.NodeType)
            {
                case ExpressionType.Not:
                    this.builder.Append(UriHelper.NOT);
                    this.builder.Append(UriHelper.SPACE);
                    this.VisitOperand(u.Operand);
                    break;
                case ExpressionType.Negate:
                case ExpressionType.NegateChecked:
                    this.builder.Append(UriHelper.SPACE);
                    this.builder.Append(UriHelper.NEGATE);
                    this.VisitOperand(u.Operand);
                    break;
                case ExpressionType.Convert:
                case ExpressionType.ConvertChecked:
                    if (u.Type != typeof(object))
                    {
                        this.builder.Append(UriHelper.CAST);
                        this.builder.Append(UriHelper.LEFTPAREN);
                        if (!IsInputReference(u.Operand))
                        {
                            this.Visit(u.Operand);
                            this.builder.Append(UriHelper.COMMA);
                        }
 
                        this.builder.Append(UriHelper.QUOTE);
                        this.builder.Append(this.TypeNameForUri(u.Type));
                        this.builder.Append(UriHelper.QUOTE);
                        this.builder.Append(UriHelper.RIGHTPAREN);
                    }
                    else
                    {
                        if (!IsInputReference(u.Operand))
                        {
                            this.Visit(u.Operand);
                        }
                    }
 
                    break;
                case ExpressionType.UnaryPlus:
                    // no-op always ignore.
                    break;
                default:
                    this.cantTranslateExpression = true;
                    break;
            }
 
            return u;
        }
 
        /// <summary>
        /// Serializes an BinaryExpression to a string
        /// </summary>
        /// <param name="b">BinaryExpression to serialize</param>
        /// <returns>serialized expression</returns>
        internal override Expression VisitBinary(BinaryExpression b)
        {
            this.VisitOperand(b.Left);
            this.builder.Append(UriHelper.SPACE);
            switch (b.NodeType)
            {
                case ExpressionType.AndAlso:
                case ExpressionType.And:
                    this.builder.Append(UriHelper.AND);
                    break;
                case ExpressionType.OrElse:
                case ExpressionType.Or:
                    this.builder.Append(UriHelper.OR);
                    break;
                case ExpressionType.Equal:
                    this.builder.Append(UriHelper.EQ);
                    break;
                case ExpressionType.NotEqual:
                    this.builder.Append(UriHelper.NE);
                    break;
                case ExpressionType.LessThan:
                    this.builder.Append(UriHelper.LT);
                    break;
                case ExpressionType.LessThanOrEqual:
                    this.builder.Append(UriHelper.LE);
                    break;
                case ExpressionType.GreaterThan:
                    this.builder.Append(UriHelper.GT);
                    break;
                case ExpressionType.GreaterThanOrEqual:
                    this.builder.Append(UriHelper.GE);
                    break;
                case ExpressionType.Add:
                case ExpressionType.AddChecked:
                    this.builder.Append(UriHelper.ADD);
                    break;
                case ExpressionType.Subtract:
                case ExpressionType.SubtractChecked:
                    this.builder.Append(UriHelper.SUB);
                    break;
                case ExpressionType.Multiply:
                case ExpressionType.MultiplyChecked:
                    this.builder.Append(UriHelper.MUL);
                    break;
                case ExpressionType.Divide:
                    this.builder.Append(UriHelper.DIV);
                    break;
                case ExpressionType.Modulo:
                    this.builder.Append(UriHelper.MOD);
                    break;
                case ExpressionType.ArrayIndex:
                case ExpressionType.Power:
                case ExpressionType.Coalesce:
                case ExpressionType.ExclusiveOr:
                case ExpressionType.LeftShift:
                case ExpressionType.RightShift:
                default:
                    this.cantTranslateExpression = true;
                    break;
            }
 
            this.builder.Append(UriHelper.SPACE);
            this.VisitOperand(b.Right);
            return b;
        }
 
        /// <summary>
        /// Serializes an TypeBinaryExpression to a string
        /// </summary>
        /// <param name="b">TypeBinaryExpression to serialize</param>
        /// <returns>serialized expression</returns>
        internal override Expression VisitTypeIs(TypeBinaryExpression b)
        {
            this.builder.Append(UriHelper.ISOF);
            this.builder.Append(UriHelper.LEFTPAREN);
 
            if (!IsInputReference(b.Expression))
            {
                this.Visit(b.Expression);
                this.builder.Append(UriHelper.COMMA);
                this.builder.Append(UriHelper.SPACE);
            }
 
            this.builder.Append(UriHelper.QUOTE);
            this.builder.Append(this.TypeNameForUri(b.TypeOperand));
            this.builder.Append(UriHelper.QUOTE);
            this.builder.Append(UriHelper.RIGHTPAREN);
 
            return b;
        }
 
        /// <summary>
        /// ParameterExpression visit method.
        /// </summary>
        /// <param name="p">The ParameterExpression expression to visit</param>
        /// <returns>The visited ParameterExpression expression </returns>
        internal override Expression VisitParameter(ParameterExpression p)
        {
            return p;
        }
 
        /// <summary>
        /// References to the current input - the resource set - do not appear in the URL.
        /// </summary>
        /// <param name="exp">The expression to test</param>
        /// <returns><c>true</c> if the expression represents a reference to the current (resource set) input; otherwise <c>false</c>.</returns>
        private static bool IsInputReference(Expression exp)
        {
            return (exp is InputReferenceExpression || exp is ParameterExpression);
        }
 
        /// <summary>Gets the type name to be used in the URI for the given <paramref name="type"/>.</summary>
        /// <param name="type">Type to get name for.</param>
        /// <returns>The name for the <paramref name="type"/>, suitable for including in a URI.</returns>
        private string TypeNameForUri(Type type)
        {
            Debug.Assert(type != null, "type != null");
            type = Nullable.GetUnderlyingType(type) ?? type;
 
            if (ClientConvert.IsKnownType(type))
            {
                if (ClientConvert.IsSupportedPrimitiveTypeForUri(type))
                {
                    return ClientConvert.ToTypeName(type);
                }
 
                // unsupported primitive type
                throw new NotSupportedException(Strings.ALinq_CantCastToUnsupportedPrimitive(type.Name));
            }
            else
            {
                return this.context.ResolveNameFromType(type) ?? type.FullName;
            }
        }
 
        /// <summary>
        /// Visits operands for Binary and Unary expressions.
        /// Will only output parens if operand is complex expression,
        /// this is so don't have unecessary parens in URI.
        /// </summary>
        /// <param name="e">The operand expression to visit</param>
        private void VisitOperand(Expression e)
        {
            if (e is BinaryExpression || e is UnaryExpression)
            {
                this.builder.Append(UriHelper.LEFTPAREN);
                this.Visit(e);
                this.builder.Append(UriHelper.RIGHTPAREN);
            }
            else
            {
                this.Visit(e);
            }
        }
 
        /// <summary>
        /// Serializes an expression to a string
        /// </summary>
        /// <param name="e">Expression to serialize</param>
        /// <returns>serialized expression</returns>
        private string Translate(Expression e)
        {
            this.Visit(e);
            return this.builder.ToString();
        }
    }
}