Tuesday, October 17, 2017

EPiFind AndAny/OrAny Filter Expression Builder

Extending the EPiServer Find Functionality to support OrAny or AndAny Filter Expression

The OrAny and AndAny filter for EPiFind will allow the developer to combine list of contents as And or Or with the other query conditions.

We had custom property Tag and TagList class which we wanted EPiFind to index so we can query the contents based on those filters.

The Tag and TagList class looks like below

Tag Class

[JsonConverter(typeof(TagJsonConverter))]
    public class Tag
    {
        private string text;
        private string value;
        private string tag;
        private bool isLoaded;

        public string Text
        {
            get
            {
                if (!isLoaded)
                    Load();

                return text;
            }

            set
            {
                text = value;
            }
        }

        public string Value
        {
            get
            {
                if (!isLoaded)
                    Load();

                return value;
            }

            set
            {
                this.value = value;
            }
        }

        public Tag()
        { }

        public Tag(string tag)
        {
            this.tag = tag;
        }

        public Tag(string value, string text)
        {
            isLoaded = true;
            this.value = value;
            this.text = text;
        }

        public string GetId(string type) => Value?.Substring(type.Length);

        public override string ToString()
        {
            return $"{Value}{Constants.Tags.NameDelimiter}{Text}";
        }

        private void Load()
        {
            isLoaded = true;
            if (!string.IsNullOrWhiteSpace(tag))
            {
                var parts = tag.Split(new[] { Constants.Tags.NameDelimiter }, 2);
                if (parts.Length == 1)
                {
                    text = parts[0];
                    value = string.Empty;
                }
                else
                {
                    value = parts[0];
                    text = parts[1];
                }
            }
        }

        public static Tag Parse(string tag)
        {
            return string.IsNullOrWhiteSpace(tag) ? null : new Tag(tag);
        }
    }


TagList Class

[JsonConverter(typeof(TagListJsonConverter))]
    public class TagList : IEnumerable
    {
        private Tag[] tags;
        private string tag;
        private bool isLoaded;

        public TagList() { }

        public TagList(string tag)
        {
            tags = new Tag[0];
            this.tag = tag;
        }

        public TagList(IEnumerable tags)
        {
            isLoaded = true;
            this.tags = tags.ToArray();
        }

        public override string ToString() => string.Join(Constants.Tags.ValueDelimiter.ToString(), this.Select(s => s.ToString()));

        public IEnumerator GetEnumerator()
        {
            if (!isLoaded)
                Load();

            return ((IEnumerable)this.tags).GetEnumerator();
        }

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

        private void Load()
        {
            isLoaded = true;
            if (!string.IsNullOrWhiteSpace(tag))
                tags = tag.Split(new char[] { Constants.Tags.ValueDelimiter }, StringSplitOptions.RemoveEmptyEntries).Select(s => new Tag(s)).ToArray();
        }
    }

We use the Tag and TagList class like below just for reference so you can get a context

[Display(Order = 20, Description = "Start typing the topic name to see suggestions. Allows multiple.")]
[AutoSuggestTags(typeof(TopicSelectionQuery))]
[BackingType(typeof(PropertyTagList))]
public virtual TagList Topics { get; set; }

[BackingType(typeof(PropertyTag))]
[Display(Order = 30, Description = "Select competition to filter the contents of this section block")]
[LinkedAutoSelection(typeof(CompetitionTagSelectionQuery), null, null, new[] { "/season/topic", "/round/topic" })]
public virtual Tag Competition { get; set; }


The Problem

We wanted to filter the contents by applying filter on Tag and TagList class. Therefore, we build a filter expression using the below code for different property.

OrTopic Filter Builder

public static ITypeSearch TopicOrFilter(this ITypeSearch typeSearchQuery, IEnumerable values)
          where T : ContentPage
        {
            if (values.IsNullOrEmpty())
                return typeSearchQuery;

            FilterBuilder filterBuilder = new FilterBuilder(typeSearchQuery.Client);

            foreach (var value in values)
            {
                filterBuilder = filterBuilder.Or(t => t.Topic.Match(value));
            }

            return typeSearchQuery.Filter(filterBuilder);
        }


