From bbe100193d1d904aad6496b5ed28879faa07112c Mon Sep 17 00:00:00 2001 From: Brandon Henricks <78803523+bluemodus-brandon@users.noreply.github.com> Date: Fri, 12 Jul 2024 21:32:44 -0400 Subject: [PATCH 1/7] WIP --- .../Interfaces/IExpressionProcessor.cs | 9 +++++ .../ComparisonExpressionProcessor.cs | 39 +++++++++++++++++++ .../Processors/EqualityExpressionProcessor.cs | 29 ++++++++++++++ .../Processors/LogicalExpressionProcessor.cs | 23 +++++++++++ .../MethodCallExpressionProcessor.cs | 13 +++++++ .../QueryParameterManager.cs | 24 ++++++++++++ 6 files changed, 137 insertions(+) create mode 100644 src/XperienceCommunity.DataContext/Interfaces/IExpressionProcessor.cs create mode 100644 src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs create mode 100644 src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs create mode 100644 src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs create mode 100644 src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs create mode 100644 src/XperienceCommunity.DataContext/QueryParameterManager.cs diff --git a/src/XperienceCommunity.DataContext/Interfaces/IExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Interfaces/IExpressionProcessor.cs new file mode 100644 index 0000000..fc80e4e --- /dev/null +++ b/src/XperienceCommunity.DataContext/Interfaces/IExpressionProcessor.cs @@ -0,0 +1,9 @@ +using System.Linq.Expressions; + +namespace XperienceCommunity.DataContext.Interfaces +{ + internal interface IExpressionProcessor where T : Expression + { + void Process(T node); + } +} diff --git a/src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs new file mode 100644 index 0000000..4d04532 --- /dev/null +++ b/src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs @@ -0,0 +1,39 @@ +using System.Linq.Expressions; +using XperienceCommunity.DataContext.Interfaces; + +namespace XperienceCommunity.DataContext.Processors +{ + internal class ComparisonExpressionProcessor : IExpressionProcessor + { + private readonly bool _isEqual; + private readonly bool _isGreaterThan; + private readonly QueryParameterManager _parameterManager; + + public ComparisonExpressionProcessor(QueryParameterManager parameterManager, bool isGreaterThan, + bool isEqual = false) + { + _parameterManager = parameterManager; + _isGreaterThan = isGreaterThan; + _isEqual = isEqual; + } + + public void Process(BinaryExpression node) + { + if (node.Left is MemberExpression member && node.Right is ConstantExpression constant) + { + var comparisonOperator = _isGreaterThan ? ">" : "<"; + + if (_isEqual) + { + comparisonOperator += "="; + } + + _parameterManager.AddParameter(member.Member.Name, $"{comparisonOperator}{constant.Value}"); + } + else + { + throw new InvalidOperationException("Invalid expression format for comparison."); + } + } + } +} diff --git a/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs new file mode 100644 index 0000000..97d6b30 --- /dev/null +++ b/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs @@ -0,0 +1,29 @@ +using System.Linq.Expressions; +using XperienceCommunity.DataContext.Interfaces; + +namespace XperienceCommunity.DataContext.Processors +{ + internal class EqualityExpressionProcessor : IExpressionProcessor + { + private readonly QueryParameterManager _parameterManager; + private readonly bool _isEqual; + + public EqualityExpressionProcessor(QueryParameterManager parameterManager, bool isEqual = true) + { + _parameterManager = parameterManager; + _isEqual = isEqual; + } + + public void Process(BinaryExpression node) + { + if (node.Left is MemberExpression member && node.Right is ConstantExpression constant) + { + _parameterManager.AddParameter(member.Member.Name, (_isEqual ? constant.Value : $"!{constant.Value}")!); + } + else + { + throw new InvalidOperationException("Invalid expression format for equality comparison."); + } + } + } +} diff --git a/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs new file mode 100644 index 0000000..5e72d77 --- /dev/null +++ b/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs @@ -0,0 +1,23 @@ +using System.Linq.Expressions; +using XperienceCommunity.DataContext.Interfaces; + +namespace XperienceCommunity.DataContext.Processors +{ + internal class LogicalExpressionProcessor : IExpressionProcessor + { + private readonly bool _isAnd; + private readonly QueryParameterManager _parameterManager; + + public LogicalExpressionProcessor(QueryParameterManager parameterManager, bool isAnd) + { + _parameterManager = parameterManager; + _isAnd = isAnd; + } + + public void Process(BinaryExpression node) + { + var logicalOperator = _isAnd ? "AND" : "OR"; + _parameterManager.AddLogicalOperator(logicalOperator); + } + } +} diff --git a/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs new file mode 100644 index 0000000..16b6b88 --- /dev/null +++ b/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs @@ -0,0 +1,13 @@ +using System.Linq.Expressions; +using XperienceCommunity.DataContext.Interfaces; + +namespace XperienceCommunity.DataContext.Processors +{ + internal class MethodCallExpressionProcessor: IExpressionProcessor + { + public void Process(MethodCallExpression node) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/XperienceCommunity.DataContext/QueryParameterManager.cs b/src/XperienceCommunity.DataContext/QueryParameterManager.cs new file mode 100644 index 0000000..2776399 --- /dev/null +++ b/src/XperienceCommunity.DataContext/QueryParameterManager.cs @@ -0,0 +1,24 @@ +using CMS.ContentEngine; + +namespace XperienceCommunity.DataContext +{ + internal class QueryParameterManager + { + private readonly ContentTypeQueryParameters _queryParameters; + + public QueryParameterManager(ContentTypeQueryParameters queryParameters) + { + _queryParameters = queryParameters; + } + public void AddParameter(string key, object value) + { + // Logic to add a query parameter + } + + public void AddLogicalOperator(string operatorValue) + { + // Logic to add a logical operator (AND/OR) + } + // Methods to manage query parameters + } +} From 05c371a5054440850d3bff752df0cda3a307af96 Mon Sep 17 00:00:00 2001 From: Brandon Henricks <78803523+bluemodus-brandon@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:27:59 -0400 Subject: [PATCH 2/7] refactored --- .../Interfaces/IExpressionProcessor.cs | 6 +- .../Processors/BinaryExpressionProcessor.cs | 92 +++++++++++++++++++ .../ComparisonExpressionProcessor.cs | 39 -------- .../MethodCallExpressionProcessor.cs | 1 + .../QueryParameterManager.cs | 50 +++++++++- 5 files changed, 143 insertions(+), 45 deletions(-) create mode 100644 src/XperienceCommunity.DataContext/Processors/BinaryExpressionProcessor.cs delete mode 100644 src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs diff --git a/src/XperienceCommunity.DataContext/Interfaces/IExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Interfaces/IExpressionProcessor.cs index fc80e4e..6af306f 100644 --- a/src/XperienceCommunity.DataContext/Interfaces/IExpressionProcessor.cs +++ b/src/XperienceCommunity.DataContext/Interfaces/IExpressionProcessor.cs @@ -2,7 +2,11 @@ namespace XperienceCommunity.DataContext.Interfaces { - internal interface IExpressionProcessor where T : Expression + internal interface IExpressionProcessor + { + } + + internal interface IExpressionProcessor: IExpressionProcessor where T : Expression { void Process(T node); } diff --git a/src/XperienceCommunity.DataContext/Processors/BinaryExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/BinaryExpressionProcessor.cs new file mode 100644 index 0000000..3034d16 --- /dev/null +++ b/src/XperienceCommunity.DataContext/Processors/BinaryExpressionProcessor.cs @@ -0,0 +1,92 @@ +using System.Linq.Expressions; +using XperienceCommunity.DataContext.Interfaces; + +namespace XperienceCommunity.DataContext.Processors +{ + internal class BinaryExpressionProcessor : IExpressionProcessor + { + private readonly QueryParameterManager _parameterManager; + + public BinaryExpressionProcessor(QueryParameterManager parameterManager) + { + _parameterManager = parameterManager; + } + + public void Process(BinaryExpression node) + { + switch (node.NodeType) + { + case ExpressionType.Equal: + ProcessEquality(node, isEqual: true); + break; + case ExpressionType.NotEqual: + ProcessEquality(node, isEqual: false); + break; + case ExpressionType.GreaterThan: + ProcessComparison(node, isGreaterThan: true); + break; + case ExpressionType.GreaterThanOrEqual: + ProcessComparison(node, isGreaterThan: true, isEqual: true); + break; + case ExpressionType.LessThan: + ProcessComparison(node, isGreaterThan: false); + break; + case ExpressionType.LessThanOrEqual: + ProcessComparison(node, isGreaterThan: false, isEqual: true); + break; + case ExpressionType.AndAlso: + ProcessLogical(node, isAnd: true); + break; + case ExpressionType.OrElse: + ProcessLogical(node, isAnd: false); + break; + default: + throw new NotSupportedException($"The binary expression type '{node.NodeType}' is not supported."); + } + } + + private void ProcessEquality(BinaryExpression node, bool isEqual) + { + if (node.Left is MemberExpression member && node.Right is ConstantExpression constant) + { + if (isEqual) + { + _parameterManager.AddEqualsCondition(member.Member.Name, constant.Value); + } + else + { + _parameterManager.AddNotEqualsCondition(member.Member.Name, constant.Value); + } + } + else + { + throw new InvalidOperationException("Invalid expression format for equality comparison."); + } + } + + private void ProcessComparison(BinaryExpression node, bool isGreaterThan, bool isEqual = false) + { + if (node.Left is MemberExpression member && node.Right is ConstantExpression constant) + { + var comparisonOperator = isGreaterThan ? ">" : "<"; + if (isEqual) + { + comparisonOperator += "="; + } + + _parameterManager.AddComparisonCondition(member.Member.Name, comparisonOperator, constant.Value); + } + else + { + throw new InvalidOperationException("Invalid expression format for comparison."); + } + } + + private void ProcessLogical(BinaryExpression node, bool isAnd) + { + var logicalOperator = isAnd ? "AND" : "OR"; + + _parameterManager.AddLogicalCondition(logicalOperator); + } + } +} diff --git a/src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs deleted file mode 100644 index 4d04532..0000000 --- a/src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Linq.Expressions; -using XperienceCommunity.DataContext.Interfaces; - -namespace XperienceCommunity.DataContext.Processors -{ - internal class ComparisonExpressionProcessor : IExpressionProcessor - { - private readonly bool _isEqual; - private readonly bool _isGreaterThan; - private readonly QueryParameterManager _parameterManager; - - public ComparisonExpressionProcessor(QueryParameterManager parameterManager, bool isGreaterThan, - bool isEqual = false) - { - _parameterManager = parameterManager; - _isGreaterThan = isGreaterThan; - _isEqual = isEqual; - } - - public void Process(BinaryExpression node) - { - if (node.Left is MemberExpression member && node.Right is ConstantExpression constant) - { - var comparisonOperator = _isGreaterThan ? ">" : "<"; - - if (_isEqual) - { - comparisonOperator += "="; - } - - _parameterManager.AddParameter(member.Member.Name, $"{comparisonOperator}{constant.Value}"); - } - else - { - throw new InvalidOperationException("Invalid expression format for comparison."); - } - } - } -} diff --git a/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs index 16b6b88..208e8a0 100644 --- a/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs +++ b/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs @@ -9,5 +9,6 @@ public void Process(MethodCallExpression node) { throw new NotImplementedException(); } + } } diff --git a/src/XperienceCommunity.DataContext/QueryParameterManager.cs b/src/XperienceCommunity.DataContext/QueryParameterManager.cs index 2776399..541d8cc 100644 --- a/src/XperienceCommunity.DataContext/QueryParameterManager.cs +++ b/src/XperienceCommunity.DataContext/QueryParameterManager.cs @@ -10,15 +10,55 @@ public QueryParameterManager(ContentTypeQueryParameters queryParameters) { _queryParameters = queryParameters; } - public void AddParameter(string key, object value) + public void AddEqualsCondition(string key, object? value) { - // Logic to add a query parameter + if (value is null) + { + return; + } + + _queryParameters.Where(where => where.WhereEquals(key, value)); + } + + public void AddNotEqualsCondition(string key, object? value) + { + if (value is null) + { + return; + } + + _queryParameters.Where(where => where.WhereNotEquals(key, value)); + } + + public void AddLogicalCondition(string logicalOperator) + { + // Assuming that Kentico's API automatically handles AND/OR in chained conditions + // Therefore, this might be a placeholder or be handled differently } - public void AddLogicalOperator(string operatorValue) + public void AddComparisonCondition(string key, string comparisonOperator, object? value) { - // Logic to add a logical operator (AND/OR) + if (value is null) + { + return; + } + switch (comparisonOperator) + { + case ">": + _queryParameters.Where(where => where.WhereGreater(key, value)); + break; + case ">=": + _queryParameters.Where(where => where.WhereGreaterOrEquals(key, value)); + break; + case "<": + _queryParameters.Where(where => where.WhereLess(key, value)); + break; + case "<=": + _queryParameters.Where(where => where.WhereLessOrEquals(key, value)); + break; + default: + throw new NotSupportedException($"Comparison operator '{comparisonOperator}' is not supported."); + } } - // Methods to manage query parameters } } From f7d28a2ec52d46202d74dc149f86ba4acba12600 Mon Sep 17 00:00:00 2001 From: Brandon Henricks <78803523+bluemodus-brandon@users.noreply.github.com> Date: Sat, 13 Jul 2024 09:35:20 -0400 Subject: [PATCH 3/7] Refactor expression processors Refactored `EqualityExpressionProcessor.cs` and `LogicalExpressionProcessor.cs` in the `XperienceCommunity.DataContext.Processors` namespace. In `EqualityExpressionProcessor.cs`, updated the method for adding equality conditions by replacing `_parameterManager.AddParameter` with `_parameterManager.AddEqualsCondition` and simplified the handling of non-equal conditions by directly passing the constant value. In `LogicalExpressionProcessor.cs`, changed the method name for adding logical conditions from `_parameterManager.AddLogicalOperator` to `_parameterManager.AddLogicalCondition` to better reflect its purpose. --- .../Processors/EqualityExpressionProcessor.cs | 2 +- .../Processors/LogicalExpressionProcessor.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs index 97d6b30..4e2d05f 100644 --- a/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs +++ b/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs @@ -18,7 +18,7 @@ public void Process(BinaryExpression node) { if (node.Left is MemberExpression member && node.Right is ConstantExpression constant) { - _parameterManager.AddParameter(member.Member.Name, (_isEqual ? constant.Value : $"!{constant.Value}")!); + _parameterManager.AddEqualsCondition(member.Member.Name, (_isEqual ? constant.Value : constant.Value)!); } else { diff --git a/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs index 5e72d77..03b14f0 100644 --- a/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs +++ b/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs @@ -17,7 +17,7 @@ public LogicalExpressionProcessor(QueryParameterManager parameterManager, bool i public void Process(BinaryExpression node) { var logicalOperator = _isAnd ? "AND" : "OR"; - _parameterManager.AddLogicalOperator(logicalOperator); + _parameterManager.AddLogicalCondition(logicalOperator); } } } From 811101353b7a155ddafa378695b344f90c366406 Mon Sep 17 00:00:00 2001 From: Brandon Henricks <78803523+bluemodus-brandon@users.noreply.github.com> Date: Sat, 13 Jul 2024 10:23:38 -0400 Subject: [PATCH 4/7] Refactor query expression processing This commit overhauls the expression processing in our custom query system, introducing a modular approach to handling expressions with the creation of the `QueryParameterManager` and new generic expression processors (`BinaryExpressionProcessor`, `MethodCallExpressionProcessor`, `UnaryExpressionProcessor`). The `QueryParameterManager` centralizes query parameter management, enhancing the system's ability to handle a variety of conditions and logical operators more efficiently. It replaces direct parameter manipulation and absorbs functionalities of removed specific expression processors. The `ContentItemQueryExpressionVisitor` has been refactored to utilize this new structure, improving maintainability and extensibility of the codebase. These changes pave the way for more dynamic and powerful query generation capabilities. --- .../ContentItemContext.cs | 4 +- .../ContentItemQueryExpressionVisitor.cs | 685 +----------------- .../PageContentContext.cs | 4 +- .../Processors/EqualityExpressionProcessor.cs | 29 - .../Processors/LogicalExpressionProcessor.cs | 23 - .../MethodCallExpressionProcessor.cs | 15 +- .../Processors/UnaryExpressionProcessor.cs | 20 + .../QueryParameterManager.cs | 135 +++- 8 files changed, 186 insertions(+), 729 deletions(-) delete mode 100644 src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs delete mode 100644 src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs create mode 100644 src/XperienceCommunity.DataContext/Processors/UnaryExpressionProcessor.cs diff --git a/src/XperienceCommunity.DataContext/ContentItemContext.cs b/src/XperienceCommunity.DataContext/ContentItemContext.cs index bcf1407..9111c6d 100644 --- a/src/XperienceCommunity.DataContext/ContentItemContext.cs +++ b/src/XperienceCommunity.DataContext/ContentItemContext.cs @@ -223,7 +223,9 @@ private ContentItemQueryBuilder BuildQuery(Expression expression, int? topN = nu subQuery.Offset(_offset.Item1.Value, _offset.Item2.Value); } - var visitor = new ContentItemQueryExpressionVisitor(subQuery); + var manager = new QueryParameterManager(subQuery); + + var visitor = new ContentItemQueryExpressionVisitor(manager); visitor.Visit(expression); }); diff --git a/src/XperienceCommunity.DataContext/ContentItemQueryExpressionVisitor.cs b/src/XperienceCommunity.DataContext/ContentItemQueryExpressionVisitor.cs index 114ac5f..4a46718 100644 --- a/src/XperienceCommunity.DataContext/ContentItemQueryExpressionVisitor.cs +++ b/src/XperienceCommunity.DataContext/ContentItemQueryExpressionVisitor.cs @@ -2,695 +2,82 @@ using System.Linq.Expressions; using System.Reflection; using CMS.ContentEngine; +using XperienceCommunity.DataContext.Interfaces; +using XperienceCommunity.DataContext.Processors; namespace XperienceCommunity.DataContext { internal sealed class ContentItemQueryExpressionVisitor : ExpressionVisitor { - private readonly ContentTypeQueryParameters _queryParameters; + private readonly QueryParameterManager _parameterManager; + private readonly Dictionary _expressionProcessors; private string? _currentMemberName; private object? _currentValue; - public ContentItemQueryExpressionVisitor(ContentTypeQueryParameters queryParameters) + public ContentItemQueryExpressionVisitor(QueryParameterManager parameterManager) { - _queryParameters = queryParameters ?? throw new ArgumentNullException(nameof(queryParameters)); - } + _parameterManager = parameterManager ?? throw new ArgumentNullException(nameof(parameterManager)); - protected override Expression VisitBinary(BinaryExpression node) - { - switch (node.NodeType) + _expressionProcessors = new Dictionary { - case ExpressionType.Equal: - ProcessEquality(node); - break; - - case ExpressionType.NotEqual: - ProcessNotEquality(node); - break; - - case ExpressionType.GreaterThan: - ProcessComparison(node, isGreaterThan: true); - break; - - case ExpressionType.GreaterThanOrEqual: - ProcessComparison(node, isGreaterThan: true, isEqual: true); - break; - - case ExpressionType.LessThan: - ProcessComparison(node, isGreaterThan: false); - break; - - case ExpressionType.LessThanOrEqual: - ProcessComparison(node, isGreaterThan: false, isEqual: true); - break; - - case ExpressionType.AndAlso: - ProcessLogicalAnd(node); - break; - - case ExpressionType.OrElse: - ProcessLogicalOr(node); - break; - - // Add support for more expression types - default: - throw new NotSupportedException($"The binary expression type '{node.NodeType}' is not supported."); - } - - return node; + { typeof(BinaryExpression), new BinaryExpressionProcessor(_parameterManager) }, + { typeof(MethodCallExpression), new MethodCallExpressionProcessor(_parameterManager) }, + { typeof(UnaryExpression), new UnaryExpressionProcessor(_parameterManager) } + }; } - protected override Expression VisitConstant(ConstantExpression node) - { - _currentValue = node.Value; - - return node; - } - - protected override Expression VisitMember(MemberExpression node) + public IExpressionProcessor GetProcessor(Type expressionType) { - if (node.Expression != null && node.Expression.NodeType == ExpressionType.Parameter) - { - _currentMemberName = node.Member.Name; - } - else + if (_expressionProcessors.TryGetValue(expressionType, out var processor)) { - _currentValue = GetMemberValue(node); + return processor; } - - return node; + throw new NotSupportedException($"The expression type '{expressionType}' is not supported."); } - protected override Expression VisitMethodCall(MethodCallExpression node) - { - if (node.Method.DeclaringType == typeof(string)) - { - switch (node.Method.Name) - { - case nameof(string.Contains): - ProcessStringContains(node); - break; - - case nameof(string.StartsWith): - ProcessStringStartsWith(node); - break; - - default: - throw new NotSupportedException($"The method '{node.Method.Name}' is not supported."); - } - } - else if (node.Method.DeclaringType == typeof(Enumerable)) - { - if (node.Method.Name == nameof(Enumerable.Contains)) - { - ProcessEnumerableContains(node); - } - else - { - throw new NotSupportedException($"The method '{node.Method.Name}' is not supported."); - } - } - else if (node.Method.DeclaringType == typeof(Queryable)) - { - switch (node.Method.Name) - { - case nameof(Queryable.Where): - return ProcessQueryableWhere(node); - - case nameof(Queryable.Select): - return ProcessQueryableSelect(node); - // Add other Queryable methods as needed - default: - throw new NotSupportedException($"The method call '{node.Method.Name}' is not supported."); - } - } - else if (node.Method.Name == nameof(Enumerable.Contains)) - { - ProcessEnumerableContains(node); - } - else - { - throw new NotSupportedException($"The method '{node.Method.Name}' is not supported."); - } - - return node; - } - - protected override Expression VisitUnary(UnaryExpression node) - { - if (node.NodeType == ExpressionType.Convert) - { - Visit(node.Operand); - } - else - { - throw new NotSupportedException($"The unary expression type '{node.NodeType}' is not supported."); - } - - return node; - } - - private static IEnumerable ExtractValues(object? value) - { - if (value is IEnumerable objectEnumerable) - { - return objectEnumerable; - } - - if (value is IEnumerable intEnumerable) - { - return intEnumerable.Cast(); - } - - if (value is IEnumerable stringEnumerable) - { - return stringEnumerable.Cast(); - } - - if (value is IEnumerable guidEnumerable) - { - return guidEnumerable.Cast(); - } - - if (value is IEnumerable enumerable) - { - var list = new List(); - - foreach (var item in enumerable) - { - var itemValues = ExtractValues(item); - list.AddRange(itemValues); - } - - return list; - } - - if (value is null) - { - return []; - } - - // Check if the object has a property that is a collection - var properties = value.GetType().GetProperties(); - - var collectionProperty = properties.FirstOrDefault(p => - p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>)); - - if (collectionProperty != null) - { - var collectionValue = collectionProperty.GetValue(value); - return ExtractValues(collectionValue); - } - - return new[] { value }; - } - - private static string? GetMemberNameFromMethodCall(MethodCallExpression methodCall) - { - if (methodCall.Object is MemberExpression memberExpression) - { - return memberExpression.Member.Name; - } - - return null; - } - - private static object? GetMemberValue(Expression? expression) - { - switch (expression) - { - case ConstantExpression constantExpression: - return constantExpression.Value; - - case MemberExpression memberExpression: - var member = memberExpression.Member; - - var objectValue = - GetMemberValue(memberExpression?.Expression); // Recursively process the expression - - if (objectValue == null) - { - throw new InvalidOperationException("The target object for the member expression is null."); - } - - return member switch - { - FieldInfo fieldInfo => fieldInfo.GetValue(objectValue), - PropertyInfo propertyInfo => propertyInfo.GetValue(objectValue), - _ => throw new NotSupportedException( - $"The member type '{member.GetType().Name}' is not supported.") - }; - - case UnaryExpression unaryExpression when unaryExpression.NodeType == ExpressionType.Convert: - return GetMemberValue(unaryExpression.Operand); - - case ParameterExpression parameterExpression: - // Handle ParameterExpression by returning null as it should be handled in its context - return null; - - default: - throw new NotSupportedException( - $"The expression type '{expression?.GetType().Name}' is not supported."); - } - } - - private static object? GetMethodCallValue(MethodCallExpression methodCall) - { - // Evaluate the method call expression to get the resulting value - var lambda = Expression.Lambda(methodCall).Compile(); - return lambda.DynamicInvoke(); - } - - private void AddWhereInCondition(string columnName, IEnumerable? values) - { - if (values == null) - { - return; - } - - if (!values.Any()) - { - // Pass an empty array to WhereIn - _queryParameters.Where(where => where.WhereIn(columnName, Array.Empty())); - return; - } - - var firstValue = values.First(); - - if (firstValue is int) - { - _queryParameters.Where(where => where.WhereIn(columnName, values.Cast().ToArray())); - } - else if (firstValue is string) - { - _queryParameters.Where(where => where.WhereIn(columnName, values.Cast().ToArray())); - } - else if (firstValue is Guid) - { - _queryParameters.Where(where => where.WhereIn(columnName, values.Cast().ToArray())); - } - else - { - return; - } - } - - private IEnumerable? ExtractCollectionValues(MemberExpression collectionExpression) - { - if (collectionExpression.Expression != null) - { - var value = GetExpressionValue(collectionExpression.Expression); - - return ExtractValues(value); - } - - return null; - } - - private static IEnumerable ExtractFieldValues(MemberExpression fieldExpression) - { - var value = GetExpressionValue(fieldExpression); - return ExtractValues(value); - } - - private static object? GetExpressionValue(Expression expression) - { - switch (expression) - { - case ConstantExpression constantExpression: - return constantExpression.Value!; - - case MemberExpression memberExpression: - var container = GetExpressionValue(memberExpression.Expression!); - var member = memberExpression.Member; - switch (member) - { - case FieldInfo fieldInfo: - return fieldInfo.GetValue(container); - - case PropertyInfo propertyInfo: - return propertyInfo.GetValue(container); - - default: - throw new NotSupportedException( - $"The member type '{member.GetType().Name}' is not supported."); - } - default: - throw new NotSupportedException( - $"The expression type '{expression.GetType().Name}' is not supported."); - } - } - - private void ProcessComparison(BinaryExpression node, bool isGreaterThan, bool isEqual = false) - { - if (node.Left is MemberExpression left) - { - if (node.Right is ConstantExpression right) - { - if (isGreaterThan) - { - if (isEqual) - { - _queryParameters.Where(where => where.WhereGreaterOrEquals(left.Member.Name, right.Value)); - } - else - { - _queryParameters.Where(where => where.WhereGreater(left.Member.Name, right.Value)); - } - } - else - { - if (isEqual) - { - _queryParameters.Where(where => where.WhereLessOrEquals(left.Member.Name, right.Value)); - } - else - { - _queryParameters.Where(where => where.WhereLess(left.Member.Name, right.Value)); - } - } - } - else if (node.Right is MemberExpression rightMember) - { - if (isGreaterThan) - { - if (isEqual) - { - _queryParameters.Where(where => - where.WhereGreaterOrEquals(left.Member.Name, GetMemberValue(rightMember))); - } - else - { - _queryParameters.Where(where => - where.WhereGreater(left.Member.Name, GetMemberValue(rightMember))); - } - } - else - { - if (isEqual) - { - _queryParameters.Where(where => - where.WhereLessOrEquals(left.Member.Name, GetMemberValue(rightMember))); - } - else - { - _queryParameters.Where(where => - where.WhereLess(left.Member.Name, GetMemberValue(rightMember))); - } - } - } - else if (node.Right is UnaryExpression rightUnary && rightUnary.NodeType == ExpressionType.Convert) - { - var value = GetMemberValue(rightUnary.Operand); - if (isGreaterThan) - { - if (isEqual) - { - _queryParameters.Where(where => where.WhereGreaterOrEquals(left.Member.Name, value)); - } - else - { - _queryParameters.Where(where => where.WhereGreater(left.Member.Name, value)); - } - } - else - { - if (isEqual) - { - _queryParameters.Where(where => where.WhereLessOrEquals(left.Member.Name, value)); - } - else - { - _queryParameters.Where(where => where.WhereLess(left.Member.Name, value)); - } - } - } - else if (node.Right is MethodCallExpression rightMethod) - { - var value = GetMethodCallValue(rightMethod); - if (isGreaterThan) - { - if (isEqual) - { - _queryParameters.Where(where => where.WhereGreaterOrEquals(left.Member.Name, value)); - } - else - { - _queryParameters.Where(where => where.WhereGreater(left.Member.Name, value)); - } - } - else - { - if (isEqual) - { - _queryParameters.Where(where => where.WhereLessOrEquals(left.Member.Name, value)); - } - else - { - _queryParameters.Where(where => where.WhereLess(left.Member.Name, value)); - } - } - } - else - { - throw new NotSupportedException( - $"The right expression type '{node.Right.GetType().Name}' is not supported."); - } - } - else if (node.Left is MethodCallExpression leftMethod && node.Right is ConstantExpression rightConst) - { - var memberName = GetMemberNameFromMethodCall(leftMethod); - - if (memberName != null) - { - if (isGreaterThan) - { - if (isEqual) - { - _queryParameters.Where(where => where.WhereGreaterOrEquals(memberName, rightConst.Value)); - } - else - { - _queryParameters.Where(where => where.WhereGreater(memberName, rightConst.Value)); - } - } - else - { - if (isEqual) - { - _queryParameters.Where(where => where.WhereLessOrEquals(memberName, rightConst.Value)); - } - else - { - _queryParameters.Where(where => where.WhereLess(memberName, rightConst.Value)); - } - } - } - else - { - throw new NotSupportedException( - $"The left method call expression '{leftMethod}' is not supported."); - } - } - else - { - throw new NotSupportedException( - $"The left expression type '{node.Left.GetType().Name}' is not supported."); - } - } - - private void ProcessEnumerableContains(MethodCallExpression node) - { - if (node.Arguments.Count == 2) - { - var collectionExpression = node.Arguments[0]; - var itemExpression = node.Arguments[1]; - - if (collectionExpression is MemberExpression collectionMember && itemExpression is ConstantExpression constantExpression) - { - var columnName = collectionMember.Member.Name; - var values = ExtractValues(constantExpression.Value); - - AddWhereInCondition(columnName, values); - } - else if (collectionExpression is MemberExpression collectionFieldExpression && itemExpression is MemberExpression itemFieldExpression) - { - var collection = ExtractFieldValues(collectionFieldExpression); - var columnName = itemFieldExpression.Member.Name; - - AddWhereInCondition(columnName, collection); - } - else - { - throw new NotSupportedException($"The expression types '{collectionExpression.GetType().Name}' and '{itemExpression.GetType().Name}' are not supported."); - } - } - else if (node.Arguments.Count == 1 && node.Object != null) - { - var collectionExpression = node.Object; - var itemExpression = node.Arguments[0]; - - if (collectionExpression is MemberExpression collectionMember && itemExpression is MemberExpression itemMember) - { - var collection = ExtractFieldValues(collectionMember); - var columnName = itemMember.Member.Name; - - AddWhereInCondition(columnName, collection); - } - else - { - throw new NotSupportedException($"The expression types '{collectionExpression.GetType().Name}' and '{itemExpression.GetType().Name}' are not supported."); - } - } - else - { - throw new NotSupportedException($"The method '{node.Method.Name}' requires either 1 or 2 arguments."); - } - } - - private void ProcessEquality(BinaryExpression node) - { - if (node.Left is MemberExpression left) - { - if (node.Right is ConstantExpression right) - { - _queryParameters.Where(where => where.WhereEquals(left.Member.Name, right.Value)); - } - else if (node.Right is MemberExpression rightMember) - { - _queryParameters.Where(where => where.WhereEquals(left.Member.Name, GetMemberValue(rightMember))); - } - else if (node.Right is UnaryExpression rightUnary && rightUnary.NodeType == ExpressionType.Convert) - { - var value = GetMemberValue(rightUnary.Operand); - _queryParameters.Where(where => where.WhereEquals(left.Member.Name, value)); - } - else if (node.Right is MethodCallExpression rightMethod) - { - var value = GetMethodCallValue(rightMethod); - _queryParameters.Where(where => where.WhereEquals(left.Member.Name, value)); - } - else - { - throw new NotSupportedException( - $"The right expression type '{node.Right.GetType().Name}' is not supported."); - } - } - else if (node.Left is MethodCallExpression leftMethod && node.Right is ConstantExpression rightConst) - { - var value = GetMethodCallValue(leftMethod); - - _queryParameters.Where(where => where.WhereEquals(value?.ToString(), rightConst.Value)); - } - else - { - throw new NotSupportedException( - $"The left expression type '{node.Left.GetType().Name}' is not supported."); - } - } - - private void ProcessLogicalAnd(BinaryExpression node) - { - Visit(node.Left); - _queryParameters.Where(where => where.And()); - Visit(node.Right); - } - - private void ProcessLogicalOr(BinaryExpression node) - { - Visit(node.Left); - _queryParameters.Where(where => where.Or()); - Visit(node.Right); - } - - private void ProcessNotEquality(BinaryExpression node) + protected override Expression VisitBinary(BinaryExpression node) { - if (node.Left is MemberExpression left) + if (_expressionProcessors.TryGetValue(node.GetType(), out var processor)) { - if (node.Right is ConstantExpression right) - { - _queryParameters.Where(where => where.WhereNotEquals(left.Member.Name, right.Value)); - } - else if (node.Right is MemberExpression rightMember) - { - _queryParameters.Where(where => - where.WhereNotEquals(left.Member.Name, GetMemberValue(rightMember))); - } - else if (node.Right is UnaryExpression rightUnary && rightUnary.NodeType == ExpressionType.Convert) - { - var value = GetMemberValue(rightUnary.Operand); - _queryParameters.Where(where => where.WhereNotEquals(left.Member.Name, value)); - } - else if (node.Right is MethodCallExpression rightMethod) - { - var value = GetMethodCallValue(rightMethod); - _queryParameters.Where(where => where.WhereNotEquals(left.Member.Name, value)); - } - else - { - throw new NotSupportedException( - $"The right expression type '{node.Right.GetType().Name}' is not supported."); - } + ((IExpressionProcessor)processor).Process(node); + return node; } - else if (node.Left is MethodCallExpression leftMethod && node.Right is ConstantExpression rightConst) - { - var value = GetMethodCallValue(leftMethod); - _queryParameters.Where(where => where.WhereNotEquals(value?.ToString(), rightConst.Value)); - } - else - { - throw new NotSupportedException( - $"The left expression type '{node.Left.GetType().Name}' is not supported."); - } + throw new NotSupportedException($"The binary expression type '{node.NodeType}' is not supported."); } - private Expression ProcessQueryableSelect(MethodCallExpression node) + protected override Expression VisitMethodCall(MethodCallExpression node) { - if (node.Arguments[1] is UnaryExpression unaryExpression && - unaryExpression.Operand is LambdaExpression lambdaExpression) + if (_expressionProcessors.TryGetValue(node.GetType(), out var processor)) { - Visit(lambdaExpression.Body); - } - else - { - throw new NotSupportedException( - $"The expression type '{node.Arguments[1].GetType().Name}' is not supported."); + ((IExpressionProcessor)processor).Process(node); + return node; } - return node; + return base.VisitMethodCall(node); } - private Expression ProcessQueryableWhere(MethodCallExpression node) + protected override Expression VisitUnary(UnaryExpression node) { - if (node.Arguments[1] is UnaryExpression unaryExpression && - unaryExpression.Operand is LambdaExpression lambdaExpression) + if (_expressionProcessors.TryGetValue(node.GetType(), out var processor)) { - Visit(lambdaExpression.Body); - } - else - { - throw new NotSupportedException( - $"The expression type '{node.Arguments[1].GetType().Name}' is not supported."); + ((IExpressionProcessor)processor).Process(node); + return node; } - return node; + return base.VisitUnary(node); } - private void ProcessStringContains(MethodCallExpression node) + protected override Expression VisitMember(MemberExpression node) { - if (node.Object is MemberExpression member && node.Arguments[0] is ConstantExpression constant) - { - _queryParameters.Where(where => where.WhereContains(member.Member.Name, constant?.Value?.ToString())); - } + _currentMemberName = node.Member.Name; + return base.VisitMember(node); } - private void ProcessStringStartsWith(MethodCallExpression node) + protected override Expression VisitConstant(ConstantExpression node) { - if (node.Object is MemberExpression member && node.Arguments[0] is ConstantExpression constant) - { - _queryParameters.Where(where => where.WhereStartsWith(member.Member.Name, constant?.Value?.ToString())); - } + _currentValue = node.Value; + return base.VisitConstant(node); } } } diff --git a/src/XperienceCommunity.DataContext/PageContentContext.cs b/src/XperienceCommunity.DataContext/PageContentContext.cs index 88f00ff..c566214 100644 --- a/src/XperienceCommunity.DataContext/PageContentContext.cs +++ b/src/XperienceCommunity.DataContext/PageContentContext.cs @@ -208,7 +208,9 @@ private ContentItemQueryBuilder BuildQuery(Expression expression, int? topN = nu subQuery.Offset(_offset.Item1.Value, _offset.Item2.Value); } - var visitor = new ContentItemQueryExpressionVisitor(subQuery); + var manager = new QueryParameterManager(subQuery); + + var visitor = new ContentItemQueryExpressionVisitor(manager); visitor.Visit(expression); }); diff --git a/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs deleted file mode 100644 index 4e2d05f..0000000 --- a/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Linq.Expressions; -using XperienceCommunity.DataContext.Interfaces; - -namespace XperienceCommunity.DataContext.Processors -{ - internal class EqualityExpressionProcessor : IExpressionProcessor - { - private readonly QueryParameterManager _parameterManager; - private readonly bool _isEqual; - - public EqualityExpressionProcessor(QueryParameterManager parameterManager, bool isEqual = true) - { - _parameterManager = parameterManager; - _isEqual = isEqual; - } - - public void Process(BinaryExpression node) - { - if (node.Left is MemberExpression member && node.Right is ConstantExpression constant) - { - _parameterManager.AddEqualsCondition(member.Member.Name, (_isEqual ? constant.Value : constant.Value)!); - } - else - { - throw new InvalidOperationException("Invalid expression format for equality comparison."); - } - } - } -} diff --git a/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs deleted file mode 100644 index 03b14f0..0000000 --- a/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Linq.Expressions; -using XperienceCommunity.DataContext.Interfaces; - -namespace XperienceCommunity.DataContext.Processors -{ - internal class LogicalExpressionProcessor : IExpressionProcessor - { - private readonly bool _isAnd; - private readonly QueryParameterManager _parameterManager; - - public LogicalExpressionProcessor(QueryParameterManager parameterManager, bool isAnd) - { - _parameterManager = parameterManager; - _isAnd = isAnd; - } - - public void Process(BinaryExpression node) - { - var logicalOperator = _isAnd ? "AND" : "OR"; - _parameterManager.AddLogicalCondition(logicalOperator); - } - } -} diff --git a/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs index 208e8a0..6a7c5c2 100644 --- a/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs +++ b/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs @@ -3,12 +3,21 @@ namespace XperienceCommunity.DataContext.Processors { - internal class MethodCallExpressionProcessor: IExpressionProcessor + internal class MethodCallExpressionProcessor : IExpressionProcessor { - public void Process(MethodCallExpression node) + private readonly QueryParameterManager _parameterManager; + + public MethodCallExpressionProcessor(QueryParameterManager parameterManager) { - throw new NotImplementedException(); + _parameterManager = parameterManager; } + public void Process(MethodCallExpression node) + { + var methodName = node.Method.Name; + var arguments = node.Arguments.Select(arg => Expression.Lambda(arg).Compile().DynamicInvoke()).ToArray(); + + _parameterManager.AddMethodCall(methodName, arguments); + } } } diff --git a/src/XperienceCommunity.DataContext/Processors/UnaryExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/UnaryExpressionProcessor.cs new file mode 100644 index 0000000..96b9be9 --- /dev/null +++ b/src/XperienceCommunity.DataContext/Processors/UnaryExpressionProcessor.cs @@ -0,0 +1,20 @@ +using System.Linq.Expressions; +using XperienceCommunity.DataContext.Interfaces; + +namespace XperienceCommunity.DataContext.Processors +{ + internal class UnaryExpressionProcessor: IExpressionProcessor + { + private readonly QueryParameterManager _parameterManager; + + public UnaryExpressionProcessor(QueryParameterManager parameterManager) + { + _parameterManager = parameterManager; + } + + public void Process(UnaryExpression node) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/XperienceCommunity.DataContext/QueryParameterManager.cs b/src/XperienceCommunity.DataContext/QueryParameterManager.cs index 541d8cc..efdccda 100644 --- a/src/XperienceCommunity.DataContext/QueryParameterManager.cs +++ b/src/XperienceCommunity.DataContext/QueryParameterManager.cs @@ -5,59 +5,148 @@ namespace XperienceCommunity.DataContext internal class QueryParameterManager { private readonly ContentTypeQueryParameters _queryParameters; + private readonly List> _whereActions; public QueryParameterManager(ContentTypeQueryParameters queryParameters) { _queryParameters = queryParameters; + _whereActions = new List>(); } - public void AddEqualsCondition(string key, object? value) + + public void AddComparisonCondition(string key, string comparisonOperator, object? value) { - if (value is null) + if (value == null) { return; } - _queryParameters.Where(where => where.WhereEquals(key, value)); + switch (comparisonOperator) + { + case ">": + _whereActions.Add(where => where.WhereGreater(key, value)); + break; + + case ">=": + _whereActions.Add(where => where.WhereGreaterOrEquals(key, value)); + break; + + case "<": + _whereActions.Add(where => where.WhereLess(key, value)); + break; + + case "<=": + _whereActions.Add(where => where.WhereLessOrEquals(key, value)); + break; + + default: + throw new NotSupportedException($"Comparison operator '{comparisonOperator}' is not supported."); + } } - public void AddNotEqualsCondition(string key, object? value) + public void AddEqualsCondition(string key, object? value) { - if (value is null) + if (value == null) { return; } - _queryParameters.Where(where => where.WhereNotEquals(key, value)); + _whereActions.Add(where => where.WhereEquals(key, value)); } public void AddLogicalCondition(string logicalOperator) { - // Assuming that Kentico's API automatically handles AND/OR in chained conditions - // Therefore, this might be a placeholder or be handled differently + switch (logicalOperator.ToUpper()) + { + case "AND": + _whereActions.Add(where => where.And()); + break; + + case "OR": + _whereActions.Add(where => where.Or()); + break; + + default: + throw new NotSupportedException($"Logical operator '{logicalOperator}' is not supported."); + } } - public void AddComparisonCondition(string key, string comparisonOperator, object? value) + public void AddMethodCall(string methodName, object?[] parameters) { - if (value is null) + if (parameters is null || parameters.Length == 0) { return; } - switch (comparisonOperator) + + switch (methodName) { - case ">": - _queryParameters.Where(where => where.WhereGreater(key, value)); - break; - case ">=": - _queryParameters.Where(where => where.WhereGreaterOrEquals(key, value)); - break; - case "<": - _queryParameters.Where(where => where.WhereLess(key, value)); - break; - case "<=": - _queryParameters.Where(where => where.WhereLessOrEquals(key, value)); + case "Contains": + var columnName = parameters?[0]?.ToString(); + + var values = parameters?.Skip(1).ToArray(); + + if (string.IsNullOrWhiteSpace(columnName)) + { + return; + } + + if (values is null) + { + return; + } + + AddWhereInCondition(columnName, values); break; + // Handle other method calls as necessary default: - throw new NotSupportedException($"Comparison operator '{comparisonOperator}' is not supported."); + throw new NotSupportedException($"Method '{methodName}' is not supported."); + } + } + + public void AddNotEqualsCondition(string key, object? value) + { + if (value == null) + { + return; + } + + _whereActions.Add(where => where.WhereNotEquals(key, value)); + } + + public void ApplyConditions() + { + _queryParameters.Where(whereParameters => + { + foreach (var action in _whereActions) + { + action(whereParameters); + } + }); + } + + private void AddWhereInCondition(string key, object?[] collection) + { + if (collection == null || collection.Length == 0) + { + return; + } + + var elementType = collection[0]?.GetType(); + + if (elementType == typeof(int)) + { + _whereActions.Add(where => where.WhereIn(key, collection.Cast().ToList())); + } + else if (elementType == typeof(string)) + { + _whereActions.Add(where => where.WhereIn(key, collection.Cast().ToList())); + } + else if (elementType == typeof(Guid)) + { + _whereActions.Add(where => where.WhereIn(key, collection.Cast().ToList())); + } + else + { + throw new NotSupportedException($"Collection of type '{elementType}' is not supported."); } } } From 906fcd71d99d4cc26d8e56534a1624b0bf953de6 Mon Sep 17 00:00:00 2001 From: Brandon Henricks <78803523+bluemodus-brandon@users.noreply.github.com> Date: Sat, 13 Jul 2024 12:55:48 -0400 Subject: [PATCH 5/7] Enhanced query construction and management This commit significantly improves the system's ability to construct and manage complex queries. Key enhancements include: - Enhanced Query Parameter Management: Added conditions support in `QueryParameterManager` for more flexible query construction. - Expanded Expression Processing: Introduced dictionaries in `ContentItemQueryExpressionVisitor` for nuanced handling of expressions, including logical, comparison, and equality operations. - Improved Expression Visitor Logic: Adjusted to ensure correct processor utilization, enhancing expression processing robustness. - Public Access to Helper Methods: Made `ExtractValues` method public in `ExpressionExtensions`, extending its utility. - New Processors for Expression Types: Added `ComparisonExpressionProcessor`, `EqualityExpressionProcessor`, and `LogicalExpressionProcessor` for specialized expression processing. - Method Call Processing Enhancements: Expanded `MethodCallExpressionProcessor` to support a wider range of method calls. - Unary Expression Processing: Enhanced `UnaryExpressionProcessor` to support NOT, Convert, and Quote operations. - Refined Query Parameter Handling: Updated `QueryParameterManager` with new methods for complex query construction and condition handling. - Clearing Conditions Post-Application: Ensured conditions are cleared in `QueryParameterManager` after application to prevent unintended accumulation. - Support for Queryable Methods: Added support in `QueryParameterManager` for `Queryable.Where` and `Queryable.Select`, enabling more complex LINQ queries. These changes collectively enhance the system's flexibility, robustness, and capability in managing complex queries. --- .../ContentItemContext.cs | 3 + .../ContentItemQueryExpressionVisitor.cs | 23 ++- .../Extensions/ExpressionExtensions.cs | 2 +- .../ComparisonExpressionProcessor.cs | 38 ++++ .../Processors/EqualityExpressionProcessor.cs | 86 +++++++++ .../Processors/LogicalExpressionProcessor.cs | 36 ++++ .../MethodCallExpressionProcessor.cs | 79 ++++++++- .../Processors/UnaryExpressionProcessor.cs | 53 +++++- .../QueryParameterManager.cs | 166 +++++++++++++++--- 9 files changed, 453 insertions(+), 33 deletions(-) create mode 100644 src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs create mode 100644 src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs create mode 100644 src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs diff --git a/src/XperienceCommunity.DataContext/ContentItemContext.cs b/src/XperienceCommunity.DataContext/ContentItemContext.cs index 9111c6d..8dc8b61 100644 --- a/src/XperienceCommunity.DataContext/ContentItemContext.cs +++ b/src/XperienceCommunity.DataContext/ContentItemContext.cs @@ -228,6 +228,9 @@ private ContentItemQueryBuilder BuildQuery(Expression expression, int? topN = nu var visitor = new ContentItemQueryExpressionVisitor(manager); visitor.Visit(expression); + + // Apply conditions before returning the query parameters + manager.ApplyConditions(); }); if (!string.IsNullOrEmpty(_language)) diff --git a/src/XperienceCommunity.DataContext/ContentItemQueryExpressionVisitor.cs b/src/XperienceCommunity.DataContext/ContentItemQueryExpressionVisitor.cs index 4a46718..dd3b2c8 100644 --- a/src/XperienceCommunity.DataContext/ContentItemQueryExpressionVisitor.cs +++ b/src/XperienceCommunity.DataContext/ContentItemQueryExpressionVisitor.cs @@ -10,6 +10,8 @@ namespace XperienceCommunity.DataContext internal sealed class ContentItemQueryExpressionVisitor : ExpressionVisitor { private readonly QueryParameterManager _parameterManager; + + private readonly Dictionary> _binaryExpressionProcessors; private readonly Dictionary _expressionProcessors; private string? _currentMemberName; private object? _currentValue; @@ -18,9 +20,20 @@ public ContentItemQueryExpressionVisitor(QueryParameterManager parameterManager) { _parameterManager = parameterManager ?? throw new ArgumentNullException(nameof(parameterManager)); + _binaryExpressionProcessors = new Dictionary> + { + { ExpressionType.Equal, new EqualityExpressionProcessor(_parameterManager) }, + { ExpressionType.NotEqual, new EqualityExpressionProcessor(_parameterManager, isEqual: false) }, + { ExpressionType.GreaterThan, new ComparisonExpressionProcessor(_parameterManager, isGreaterThan: true) }, + { ExpressionType.GreaterThanOrEqual, new ComparisonExpressionProcessor(_parameterManager, isGreaterThan: true, isEqual: true) }, + { ExpressionType.LessThan, new ComparisonExpressionProcessor(_parameterManager, isGreaterThan: false) }, + { ExpressionType.LessThanOrEqual, new ComparisonExpressionProcessor(_parameterManager, isGreaterThan: false, isEqual: true) }, + { ExpressionType.AndAlso, new LogicalExpressionProcessor(_parameterManager, isAnd: true) }, + { ExpressionType.OrElse, new LogicalExpressionProcessor(_parameterManager, isAnd: false) } + }; + _expressionProcessors = new Dictionary { - { typeof(BinaryExpression), new BinaryExpressionProcessor(_parameterManager) }, { typeof(MethodCallExpression), new MethodCallExpressionProcessor(_parameterManager) }, { typeof(UnaryExpression), new UnaryExpressionProcessor(_parameterManager) } }; @@ -37,9 +50,9 @@ public IExpressionProcessor GetProcessor(Type expressionType) protected override Expression VisitBinary(BinaryExpression node) { - if (_expressionProcessors.TryGetValue(node.GetType(), out var processor)) + if (_binaryExpressionProcessors.TryGetValue(node.NodeType, out var processor)) { - ((IExpressionProcessor)processor).Process(node); + processor.Process(node); return node; } @@ -48,7 +61,7 @@ protected override Expression VisitBinary(BinaryExpression node) protected override Expression VisitMethodCall(MethodCallExpression node) { - if (_expressionProcessors.TryGetValue(node.GetType(), out var processor)) + if (_expressionProcessors.TryGetValue(typeof(MethodCallExpression), out var processor)) { ((IExpressionProcessor)processor).Process(node); return node; @@ -59,7 +72,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node) protected override Expression VisitUnary(UnaryExpression node) { - if (_expressionProcessors.TryGetValue(node.GetType(), out var processor)) + if (_expressionProcessors.TryGetValue(typeof(UnaryExpression), out var processor)) { ((IExpressionProcessor)processor).Process(node); return node; diff --git a/src/XperienceCommunity.DataContext/Extensions/ExpressionExtensions.cs b/src/XperienceCommunity.DataContext/Extensions/ExpressionExtensions.cs index 8317986..8f34786 100644 --- a/src/XperienceCommunity.DataContext/Extensions/ExpressionExtensions.cs +++ b/src/XperienceCommunity.DataContext/Extensions/ExpressionExtensions.cs @@ -82,7 +82,7 @@ internal static IEnumerable ExtractFieldValues(this MemberExpression fie return null; } - private static IEnumerable ExtractValues(object? value) + public static IEnumerable ExtractValues(this object? value) { if (value is null) { diff --git a/src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs new file mode 100644 index 0000000..2464fbb --- /dev/null +++ b/src/XperienceCommunity.DataContext/Processors/ComparisonExpressionProcessor.cs @@ -0,0 +1,38 @@ +using System.Linq.Expressions; +using XperienceCommunity.DataContext.Interfaces; + +namespace XperienceCommunity.DataContext.Processors +{ + internal class ComparisonExpressionProcessor : IExpressionProcessor + { + private readonly QueryParameterManager _parameterManager; + private readonly bool _isGreaterThan; + private readonly bool _isEqual; + + public ComparisonExpressionProcessor(QueryParameterManager parameterManager, bool isGreaterThan, bool isEqual = false) + { + _parameterManager = parameterManager; + _isGreaterThan = isGreaterThan; + _isEqual = isEqual; + } + + public void Process(BinaryExpression node) + { + if (node.Left is MemberExpression member && node.Right is ConstantExpression constant) + { + var comparisonOperator = _isGreaterThan ? ">" : "<"; + + if (_isEqual) + { + comparisonOperator += "="; + } + + _parameterManager.AddComparisonCondition(member.Member.Name, comparisonOperator, constant.Value); + } + else + { + throw new InvalidOperationException("Invalid expression format for comparison."); + } + } + } +} diff --git a/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs new file mode 100644 index 0000000..2ff036c --- /dev/null +++ b/src/XperienceCommunity.DataContext/Processors/EqualityExpressionProcessor.cs @@ -0,0 +1,86 @@ +using System.Linq.Expressions; +using XperienceCommunity.DataContext.Interfaces; + +namespace XperienceCommunity.DataContext.Processors +{ + internal class EqualityExpressionProcessor : IExpressionProcessor + { + private readonly QueryParameterManager _parameterManager; + private readonly bool _isEqual; + + public EqualityExpressionProcessor(QueryParameterManager parameterManager, bool isEqual = true) + { + _parameterManager = parameterManager; + _isEqual = isEqual; + } + + public void Process(BinaryExpression node) + { + if (node.Left is MemberExpression leftMember && node.Right is ConstantExpression rightConstant) + { + ProcessMemberToConstant(leftMember, rightConstant); + } + else if (node.Left is ConstantExpression leftConstant && node.Right is MemberExpression rightMember) + { + ProcessMemberToConstant(rightMember, leftConstant); + } + else if (node.Left is MemberExpression memberLeft && node.Right is UnaryExpression unaryRight) + { + ProcessMemberToUnary(memberLeft, unaryRight); + } + else if (node.Left is UnaryExpression unaryLeft && node.Right is MemberExpression memberRight) + { + ProcessMemberToUnary(memberRight, unaryLeft); + } + else if (node.Left is MemberExpression leftMemberExpression && node.Right is MemberExpression rightMemberExpression) + { + ProcessMemberToMember(leftMemberExpression, rightMemberExpression); + } + else + { + throw new InvalidOperationException("Invalid expression format for equality comparison."); + } + } + + private void ProcessMemberToConstant(MemberExpression member, ConstantExpression constant) + { + if (_isEqual) + { + _parameterManager.AddEqualsCondition(member.Member.Name, constant.Value); + } + else + { + _parameterManager.AddNotEqualsCondition(member.Member.Name, constant.Value); + } + } + + private void ProcessMemberToUnary(MemberExpression member, UnaryExpression unary) + { + if (unary.Operand is ConstantExpression constant) + { + ProcessMemberToConstant(member, constant); + } + else + { + throw new InvalidOperationException("Invalid unary expression format for equality comparison."); + } + } + + private void ProcessMemberToMember(MemberExpression leftMember, MemberExpression rightMember) + { + // Evaluate the right member to get its value + var lambda = Expression.Lambda(rightMember); + var compiled = lambda.Compile(); + var rightValue = compiled.DynamicInvoke(); + + if (_isEqual) + { + _parameterManager.AddEqualsCondition(leftMember.Member.Name, rightValue); + } + else + { + _parameterManager.AddNotEqualsCondition(leftMember.Member.Name, rightValue); + } + } + } +} diff --git a/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs new file mode 100644 index 0000000..85737a5 --- /dev/null +++ b/src/XperienceCommunity.DataContext/Processors/LogicalExpressionProcessor.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using CMS.ContentEngine; +using XperienceCommunity.DataContext.Interfaces; + +namespace XperienceCommunity.DataContext.Processors +{ + internal class LogicalExpressionProcessor : IExpressionProcessor + { + private readonly QueryParameterManager _parameterManager; + private readonly bool _isAnd; + + public LogicalExpressionProcessor(QueryParameterManager parameterManager, bool isAnd) + { + _parameterManager = parameterManager; + _isAnd = isAnd; + } + + public void Process(BinaryExpression node) + { + + if (_isAnd) + { + _parameterManager.AddLogicalCondition("AND"); + } + else + { + _parameterManager.AddLogicalCondition("OR"); + } + } + } +} diff --git a/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs index 6a7c5c2..7c8c216 100644 --- a/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs +++ b/src/XperienceCommunity.DataContext/Processors/MethodCallExpressionProcessor.cs @@ -14,10 +14,83 @@ public MethodCallExpressionProcessor(QueryParameterManager parameterManager) public void Process(MethodCallExpression node) { - var methodName = node.Method.Name; - var arguments = node.Arguments.Select(arg => Expression.Lambda(arg).Compile().DynamicInvoke()).ToArray(); + if (node.Method.DeclaringType == typeof(string)) + { + switch (node.Method.Name) + { + case nameof(string.Contains): + _parameterManager.AddStringContains(node); + break; - _parameterManager.AddMethodCall(methodName, arguments); + case nameof(string.StartsWith): + _parameterManager.AddStringStartsWith(node); + break; + + default: + throw new NotSupportedException($"The method '{node.Method.Name}' is not supported."); + } + } + else if (node.Method.DeclaringType == typeof(Enumerable)) + { + if (node.Method.Name == nameof(Enumerable.Contains)) + { + _parameterManager.AddEnumerableContains(node); + } + else + { + throw new NotSupportedException($"The method '{node.Method.Name}' is not supported."); + } + } + else if (node.Method.DeclaringType == typeof(Queryable)) + { + switch (node.Method.Name) + { + case nameof(Queryable.Where): + _parameterManager.AddQueryableWhere(node); + break; + case nameof(Queryable.Select): + _parameterManager.AddQueryableSelect(node); + break; + // Add other Queryable methods as needed + default: + throw new NotSupportedException($"The method call '{node.Method.Name}' is not supported."); + } + } + else if (node.Method.Name == nameof(Enumerable.Contains)) + { + _parameterManager.AddEnumerableContains(node); + } + else + { + throw new NotSupportedException($"The method '{node.Method.Name}' is not supported."); + } + + } + + private void ProcessContainsMethod(MethodCallExpression node, object?[] arguments) + { + if (node.Object is MemberExpression memberExpression) + { + if (arguments[0] == null) + { + return; + } + // Handle cases like "x.Name.Contains("test")" + _parameterManager.AddMethodCall("Contains", memberExpression.Member.Name, arguments[0]!); + } + else if (arguments.Length == 2 && arguments[1] is MemberExpression collectionMember) + { + if (arguments[0] == null) + { + return; + } + // Handle cases like "collection.Contains(x.Name)" + _parameterManager.AddMethodCall("Contains", collectionMember.Member.Name, arguments[0]!); + } + else + { + throw new InvalidOperationException("Invalid expression format for Contains method."); + } } } } diff --git a/src/XperienceCommunity.DataContext/Processors/UnaryExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/UnaryExpressionProcessor.cs index 96b9be9..0a9a0ea 100644 --- a/src/XperienceCommunity.DataContext/Processors/UnaryExpressionProcessor.cs +++ b/src/XperienceCommunity.DataContext/Processors/UnaryExpressionProcessor.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using CMS.DataEngine; using XperienceCommunity.DataContext.Interfaces; namespace XperienceCommunity.DataContext.Processors @@ -14,7 +15,57 @@ public UnaryExpressionProcessor(QueryParameterManager parameterManager) public void Process(UnaryExpression node) { - throw new NotImplementedException(); + switch (node.NodeType) + { + case ExpressionType.Not: + ProcessNot(node); + break; + case ExpressionType.Convert: + ProcessConvert(node); + break; + case ExpressionType.Quote: + ProcessQuote(node); + break; + default: + throw new NotSupportedException($"The unary expression type '{node.NodeType}' is not supported."); + } + } + private void ProcessNot(UnaryExpression node) + { + if (node.Operand is BinaryExpression binaryExpression) + { + var binaryProcessor = new BinaryExpressionProcessor(_parameterManager); + binaryProcessor.Process(binaryExpression); + + // Apply negation logic + // Assuming we are handling a NOT operation by wrapping the condition in a NOT statement + // This is a simplified example and may need adjustment based on the actual usage context + //_parameterManager.AddLogicalCondition("AND", new WhereCondition() + //.Where(w => w.Not(_parameterManager.GetWhereParameters()))); + } + else + { + throw new InvalidOperationException("Invalid expression format for NOT operation."); + } + } + + private void ProcessConvert(UnaryExpression node) + { + // Handle type conversion logic + // This is a placeholder implementation, and you may need to adjust based on actual usage context + // In most cases, the Convert operation may not need special handling for building query conditions + } + private void ProcessQuote(UnaryExpression node) + { + // Handle quote logic + // This may involve visiting the operand or other processing specific to the 'Quote' expression + Visit(node.Operand); + } + + private void Visit(Expression node) + { + var visitor = new ContentItemQueryExpressionVisitor(_parameterManager); + visitor.Visit(node); } } } diff --git a/src/XperienceCommunity.DataContext/QueryParameterManager.cs b/src/XperienceCommunity.DataContext/QueryParameterManager.cs index efdccda..b7462c8 100644 --- a/src/XperienceCommunity.DataContext/QueryParameterManager.cs +++ b/src/XperienceCommunity.DataContext/QueryParameterManager.cs @@ -1,4 +1,6 @@ -using CMS.ContentEngine; +using System.Linq.Expressions; +using CMS.ContentEngine; +using XperienceCommunity.DataContext.Extensions; namespace XperienceCommunity.DataContext { @@ -70,35 +72,45 @@ public void AddLogicalCondition(string logicalOperator) } } - public void AddMethodCall(string methodName, object?[] parameters) + public void AddMethodCall(string methodName, params object[] parameters) { - if (parameters is null || parameters.Length == 0) - { - return; - } - switch (methodName) { case "Contains": - var columnName = parameters?[0]?.ToString(); + AddContainsMethod(parameters); + break; + // Add cases for other supported methods here + default: + throw new NotSupportedException($"Method '{methodName}' is not supported."); + } + } - var values = parameters?.Skip(1).ToArray(); + private void AddContainsMethod(object[] parameters) + { + if (parameters.Length != 2) + { + throw new InvalidOperationException("Invalid parameters for Contains method."); + } - if (string.IsNullOrWhiteSpace(columnName)) - { - return; - } + var key = parameters[0].ToString(); - if (values is null) - { - return; - } + var collection = parameters[1]; - AddWhereInCondition(columnName, values); - break; - // Handle other method calls as necessary - default: - throw new NotSupportedException($"Method '{methodName}' is not supported."); + if (collection is IEnumerable intCollection) + { + _whereActions.Add(where => where.WhereIn(key, intCollection.ToList())); + } + else if (collection is IEnumerable stringCollection) + { + _whereActions.Add(where => where.WhereIn(key, stringCollection.ToList())); + } + else if (collection is IEnumerable guidCollection) + { + _whereActions.Add(where => where.WhereIn(key, guidCollection.ToList())); + } + else + { + throw new NotSupportedException($"Collection of type '{collection.GetType()}' is not supported."); } } @@ -121,9 +133,117 @@ public void ApplyConditions() action(whereParameters); } }); + + // Clear the conditions after applying them + _whereActions.Clear(); + } + + public ContentTypeQueryParameters GetQueryParameters() + { + return _queryParameters; + } + + public void AddStringContains(MethodCallExpression node) + { + if (node.Object is MemberExpression member && node.Arguments[0] is ConstantExpression constant) + { + _queryParameters.Where(where => where.WhereContains(member.Member.Name, constant?.Value?.ToString())); + } + } + + public void AddStringStartsWith(MethodCallExpression node) + { + if (node.Object is MemberExpression member && node.Arguments[0] is ConstantExpression constant) + { + _queryParameters.Where(where => where.WhereStartsWith(member.Member.Name, constant?.Value?.ToString())); + } + } + + public void AddEnumerableContains(MethodCallExpression node) + { + if (node.Arguments.Count == 2) + { + var collectionExpression = node.Arguments[0]; + var itemExpression = node.Arguments[1]; + + if (collectionExpression is MemberExpression collectionMember && itemExpression is ConstantExpression constantExpression) + { + var columnName = collectionMember.Member.Name; + var values = constantExpression.Value.ExtractValues().ToArray(); + + AddWhereInCondition(columnName, values); + } + else if (collectionExpression is MemberExpression collectionFieldExpression && itemExpression is MemberExpression itemFieldExpression) + { + var collection = collectionFieldExpression.ExtractFieldValues().ToArray(); + var columnName = itemFieldExpression.Member.Name; + + AddWhereInCondition(columnName, collection); + } + else + { + throw new NotSupportedException($"The expression types '{collectionExpression.GetType().Name}' and '{itemExpression.GetType().Name}' are not supported."); + } + } + else if (node.Arguments.Count == 1 && node.Object != null) + { + var collectionExpression = node.Object; + var itemExpression = node.Arguments[0]; + + if (collectionExpression is MemberExpression collectionMember && itemExpression is MemberExpression itemMember) + { + var collection = collectionMember.ExtractFieldValues().ToArray(); + var columnName = itemMember.Member.Name; + + AddWhereInCondition(columnName, collection); + } + else + { + throw new NotSupportedException($"The expression types '{collectionExpression.GetType().Name}' and '{itemExpression.GetType().Name}' are not supported."); + } + } + else + { + throw new NotSupportedException($"The method '{node.Method.Name}' requires either 1 or 2 arguments."); + } + } + + + public Expression AddQueryableSelect(MethodCallExpression node) + { + if (node.Arguments[1] is UnaryExpression unaryExpression && + unaryExpression.Operand is LambdaExpression lambdaExpression) + { + var visitor = new ContentItemQueryExpressionVisitor(this); + visitor.Visit(lambdaExpression.Body); + } + else + { + throw new NotSupportedException( + $"The expression type '{node.Arguments[1].GetType().Name}' is not supported."); + } + + return node; + } + + public Expression AddQueryableWhere(MethodCallExpression node) + { + if (node.Arguments[1] is UnaryExpression unaryExpression && + unaryExpression.Operand is LambdaExpression lambdaExpression) + { + var visitor = new ContentItemQueryExpressionVisitor(this); + visitor.Visit(lambdaExpression.Body); + } + else + { + throw new NotSupportedException( + $"The expression type '{node.Arguments[1].GetType().Name}' is not supported."); + } + + return node; } - private void AddWhereInCondition(string key, object?[] collection) + public void AddWhereInCondition(string key, object?[] collection) { if (collection == null || collection.Length == 0) { From c29a7afad6e9efa34dec8987ebca055308b80d21 Mon Sep 17 00:00:00 2001 From: Brandon Henricks <78803523+bluemodus-brandon@users.noreply.github.com> Date: Sat, 13 Jul 2024 18:29:51 -0400 Subject: [PATCH 6/7] Add support for AND/OR in Expression Processor Added handling for `ExpressionType.And` and `ExpressionType.Or` in the `BinaryExpressionProcessor` class within the `XperienceCommunity.DataContext.Processors` namespace. This update enables the processor to directly manage logical AND and OR expressions by invoking the `ProcessLogical` method with appropriate boolean flags for each new case. --- .../Processors/BinaryExpressionProcessor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/XperienceCommunity.DataContext/Processors/BinaryExpressionProcessor.cs b/src/XperienceCommunity.DataContext/Processors/BinaryExpressionProcessor.cs index 3034d16..45797e1 100644 --- a/src/XperienceCommunity.DataContext/Processors/BinaryExpressionProcessor.cs +++ b/src/XperienceCommunity.DataContext/Processors/BinaryExpressionProcessor.cs @@ -40,6 +40,12 @@ public void Process(BinaryExpression node) case ExpressionType.OrElse: ProcessLogical(node, isAnd: false); break; + case ExpressionType.And: + ProcessLogical(node, true); + break; + case ExpressionType.Or: + ProcessLogical(node, false); + break; default: throw new NotSupportedException($"The binary expression type '{node.NodeType}' is not supported."); } From 2e80c3674cbb0585a671a7938e7156f82e21ed90 Mon Sep 17 00:00:00 2001 From: Brandon Henricks <78803523+bluemodus-brandon@users.noreply.github.com> Date: Sun, 14 Jul 2024 10:30:31 -0400 Subject: [PATCH 7/7] Enhanced null safety and cache handling Updated `GetOrCacheAsync` in `ContentItemContext.cs` and `PageContentContext.cs` to support nullable return types, improving null safety. Modified result handling in these files to ensure consistent return types and enhanced cache logic by checking for non-null results before caching. Annotated `ExecuteQueryAsync` and `ToListAsync` with `[return: NotNull]` to enforce non-null returns, and updated namespace imports to include `System.Diagnostics.CodeAnalysis` for static analysis suppression. These changes collectively enhance code reliability and maintainability. --- .../ContentItemContext.cs | 15 ++++++++++++--- .../ContentQueryExecutor.cs | 8 +++++--- .../PageContentContext.cs | 19 +++++++++++++++---- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/XperienceCommunity.DataContext/ContentItemContext.cs b/src/XperienceCommunity.DataContext/ContentItemContext.cs index 8dc8b61..1d41b4c 100644 --- a/src/XperienceCommunity.DataContext/ContentItemContext.cs +++ b/src/XperienceCommunity.DataContext/ContentItemContext.cs @@ -65,7 +65,7 @@ public ContentItemContext(IWebsiteChannelContext websiteChannelContext, () => _contentQueryExecutor.ExecuteQueryAsync(queryBuilder, queryOptions, cancellationToken), GetCacheKey(queryBuilder)); - return result.FirstOrDefault(); + return result?.FirstOrDefault(); } public IDataContext IncludeTotalCount(bool includeTotalCount) @@ -132,9 +132,11 @@ public async Task> ToListAsync(CancellationToken cancellationToke var queryOptions = CreateQueryOptions(); - return await GetOrCacheAsync( + var results= await GetOrCacheAsync( () => _contentQueryExecutor.ExecuteQueryAsync(queryBuilder, queryOptions, cancellationToken), GetCacheKey(queryBuilder)); + + return results ?? []; } /// @@ -272,7 +274,7 @@ private string GetCacheKey(ContentItemQueryBuilder queryBuilder) /// The function to execute if cache is bypassed or data is not found. /// The cache key. /// A task that represents the asynchronous operation. The task result contains the cached or executed data. - private async Task GetOrCacheAsync(Func> executeFunc, string cacheKey) where T : class + private async Task GetOrCacheAsync(Func> executeFunc, string cacheKey) where T : class { if (_websiteChannelContext.IsPreview) { @@ -285,6 +287,13 @@ private async Task GetOrCacheAsync(Func> executeFunc, string cache { var result = await executeFunc(); + cs.BoolCondition = result != null; + + if (!cs.Cached) + { + return result; + } + cs.CacheDependency = CacheHelper.GetCacheDependency(GetCacheDependencies(result)); return result; diff --git a/src/XperienceCommunity.DataContext/ContentQueryExecutor.cs b/src/XperienceCommunity.DataContext/ContentQueryExecutor.cs index 5f1ac19..ff2f59a 100644 --- a/src/XperienceCommunity.DataContext/ContentQueryExecutor.cs +++ b/src/XperienceCommunity.DataContext/ContentQueryExecutor.cs @@ -1,4 +1,5 @@ -using CMS.ContentEngine; +using System.Diagnostics.CodeAnalysis; +using CMS.ContentEngine; using Microsoft.Extensions.Logging; using XperienceCommunity.DataContext.Interfaces; @@ -20,6 +21,7 @@ public ContentQueryExecutor(ILogger> logger, IContentQue _processors = processors ?? []; } + [return: NotNull] public async Task> ExecuteQueryAsync(ContentItemQueryBuilder queryBuilder, ContentQueryExecutionOptions queryOptions, CancellationToken cancellationToken) { @@ -30,7 +32,7 @@ public async Task> ExecuteQueryAsync(ContentItemQueryBuilder quer if (!_processors.Any()) { - return results; + return results ?? []; } foreach (var result in results) @@ -41,7 +43,7 @@ public async Task> ExecuteQueryAsync(ContentItemQueryBuilder quer } } - return results; + return results ?? []; } catch (Exception ex) { diff --git a/src/XperienceCommunity.DataContext/PageContentContext.cs b/src/XperienceCommunity.DataContext/PageContentContext.cs index c566214..d3af770 100644 --- a/src/XperienceCommunity.DataContext/PageContentContext.cs +++ b/src/XperienceCommunity.DataContext/PageContentContext.cs @@ -1,4 +1,5 @@ -using System.Linq.Expressions; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; @@ -50,7 +51,7 @@ public PageContentContext(IProgressiveCache cache, PageContentQueryExecutor p () => _pageContentQueryExecutor.ExecuteQueryAsync(queryBuilder, queryOptions, cancellationToken), GetCacheKey(queryBuilder)); - return result.FirstOrDefault(); + return result?.FirstOrDefault(); } public IPageContentContext InChannel(string channelName) @@ -105,6 +106,7 @@ public IDataContext Take(int count) return this; } + [return: NotNull] public async Task> ToListAsync(CancellationToken cancellationToken = default) { ValidateQuery(); @@ -113,9 +115,11 @@ public async Task> ToListAsync(CancellationToken cancellationToke var queryOptions = CreateQueryOptions(); - return await GetOrCacheAsync( + var result = await GetOrCacheAsync( () => _pageContentQueryExecutor.ExecuteQueryAsync(queryBuilder, queryOptions, cancellationToken), GetCacheKey(queryBuilder)); + + return result ?? []; } public IDataContext Where(Expression> predicate) @@ -254,7 +258,7 @@ private string GetCacheKey(ContentItemQueryBuilder queryBuilder) /// The function to execute if cache is bypassed or data is not found. /// The cache key. /// A task that represents the asynchronous operation. The task result contains the cached or executed data. - private async Task GetOrCacheAsync(Func> executeFunc, string cacheKey) where T : class + private async Task GetOrCacheAsync(Func> executeFunc, string cacheKey) where T : class { if (_websiteChannelContext.IsPreview) { @@ -267,6 +271,13 @@ private async Task GetOrCacheAsync(Func> executeFunc, string cache { var result = await executeFunc(); + cs.BoolCondition = result != null; + + if (!cs.Cached) + { + return result; + } + cs.CacheDependency = CacheHelper.GetCacheDependency(GetCacheDependencies(result)); return result;