using System; using System.Collections.Generic; using System.Linq; using System.Dynamic; using Umbraco.Core; using Umbraco.Core.Dynamics; using System.Collections; using System.Reflection; using Umbraco.Core.Models; using Umbraco.Web.Dynamics; namespace Umbraco.Web.Models { /// /// A collection of DynamicPublishedContent items /// /// /// Implements many of the dynamic methods required for execution against this list. It also ensures /// that the correct OwnersCollection properties is assigned to the underlying PublishedContentBase object /// of the DynamicPublishedContent item (so long as the IPublishedContent item is actually PublishedContentBase). /// All relates to this issue here: http://issues.umbraco.org/issue/U4-1797 /// public class DynamicPublishedContentList : DynamicObject, IEnumerable, IEnumerable { internal List Items { get; set; } public DynamicPublishedContentList() { Items = new List(); } public DynamicPublishedContentList(IEnumerable items) { var list = items.ToList(); //set the owners list for each item list.ForEach(x => SetOwnersList(x, this)); Items = list; } public DynamicPublishedContentList(IEnumerable items) { var list = items.Select(x => new DynamicPublishedContent(x)).ToList(); //set the owners list for each item list.ForEach(x => SetOwnersList(x, this)); Items = list; } private static void SetOwnersList(IPublishedContent content, IEnumerable list) { var publishedContentBase = content as IOwnerCollectionAware; if (publishedContentBase != null) { publishedContentBase.OwnersCollection = 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: 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 firstArg = args.FirstOrDefault(); //this is to check for 'DocumentTypeAlias' vs 'NodeTypeAlias' for compatibility if (firstArg != null && firstArg.ToString().InvariantStartsWith("NodeTypeAlias")) { firstArg = "DocumentTypeAlias" + firstArg.ToString().Substring("NodeTypeAlias".Length); } var name = binder.Name; if (name == "Single") { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] {} : args.Skip(1).ToArray(); var single = this.Single(predicate, values); result = new DynamicPublishedContent(single); return true; } if (name == "SingleOrDefault") { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] { } : args.Skip(1).ToArray(); var single = this.SingleOrDefault(predicate, values); if (single == null) result = new DynamicNull(); else result = new DynamicPublishedContent(single); return true; } if (name == "First") { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] { } : args.Skip(1).ToArray(); var first = this.First(predicate, values); result = new DynamicPublishedContent(first); return true; } if (name == "FirstOrDefault") { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] { } : args.Skip(1).ToArray(); var first = this.FirstOrDefault(predicate, values); if (first == null) result = new DynamicNull(); else result = new DynamicPublishedContent(first); return true; } if (name == "Last") { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] { } : args.Skip(1).ToArray(); var last = this.Last(predicate, values); result = new DynamicPublishedContent(last); return true; } if (name == "LastOrDefault") { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] { } : args.Skip(1).ToArray(); var last = this.LastOrDefault(predicate, values); if (last == null) result = new DynamicNull(); else result = new DynamicPublishedContent(last); return true; } if (name == "Where") { string predicate = firstArg.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(firstArg.ToString()).ToList()); return true; } if (name == "Take") { result = new DynamicPublishedContentList(this.Take((int)firstArg)); return true; } if (name == "Skip") { result = new DynamicPublishedContentList(this.Skip((int)firstArg)); return true; } if (name == "InGroupsOf") { int groupSize = 0; if (int.TryParse(firstArg.ToString(), out groupSize)) { result = InGroupsOf(groupSize); return true; } result = new DynamicNull(); return true; } if (name == "GroupedInto") { int groupCount = 0; if (int.TryParse(firstArg.ToString(), out groupCount)) { result = GroupedInto(groupCount); return true; } result = new DynamicNull(); return true; } if (name == "GroupBy") { result = GroupBy(firstArg.ToString()); return true; } if (name == "Average" || name == "Min" || name == "Max" || name == "Sum") { result = Aggregate(args, name); return true; } if (name == "Union") { if ((firstArg as IEnumerable) != null) { result = new DynamicPublishedContentList(this.Items.Union(firstArg as IEnumerable)); return true; } if ((firstArg as DynamicPublishedContentList) != null) { result = new DynamicPublishedContentList(this.Items.Union((firstArg as DynamicPublishedContentList).Items)); return true; } } if (name == "Except") { if ((firstArg as IEnumerable) != null) { result = new DynamicPublishedContentList(this.Items.Except(firstArg as IEnumerable, new DynamicPublishedContentIdEqualityComparer())); return true; } } if (name == "Intersect") { if ((firstArg as IEnumerable) != null) { result = new DynamicPublishedContentList(this.Items.Intersect(firstArg as IEnumerable, 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; } //ok, now lets try to match by member, property, extensino method var attempt = DynamicInstanceHelper.TryInvokeMember(this, binder, args, new[] { typeof (IEnumerable), typeof (DynamicPublishedContentList) }); if (attempt.Success) { result = attempt.Result.ObjectResult; //need to check the return type and possibly cast if result is from an extension method found if (attempt.Result.Reason == DynamicInstanceHelper.TryInvokeMemberSuccessReason.FoundExtensionMethod) { //we don't need to cast if the result is already DynamicPublishedContentList if (attempt.Result.ObjectResult != null && (!(attempt.Result.ObjectResult is DynamicPublishedContentList))) { if (attempt.Result.ObjectResult is IPublishedContent) { result = new DynamicPublishedContent((IPublishedContent)attempt.Result.ObjectResult); } else if (attempt.Result.ObjectResult is IEnumerable) { result = new DynamicPublishedContentList((IEnumerable)attempt.Result.ObjectResult); } else if (attempt.Result.ObjectResult is IEnumerable) { result = new DynamicPublishedContentList((IEnumerable)attempt.Result.ObjectResult); } } } return true; } //this is the result of an extension method execution gone wrong so we return dynamic null if (attempt.Result.Reason == DynamicInstanceHelper.TryInvokeMemberSuccessReason.FoundExtensionMethod && attempt.Error != null && attempt.Error is TargetInvocationException) { result = new DynamicNull(); return true; } result = null; return false; } private T Aggregate(IEnumerable 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; } public T Single(string predicate, params object[] values) { return predicate.IsNullOrWhiteSpace() ? ((IQueryable) Items.AsQueryable()).Single() : Where(predicate, values).Single(); } public T SingleOrDefault(string predicate, params object[] values) { return predicate.IsNullOrWhiteSpace() ? ((IQueryable)Items.AsQueryable()).SingleOrDefault() : Where(predicate, values).SingleOrDefault(); } public T First(string predicate, params object[] values) { return predicate.IsNullOrWhiteSpace() ? ((IQueryable)Items.AsQueryable()).First() : Where(predicate, values).First(); } public T FirstOrDefault(string predicate, params object[] values) { return predicate.IsNullOrWhiteSpace() ? ((IQueryable)Items.AsQueryable()).FirstOrDefault() : Where(predicate, values).FirstOrDefault(); } public T Last(string predicate, params object[] values) { return predicate.IsNullOrWhiteSpace() ? ((IQueryable)Items.AsQueryable()).Last() : Where(predicate, values).Last(); } public T LastOrDefault(string predicate, params object[] values) { return predicate.IsNullOrWhiteSpace() ? ((IQueryable)Items.AsQueryable()).LastOrDefault() : Where(predicate, values).LastOrDefault(); } 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, () => typeof(DynamicPublishedContentListOrdering)); } 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 DynamicQueryable.Select(Items.AsQueryable(), predicate, values); } /// /// Allows the adding of an item from the collection /// /// public void Add(DynamicPublishedContent publishedContent) { SetOwnersList(publishedContent, this); this.Items.Add(publishedContent); } /// /// Allows the removal of an item from the collection /// /// public void Remove(DynamicPublishedContent publishedContent) { if (this.Items.Contains(publishedContent)) { //set owners list to null SetOwnersList(publishedContent, null); this.Items.Remove(publishedContent); } } public bool IsNull() { return false; } public bool HasValue() { return true; } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public IEnumerator GetEnumerator() { return Items.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } }