using System; using System.Collections.Generic; using System.Linq; using System.Dynamic; using Umbraco.Core.Dynamics; using System.Collections; using System.Reflection; namespace Umbraco.Core.Models { public class DynamicPublishedContentList : DynamicObject, IEnumerable { internal List Items { get; set; } public DynamicPublishedContentList() { Items = new List(); } public DynamicPublishedContentList(IEnumerable items) { var list = items.ToList(); Items = list; } public DynamicPublishedContentList(IEnumerable items) { var list = items.Select(x => new DynamicPublishedContentBase(x)).ToList(); Items = list; } public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) { int index = (int)indexes[0]; try { result = this.Items.ElementAt(index); return true; } catch (IndexOutOfRangeException) { result = new DynamicNull(); return true; } } public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { //TODO: We MUST cache the result here, it is very expensive to keep finding extension methods and processing this stuff! //TODO: Nowhere here are we checking if args is the correct length! //NOTE: For many of these we could actually leave them out since we are executing custom extension methods and because // we implement IEnumerable they will execute just fine, however, to do that will be quite a bit slower than checking here. var name = binder.Name; if (name == "Where") { string predicate = args.First().ToString(); var values = args.Skip(1).ToArray(); //TODO: We are pre-resolving the where into a ToList() here which will have performance impacts if there where clauses // are nested! We should somehow support an QueryableDocumentList! result = new DynamicPublishedContentList(this.Where(predicate, values).ToList()); return true; } if (name == "OrderBy") { //TODO: We are pre-resolving the where into a ToList() here which will have performance impacts if there where clauses // are nested! We should somehow support an QueryableDocumentList! result = new DynamicPublishedContentList(this.OrderBy(args.First().ToString()).ToList()); return true; } if (name == "Take") { result = new DynamicPublishedContentList(this.Take((int)args.First())); return true; } if (name == "Skip") { result = new DynamicPublishedContentList(this.Skip((int)args.First())); return true; } if (name == "InGroupsOf") { int groupSize = 0; if (int.TryParse(args.First().ToString(), out groupSize)) { result = this.InGroupsOf(groupSize); return true; } result = new DynamicNull(); return true; } if (name == "GroupedInto") { int groupCount = 0; if (int.TryParse(args.First().ToString(), out groupCount)) { result = this.GroupedInto(groupCount); return true; } result = new DynamicNull(); return true; } if (name == "GroupBy") { result = this.GroupBy(args.First().ToString()); return true; } if (name == "Average" || name == "Min" || name == "Max" || name == "Sum") { result = Aggregate(args, name); return true; } if (name == "Union") { if ((args.First() as IEnumerable) != null) { result = new DynamicPublishedContentList(this.Items.Union(args.First() as IEnumerable)); return true; } if ((args.First() as DynamicPublishedContentList) != null) { result = new DynamicPublishedContentList(this.Items.Union((args.First() as DynamicPublishedContentList).Items)); return true; } } if (name == "Except") { if ((args.First() as IEnumerable) != null) { result = new DynamicPublishedContentList(this.Items.Except(args.First() as IEnumerable, new DynamicPublishedContentIdEqualityComparer())); return true; } if ((args.First() as DynamicPublishedContentList) != null) { result = new DynamicPublishedContentList(this.Items.Except((args.First() as DynamicPublishedContentList).Items, new DynamicPublishedContentIdEqualityComparer())); return true; } } if (name == "Intersect") { if ((args.First() as IEnumerable) != null) { result = new DynamicPublishedContentList(this.Items.Intersect(args.First() as IEnumerable, new DynamicPublishedContentIdEqualityComparer())); return true; } if ((args.First() as DynamicPublishedContentList) != null) { result = new DynamicPublishedContentList(this.Items.Intersect((args.First() as DynamicPublishedContentList).Items, new DynamicPublishedContentIdEqualityComparer())); return true; } } if (name == "Distinct") { result = new DynamicPublishedContentList(this.Items.Distinct(new DynamicPublishedContentIdEqualityComparer())); return true; } if (name == "Pluck" || name == "Select") { result = Pluck(args); return true; } try { //Property? result = Items.GetType().InvokeMember(binder.Name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.GetProperty, null, Items, args); return true; } catch (MissingMethodException) { try { //Static or Instance Method? result = Items.GetType().InvokeMember(binder.Name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.InvokeMethod, null, Items, args); return true; } catch (MissingMethodException) { try { result = ExecuteExtensionMethod(args, name); return true; } catch (TargetInvocationException) { //We do this to enable error checking of Razor Syntax when a method e.g. ElementAt(2) is used. //When the Script is tested, there's no Children which means ElementAt(2) is invalid (IndexOutOfRange) //Instead, we are going to return an empty DynamicNode. result = DynamicPublishedContentBase.Empty(); return true; } catch { result = null; return false; } } } catch { result = null; return false; } } private T Aggregate(List data, string name) where T : struct { switch (name) { case "Min": return data.Min(); case "Max": return data.Max(); case "Average": if (typeof(T) == typeof(int)) { return (T)Convert.ChangeType((data as List).Average(), typeof(T)); } if (typeof(T) == typeof(decimal)) { return (T)Convert.ChangeType((data as List).Average(), typeof(T)); } break; case "Sum": if (typeof(T) == typeof(int)) { return (T)Convert.ChangeType((data as List).Sum(), typeof(T)); } if (typeof(T) == typeof(decimal)) { return (T)Convert.ChangeType((data as List).Sum(), typeof(T)); } break; } return default(T); } private object Aggregate(object[] args, string name) { object result; string predicate = args.First().ToString(); var values = args.Skip(1).ToArray(); var query = (IQueryable)this.Select(predicate, values); object firstItem = query.FirstOrDefault(); if (firstItem == null) { result = new DynamicNull(); } else { var types = from i in query group i by i.GetType() into g where g.Key != typeof(DynamicNull) orderby g.Count() descending select new { g, Instances = g.Count() }; var dominantType = types.First().g.Key; //remove items that are not the dominant type //e.g. string,string,string,string,false[DynamicNull],string var itemsOfDominantTypeOnly = query.ToList(); itemsOfDominantTypeOnly.RemoveAll(item => !item.GetType().IsAssignableFrom(dominantType)); if (dominantType == typeof(string)) { throw new ArgumentException("Can only use aggregate methods on properties which are numeric"); } else if (dominantType == typeof(int)) { List data = (List)itemsOfDominantTypeOnly.Cast().ToList(); return Aggregate(data, name); } else if (dominantType == typeof(decimal)) { List data = (List)itemsOfDominantTypeOnly.Cast().ToList(); return Aggregate(data, name); } else if (dominantType == typeof(bool)) { throw new ArgumentException("Can only use aggregate methods on properties which are numeric or datetime"); } else if (dominantType == typeof(DateTime)) { if (name != "Min" || name != "Max") { throw new ArgumentException("Can only use aggregate min or max methods on properties which are datetime"); } List data = (List)itemsOfDominantTypeOnly.Cast().ToList(); return Aggregate(data, name); } else { result = query.ToList(); } } return result; } private object Pluck(object[] args) { object result; string predicate = args.First().ToString(); var values = args.Skip(1).ToArray(); var query = (IQueryable)this.Select(predicate, values); object firstItem = query.FirstOrDefault(); if (firstItem == null) { result = new List(); } else { var types = from i in query group i by i.GetType() into g where g.Key != typeof(DynamicNull) orderby g.Count() descending select new { g, Instances = g.Count() }; var dominantType = types.First().g.Key; //remove items that are not the dominant type //e.g. string,string,string,string,false[DynamicNull],string var itemsOfDominantTypeOnly = query.ToList(); itemsOfDominantTypeOnly.RemoveAll(item => !item.GetType().IsAssignableFrom(dominantType)); if (dominantType == typeof(string)) { result = (List)itemsOfDominantTypeOnly.Cast().ToList(); } else if (dominantType == typeof(int)) { result = (List)itemsOfDominantTypeOnly.Cast().ToList(); } else if (dominantType == typeof(decimal)) { result = (List)itemsOfDominantTypeOnly.Cast().ToList(); } else if (dominantType == typeof(bool)) { result = (List)itemsOfDominantTypeOnly.Cast().ToList(); } else if (dominantType == typeof(DateTime)) { result = (List)itemsOfDominantTypeOnly.Cast().ToList(); } else { result = query.ToList(); } } return result; } private object ExecuteExtensionMethod(object[] args, string name) { object result = null; var methodTypesToFind = new[] { typeof(IEnumerable), typeof(DynamicPublishedContentList) }; //find known extension methods that match the first type in the list MethodInfo toExecute = null; foreach(var t in methodTypesToFind) { toExecute = ExtensionMethodFinder.FindExtensionMethod(t, args, name, false); if (toExecute != null) break; } if (toExecute != null) { if (toExecute.GetParameters().First().ParameterType == typeof(DynamicPublishedContentList)) { var genericArgs = (new[] { this }).Concat(args); result = toExecute.Invoke(null, genericArgs.ToArray()); } else if (TypeHelper.IsTypeAssignableFrom(toExecute.GetParameters().First().ParameterType)) { //if it is IQueryable, we'll need to cast Items AsQueryable var genericArgs = (new[] { Items.AsQueryable() }).Concat(args); result = toExecute.Invoke(null, genericArgs.ToArray()); } else { var genericArgs = (new[] { Items }).Concat(args); result = toExecute.Invoke(null, genericArgs.ToArray()); } } else { throw new MissingMethodException(); } if (result != null) { if (result is IPublishedContent) { result = new DynamicPublishedContentBase((IPublishedContent)result); } if (result is IEnumerable) { result = new DynamicPublishedContentList((IEnumerable)result); } if (result is IEnumerable) { result = new DynamicPublishedContentList((IEnumerable)result); } } return result; } public IQueryable Where(string predicate, params object[] values) { return ((IQueryable)Items.AsQueryable()).Where(predicate, values); } public IQueryable OrderBy(string key) { return ((IQueryable)Items.AsQueryable()).OrderBy(key); } public DynamicGrouping GroupBy(string key) { var group = new DynamicGrouping(this, key); return group; } public DynamicGrouping GroupedInto(int groupCount) { int groupSize = (int)Math.Ceiling(((decimal)Items.Count() / groupCount)); return new DynamicGrouping( this .Items .Select((node, index) => new KeyValuePair(index, node)) .GroupBy(kv => (object)(kv.Key / groupSize)) .Select(item => new Grouping() { Key = item.Key, Elements = item.Select(inner => inner.Value) })); } public DynamicGrouping InGroupsOf(int groupSize) { return new DynamicGrouping( this .Items .Select((node, index) => new KeyValuePair(index, node)) .GroupBy(kv => (object)(kv.Key / groupSize)) .Select(item => new Grouping() { Key = item.Key, Elements = item.Select(inner => inner.Value) })); } public IQueryable Select(string predicate, params object[] values) { return Items.AsQueryable().Select(predicate, values); } public void Add(DynamicPublishedContentBase publishedContent) { this.Items.Add(publishedContent); } public void Remove(DynamicPublishedContentBase publishedContent) { if (this.Items.Contains(publishedContent)) { this.Items.Remove(publishedContent); } } public bool IsNull() { return false; } public bool HasValue() { return true; } public IEnumerator GetEnumerator() { return Items.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } }