diff --git a/src/VirtoCommerce.NotificationsModule.LiquidRenderer/Filters/ArrayFilter.cs b/src/VirtoCommerce.NotificationsModule.LiquidRenderer/Filters/ArrayFilter.cs
new file mode 100644
index 0000000..3e1b603
--- /dev/null
+++ b/src/VirtoCommerce.NotificationsModule.LiquidRenderer/Filters/ArrayFilter.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using VirtoCommerce.Platform.Core.Common;
+
+namespace VirtoCommerce.NotificationsModule.LiquidRenderer.Filters
+{
+ public static class ArrayFilter
+ {
+ ///
+ /// Filter the elements of an array by a given condition
+ /// {% assign filtered = items | where: 'propertyName' '==' 'propertyValue' %}
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static object Where(object input, string propertyName, string operationName, string propertyValue)
+ {
+ if (input is not IEnumerable enumerable)
+ {
+ throw new ArgumentException("Input is not a collection", nameof(input));
+ }
+
+ var elementType = GetEnumerableElementType(enumerable.GetType());
+ var property = elementType.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
+
+ if (property is null)
+ {
+ throw new ArgumentException($"Unknown property '{propertyName}'", nameof(propertyName));
+ }
+
+ var parameterX = Expression.Parameter(elementType, "x");
+ var left = Expression.Property(parameterX, property);
+ var value = ParsePropertyValue(propertyValue, left.Type);
+ var right = Expression.Constant(value, left.Type);
+
+ BinaryExpression operation;
+
+ if (operationName.EqualsInvariant("=="))
+ {
+ operation = Expression.Equal(left, right);
+ }
+ else if (operationName.EqualsInvariant("!="))
+ {
+ operation = Expression.NotEqual(left, right);
+ }
+ else if (operationName.EqualsInvariant(">"))
+ {
+ operation = Expression.GreaterThan(left, right);
+ }
+ else if (operationName.EqualsInvariant(">="))
+ {
+ operation = Expression.GreaterThanOrEqual(left, right);
+ }
+ else if (operationName.EqualsInvariant("<"))
+ {
+ operation = Expression.LessThan(left, right);
+ }
+ else if (operationName.EqualsInvariant("<="))
+ {
+ operation = Expression.LessThanOrEqual(left, right);
+ }
+ else if (operationName.EqualsInvariant("contains"))
+ {
+ Expression expression;
+
+ if (property.PropertyType == typeof(string))
+ {
+ var containsMethod = typeof(string).GetMethods().First(x => x.Name == "Contains");
+ expression = Expression.Call(left, containsMethod, right);
+ }
+ else
+ {
+ var containsMethod = typeof(Enumerable).GetMethods()
+ .First(x => x.Name == "Contains" && x.GetParameters().Length == 2)
+ .MakeGenericMethod(value.GetType());
+
+ expression = Expression.Call(containsMethod, left, right);
+ }
+
+ operation = Expression.Equal(expression, Expression.Constant(true));
+ }
+ else
+ {
+ throw new ArgumentException($"Unknown operation '{operationName}'", nameof(operationName));
+ }
+
+ var delegateType = typeof(Func<,>).MakeGenericType(elementType, typeof(bool));
+
+ // Construct expression: Func = (x) => x.propertyName == propertyValue
+ var lambda = Expression.Lambda(delegateType, operation, parameterX);
+
+ // Find Queryable.Where(Expression>) method
+ var whereMethod = typeof(Queryable).GetMethods()
+ .Where(x => x.Name == "Where")
+ .Select(x => new { M = x, P = x.GetParameters() })
+ .Where(x => x.P.Length == 2 &&
+ x.P[0].ParameterType.IsGenericType &&
+ x.P[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>) &&
+ x.P[1].ParameterType.IsGenericType &&
+ x.P[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>))
+ .Select(x => new { x.M, A = x.P[1].ParameterType.GetGenericArguments() })
+ .Where(x => x.A[0].IsGenericType &&
+ x.A[0].GetGenericTypeDefinition() == typeof(Func<,>))
+ .Select(x => new { x.M, A = x.A[0].GetGenericArguments() })
+ .Where(x => x.A[0].IsGenericParameter &&
+ x.A[1] == typeof(bool))
+ .Select(x => x.M)
+ .SingleOrDefault();
+
+ var result = whereMethod?.MakeGenericMethod(elementType).Invoke(null, new object[] { enumerable.AsQueryable(), lambda });
+
+ return result;
+ }
+
+
+ private static object ParsePropertyValue(string value, Type valueType)
+ {
+ if ((valueType == typeof(int) || valueType == typeof(int?)) && int.TryParse(value, out var intValue))
+ {
+ return intValue;
+ }
+
+ if (valueType == typeof(double) && double.TryParse(value, out var doubleValue))
+ {
+ return doubleValue;
+ }
+
+ if (valueType == typeof(decimal) && decimal.TryParse(value, out var decimalValue))
+ {
+ return decimalValue;
+ }
+
+ if (valueType == typeof(TimeSpan) && TimeSpan.TryParse(value, out var timespan))
+ {
+ return timespan;
+ }
+
+ if (valueType == typeof(DateTime) && DateTime.TryParse(value, out var dateTime))
+ {
+ return dateTime;
+ }
+
+ if (valueType == typeof(char) && char.TryParse(value, out var charValue))
+ {
+ return charValue;
+ }
+
+ if (valueType == typeof(bool) && bool.TryParse(value, out var boolValue))
+ {
+ return boolValue;
+ }
+
+ return value;
+ }
+
+ private static Type GetEnumerableElementType(Type type)
+ {
+ return type.GetInterfaces()
+ .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>))
+ .Select(x => x.GetGenericArguments()[0])
+ .FirstOrDefault();
+ }
+ }
+}
diff --git a/src/VirtoCommerce.NotificationsModule.Web/Module.cs b/src/VirtoCommerce.NotificationsModule.Web/Module.cs
index 5c8d76c..39ab039 100644
--- a/src/VirtoCommerce.NotificationsModule.Web/Module.cs
+++ b/src/VirtoCommerce.NotificationsModule.Web/Module.cs
@@ -119,6 +119,7 @@ public void Initialize(IServiceCollection serviceCollection)
{
builder.AddCustomLiquidFilterType(typeof(TranslationFilter));
builder.AddCustomLiquidFilterType(typeof(UrlFilters));
+ builder.AddCustomLiquidFilterType(typeof(ArrayFilter));
builder.SetRendererLoopLimit(Configuration["Notifications:LiquidRenderOptions:LoopLimit"].TryParse(ModuleConstants.DefaultLiquidRendererLoopLimit));
});
}
diff --git a/tests/VirtoCommerce.NotificationsModule.Tests/UnitTests/LiquidTemplateRendererUnitTests.cs b/tests/VirtoCommerce.NotificationsModule.Tests/UnitTests/LiquidTemplateRendererUnitTests.cs
index 2ffae7a..0c33d21 100644
--- a/tests/VirtoCommerce.NotificationsModule.Tests/UnitTests/LiquidTemplateRendererUnitTests.cs
+++ b/tests/VirtoCommerce.NotificationsModule.Tests/UnitTests/LiquidTemplateRendererUnitTests.cs
@@ -38,7 +38,7 @@ public LiquidTemplateRendererUnitTests()
_notificationLayoutSearchService.Setup(x => x.SearchAsync(It.IsAny(), It.IsAny())).ReturnsAsync(notificationLayoutSearchResult);
Func factory = () => new LayoutTemplateLoader(_notificationLayoutServiceMock.Object);
- _liquidTemplateRenderer = new LiquidTemplateRenderer(Options.Create(new LiquidRenderOptions() { CustomFilterTypes = new HashSet { typeof(UrlFilters), typeof(TranslationFilter) } }), factory, _notificationLayoutSearchService.Object);
+ _liquidTemplateRenderer = new LiquidTemplateRenderer(Options.Create(new LiquidRenderOptions() { CustomFilterTypes = new HashSet { typeof(UrlFilters), typeof(TranslationFilter), typeof(ArrayFilter) } }), factory, _notificationLayoutSearchService.Object);
//TODO
if (!AbstractTypeFactory.AllTypeInfos.SelectMany(x => x.AllSubclasses).Contains(typeof(NotificationScriptObject)))
@@ -152,6 +152,43 @@ public async Task RenderAsync_ContextHasLayoutId_LayoutRendered()
Assert.Equal("header test_content footer", result);
}
+ [Theory]
+ [InlineData("Group", "==", "Group2", "Item2", "Item4")]
+ [InlineData("Group", "!=", "Group22", "Item1", "Item4")]
+ [InlineData("Value", ">", "2", "Item3", "Item5")]
+ [InlineData("Value", ">=", "2", "Item2", "Item5")]
+ [InlineData("Value", "<", "4", "Item1", "Item3")]
+ [InlineData("Value", "<=", "4", "Item1", "Item4")]
+ [InlineData("Group", "contains", "2", "Item2", "Item5")]
+ public async Task RenderAsync_FilterArray(string propertyName, string operationName, string propertyValue, string expectedFirstName, string expectedLastName)
+ {
+ var filter = $"'{propertyName}' '{operationName}' '{propertyValue}'";
+
+ var context = new NotificationRenderContext
+ {
+ Template = "{{ filtered = items | where: " + filter + " }}" +
+ "{{ first_item = filtered | array.first }}" +
+ "{{ last_item = filtered | array.last }}" +
+ "First: {{ first_item.name }}, Last: {{ last_item.name }}",
+ Model = new
+ {
+ Items = new[]
+ {
+ new { Group = "Group1", Name = "Item1", Value = 1 },
+ new { Group = "Group2", Name = "Item2", Value = 2 },
+ new { Group = "Group2", Name = "Item3", Value = 3 },
+ new { Group = "Group2", Name = "Item4", Value = 4 },
+ new { Group = "Group22", Name = "Item5", Value = 5 },
+ }
+ },
+ };
+
+ var expectedResult = $"First: {expectedFirstName}, Last: {expectedLastName}";
+
+ var result = await _liquidTemplateRenderer.RenderAsync(context);
+ Assert.Equal(expectedResult, result);
+ }
+
public static IEnumerable