using System; using System.Collections.Generic; using System.ComponentModel; using System.Net; using System.Web.Http; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using System.Linq; using System.Net.Http; using System.Net.Http.Formatting; using Umbraco.Core.Models; using Constants = Umbraco.Core.Constants; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using System.Web.Http.Controllers; using Umbraco.Core.Models.Entities; using Umbraco.Core.Xml; using Umbraco.Web.Models.Mapping; using Umbraco.Web.Search; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors { /// /// The API controller used for getting entity objects, basic name, icon, id representation of umbraco objects that are based on CMSNode /// /// /// Some objects such as macros are not based on CMSNode /// [EntityControllerConfiguration] [PluginController("UmbracoApi")] public class EntityController : UmbracoAuthorizedJsonController { /// /// Configures this controller with a custom action selector /// private class EntityControllerConfigurationAttribute : Attribute, IControllerConfiguration { public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) { controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( //This is a special case, we'll accept a String here so that we can get page members when the special "all-members" //id is passed in eventually we'll probably want to support GUID + Udi too new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetPagedChildren", "id", typeof(int), typeof(string)), new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetPath", "id", typeof(int), typeof(Guid), typeof(Udi)), new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)), new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetByIds", "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[])))); } } private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher(); private readonly SearchableTreeCollection _searchableTreeCollection; public EntityController(SearchableTreeCollection searchableTreeCollection) { _searchableTreeCollection = searchableTreeCollection; } /// /// Returns an Umbraco alias given a string /// /// /// /// public dynamic GetSafeAlias(string value, bool camelCase = true) { var returnValue = string.IsNullOrWhiteSpace(value) ? string.Empty : value.ToSafeAlias(camelCase); dynamic returnObj = new System.Dynamic.ExpandoObject(); returnObj.alias = returnValue; returnObj.original = value; returnObj.camelCase = camelCase; return returnObj; } /// /// Searches for results based on the entity type /// /// /// /// /// A starting point for the search, generally a node id, but for members this is a member type alias /// /// [HttpGet] public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null) { //TODO: Should we restrict search results based on what app the user has access to? // - Theoretically you shouldn't be able to see member data if you don't have access to members right? if (string.IsNullOrEmpty(query)) return Enumerable.Empty(); return ExamineSearch(query, type, searchFrom); } /// /// Searches for all content that the user is allowed to see (based on their allowed sections) /// /// /// /// /// Even though a normal entity search will allow any user to search on a entity type that they may not have access to edit, we need /// to filter these results to the sections they are allowed to edit since this search function is explicitly for the global search /// so if we showed entities that they weren't allowed to edit they would get errors when clicking on the result. /// /// The reason a user is allowed to search individual entity types that they are not allowed to edit is because those search /// methods might be used in things like pickers in the content editor. /// [HttpGet] public IDictionary SearchAll(string query) { var result = new Dictionary(); if (string.IsNullOrEmpty(query)) return result; var allowedSections = Security.CurrentUser.AllowedSections.ToArray(); var searchableTrees = _searchableTreeCollection.AsReadOnlyDictionary(); foreach (var searchableTree in searchableTrees) { if (allowedSections.Contains(searchableTree.Value.AppAlias)) { var tree = Services.ApplicationTreeService.GetByAlias(searchableTree.Key); if (tree == null) continue; //shouldn't occur var searchableTreeAttribute = searchableTree.Value.SearchableTree.GetType().GetCustomAttribute(false); var treeAttribute = tree.GetTreeAttribute(); long total; result[treeAttribute.GetRootNodeDisplayName(Services.TextService)] = new TreeSearchResult { Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out total), TreeAlias = searchableTree.Key, AppAlias = searchableTree.Value.AppAlias, JsFormatterService = searchableTreeAttribute == null ? "" : searchableTreeAttribute.ServiceName, JsFormatterMethod = searchableTreeAttribute == null ? "" : searchableTreeAttribute.MethodName }; } } return result; } /// /// Gets the path for a given node ID /// /// /// /// public IEnumerable GetPath(int id, UmbracoEntityTypes type) { var foundContent = GetResultForId(id, type); return foundContent.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse); } /// /// Gets the path for a given node ID /// /// /// /// public IEnumerable GetPath(Guid id, UmbracoEntityTypes type) { var foundContent = GetResultForKey(id, type); return foundContent.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse); } /// /// Gets the path for a given node ID /// /// /// /// public IEnumerable GetPath(Udi id, UmbracoEntityTypes type) { var guidUdi = id as GuidUdi; if (guidUdi != null) { return GetPath(guidUdi.Guid, type); } throw new HttpResponseException(HttpStatusCode.NotFound); } /// /// Gets the url of an entity /// /// Int id of the entity to fetch URL for /// The tpye of entity such as Document, Media, Member /// The URL or path to the item public HttpResponseMessage GetUrl(int id, UmbracoEntityTypes type) { var returnUrl = string.Empty; if (type == UmbracoEntityTypes.Document) { var foundUrl = Umbraco.Url(id); if (string.IsNullOrEmpty(foundUrl) == false && foundUrl != "#") { returnUrl = foundUrl; return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(returnUrl) }; } } var ancestors = GetResultForAncestors(id, type); //if content, skip the first node for replicating NiceUrl defaults if(type == UmbracoEntityTypes.Document) { ancestors = ancestors.Skip(1); } returnUrl = "/" + string.Join("/", ancestors.Select(x => x.Name)); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(returnUrl) }; } /// /// Gets an entity by a xpath query /// /// /// /// /// public EntityBasic GetByQuery(string query, int nodeContextId, UmbracoEntityTypes type) { //TODO: Rename this!!! It's misleading, it should be GetByXPath if (type != UmbracoEntityTypes.Document) throw new ArgumentException("Get by query is only compatible with enitities of type Document"); var q = ParseXPathQuery(query, nodeContextId); var node = Umbraco.ContentSingleAtXPath(q); if (node == null) return null; return GetById(node.Id, type); } //PP: wip in progress on the query parser private string ParseXPathQuery(string query, int id) { return UmbracoXPathPathSyntaxParser.ParseXPathQuery( xpathExpression: query, nodeContextId: id, getPath: nodeid => { var ent = Services.EntityService.Get(nodeid); return ent.Path.Split(',').Reverse(); }, publishedContentExists: i => Umbraco.Content(i) != null); } #region GetById /// /// Gets an entity by it's id /// /// /// /// public EntityBasic GetById(int id, UmbracoEntityTypes type) { return GetResultForId(id, type); } /// /// Gets an entity by it's key /// /// /// /// public EntityBasic GetById(Guid id, UmbracoEntityTypes type) { return GetResultForKey(id, type); } /// /// Gets an entity by it's UDI /// /// /// /// public EntityBasic GetById(Udi id, UmbracoEntityTypes type) { var guidUdi = id as GuidUdi; if (guidUdi != null) { return GetResultForKey(guidUdi.Guid, type); } throw new HttpResponseException(HttpStatusCode.NotFound); } #endregion #region GetByIds /// /// Get entities by integer ids /// /// /// /// /// /// We allow for POST because there could be quite a lot of Ids /// [HttpGet] [HttpPost] public IEnumerable GetByIds([FromJsonPath]int[] ids, UmbracoEntityTypes type) { if (ids == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return GetResultForIds(ids, type); } /// /// Get entities by GUID ids /// /// /// /// /// /// We allow for POST because there could be quite a lot of Ids /// [HttpGet] [HttpPost] public IEnumerable GetByIds([FromJsonPath]Guid[] ids, UmbracoEntityTypes type) { if (ids == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return GetResultForKeys(ids, type); } /// /// Get entities by UDIs /// /// /// A list of UDIs to lookup items by, all UDIs must be of the same UDI type! /// /// /// /// /// We allow for POST because there could be quite a lot of Ids. /// [HttpGet] [HttpPost] public IEnumerable GetByIds([FromJsonPath]Udi[] ids, [FromUri]UmbracoEntityTypes type) { if (ids == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } if (ids.Length == 0) { return Enumerable.Empty(); } //all udi types will need to be the same in this list so we'll determine by the first //currently we only support GuidIdi for this method var guidUdi = ids[0] as GuidUdi; if (guidUdi != null) { return GetResultForKeys(ids.Select(x => ((GuidUdi)x).Guid).ToArray(), type); } throw new HttpResponseException(HttpStatusCode.NotFound); } #endregion public IEnumerable GetChildren(int id, UmbracoEntityTypes type) { return GetResultForChildren(id, type); } /// /// Get paged child entities by id /// /// /// /// /// /// /// /// /// public PagedResult GetPagedChildren( string id, UmbracoEntityTypes type, int pageNumber, int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") { int intId; if (int.TryParse(id, out intId)) { return GetPagedChildren(intId, type, pageNumber, pageSize, orderBy, orderDirection, filter); } Guid guidId; if (Guid.TryParse(id, out guidId)) { //Not supported currently throw new HttpResponseException(HttpStatusCode.NotFound); } Udi udiId; if (Udi.TryParse(id, out udiId)) { //Not supported currently throw new HttpResponseException(HttpStatusCode.NotFound); } //so we don't have an INT, GUID or UDI, it's just a string, so now need to check if it's a special id or a member type if (id == Constants.Conventions.MemberTypes.AllMembersListId) { //the EntityService can search paged members from the root intId = -1; return GetPagedChildren(intId, type, pageNumber, pageSize, orderBy, orderDirection, filter); } //the EntityService cannot search members of a certain type, this is currently not supported and would require //quite a bit of plumbing to do in the Services/Repository, we'll revert to a paged search long total; var searchResult = _treeSearcher.ExamineSearch(Umbraco, filter ?? "", type, pageSize, pageNumber - 1, out total, id); return new PagedResult(total, pageNumber, pageSize) { Items = searchResult }; } /// /// Get paged child entities by id /// /// /// /// /// /// /// /// /// public PagedResult GetPagedChildren( int id, UmbracoEntityTypes type, int pageNumber, int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") { if (pageNumber <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); if (pageSize <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); var objectType = ConvertToObjectType(type); if (objectType.HasValue) { long totalRecords; var entities = Services.EntityService.GetPagedChildren(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); if (totalRecords == 0) { return new PagedResult(0, 0, 0); } var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) { Items = entities.Select(Mapper.Map) }; return pagedResult; } //now we need to convert the unknown ones switch (type) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Domain: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + type); } } public PagedResult GetPagedDescendants( int id, UmbracoEntityTypes type, int pageNumber, int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") { if (pageNumber <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); if (pageSize <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); var objectType = ConvertToObjectType(type); if (objectType.HasValue) { IEnumerable entities; long totalRecords; if (id == Constants.System.Root) { // root is special: we reduce it to start nodes int[] aids = null; switch (type) { case UmbracoEntityTypes.Document: aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); break; case UmbracoEntityTypes.Media: aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); break; } entities = aids == null || aids.Contains(Constants.System.Root) ? Services.EntityService.GetPagedDescendants(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed: false) : Services.EntityService.GetPagedDescendants(aids, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); } else { entities = Services.EntityService.GetPagedDescendants(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); } if (totalRecords == 0) { return new PagedResult(0, 0, 0); } var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) { Items = entities.Select(Mapper.Map) }; return pagedResult; } //now we need to convert the unknown ones switch (type) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Domain: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + type); } } [HttpQueryStringFilter("queryStrings")] public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, FormDataCollection queryStrings) { return GetResultForAncestors(id, type, queryStrings); } /// /// Searches for results based on the entity type /// /// /// /// /// private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null) { long total; return _treeSearcher.ExamineSearch(Umbraco, query, entityType, 200, 0, out total, searchFrom); } private IEnumerable GetResultForChildren(int id, UmbracoEntityTypes entityType) { var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { //TODO: Need to check for Object types that support hierarchic here, some might not. return Services.EntityService.GetChildren(id, objectType.Value) .WhereNotNull() .Select(Mapper.Map); } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.Domain: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, FormDataCollection queryStrings = null) { var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { //TODO: Need to check for Object types that support hierarchic here, some might not. var ids = Services.EntityService.Get(id).Path.Split(',').Select(int.Parse).Distinct().ToArray(); int[] aids = null; switch (entityType) { case UmbracoEntityTypes.Document: aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); break; case UmbracoEntityTypes.Media: aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); break; } if (aids != null) { var lids = new List(); var ok = false; foreach (var i in ids) { if (ok) { lids.Add(i); continue; } if (aids.Contains(i)) { lids.Add(i); ok = true; } } ids = lids.ToArray(); } var culture = queryStrings?.GetValue("culture"); return ids.Length == 0 ? Enumerable.Empty() : Services.EntityService.GetAll(objectType.Value, ids) .WhereNotNull() .OrderBy(x => x.Level) .Select(x => Mapper.Map(x, opts => { opts.SetCulture(culture);})); } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Domain: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private IEnumerable GetResultForKeys(Guid[] keys, UmbracoEntityTypes entityType) { if (keys.Length == 0) return Enumerable.Empty(); var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { var entities = Services.EntityService.GetAll(objectType.Value, keys) .WhereNotNull() .Select(Mapper.Map); // entities are in "some" order, put them back in order var xref = entities.ToDictionary(x => x.Key); var result = keys.Select(x => xref.ContainsKey(x) ? xref[x] : null).Where(x => x != null); return result; } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Domain: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private IEnumerable GetResultForIds(int[] ids, UmbracoEntityTypes entityType) { if (ids.Length == 0) return Enumerable.Empty(); var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { var entities = Services.EntityService.GetAll(objectType.Value, ids) .WhereNotNull() .Select(Mapper.Map); // entities are in "some" order, put them back in order var xref = entities.ToDictionary(x => x.Id); var result = ids.Select(x => xref.ContainsKey(x) ? xref[x] : null).Where(x => x != null); return result; } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Domain: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private EntityBasic GetResultForKey(Guid key, UmbracoEntityTypes entityType) { var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { var found = Services.EntityService.Get(key, objectType.Value); if (found == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return Mapper.Map(found); } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Domain: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private EntityBasic GetResultForId(int id, UmbracoEntityTypes entityType) { var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { var found = Services.EntityService.Get(id, objectType.Value); if (found == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return Mapper.Map(found); } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.PropertyType: case UmbracoEntityTypes.PropertyGroup: case UmbracoEntityTypes.Domain: case UmbracoEntityTypes.Language: case UmbracoEntityTypes.User: case UmbracoEntityTypes.Macro: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private static UmbracoObjectTypes? ConvertToObjectType(UmbracoEntityTypes entityType) { switch (entityType) { case UmbracoEntityTypes.Document: return UmbracoObjectTypes.Document; case UmbracoEntityTypes.Media: return UmbracoObjectTypes.Media; case UmbracoEntityTypes.MemberType: return UmbracoObjectTypes.MediaType; case UmbracoEntityTypes.MemberGroup: return UmbracoObjectTypes.MemberGroup; case UmbracoEntityTypes.MediaType: return UmbracoObjectTypes.MediaType; case UmbracoEntityTypes.DocumentType: return UmbracoObjectTypes.DocumentType; case UmbracoEntityTypes.Stylesheet: return UmbracoObjectTypes.Stylesheet; case UmbracoEntityTypes.Member: return UmbracoObjectTypes.Member; case UmbracoEntityTypes.DataType: return UmbracoObjectTypes.DataType; default: //There is no UmbracoEntity conversion (things like Macros, Users, etc...) return null; } } // fixme - need to implement GetAll for backoffice controllers - dynamics? public IEnumerable GetAll(UmbracoEntityTypes type, string postFilter, [FromUri]IDictionary postFilterParams) { return GetResultForAll(type, postFilter, postFilterParams); } /// /// Gets the result for the entity list based on the type /// /// /// A string where filter that will filter the results dynamically with linq - optional /// the parameters to fill in the string where filter - optional /// private IEnumerable GetResultForAll(UmbracoEntityTypes entityType, string postFilter = null, IDictionary postFilterParams = null) { var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) { //TODO: Should we order this by something ? var entities = Services.EntityService.GetAll(objectType.Value).WhereNotNull().Select(Mapper.Map); return ExecutePostFilter(entities, postFilter, postFilterParams); } //now we need to convert the unknown ones switch (entityType) { case UmbracoEntityTypes.Template: var templates = Services.FileService.GetTemplates(); var filteredTemplates = ExecutePostFilter(templates, postFilter, postFilterParams); return filteredTemplates.Select(Mapper.Map); case UmbracoEntityTypes.Macro: //Get all macros from the macro service var macros = Services.MacroService.GetAll().WhereNotNull().OrderBy(x => x.Name); var filteredMacros = ExecutePostFilter(macros, postFilter, postFilterParams); return filteredMacros.Select(Mapper.Map); case UmbracoEntityTypes.PropertyType: //get all document types, then combine all property types into one list var propertyTypes = Services.ContentTypeService.GetAll().Cast() .Concat(Services.MediaTypeService.GetAll()) .ToArray() .SelectMany(x => x.PropertyTypes) .DistinctBy(composition => composition.Alias); var filteredPropertyTypes = ExecutePostFilter(propertyTypes, postFilter, postFilterParams); return Mapper.Map, IEnumerable>(filteredPropertyTypes); case UmbracoEntityTypes.PropertyGroup: //get all document types, then combine all property types into one list var propertyGroups = Services.ContentTypeService.GetAll().Cast() .Concat(Services.MediaTypeService.GetAll()) .ToArray() .SelectMany(x => x.PropertyGroups) .DistinctBy(composition => composition.Name); var filteredpropertyGroups = ExecutePostFilter(propertyGroups, postFilter, postFilterParams); return Mapper.Map, IEnumerable>(filteredpropertyGroups); case UmbracoEntityTypes.User: long total; var users = Services.UserService.GetAll(0, int.MaxValue, out total); var filteredUsers = ExecutePostFilter(users, postFilter, postFilterParams); return Mapper.Map, IEnumerable>(filteredUsers); case UmbracoEntityTypes.Domain: case UmbracoEntityTypes.Language: default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); } } private IEnumerable ExecutePostFilter(IEnumerable entities, string postFilter, IDictionary postFilterParams) { // if a post filter is assigned then try to execute it if (postFilter.IsNullOrWhiteSpace() == false) { // fixme - trouble is, we've killed the dynamic Where thing! throw new NotImplementedException("oops"); //return postFilterParams == null // ? entities.AsQueryable().Where(postFilter).ToArray() // : entities.AsQueryable().Where(postFilter, postFilterParams).ToArray(); } return entities; } } }