public void CanFilterPagedDataWithPartialMatch() { // Arrange var items = MockModel.ArrangeFixture(); var pagedSize = 5; var expectedResults = items.Count - 1; var settings = new PagedDataSettings() { TotalPerPage = pagedSize }; settings.Filter.Add(new FilterSettings() { Property = "Label", Value = "Label", IsExactMatch = false }); // Act var result = _dataPager.GetPagedData(items.AsQueryable(), settings); // Assert Assert.True(result.TotalRecords == expectedResults, "Results differ of what they should be. Possibly the filter on the DataPager class is not working."); Assert.True(result.Result.Count == pagedSize, "Total items in first paged batch does not match the configured page size."); }
public void CanFilterAndSortPostQueryPathsByDescending() { // Arrange var items = MockModel.ArrangeFixture(); var pagedSize = 5; var settings = new PagedDataSettings() { TotalPerPage = pagedSize }; settings.Filter.Add(new FilterSettings() { Property = "ChildCollection.Label", Value = "First" }); settings.Sorting.Add(new SortingSettings() { PostQuerySortingPath = "ChildCollection.Label", Order = SortOrderEnum.DESC }); // Act var result = _dataPager.GetPagedData(items.AsQueryable(), settings); // Assert Assert.True(result.Result[0].ChildCollection[0].Label.Contains("Third")); Assert.True(result.Result[0].ChildCollection[1].Label.Contains("Second")); Assert.True(result.Result[0].ChildCollection[2].Label.Contains("First")); }
public void CanFilterPagedDataWithExactMatch() { // Arrange var items = MockModel.ArrangeFixture(); var pagedSize = 5; var expectedResults = 1; var settings = new PagedDataSettings() { TotalPerPage = pagedSize }; settings.Filter.Add(new FilterSettings() { Property = "Label", Value = items.Last().Label, IsExactMatch = true }); // Act var result = _dataPager.GetPagedData(items.AsQueryable(), settings); // Assert Assert.True(result.TotalRecords == expectedResults, "Results differ of what they should be. Possibly the filter on the DataPager class is not working."); Assert.True(result.Result.Count == expectedResults, "Returned results differ than total records."); }
public void CanFilterPostQueryPathsByExactMatch() { // Arrange var items = MockModel.ArrangeFixture(); var pagedSize = 5; var settings = new PagedDataSettings() { TotalPerPage = pagedSize }; settings.Filter.Add(new FilterSettings() { Property = "ChildCollection.Label", Value = "First", IsExactMatch = true }); // Act var result = _dataPager.GetPagedData(items.AsQueryable(), settings); // Assert Assert.True(result.TotalRecords == 0); Assert.True(result.Result.Count() == 0); }
/// <summary> /// Merges two filters into one final query. /// </summary> /// <param name="query">The IQueryable instance to be parsed.</param> /// <returns>The string lambda expression.</returns> private Expression <Func <Entity, bool> > MergeFilters(PagedDataSettings settings, Expression <Func <Entity, bool> > expressionLeft, Expression <Func <Entity, bool> > expressionRight, bool isAllSearch = false) { if (expressionLeft == null && expressionRight == null) { return(x => 1 == 1); } else if (expressionLeft != null && expressionRight != null) { if (isAllSearch) { return(PredicateBuilder.Or(expressionLeft, expressionRight)); } else { return(PredicateBuilder.And(expressionLeft, expressionRight)); } } else if (expressionLeft == null) { return(expressionRight); } else { return(expressionLeft); } }
/// <summary> /// Filters the final paged result AFTER the projection was executed in database by adapters. /// </summary> /// <remarks> /// This method is useful for children collection filter. The only way to accomplish through LINQ. /// </remarks> private IList PostQueryCallbacksInvoker(IList fetchedResult, PagedDataSettings settings) { fetchedResult = this.PostQueryFilter(fetchedResult, settings); fetchedResult = this.PostQuerySort(fetchedResult, settings); return(fetchedResult); }
/// <summary> /// Applies in memory sorting to IList. /// </summary> /// <param name="fetchedResult">This is the return from EF query after going to DB.</param> /// <param name="settings">Paged data source settings.</param> /// <returns>Sorted collection result.</returns> private IList PostQuerySort(IList fetchedResult, PagedDataSettings settings) { // Generates the order clause based on supplied parameters if (settings.Sorting != null && settings.Sorting.Count > 0) { var validOrderSettings = settings.Sorting.Where(x => !String.IsNullOrEmpty(x.PostQuerySortingPath)).GroupBy(x => x.Property).Select(y => y.FirstOrDefault()); foreach (var o in validOrderSettings) { foreach (var result in fetchedResult) { // Only supports if it is immediately the first level. We checked this above =) var navigationPropertyCollection = o.PostQuerySortingPath.Split('.')[0]; int collectionPathTotal = 0; var propInfo = result.GetNestedPropInfo(navigationPropertyCollection, out collectionPathTotal); // Apparently String implements IEnumerable, since it is a collection of chars if (propInfo != null && (propInfo.PropertyType != typeof(string) || typeof(IEnumerable).IsAssignableFrom(propInfo.PropertyType))) { // Gets the property reference var collectionProp = result.GetPropValue(navigationPropertyCollection); result.RearrangeCollectionInstance(navigationPropertyCollection, o.PostQuerySortingPath.Substring(navigationPropertyCollection.Length + 1) + " " + o.Order.ToString()); } } } } return(fetchedResult); }
/// <summary> /// Returns a collection of data results that can be paged. /// </summary> /// <param name="queryableToPage">IQueryable instance of <see cref="Entity"/> which will act as data source for the pagination.</param> /// <param name="settings">Settings for the search.</param> /// <param name="PreConditionsToPagedDataFilter">Pre condition Expression Filters.</param> /// <param name="ExtraPagedDataFilter">Extra conditions Expression Filters to be applied along with settings filters.</param> /// <returns>Filled PageData results instance.</returns> public IPagedDataResult <Entity> GetPagedData(IQueryable <Entity> queryableToPage, PagedDataSettings settings, Expression <Func <Entity, bool> > PreConditionsToPagedDataFilter = null, Expression <Func <Entity, bool> > ExtraPagedDataFilter = null) { try { IQueryable <Entity> pagedDataQuery = queryableToPage; // Applies pre conditioning filtering to the data source. (This is a pre-filter that executes before the filters instructed by PagedDataSettings). if (PreConditionsToPagedDataFilter != null) { pagedDataQuery = pagedDataQuery.Where(PreConditionsToPagedDataFilter); } // Adds composed filter to the query here (This is the default filter inspector bult-in for the search). // This is a merge result from default query engine + customized queries from devs (ExtraPagedDataFilter method). pagedDataQuery = pagedDataQuery.AsExpandable().Where(MergeFilters(settings, DefaultPagedDataFilter(settings), ExtraPagedDataFilter, settings.SearchInALL)); // Adds sorting capabilities pagedDataQuery = this.AddSorting(pagedDataQuery, settings); // Total number of records regardless of paging. var totalRecordsInDB = pagedDataQuery.AsExpandable().Count(); // Shapes final result model. Post query filters to inner collection data are applied at this moment. return(pagedDataQuery.Skip((settings.Page - 1) * settings.TotalPerPage).Take(settings.TotalPerPage).AsExpandable().BuildUpResult(totalRecordsInDB, (p) => PostQueryCallbacksInvoker(p, settings))); } catch (Exception ex) { throw new Exception($"There was an error paging the datasource for entity: {nameof(Entity)}. Exception Details: {ex.Message}"); } }
public void MustTranformPagedDataAttributesWhenSorting() { // Arrange var items = MockModel.ArrangeFixture(); var settings = new PagedDataSettings() { }; settings.Sorting.Add(new SortingSettings() { Property = "UIFirstProperty" }); settings.Sorting.Add(new SortingSettings() { Property = "UISecondProperty" }); // Act var transformedSettings = DataPagerAdapter.TransformSettings(settings, this.GetType().GetMethod(nameof(MustTranformPagedDataAttributesWhenSorting))); // Assert Assert.Equal(transformedSettings.Filter.Count, settings.Filter.Count); Assert.Equal(transformedSettings.Sorting.Count, settings.Sorting.Count); Assert.True(transformedSettings.Sorting.Where(x => x.Property == "BackEndFirstProperty").Any()); Assert.True(transformedSettings.Sorting.Where(x => x.Property == "BackEndSecondProperty").Any()); }
public void MustTranformPagedDataAttributesWhenFiltering() { // Arrange var items = MockModel.ArrangeFixture(); var settings = new PagedDataSettings() { }; settings.Filter.Add(new FilterSettings() { Property = "UIFirstProperty", Value = items.First().Label, IsExactMatch = true }); settings.Filter.Add(new FilterSettings() { Property = "UISecondProperty", Value = items.Last().Label, IsExactMatch = true }); // Act var transformedSettings = DataPagerAdapter.TransformSettings(settings, this.GetType().GetMethod(nameof(MustTranformPagedDataAttributesWhenFiltering))); // Assert Assert.Equal(transformedSettings.Filter.Count, settings.Filter.Count); Assert.Equal(transformedSettings.Sorting.Count, settings.Sorting.Count); Assert.True(transformedSettings.Filter.Where(x => x.Property == "BackEndFirstProperty").Any()); Assert.True(transformedSettings.Filter.Where(x => x.Property == "BackEndSecondProperty").Any()); Assert.True(transformedSettings.Filter.Where(x => x.Property == "BackEndFirstProperty").FirstOrDefault().Value.Contains("First")); Assert.True(transformedSettings.Filter.Where(x => x.Property == "BackEndSecondProperty").FirstOrDefault().Value.Contains("Nineth")); }
/// <summary> /// Filters paged data based on <see cref="PagedDataSourceSettings"/>. /// </summary> /// <param name="settings">Custom settings to be dynamically converted and apply as filters to the result set.</param> /// <returns> /// Filtered result set from MongoDB. /// </returns> /// <remarks> /// This method was overriden here because MongoDB driver is not mature enough as EntityFramework, /// so some filter settings from the default <see cref="QueryableDataSourcePager{Key, Entity}"/> where throwing runtime exceptions. /// </remarks> protected override Expression <Func <Entity, bool> > DefaultPagedDataFilter(PagedDataSettings settings) { bool firstExecution = true; var queryLinq = string.Empty; // Holds Parameters values per index of this list (@0, @1, @2, etc). var paramValues = new List <object>(); if (settings.Filter != null && settings.Filter.Count > 0) { var validFilterSettings = settings.Filter.Where(x => !String.IsNullOrEmpty(x.Property) && !String.IsNullOrEmpty(x.Value)).GroupBy(x => x.Property).Select(y => y.FirstOrDefault()); foreach (var pFilter in validFilterSettings) { int collectionPathTotal = 0; var propInfo = this.GetValidatedPropertyInfo(pFilter.Property, out collectionPathTotal); string nullableValueOperator = ""; // Apparently String implements IEnumerable, since it is a collection of chars if (propInfo != null && (propInfo.PropertyType == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(propInfo.PropertyType))) { if (Nullable.GetUnderlyingType(propInfo.PropertyType) != null) { nullableValueOperator = ".Value"; } if (collectionPathTotal == 0) { if (propInfo.PropertyType == typeof(string)) { // Applying filter to nullable entity's property. if (pFilter.IsExactMatch) { queryLinq += (firstExecution ? string.Empty : " " + pFilter.Conjunction + " ") + pFilter.Property + nullableValueOperator + ".ToUpper() == @" + paramValues.Count; } else { queryLinq += (firstExecution ? string.Empty : " " + pFilter.Conjunction + " ") + pFilter.Property + nullableValueOperator + ".ToUpper().Contains(@" + paramValues.Count + ")"; } paramValues.Add(pFilter.Value.ToUpper()); firstExecution = false; } else { throw new NotImplementedException($"{nameof(DataPagerMongoDB<Key, Entity>)} only supports string properties filtering at the moment."); } } else { throw new NotImplementedException($"Inner collection filtering is not supported by {nameof(DataPagerMongoDB<Key, Entity>)}"); } } } } // Returns current default query as expression. return(queryLinq.ParseLambda <Entity>(paramValues.ToArray())); }
public void ShouldSetDefaultSortingByAttributes() { // Arrange var items = MockModel.ArrangeFixture(); var settings = new PagedDataSettings() { }; // Act var transformedSettings = DataPagerAdapter.TransformSettings(settings, this.GetType().GetMethod(nameof(ShouldSetDefaultSortingByAttributes))); // Assert Assert.Equal(transformedSettings.Sorting.Count, 1); Assert.True(transformedSettings.Sorting.Where(x => x.Property == "MyDefaultSortingProperty").Any()); }
/// <summary> /// This method translates a <see cref="PagedDataSettings"/> payload to another configured instance based on /// any <see cref="PagedDataFilterAttribute"/> or <see cref="PagedDataDefaultSortingAttribute"/> /// that may have been applied to a controller method. /// </summary> /// <param name="settings"> /// Payload received by the controller serialized by the ASP.NET default serializer or anyother type /// of configuration you may use. /// </param> /// <param name="callingMethodInfo"> /// The method base information that contains all the attributes in order to perform the adapter operation. /// </param> /// <remarks> /// This method needs to be called directly in the Controller method. /// It won't work if called inside another method called by the controller. /// It needs to be right after the controller method's execution call stack. /// </remarks> /// <returns>Updated/Adapted settings that are ready to be supplied to the <see cref="DataPager{Key, Entity}"/> class.</returns> public static PagedDataSettings TransformSettings(PagedDataSettings settings, MethodBase callingMethodInfo) { // TODO: Not yet supported on .NET Core, refer to this: https://github.com/dotnet/corefx/issues/1797 // Gets previous calling method information to get whatever attribute that may have been applied. //StackTrace stackTrace = new StackTrace(); //MethodBase methodBase = stackTrace.GetFrame(1).GetMethod(); var props = callingMethodInfo.GetCustomAttributes <PagedDataFilterAttribute>(); var sorting = callingMethodInfo.GetCustomAttribute <PagedDataDefaultSortingAttribute>(); // Checks if the consumer of the search wants to filter in ALL fields. if (settings.Filter.Where(x => x.Property.ToUpper() == PagedDataSettings.FILTERALLIDENTIFIER).Any()) { // Overriding all possible conjunctions set as OR since this is a "ALL" search. foreach (var filter in settings.Filter) { filter.Conjunction = LogicalConjunctionEnum.AND; } TranslateALLSearchFilters(props.Where(x => x.IgnoreOnSearch == false), settings); } TranslateDefaultFilters(props.Where(x => x.IgnoreOnSearch == false), settings); foreach (var property in props) { if (settings.Sorting != null) { // Searchs replacements for sort queries. var item = settings.Sorting.Where(x => x.Property == property.MapsFrom).FirstOrDefault(); if (item != null) { item.Property = property.MapsTo; // Only gets the first item if it is piped for sorting. // TODO: Implement piped sorting for future releases. item.PostQuerySortingPath = property.PostQueryFilterPath.Split('|').First(); } } } TranslateDefaultSorting(sorting, settings); return(settings); }
public void CanGetPagedData() { // Arrange var items = MockModel.ArrangeFixture(); var pagedSize = 5; var settings = new PagedDataSettings() { TotalPerPage = pagedSize }; // Act var result = _dataPager.GetPagedData(items.AsQueryable(), settings); // Assert Assert.True(result.TotalRecords == items.Count, "Total records in the paged result does not match the total in the fixture collection."); Assert.True(result.Result.Count == pagedSize, "Total items in first paged batch does not match the configured page size."); }
/// <summary> /// Adapts data that was sent by any consumer (Can be API or UI) to a DB persisted model. /// </summary> /// <param name="callerClassType"> /// This is the caller class type. Just use "this" as a reference. /// </param> /// <param name="callingMethodName"> /// This is the method of the calling name. Use it as "nameof(MyMethod)". /// </param> /// <param name="settings"> /// The initial settings sent by any consumer of page data functionality. /// </param> /// <returns> /// Returns a new settings model that can be used to filter DB model entities. /// </returns> /// <example> /// PagedDataSettings.TransformSettings(this, nameof(MyMethod), MySettingsPayload); /// </example> public static PagedDataSettings TransformSettings(Type callerClassType, string callingMethodName, PagedDataSettings settings) { var transformedSettings = new PagedDataSettings(); // We take the input and transform it back to the user. // Gets previous calling method data with the decorated attributes. MethodBase methodBase = callerClassType.GetMethod(callingMethodName); var props = methodBase.GetCustomAttributes <PagedDataAdapterAttribute>(); var sorting = methodBase.GetCustomAttribute <PagedDataDefaultSortingAttribute>(); // Checks if caller wants to filter in ALL fields. if (settings.Filter.Where(x => x.Property.ToUpper() == PagedDataSettings.FILTERALLIDENTIFIER).Any()) { // Overriding all possible conjunctions set as OR since this is an "ALL" search. foreach (var filter in settings.Filter) { filter.Conjunction = LogicalConjunctionEnum.AND; } InspectForAllFilter(props, settings, transformedSettings); } else { // TODO: Implement "OR" in client side / consumer if needed/request by someone. if (settings.Filter.Where(x => x.Conjunction != LogicalConjunctionEnum.AND).Any()) { throw new NotImplementedException($"'Or' filtering not supported by: {nameof(PagedDataAdapter)} transformation class."); } InspectFilterSettings(props, settings, transformedSettings); } // Copies (or adds default) sorting configurations. InspectSorting(props, sorting, settings, transformedSettings); return(settings); }
public void CanSortPagedDataByDescending() { // Arrange var items = MockModel.ArrangeFixture(); var pagedSize = 5; var settings = new PagedDataSettings() { TotalPerPage = pagedSize }; settings.Sorting.Add(new SortingSettings() { Property = "Label", Order = SortOrderEnum.DESC // Should default to ASC }); // Act var result = _dataPager.GetPagedData(items.AsQueryable(), settings); // Assert Assert.True(result.Result.First().Label == "Third Label"); Assert.True(result.Result.Last().Label == "Nineth"); }
/// <summary> /// Adds default filter mechanism to GetPagedData method. /// </summary> /// <remarks> /// This method allows multi-navigation property filter as long as they are not collections. /// It also supports collection BUT the collection needs to be the immediate first level of navigation property, and you can't use more than one depth. /// </remarks> /// <param name="settings">Current filter settings supplied by the consumer.</param> /// <returns>Expression to be embedded to the IQueryable filter instance.</returns> protected virtual Expression <Func <Entity, bool> > DefaultPagedDataFilter(PagedDataSettings settings) { bool hasErrors = false; // Identifies if one of the parameters had wrong data bool firstExecution = true; var queryLinq = string.Empty; // Holds Parameters values per index of this list (@0, @1, @2, etc). var paramValues = new List <object>(); if (settings.Filter != null && settings.Filter.Count > 0) { var validFilterSettings = settings.Filter.Where(x => !String.IsNullOrEmpty(x.Property) && !String.IsNullOrEmpty(x.Value)).GroupBy(x => x.Property).Select(y => y.FirstOrDefault()); foreach (var pFilter in validFilterSettings) { int collectionPathTotal = 0; var propInfo = this.GetValidatedPropertyInfo(pFilter.Property, out collectionPathTotal); string nullableValueOperator = string.Empty; // Apparently String implements IEnumerable, since it is a collection of chars if (propInfo != null && (propInfo.PropertyType == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(propInfo.PropertyType))) { if (Nullable.GetUnderlyingType(propInfo.PropertyType) != null) { nullableValueOperator = ".Value"; } if (collectionPathTotal == 0) { if (propInfo.PropertyType.IsAssignableFrom(typeof(DateTime))) { // Applies filter do DateTime properties DateTime castedDateTime; if (DateTime.TryParseExact(pFilter.Value, UI_DATE_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out castedDateTime)) { // Successfully casted the value to a datetime. queryLinq += (firstExecution ? string.Empty : " " + pFilter.Conjunction + " ") + "DbFunctions.TruncateTime(" + pFilter.Property + nullableValueOperator + ") == @" + paramValues.Count; paramValues.Add(castedDateTime.Date); firstExecution = false; } else { hasErrors = true; break; } } else { // Applying filter to nullable entity's property. if (pFilter.IsExactMatch) { queryLinq += (firstExecution ? string.Empty : " " + pFilter.Conjunction + " ") + pFilter.Property + nullableValueOperator + ".ToString().ToUpper() == @" + paramValues.Count; } else { queryLinq += (firstExecution ? string.Empty : " " + pFilter.Conjunction + " ") + pFilter.Property + nullableValueOperator + ".ToString().ToUpper().Contains(@" + paramValues.Count + ")"; } paramValues.Add(pFilter.Value.ToUpper()); firstExecution = false; } } else { // Only supports if it is immediately the first level. We checked this above =) var navigationPropertyCollection = pFilter.Property.Split('.')[0]; // Sub collection filter LINQ // Applies filter do DateTime properties if (propInfo.PropertyType.IsAssignableFrom(typeof(DateTime))) { DateTime castedDateTime; if (DateTime.TryParseExact(pFilter.Value, UI_DATE_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out castedDateTime)) { // Successfully casted the value to a datetime. queryLinq += (firstExecution ? string.Empty : " " + pFilter.Conjunction + " ") + navigationPropertyCollection + ".Where(DbFunctions.TruncateTime(" + pFilter.Property.Remove(0, navigationPropertyCollection.Length + 1) + nullableValueOperator + ") == @" + paramValues.Count + ").Count() > 0"; paramValues.Add(castedDateTime.Date); firstExecution = false; } else { hasErrors = true; break; } } else { if (pFilter.IsExactMatch) { queryLinq += (firstExecution ? string.Empty : " " + pFilter.Conjunction + " ") + navigationPropertyCollection + ".Where(" + pFilter.Property.Remove(0, navigationPropertyCollection.Length + 1) + nullableValueOperator + ".ToString().ToUpper() == @" + paramValues.Count + ").Count() > 0"; } else { queryLinq += (firstExecution ? string.Empty : " " + pFilter.Conjunction + " ") + navigationPropertyCollection + ".Where(" + pFilter.Property.Remove(0, navigationPropertyCollection.Length + 1) + nullableValueOperator + ".ToString().ToUpper().Contains(@" + paramValues.Count + ")).Count() > 0"; } paramValues.Add(pFilter.Value.ToUpper()); firstExecution = false; } } } } } // Returns current default query as expression. if (!hasErrors || settings.SearchInALL) { return(queryLinq.ParseLambda <Entity>(paramValues.ToArray())); } else { return("1 != 1".ParseLambda <Entity>(null)); //Invalidates the result set } }
/// <summary> /// Inspects if "ALL" option should be applied. This will gather all attributes decorated in the method and shape possible filter results. /// </summary> private static void InspectForAllFilter(IEnumerable <PagedDataAdapterAttribute> props, PagedDataSettings initialSettings, PagedDataSettings transformedSettings) { bool firstRun = true; initialSettings.SearchInALL = true; // Saves all added filters which are not ALL. This may be extra filters that are not mapped directly to DB Entities. var otherFilters = initialSettings.Filter.Where(x => x.Property.ToUpper() != PagedDataSettings.FILTERALLIDENTIFIER).ToList(); // Gets the value in "All" search if supplied. var filterValue = initialSettings.Filter.Where(x => x.Property.ToUpper() == PagedDataSettings.FILTERALLIDENTIFIER).FirstOrDefault().Value; // Inspecting each property sent by the consumer of Paged Data. foreach (var property in props) { var attrValue = property.MapsTo; transformedSettings.Filter.Add(new FilterSettings() { PostQueryFilterPath = property.InMemoryPath, Property = property.MapsTo, IsExactMatch = false, Value = filterValue ?? string.Empty, Conjunction = firstRun ? LogicalConjunctionEnum.AND : LogicalConjunctionEnum.OR }); firstRun = false; } // Removes all duplicates from filters that could be mapped. We will leave just filters that were not applied here. otherFilters.RemoveAll(x => transformedSettings.Filter.Where(y => y.Property == x.Property).Any()); // After we copied all filters that could be mapped by the adapter decorators, we are also pushing the ones that could not (Since they can be extra filters). transformedSettings.Filter = transformedSettings.Filter.Concat(otherFilters).ToList(); }
/// <summary> /// Applies filter to inner collections of a query result set from database. /// This is applied as a memory LINQ To Objects filter. /// </summary> /// <param name="fetchedResult">This is the return from EF query after going to DB.</param> /// <param name="settings">Paged data source settings.</param> /// <returns>Filtered collection result.</returns> private IList PostQueryFilter(IList fetchedResult, PagedDataSettings settings) { if (settings.Filter != null && settings.Filter.Count > 0 && !settings.SearchInALL) { var validFilterSettings = settings.Filter .Where(x => !String.IsNullOrEmpty(x.Value) && !String.IsNullOrEmpty(x.PostQueryFilterPath)) .GroupBy(x => x.Property) .Select(y => y.FirstOrDefault()); if (validFilterSettings.Count() > 0) { foreach (var result in fetchedResult) { string bufferedNavigationProperty = string.Empty; // TODO: Check if this needs to be refactored. bool firstExecution = true; // Identifies if it is the first filter added in order to apply or not conjunctions. var queryLinq = string.Empty; // Final lambda query to be applied to resulting data source which is already Linq-To-Objects var paramValues = new List <object>(); // Holds Parameters values per index of this list (@0, @1, @2, etc). foreach (var pFilter in validFilterSettings) { // This allows piping for DTO in memory filtering paths. var pipes = pFilter.PostQueryFilterPath.Split('|'); bool piped = false; // Set this to true in the end if we run more than one pipe. string pipedQuery = ""; foreach (var pipe in pipes) { // Only supports if it is immediately the first level. We checked this above =) var navigationPropertyCollection = pipe.Split('.')[0]; // We are buffering the query, but if the property has changed, then we will execute and replace the value inMemory and move on if (!firstExecution && bufferedNavigationProperty != navigationPropertyCollection) { result.ReplaceCollectionInstance(bufferedNavigationProperty, queryLinq); // Assign brand new values to move on as a new. Resetting here since seems the collection property CHANGED. queryLinq = string.Empty; firstExecution = true; pipedQuery = string.Empty; } bufferedNavigationProperty = navigationPropertyCollection; int collectionPathTotal = 0; var propInfo = result.GetNestedPropInfo(navigationPropertyCollection, out collectionPathTotal); // Tests if the current property to be filtered is NULLABLE in order to add ".Value" to string lambda query. var nullableValueOperator = string.Empty; var pipeProperty = pipe.Remove(0, navigationPropertyCollection.Length + 1); // Clean pipe without collection prefix. // Apparently String implements IEnumerable, since it is a collection of chars if (propInfo != null && (propInfo.PropertyType != typeof(string) || typeof(IEnumerable).IsAssignableFrom(propInfo.PropertyType))) { // Nullable subproperty field. if (Nullable.GetUnderlyingType(result.GetNestedPropInfo(pipe).PropertyType) != null) { nullableValueOperator = ".Value"; } // Sub collection filter LINQ // Applies filter do DateTime properties if (result.GetNestedPropInfo(pipe).PropertyType.IsAssignableFrom(typeof(DateTime))) { // Applies filter do DateTime properties DateTime castedDateTime; if (DateTime.TryParseExact(pFilter.Value, UI_DATE_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out castedDateTime)) { // Successfully casted the value to a datetime. queryLinq += (!piped ? string.Empty : " OR ") + pipeProperty + nullableValueOperator + ".Date == @" + paramValues.Count; paramValues.Add(castedDateTime.Date); } } else { // Sub collection filter LINQ if (pFilter.IsExactMatch) { pipedQuery += (!piped ? string.Empty : " OR ") + pipeProperty + nullableValueOperator + ".ToString().ToUpper() == \"" + pFilter.Value.ToUpper() + "\""; } else { pipedQuery += (!piped ? string.Empty : " OR ") + pipeProperty + nullableValueOperator + ".ToString().ToUpper().Contains(\"" + pFilter.Value.ToUpper() + "\")"; } } } piped = true; } if (!String.IsNullOrEmpty(pipedQuery)) { queryLinq += (firstExecution ? string.Empty : " " + pFilter.Conjunction + " ") + "(" + pipedQuery + ")"; } firstExecution = false; } result.ReplaceCollectionInstance(bufferedNavigationProperty, queryLinq); } } } return(fetchedResult); }
private static void TranslateDefaultFilters(IEnumerable <PagedDataFilterAttribute> props, PagedDataSettings settings) { // If it can find, then do the job. Otherwise we will fallback to whatever the UI sends directly to IQueryable. (No security issue since this is just for filter/ordering. foreach (var property in props) { // Translates a prom from => to relationship. var filterProp = settings.Filter.Where(x => x.Property.Equals(property.MapsFrom)).FirstOrDefault(); if (filterProp != null) { filterProp.Property = property.MapsTo; filterProp.PostQueryFilterPath = String.IsNullOrEmpty(property.PostQueryFilterPathExplict) ? property.PostQueryFilterPath : property.PostQueryFilterPathExplict; } } }
/// <summary> /// Returns a collection of data results that can be paged. /// </summary> /// <param name="settings">Settings for the search.</param> /// <returns>Filled PagedData instance.</returns> public IPagedDataResult <Entity> GetPagedData(PagedDataSettings settings) { return(_dataSourcePager.GetPagedData(GetQueryable(), settings, AddPreConditionsPagedDataFilter(settings), AddExtraPagedDataFilter(settings))); }
private static void TranslateDefaultSorting(PagedDataDefaultSortingAttribute sortingAttribute, PagedDataSettings settings) { if (settings.Sorting == null || settings.Sorting.Count == 0) { if (settings.Sorting == null) { settings.Sorting = new List <SortingSettings>(); } if (sortingAttribute != null) { settings.Sorting.Add(new SortingSettings() { Property = sortingAttribute.Property, Order = sortingAttribute.IsAscending ? SortOrderEnum.ASC : SortOrderEnum.DESC }); } } }
/// <summary> /// Configures settins for sorting if specified by user, or adds default sorting field that must be specified. /// </summary> private static void InspectSorting(IEnumerable <PagedDataAdapterAttribute> props, PagedDataDefaultSortingAttribute sortingAttribute, PagedDataSettings settings, PagedDataSettings transformedSettings) { foreach (var property in props) { if (settings.Sorting != null) { // Searchs filter configurations sent by consumer. var item = settings.Sorting.Where(x => x.Property == property.PropFrom).FirstOrDefault(); if (item != null) { transformedSettings.Sorting.Add( new SortingSettings() { Property = property.MapsTo, PostQuerySortingPath = property.InMemoryPath.Split('|').First() // Only gets the first if it is piped for sorting. }); } } } // Only applies this default sorting if consumer specified none. if (transformedSettings.Sorting == null || transformedSettings.Sorting.Count == 0) { if (sortingAttribute != null) { transformedSettings.Sorting.Add(new SortingSettings() { Property = sortingAttribute.Property, Order = sortingAttribute.IsAscending ? SortOrderEnum.ASC : SortOrderEnum.DESC }); } else { throw new MissingMemberException("No sorting was specified by the consumer and no default sorting property was configured among the adapters therefore it is not possible to excecute the paged data query."); } } }
/// <summary> /// Adds extra filter to PagedData method. /// </summary> /// <remarks> /// Override this method in <see cref="Repository{Key, Entity}{Key, Entity}"/> implementation /// if you want to add custom filter to your paged data source. /// </remarks> /// <param name="settings">Current filter settings supplied by the consumer.</param> /// <returns>Expression to be embedded to the IQueryable filter instance.</returns> protected virtual Expression <Func <Entity, bool> > AddExtraPagedDataFilter(PagedDataSettings settings) { // Needs to be overriden by devs to add behavior to this. // Change the injected filter on concrete repositories. return(null); }
private static void TranslateALLSearchFilters(IEnumerable <PagedDataFilterAttribute> props, PagedDataSettings settings) { bool firstExecution = true; settings.SearchInALL = true; // Saves all previous filters (Some cases we need it, as in TabView case). var otherFilters = settings.Filter.Where(x => x.Property.ToUpper() != PagedDataSettings.FILTERALLIDENTIFIER).ToList(); // Gets "All" supplied value var filterValue = settings.Filter.Where(x => x.Property.ToUpper() == PagedDataSettings.FILTERALLIDENTIFIER).FirstOrDefault().Value; // Resets all prior filter settings.Filter.Clear(); foreach (var property in props) { var attrValue = property.MapsTo; settings.Filter.Add(new FilterSettings() { PostQueryFilterPath = String.IsNullOrEmpty(property.PostQueryFilterPathExplict) ? property.PostQueryFilterPath : property.PostQueryFilterPathExplict, Property = property.MapsTo, IsExactMatch = false, Value = filterValue ?? string.Empty, Conjunction = firstExecution ? LogicalConjunctionEnum.AND : LogicalConjunctionEnum.OR }); firstExecution = false; } // Removes all duplicates from pre existing filters. otherFilters.RemoveAll(x => settings.Filter.Where(y => y.Property == x.Property).Any()); // Adds any other existing filters. settings.Filter = settings.Filter.Concat(otherFilters).ToList(); }
/// <summary> /// Returns a paged, filtered and sorted collection. /// </summary> /// <param name="settings">Settings model for the search.</param> /// <returns>Collection of filtered items result.</returns> public IPagedDataResult <Entity> GetPagedData(PagedDataSettings settings) { return(_dataPager.GetPagedData(Collection.AsQueryable(), settings, this.AddPreConditionsPagedDataFilter(settings), this.AddExtraPagedDataFilter(settings))); }
/// <summary> /// Returns a collection of data results that can be paged. /// </summary> /// <param name="settings">Settings for the search.</param> /// <returns>Filled PagedData instance.</returns> public IPagedDataResult <Entity> GetPagedData(PagedDataSettings settings) { return(_dataSourcePager.GetPagedData((IQueryable <Entity>) this.List(), settings, this.AddPreConditionsPagedDataFilter(settings), this.AddExtraPagedDataFilter(settings))); }
/// <summary> /// Adds default sorting mechanism to GetPagedData method. /// </summary> /// <remarks> /// This method allows multi-navigation property filter as long as they are not collections. /// It also supports collection BUT the collection needs to be the immediate first level of navigation property, and you can't use more than one depth. /// /// - The input IQueryable is being returned. Seems if you try to apply changes by reference, you don't get it outside of this method. May be implicit LINQ behavior. /// </remarks> /// <param name="settings">Current sorting settings supplied by the consumer.</param> /// <returns>Expression to be embedded to the IQueryable instance.</returns> private IQueryable <Entity> AddSorting(IQueryable <Entity> pagedDataQuery, PagedDataSettings settings) { bool noFilterApplied = true; // Generates the order clause based on supplied parameters if (settings.Sorting != null && settings.Sorting.Count > 0) { var bufferedSortByClause = string.Empty; var validOrderSettings = settings.Sorting.Where(x => !String.IsNullOrEmpty(x.Property) && String.IsNullOrEmpty(x.PostQuerySortingPath)).GroupBy(x => x.Property).Select(y => y.FirstOrDefault()); foreach (var o in validOrderSettings) { int collectionPathTotal = 0; var propInfo = this.GetValidatedPropertyInfo(o.Property, out collectionPathTotal); // Apparently String implements IEnumerable, since it is a collection of chars if (propInfo != null && (propInfo.PropertyType == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(propInfo.PropertyType))) { // Just applying DB filters to non collection related properties. if (collectionPathTotal == 0) { if (noFilterApplied) { noFilterApplied = false; } bufferedSortByClause += o.Property + " " + o.Order.ToString() + ","; } else if (collectionPathTotal == 1) { // Deferring this to a post query sorting event as it can't be evaluated by a DB Query Graph Result. if (string.IsNullOrEmpty(o.PostQuerySortingPath)) { o.PostQuerySortingPath = o.Property; } } else { // Only one level of deep collection supported at the moment. throw new Exception($"The Advanced Search Engine only supports sorting in a one level nested collection path. Any dot notation path that contains more than one inner collection property is not supported."); } } } // Applies the buffered sort by if (!noFilterApplied) { bufferedSortByClause = bufferedSortByClause.Substring(0, bufferedSortByClause.Length - 1); // Removes last comma pagedDataQuery = pagedDataQuery.OrderBy(bufferedSortByClause); } } // If there is no sorting configured, we need to add a default fallback one, which we will use the first property. Without this can't use LINQ Skip/Take if (settings.Sorting == null || settings.Sorting.Count == 0 || noFilterApplied) { var propCollection = typeof(Entity).GetTypeInfo().GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(x => !typeof(IEnumerable).IsAssignableFrom(x.PropertyType)).ToList(); if (propCollection.Count() > 0) { var firstFieldOfEntity = propCollection[0].Name; pagedDataQuery = pagedDataQuery.OrderBy(firstFieldOfEntity + " " + SortOrderEnum.DESC.ToString()); } else { throw new Exception($"The supplied Entity {nameof(Entity)} has no public properties, therefore the method can't continue the sorting operation."); } } return(pagedDataQuery); }
/// <summary> /// Adds precondition global filters to paged data source. /// Rely on this if you want to add security filters. /// </summary> /// <remarks> /// Override this method in <see cref="Repository{Key, Entity}{Key, Entity}"/> implementation /// if you want to add pre conditions global filters to your paged data source. /// </remarks> /// <param name="settings">Current filter settings supplied by the consumer.</param> /// <returns>Expression to be embedded to the IQueryable filter instance.</returns> protected virtual Expression <Func <Entity, bool> > AddPreConditionsPagedDataFilter(PagedDataSettings settings) { // Needs to be overriden by devs to add behavior to this. return(null); }
private static void InspectFilterSettings(IEnumerable <PagedDataAdapterAttribute> props, PagedDataSettings settings, PagedDataSettings transformedSettings) { // If it can find, then do the job. Otherwise we will fallback to whatever the UI sends directly to IQueryable. (No security issue since this is just for filter/ordering. foreach (var property in props) { // Searchs for filters that were decorated. var filterProp = settings.Filter.Where(x => x.Property.Equals(property.PropFrom)).FirstOrDefault(); if (filterProp != null) { transformedSettings.Filter.Add( new FilterSettings() { Property = property.MapsTo, PostQueryFilterPath = property.InMemoryPath }); } } }