/// <summary> /// A more flexible filtering method than Filter(). Filter() will always return materialized items. /// </summary> public IEnumerable <TEntityInterface> FilterOrQuery(IEnumerable <TEntityInterface> items, object parameter, Type parameterType) { bool preferQuery = items is IQueryable; // If exists use Filter(IQueryable, TParameter) or Filter(IEnumerable, TParameter) { ReadingOption enumerableFilter = () => { var reader = Reflection.RepositoryEnumerableFilterMethod(parameterType); if (reader == null) { return(null); } return(() => { _logger.Trace(() => "Filtering using enumerable Filter(items, " + reader.GetParameters()[1].ParameterType.FullName + ")"); Reflection.MaterializeEntityList(ref items); return (IEnumerable <TEntityInterface>)reader.InvokeEx(_repository.Value, items, parameter); }); }; ReadingOption queryableFilter = () => { var reader = Reflection.RepositoryQueryableFilterMethod(parameterType); if (reader == null) { return(null); } return(() => { _logger.Trace(() => "Filtering using queryable Filter(items, " + reader.GetParameters()[1].ParameterType.FullName + ")"); var query = Reflection.AsQueryable(items); return (IEnumerable <TEntityInterface>)reader.InvokeEx(_repository.Value, query, parameter); }); }; ReadingOptions options; if (!preferQuery) { options = new ReadingOptions { enumerableFilter, queryableFilter } } ; else { options = new ReadingOptions { queryableFilter, enumerableFilter } }; var readingMethod = options.FirstOptionOrNull(); if (readingMethod != null) { return(readingMethod()); } } // If the parameter is FilterAll, unless explicitly implemented above, return all if (typeof(FilterAll).IsAssignableFrom(parameterType)) { _logger.Trace(() => "Filtering all items returned."); return(items); } // If the parameter is a generic filter, unless explicitly implemented above, execute it if (parameterType == typeof(FilterCriteria)) { _logger.Trace(() => "Filtering using generic filter"); return(ExecuteGenericFilter(new[] { (FilterCriteria)parameter }, preferQuery, items)); } if (typeof(IEnumerable <FilterCriteria>).IsAssignableFrom(parameterType)) { _logger.Trace(() => "Filtering using generic filters"); return(ExecuteGenericFilter((IEnumerable <FilterCriteria>)parameter, preferQuery, items)); } // If the parameter is a generic property filter, unless explicitly implemented above, use queryable items.Where(property filter) if (typeof(IEnumerable <PropertyFilter>).IsAssignableFrom(parameterType)) { _logger.Trace(() => "Reading using items.AsQueryable().Where(property filter"); // The filterExpression must use EntityType or EntityNavigationType, depending on the provided query. var itemType = items.GetType().GetInterface("IEnumerable`1").GetGenericArguments()[0]; var filterExpression = _genericFilterHelper.ToExpression((IEnumerable <PropertyFilter>)parameter, itemType); if (Reflection.IsQueryable(items)) { var query = Reflection.AsQueryable(items); return(Reflection.Where(query, filterExpression)); } else { return(Reflection.Where(items, filterExpression.Compile())); } } // If the parameter is a filter expression, unless explicitly implemented above, use queryable items.Where(parameter) if (Reflection.IsPredicateExpression(parameterType)) { _logger.Trace(() => "Filtering using items.AsQueryable().Where(" + parameterType.Name + ")"); var query = Reflection.AsQueryable(items); return(Reflection.Where(query, (Expression)parameter)); } // If the parameter is a filter function, unless explicitly implemented above, use materialized items.Where(parameter) if (typeof(Func <TEntityInterface, bool>).IsAssignableFrom(parameterType)) { _logger.Trace(() => "Filtering using items.Where(" + parameterType.Name + ")"); var filterFunction = parameter as Func <TEntityInterface, bool>; Reflection.MaterializeEntityList(ref items); if (filterFunction != null) { return(items.Where(filterFunction)); } } // If the parameter is a IEnumarable<Guid>, it will be interpreted as filter by IDs. if (typeof(IEnumerable <Guid>).IsAssignableFrom(parameterType)) { _logger.Trace(() => "Filtering using items.Where(item => guids.Contains(item.ID))"); if (!(parameter is List <Guid>)) { parameter = ((IEnumerable <Guid>)parameter).ToList(); } if (items is IQueryable <TEntityInterface> ) // Use queryable Where function with bool expression instead of bool function. { // The query is built by reflection to avoid an obscure problem with complex query in NHibernate: // using generic parameter TEntityInterface or IEntity for a query parameter fails with exception on some complex scenarios. var filterPredicateParameter = Expression.Parameter(Reflection.EntityType, "item"); var filterPredicate = Expression.Lambda( Expression.Call( Expression.Constant(parameter), typeof(List <Guid>).GetMethod("Contains"), new[] { Expression.Property(filterPredicateParameter, "ID") }), filterPredicateParameter); return(Reflection.Where((IQueryable <TEntityInterface>)items, EFExpression.OptimizeContains(filterPredicate))); } return(items.Where(item => ((List <Guid>)parameter).Contains(item.ID))); } string errorMessage = $"{EntityName} does not implement a filter with parameter {parameterType.FullName}."; if (Reflection.RepositoryLoadWithParameterMethod(parameterType) != null) { errorMessage += " There is a loader method with this parameter implemented: Try reordering filters to use the " + parameterType.Name + " first."; throw new ClientException(errorMessage); } else { throw new FrameworkException(errorMessage); } }
public LambdaExpression ToExpression(IEnumerable <PropertyFilter> propertyFilters, Type parameterType) { ParameterExpression parameter = Expression.Parameter(parameterType, "p"); if (propertyFilters == null || !propertyFilters.Any()) { return(Expression.Lambda(Expression.Constant(true), parameter)); } Expression resultCondition = null; foreach (var filter in propertyFilters) { if (string.IsNullOrEmpty(filter.Property)) { continue; } Expression memberAccess = null; foreach (var property in filter.Property.Split('.')) { var parentExpression = memberAccess ?? (Expression)parameter; if (parentExpression.Type.GetProperty(property) == null) { throw new ClientException("Invalid generic filter parameter: Type '" + parentExpression.Type.FullName + "' does not have property '" + property + "'."); } memberAccess = Expression.Property(parentExpression, property); } // Change the type of the parameter 'value'. It is necessary for comparisons. bool propertyIsNullableValueType = memberAccess.Type.IsGenericType && memberAccess.Type.GetGenericTypeDefinition() == typeof(Nullable <>); Type propertyBasicType = propertyIsNullableValueType ? memberAccess.Type.GetGenericArguments().Single() : memberAccess.Type; ConstantExpression constant; // Operations 'equal' and 'notequal' are supported for backward compatibility. if (new[] { "equals", "equal", "notequals", "notequal", "greater", "greaterequal", "less", "lessequal" }.Contains(filter.Operation, StringComparer.OrdinalIgnoreCase)) { // Constant value should be of same type as the member it is compared to. object convertedValue; if (filter.Value == null || propertyBasicType.IsInstanceOfType(filter.Value)) { convertedValue = filter.Value; } // Guid object's type was not automatically recognized when deserializing from JSON: else if (propertyBasicType == typeof(Guid) && filter.Value is string) { convertedValue = Guid.Parse(filter.Value.ToString()); } // DateTime object's type was not automatically recognized when deserializing from JSON: else if (propertyBasicType == typeof(DateTime) && filter.Value is string) { convertedValue = ParseJsonDateTime((string)filter.Value, filter.Property, propertyBasicType); } else if ((propertyBasicType == typeof(decimal) || propertyBasicType == typeof(int)) && filter.Value is string) { throw new FrameworkException($"Invalid JSON format of {propertyBasicType.Name} property '{filter.Property}'. Numeric value should not be passed as a string in JSON serialized object."); } else { convertedValue = Convert.ChangeType(filter.Value, propertyBasicType); } if (convertedValue == null && memberAccess.Type.IsValueType && !propertyIsNullableValueType) { Type nullableMemberType = typeof(Nullable <>).MakeGenericType(memberAccess.Type); memberAccess = Expression.Convert(memberAccess, nullableMemberType); } constant = Expression.Constant(convertedValue, memberAccess.Type); } else if (new[] { "startswith", "endswith", "contains", "notcontains" }.Contains(filter.Operation, StringComparer.OrdinalIgnoreCase)) { // Constant value should be string. constant = Expression.Constant(filter.Value.ToString(), typeof(string)); } else if (new[] { "datein", "datenotin" }.Contains(filter.Operation, StringComparer.OrdinalIgnoreCase)) { constant = null; } else if (new[] { "in", "notin" }.Contains(filter.Operation, StringComparer.OrdinalIgnoreCase)) { if (filter.Value == null) { throw new ClientException($"Invalid generic filter parameter for operation '{filter.Operation}' on {propertyBasicType.Name} property '{filter.Property}'." + $" The provided value is null, instead of an Array."); } // The list element should be of same type as the member it is compared to. var parameterMismatchInfo = new Lazy <string>(() => $"Invalid generic filter parameter for operation '{filter.Operation}' on {propertyBasicType.Name} property '{filter.Property}'." + $" The provided value type is '{filter.Value.GetType()}', instead of an Array of {propertyBasicType.Name}."); if (!(filter.Value is IEnumerable)) { throw new ClientException(parameterMismatchInfo.Value); } var list = (IEnumerable)filter.Value; // Guid object's type was not automatically recognized when deserializing from JSON: if (propertyBasicType == typeof(Guid) && list is IEnumerable <string> ) { list = ((IEnumerable <string>)list).Select(s => !string.IsNullOrEmpty(s) ? (Guid?)Guid.Parse(s) : null).ToList(); } // DateTime object's type was not automatically recognized when deserializing from JSON: if (propertyBasicType == typeof(DateTime) && list is IEnumerable <string> ) { list = ((IEnumerable <string>)list).Select(s => ParseJsonDateTime(s, filter.Property, propertyBasicType)).ToList(); } // Adjust the list element type to exactly match the property type: if (GetElementType(list) != memberAccess.Type) { if (list is IList && ((IList)list).Count == 0) { list = EmptyList(memberAccess.Type); } else if (propertyBasicType == typeof(Guid)) { AdjustListTypeNullable <Guid>(ref list, propertyIsNullableValueType); } else if (propertyBasicType == typeof(DateTime)) { AdjustListTypeNullable <DateTime>(ref list, propertyIsNullableValueType); } else if (propertyBasicType == typeof(int)) { AdjustListTypeNullable <int>(ref list, propertyIsNullableValueType); } else if (propertyBasicType == typeof(decimal)) { AdjustListTypeNullable <decimal>(ref list, propertyIsNullableValueType); } if (!(GetElementType(list).IsAssignableFrom(memberAccess.Type))) { throw new ClientException(parameterMismatchInfo.Value); } } constant = Expression.Constant(list, list.GetType()); } else { throw new ClientException($"Unsupported generic filter operation '{filter.Operation}' on a property."); } Expression expression; switch (filter.Operation.ToLower()) { case "equals": case "equal": if (propertyBasicType == typeof(Guid) && constant.Value is Guid constantIdEquals) { // Using a different expression instead of the constant, to force Entity Framework to // use query parameter instead of hardcoding the constant value (literal) into the generated query. // Query with parameter will allow cache reuse for both EF LINQ compiler and database SQL compiler. if (memberAccess.Type == typeof(Guid?)) { Expression <Func <Guid?> > idLambda = () => constantIdEquals; expression = Expression.Equal(memberAccess, idLambda.Body); } else { Expression <Func <Guid> > idLambda = () => constantIdEquals; expression = Expression.Equal(memberAccess, idLambda.Body); } } else if (propertyBasicType == typeof(string) && constant.Value != null) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("EqualsCaseInsensitive"), memberAccess, constant); } else { expression = Expression.Equal(memberAccess, constant); } break; case "notequals": case "notequal": if (propertyBasicType == typeof(Guid) && constant.Value is Guid constantIdNotEquals) { // Using a different expression instead of the constant, to force Entity Framework to // use query parameter instead of hardcoding the constant value (literal) into the generated query. // Query with parameter will allow cache reuse for both EF LINQ compiler and database SQL compiler. Expression <Func <object> > idLambda = () => constantIdNotEquals; expression = Expression.NotEqual(memberAccess, Expression.Convert(idLambda.Body, memberAccess.Type)); } else if (propertyBasicType == typeof(string) && constant.Value != null) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("NotEqualsCaseInsensitive"), memberAccess, constant); } else { expression = Expression.NotEqual(memberAccess, constant); } break; case "greater": if (propertyBasicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsGreaterThen"), memberAccess, constant); } else if (propertyBasicType == typeof(Guid)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("GuidIsGreaterThan"), memberAccess, constant); } else { expression = Expression.GreaterThan(memberAccess, constant); } break; case "greaterequal": if (propertyBasicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsGreaterThenOrEqual"), memberAccess, constant); } else if (propertyBasicType == typeof(Guid)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("GuidIsGreaterThanOrEqual"), memberAccess, constant); } else { expression = Expression.GreaterThanOrEqual(memberAccess, constant); } break; case "less": if (propertyBasicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsLessThen"), memberAccess, constant); } else if (propertyBasicType == typeof(Guid)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("GuidIsLessThan"), memberAccess, constant); } else { expression = Expression.LessThan(memberAccess, constant); } break; case "lessequal": if (propertyBasicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsLessThenOrEqual"), memberAccess, constant); } else if (propertyBasicType == typeof(Guid)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("GuidIsLessThanOrEqual"), memberAccess, constant); } else { expression = Expression.LessThanOrEqual(memberAccess, constant); } break; case "startswith": case "endswith": { Expression stringMember; if (propertyBasicType == typeof(string)) { stringMember = memberAccess; } else { var castMethod = typeof(DatabaseExtensionFunctions).GetMethod("CastToString", new[] { memberAccess.Type }); if (castMethod == null) { throw new FrameworkException("Generic filter operation '" + filter.Operation + "' is not supported on property type '" + propertyBasicType.Name + "'. There is no overload of 'DatabaseExtensionFunctions.CastToString' function for the type."); } stringMember = Expression.Call(castMethod, memberAccess); } string dbMethodName = filter.Operation.Equals("startswith", StringComparison.OrdinalIgnoreCase) ? "StartsWithCaseInsensitive" : "EndsWithCaseInsensitive"; expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod(dbMethodName), stringMember, constant); break; } case "contains": case "notcontains": { Expression stringMember; if (propertyBasicType == typeof(string)) { stringMember = memberAccess; } else { var castMethod = typeof(DatabaseExtensionFunctions).GetMethod("CastToString", new[] { memberAccess.Type }); if (castMethod == null) { throw new FrameworkException("Generic filter operation '" + filter.Operation + "' is not supported on property type '" + propertyBasicType.Name + "'. There is no overload of 'DatabaseExtensionFunctions.CastToString' function for the type."); } stringMember = Expression.Call(castMethod, memberAccess); } expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("ContainsCaseInsensitive"), stringMember, constant); if (filter.Operation.Equals("notcontains", StringComparison.OrdinalIgnoreCase)) { expression = Expression.Not(expression); } break; } case "datein": case "datenotin": { if (propertyBasicType != typeof(DateTime)) { throw new FrameworkException("Generic filter operation '" + filter.Operation + "' is not supported for property type '" + propertyBasicType.Name + "'. Expected property type 'DateTime'."); } var match = DateRangeRegex.Match(filter.Value.ToString()); if (!match.Success) { throw new FrameworkException("Generic filter operation '" + filter.Operation + "' expects format 'yyyy-mm-dd', 'yyyy-mm' or 'yyyy'. Value '" + filter.Value + "' has invalid format."); } DateTime?date1, date2; int year = int.Parse(match.Groups["y"].Value); if (string.IsNullOrEmpty(match.Groups["m"].Value)) { date1 = new DateTime(year, 1, 1); date2 = date1.Value.AddYears(1); } else { int month = int.Parse(match.Groups["m"].Value); if (string.IsNullOrEmpty(match.Groups["d"].Value)) { date1 = new DateTime(year, month, 1); date2 = date1.Value.AddMonths(1); } else { int day = int.Parse(match.Groups["d"].Value); date1 = new DateTime(year, month, day); date2 = date1.Value.AddDays(1); } } expression = Expression.AndAlso( Expression.GreaterThanOrEqual(memberAccess, Expression.Constant(date1, typeof(DateTime?))), Expression.LessThan(memberAccess, Expression.Constant(date2, typeof(DateTime?)))); if (filter.Operation.Equals("datenotin", StringComparison.OrdinalIgnoreCase)) { expression = Expression.Not(expression); } break; } case "in": case "notin": { Type collectionElement = GetElementType(constant.Type); Expression convertedMemberAccess = memberAccess.Type != collectionElement ? Expression.Convert(memberAccess, collectionElement) : memberAccess; Type collectionBasicType = typeof(IQueryable).IsAssignableFrom(constant.Type) ? typeof(Queryable) : typeof(Enumerable); var containsMethod = collectionBasicType.GetMethods() .Single(m => m.Name == "Contains" && m.GetParameters().Length == 2) .MakeGenericMethod(collectionElement); expression = EFExpression.OptimizeContains(Expression.Call(containsMethod, constant, convertedMemberAccess)); if (filter.Operation.Equals("notin", StringComparison.OrdinalIgnoreCase)) { expression = Expression.Not(expression); } break; } default: throw new FrameworkException("Unsupported generic filter operation '" + filter.Operation + "' on property (while generating expression)."); } resultCondition = resultCondition != null?Expression.AndAlso(resultCondition, expression) : expression; } return(Expression.Lambda(resultCondition, parameter)); }
public IEnumerable <TEntityInterface> Read(object parameter, Type parameterType, bool preferQuery) { // Use Load(parameter), Query(parameter) or Filter(Query(), parameter), if any of the options exist. ReadingOption loaderWithParameter = () => { var reader = Reflection.RepositoryLoadWithParameterMethod(parameterType); if (reader == null) { return(null); } return(() => { _logger.Trace(() => "Reading using Load(" + reader.GetParameters()[0].ParameterType.FullName + ")"); return (IEnumerable <TEntityInterface>)reader.InvokeEx(_repository.Value, parameter); }); }; ReadingOption queryWithParameter = () => { var reader = Reflection.RepositoryQueryWithParameterMethod(parameterType); if (reader == null) { return(null); } return(() => { _logger.Trace(() => "Reading using Query(" + reader.GetParameters()[0].ParameterType.FullName + ")"); return (IEnumerable <TEntityInterface>)reader.InvokeEx(_repository.Value, parameter); }); }; ReadingOption queryThenQueryableFilter = () => { if (Reflection.RepositoryQueryMethod == null) { return(null); } var reader = Reflection.RepositoryQueryableFilterMethod(parameterType); if (reader == null) { return(null); } return(() => { _logger.Trace(() => "Reading using queryable Filter(Query(), " + reader.GetParameters()[1].ParameterType.FullName + ")"); var query = Reflection.RepositoryQueryMethod.InvokeEx(_repository.Value); return (IEnumerable <TEntityInterface>)reader.InvokeEx(_repository.Value, query, parameter); }); }; ReadingOption queryAll = () => { if (!typeof(FilterAll).IsAssignableFrom(parameterType)) { return(null); } var reader = Reflection.RepositoryQueryMethod; if (reader == null) { return(null); } return(() => { _logger.Trace(() => "Reading using Query()"); return (IEnumerable <TEntityInterface>)reader.InvokeEx(_repository.Value); }); }; { ReadingOptions options; if (!preferQuery) { options = new ReadingOptions { loaderWithParameter, queryWithParameter, queryThenQueryableFilter } } ; else { options = new ReadingOptions { queryWithParameter, queryThenQueryableFilter, queryAll, loaderWithParameter } }; var readingMethod = options.FirstOptionOrNull(); if (readingMethod != null) { return(readingMethod()); } } // If the parameter is FilterAll, unless explicitly implemented above, use Load() or Query() if any option exists if (typeof(FilterAll).IsAssignableFrom(parameterType)) { var options = new ReadingOptions { () => { var reader = Reflection.RepositoryLoadMethod; if (reader == null) { return(null); } return(() => { _logger.Trace(() => "Reading using Load()"); return (IEnumerable <TEntityInterface>)reader.InvokeEx(_repository.Value); }); }, () => { var reader = Reflection.RepositoryQueryMethod; if (reader == null) { return(null); } return(() => { _logger.Trace(() => "Reading using Query()"); return (IEnumerable <TEntityInterface>)reader.InvokeEx(_repository.Value); }); } }; if (preferQuery) { options.Reverse(); } var readingMethod = options.FirstOptionOrNull(); if (readingMethod != null) { return(readingMethod()); } } // If the parameter is a generic filter, unless explicitly implemented above, execute it if (parameterType == typeof(FilterCriteria)) { _logger.Trace(() => "Reading using generic filter"); return(ExecuteGenericFilter(new[] { (FilterCriteria)parameter }, preferQuery)); } if (typeof(IEnumerable <FilterCriteria>).IsAssignableFrom(parameterType)) { _logger.Trace(() => "Reading using generic filters"); return(ExecuteGenericFilter((IEnumerable <FilterCriteria>)parameter, preferQuery)); } // If the parameter is a generic property filter, unless explicitly implemented above, use Query().Where(property filter) if (Reflection.RepositoryQueryMethod != null && typeof(IEnumerable <PropertyFilter>).IsAssignableFrom(parameterType)) { _logger.Trace(() => "Reading using Query().Where(property filter"); var query = (IQueryable <TEntityInterface>)Reflection.RepositoryQueryMethod.InvokeEx(_repository.Value); var filterExpression = _genericFilterHelper.ToExpression((IEnumerable <PropertyFilter>)parameter, Reflection.EntityNavigationType); return(Reflection.Where(query, filterExpression)); } // If the parameter is a filter expression, unless explicitly implemented above, use Query().Where(parameter) if (Reflection.RepositoryQueryMethod != null && Reflection.IsPredicateExpression(parameterType)) { _logger.Trace(() => "Reading using Query().Where(" + parameterType.Name + ")"); var query = (IQueryable <TEntityInterface>)Reflection.RepositoryQueryMethod.InvokeEx(_repository.Value); return(Reflection.Where(query, (Expression)parameter)); } // If the parameter is a IEnumarable<Guid>, it will be interpreted as filter by IDs. if (Reflection.RepositoryQueryMethod != null && typeof(IEnumerable <Guid>).IsAssignableFrom(parameterType)) { _logger.Trace(() => "Reading using Query().Where(item => guids.Contains(item.ID))"); if (!(parameter is List <Guid>)) { parameter = ((IEnumerable <Guid>)parameter).ToList(); } var query = (IQueryable <TEntityInterface>)Reflection.RepositoryQueryMethod.InvokeEx(_repository.Value); // The query is built by reflection to avoid an obscure problem with complex query in NHibernate: // using generic parameter TEntityInterface or IEntity for a query parameter fails with exception on some complex scenarios. var filterPredicateParameter = Expression.Parameter(Reflection.EntityType, "item"); var filterPredicate = Expression.Lambda( Expression.Call( Expression.Constant(parameter), typeof(List <Guid>).GetMethod("Contains"), new[] { Expression.Property(filterPredicateParameter, "ID") }), filterPredicateParameter); return(Reflection.Where(query, EFExpression.OptimizeContains(filterPredicate))); } // It there is only enumerable filter available, use inefficient loader with in-memory filtering: Filter(Load(), parameter) { var reader = Reflection.RepositoryEnumerableFilterMethod(parameterType); if (reader != null) { IEnumerable <TEntityInterface> items; try { items = Read(null, typeof(FilterAll), preferQuery: false); } catch (FrameworkException) { items = null; } if (items != null) { _logger.Trace(() => "Reading using enumerable Filter(all, " + reader.GetParameters()[1].ParameterType.FullName + ")"); Reflection.MaterializeEntityList(ref items); return((IEnumerable <TEntityInterface>)reader.InvokeEx(_repository.Value, items, parameter)); } } } throw new FrameworkException($"{EntityName} does not implement a loader, a query or a filter with parameter {parameterType.FullName}."); }