but we wanted this to be generic so we do not have to add separate function for each paramter.

Therefore we added below generic code

The AndAny Function

public static ITypeSearch AndAny(this ITypeSearch typeSearchQuery, IEnumerable values, Expression> filter)
            where T : ContentPage
        {
            if (values.IsNullOrEmpty())
                return typeSearchQuery;

            FilterBuilder filterBuilder = new FilterBuilder(typeSearchQuery.Client);

            /* Expression visitor to modify provided filter to epiFind filter expression */
            var resolver = new ParameterReplacerVisitor, Func>();

            /* Parameter to be replaced with constant */
            var parameter = filter.Parameters[1];
            foreach (var value in values)
            {
                resolver.Source = parameter;
                resolver.Value = value;

                filterBuilder = filterBuilder.Or(resolver.ReplaceValue(filter));
            }

            return typeSearchQuery.Filter(filterBuilder);
        }

The OrAny Function

public static ITypeSearch OrAny(this ITypeSearch typeSearchQuery, IEnumerable values, Expression> filter)
            where T : ContentPage
        {
            if (values.IsNullOrEmpty())
                return typeSearchQuery;

            FilterBuilder filterBuilder = new FilterBuilder(typeSearchQuery.Client);

            /* Expression visitor to modify provided filter to epiFind filter expression */
            var resolver = new ParameterReplacerVisitor, Func>();

            /* Parameter to be replaced with constant */
            var parameter = filter.Parameters[1];
            foreach (var value in values)
            {
                resolver.Source = parameter;
                resolver.Value = value;

                filterBuilder = filterBuilder.Or(resolver.ReplaceValue(filter));
            }

            return typeSearchQuery.OrFilter(filterBuilder);
        }

The Other Problem.

The EPiFind filter exprssion expects fitler expression with one parameter but our expression contains the two parameter. Therefore, we need to resolve parameter expression to align with the functions available for filter builder.

public class ParameterReplacerVisitor : System.Linq.Expressions.ExpressionVisitor
    {

        public ParameterReplacerVisitor()
        {
        }

        public ParameterExpression Source { get; set; }

        public object Value { get; set; }

        public Expression ReplaceValue(Expression exp)
        {
            var expNew = Visit(exp) as LambdaExpression;
            return Expression.Lambda(expNew.Body, expNew.Parameters);
        }

        /// 
        /// This functoin will replace the parameter expression with constant values if parameter name and type matches with provided value
        /// for e.g (num1, num2) => num1 + num2 will be replaced (num1) => num1 + 3. if we want to replace num2 with 3.
        /// 
        protected override Expression VisitParameter(ParameterExpression node)
        {
            // Replace the source with the target, visit other params as usual.
            if (node.Type == Source.Type && node.Name == Source.Name)
            {
                return Expression.Constant(Value);
            }

            return base.VisitParameter(node);
        }

        /// 
        /// This functoin will replace the member expression with constant values if parameter name and type matches with provided value
        /// for e.g (num1, classObj) => num1 + classObj.num2 will be replaced (num1) => num1 + 3. if we want to replace classObj.num2 with 3.
        /// 
        protected override Expression VisitMember(MemberExpression m)
        {
            if (m.Expression != null
                && m.Expression.NodeType == ExpressionType.Parameter
                && m.Expression.Type == Source.Type && ((ParameterExpression)m.Expression).Name == Source.Name)
            {
                object newVal;
                if (m.Member is FieldInfo)
                    newVal = ((FieldInfo)m.Member).GetValue(Value);
                else if (m.Member is PropertyInfo)
                    newVal = ((PropertyInfo)m.Member).GetValue(Value, null);
                else
                    newVal = null;
                return Expression.Constant(newVal);
            }

            return base.VisitMember(m);
        }

        /// 
        /// In this function we are reducing the number of parameters in lambda expression
        /// for e.g as we are replacing parameter with constants therefore, (num1, num2) => will become (num1) =>
        /// 
        protected override Expression VisitLambda(Expression node)
        {
            var parameters = node.Parameters.Where(p => p.Name != Source.Name || p.Type != Source.Type).ToList();
            return Expression.Lambda(Visit(node.Body), parameters);
        }
    }



Monday, October 9, 2017

Custom Property In EPiServer And EPiFInd Indexing

In one of our project we had to build the autosuggest property. However, the default type of autosuggest property is string but we wanted to store custom object so we can index through EPiFind with additional values.

Therefore, we implemented custom class to store additional values and also needed JsonConverter for 2 reasons

  1. Convert Class to String for Dojo Framework so existing autosuggest property works as expected
  2. Convert and index object for EPiFind differently
We will delimte the value of autosuggest property with '-' to break it into multiple property and save it and index it properly.

Define the property


[BackingType(typeof(PropertyTag))]
[Display(Order = 45, Description = "Select competition to filter the contents of the page")]
[AutoSuggestSelection(typeof(TagSelectionQuery), AllowCustomValues = false)]
public virtual Tag Competition { get; set; }


Define the PropertyTag Class



    [PropertyDefinitionTypePlugIn(Description = "A property to tag content", DisplayName = "Tag")]
    [JsonConverter(typeof(TagJsonConverter))]
    public class PropertyTag : PropertyLongString
    {
        public override PropertyDataType Type => PropertyDataType.LongString;

        public override Type PropertyValueType => typeof(Tag);

        public override object Value
        {
            get { return Tag.Parse(base.Value?.ToString()); }
            set { base.Value = value?.ToString(); }
        }

        public override object SaveData(PropertyDataCollection properties)
        {
            return this.LongString;
        }
    }


Define the JsonConverter



 internal class TagJsonConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return true;
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.Value is Tag tag)
                return tag;

            return new Tag(reader.Value.ToString());
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            if (value == null)
            {
                writer.WriteNull();
                return;
            }

            if (writer is MaxDepthJsonWriter && value is Tag tag)
            {
                writer.WriteStartObject();
                writer.WritePropertyName($"Text{TypeSuffix.String}");
                writer.WriteValue(tag.Text);
                writer.WritePropertyName($"Value{TypeSuffix.String}");
                writer.WriteValue(tag.Value);
                writer.WriteEndObject();
                return;
            }

            if (value is Tag itag)
            {
                writer.WriteValue(itag.ToString());
            }
            else
            {
                writer.WriteValue(value.ToString());
            }
        }
    }


Define Tag Class




 [JsonConverter(typeof(TagJsonConverter))]
    public class Tag
    {
        private string text;
        private string value;
        private string tag;
        private bool isLoaded;

        public string Text
        {
            get
            {
                if (!isLoaded)
                    Load();

                return text;
            }

            set
            {
                text = value;
            }
        }

        public string Value
        {
            get
            {
                if (!isLoaded)
                    Load();

                return value;
            }

            set
            {
                this.value = value;
            }
        }

        public Tag()
        { }

        public Tag(string tag)
        {
            this.tag = tag;
        }

        public Tag(string value, string text)
        {
            isLoaded = true;
            this.value = value;
            this.text = text;
        }

        public string GetId(string type) => Value?.Substring(type.Length);

        public override string ToString()
        {
            return $"{Value}{ "-" }{Text}";
        }

        private void Load()
        {
            isLoaded = true;
            if (!string.IsNullOrWhiteSpace(tag))
            {
                var parts = tag.Split(new[] { "-" }, 2);
                if (parts.Length == 1)
                {
                    text = parts[0];
                    value = string.Empty;
                }
                else
                {
                    value = parts[0];
                    text = parts[1];
                }
            }
        }

        public static Tag Parse(string tag)
        {
            return string.IsNullOrWhiteSpace(tag) ? null : new Tag(tag);
        }
    }

Define TagSelectionQuery


    [ServiceConfiguration(typeof(ISelectionQuery))]
    public class TagSelectionQuery : ISelectionQuery
    {
        private const int Depth = 0;

        protected virtual string Prefix { get; } = Constants.Tags.Competition;

        public IEnumerable GetItems(string query)
        {
            /* Return List Of Select Items With Value Delimited by '-' For value and text of tag
        }

        ///         /// Will be called when initializing an editor with an existing value to get the corresponding text representation.
        ///         public ISelectItem GetItemByValue(string value) => /* Return SelectItem */;
    }


Pro Tip: Make Sure Admin For Property Looks Like below