diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj index 006f98d86..bcc5db494 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj @@ -33,12 +33,15 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 4d0e931a0..150a6d7a8 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -7570,6 +7570,11 @@ A value that corresponds to allowing 'All' function in $filter. + + + A value that corresponds to allowing 'MatchesPattern' function in $filter. + + A value that corresponds to allowing all string related functions in $filter. @@ -8074,7 +8079,7 @@ String related: contains, endswith, startswith, length, indexof, substring, tolower, toupper, trim, - concat e.g. ~/Customers?$filter=length(CompanyName) eq 19 + concat, matchesPattern e.g. ~/Customers?$filter=length(CompanyName) eq 19 DateTime related: @@ -9102,6 +9107,14 @@ The query binder context. The LINQ created. + + + Binds a 'matchesPattern' function to create a LINQ . + + The query node to bind. + The query binder context. + The LINQ created. + Binds date related functions to create a LINQ . @@ -11577,7 +11590,7 @@ The allowed functions include the following: - String related: contains, endswith, startswith, length, indexof, substring, tolower, toupper, trim, concat + String related: contains, endswith, startswith, length, indexof, substring, tolower, toupper, trim, concat, matchesPattern e.g. ~/Customers?$filter=length(CompanyName) eq 19 diff --git a/src/Microsoft.AspNetCore.OData/Query/AllowedFunctions.cs b/src/Microsoft.AspNetCore.OData/Query/AllowedFunctions.cs index fde630bb2..bcdca1897 100644 --- a/src/Microsoft.AspNetCore.OData/Query/AllowedFunctions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/AllowedFunctions.cs @@ -150,10 +150,15 @@ public enum AllowedFunctions /// All = 0x10000000, + /// + /// A value that corresponds to allowing 'MatchesPattern' function in $filter. + /// + MatchesPattern = 0x20000000, + /// /// A value that corresponds to allowing all string related functions in $filter. /// - AllStringFunctions = StartsWith | EndsWith | Contains | Length | IndexOf | Concat | Substring | ToLower | ToUpper | Trim, + AllStringFunctions = StartsWith | EndsWith | Contains | Length | IndexOf | Concat | Substring | ToLower | ToUpper | Trim | MatchesPattern, /// /// A value that corresponds to allowing all datetime related functions in $filter. diff --git a/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs b/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs index 3f44db2c7..268953a90 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs @@ -16,6 +16,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Text.RegularExpressions; using Microsoft.OData.Edm; namespace Microsoft.AspNetCore.OData.Query @@ -36,6 +37,7 @@ internal class ClrCanonicalFunctions internal const string ToupperFunctionName = "toupper"; internal const string TrimFunctionName = "trim"; internal const string ConcatFunctionName = "concat"; + internal const string MatchesPatternFunctionName = "matchesPattern"; internal const string YearFunctionName = "year"; internal const string MonthFunctionName = "month"; internal const string DayFunctionName = "day"; @@ -66,6 +68,7 @@ internal class ClrCanonicalFunctions public static readonly MethodInfo ToUpper = MethodOf(_ => _defaultString.ToUpper()); public static readonly MethodInfo Trim = MethodOf(_ => _defaultString.Trim()); public static readonly MethodInfo Concat = MethodOf(_ => String.Concat(default(string), default(string))); + public static readonly MethodInfo MatchesMattern = MethodOf(_ => Regex.IsMatch(default(string), default(string), default(RegexOptions))); // math functions public static readonly MethodInfo CeilingOfDouble = MethodOf(_ => Math.Ceiling(default(double))); diff --git a/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.Config.cs b/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.Config.cs index 856ce9012..8db9a7cb0 100644 --- a/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.Config.cs +++ b/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.Config.cs @@ -150,7 +150,7 @@ public AllowedQueryOptions AllowedQueryOptions /// /// String related: /// contains, endswith, startswith, length, indexof, substring, tolower, toupper, trim, - /// concat e.g. ~/Customers?$filter=length(CompanyName) eq 19 + /// concat, matchesPattern e.g. ~/Customers?$filter=length(CompanyName) eq 19 /// /// /// DateTime related: diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs index 0985ff1cb..c4d4196bb 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs @@ -13,6 +13,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Text.RegularExpressions; using System.Xml.Linq; using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.AspNetCore.OData.Common; @@ -199,6 +200,9 @@ public virtual Expression BindSingleValueFunctionCallNode(SingleValueFunctionCal case ClrCanonicalFunctions.ConcatFunctionName: return BindConcat(node); + case ClrCanonicalFunctions.MatchesPatternFunctionName: + return BindMatchesPattern(node); + case ClrCanonicalFunctions.YearFunctionName: case ClrCanonicalFunctions.MonthFunctionName: case ClrCanonicalFunctions.DayFunctionName: @@ -545,6 +549,21 @@ private Expression BindConcat(SingleValueFunctionCallNode node) return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Concat, QuerySettings, arguments); } + private Expression BindMatchesPattern(SingleValueFunctionCallNode node) + { + Contract.Assert("matchesPattern" == node.Name); + + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); + + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + + //add argument that must be ECMAScript compatible regex + arguments = new[] { arguments[0], arguments[1], Expression.Constant(RegexOptions.ECMAScript) }; + + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.MatchesMattern, QuerySettings, arguments); + } + private Expression BindTrim(SingleValueFunctionCallNode node) { Contract.Assert("trim" == node.Name); diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs index 9c63e8e5d..496800ed7 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Text.RegularExpressions; using Microsoft.AspNetCore.OData.Common; using Microsoft.AspNetCore.OData.Edm; using Microsoft.OData; @@ -68,6 +69,9 @@ public virtual Expression BindSingleValueFunctionCallNode(SingleValueFunctionCal case ClrCanonicalFunctions.ConcatFunctionName: return BindConcat(node, context); + case ClrCanonicalFunctions.MatchesPatternFunctionName: + return BindMatchesPattern(node, context); + case ClrCanonicalFunctions.YearFunctionName: case ClrCanonicalFunctions.MonthFunctionName: case ClrCanonicalFunctions.DayFunctionName: @@ -337,6 +341,27 @@ protected virtual Expression BindConcat(SingleValueFunctionCallNode node, QueryB return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Concat, context.QuerySettings, arguments); } + /// + /// Binds a 'matchesPattern' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindMatchesPattern(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "matchesPattern"); + + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); + + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + + //add argument that must be ECMAScript compatible regex + arguments = new[] { arguments[0], arguments[1], Expression.Constant(RegexOptions.ECMAScript) }; + + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.MatchesMattern, context.QuerySettings, arguments); + } + /// /// Binds date related functions to create a LINQ . /// diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs index bc120ec74..80180cbb1 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs @@ -816,6 +816,9 @@ private static AllowedFunctions ToODataFunction(string functionName) case ClrCanonicalFunctions.LengthFunctionName: result = AllowedFunctions.Length; break; + case ClrCanonicalFunctions.MatchesPatternFunctionName: + result = AllowedFunctions.MatchesPattern; + break; case ClrCanonicalFunctions.MinuteFunctionName: result = AllowedFunctions.Minute; break; diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/ODataValidationSettings.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/ODataValidationSettings.cs index 3d07c574c..d66200817 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/ODataValidationSettings.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/ODataValidationSettings.cs @@ -57,7 +57,7 @@ public AllowedArithmeticOperators AllowedArithmeticOperators /// /// The allowed functions include the following: /// - /// String related: contains, endswith, startswith, length, indexof, substring, tolower, toupper, trim, concat + /// String related: contains, endswith, startswith, length, indexof, substring, tolower, toupper, trim, concat, matchesPattern /// /// e.g. ~/Customers?$filter=length(CompanyName) eq 19 /// diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl index a8e204cae..41ec65407 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net5.bsl @@ -1110,9 +1110,9 @@ FlagsAttribute(), public enum Microsoft.AspNetCore.OData.Query.AllowedFunctions : int { All = 268435456 AllDateTimeFunctions = 7010304 - AllFunctions = 535494655 + AllFunctions = 1072365567 AllMathFunctions = 58720256 - AllStringFunctions = 1023 + AllStringFunctions = 536871935 Any = 134217728 Cast = 1024 Ceiling = 33554432 @@ -1127,6 +1127,7 @@ public enum Microsoft.AspNetCore.OData.Query.AllowedFunctions : int { IndexOf = 16 IsOf = 67108864 Length = 8 + MatchesPattern = 536870912 Minute = 524288 Month = 8192 None = 0 @@ -2712,6 +2713,7 @@ public abstract class Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder { public virtual System.Linq.Expressions.Expression BindInNode (Microsoft.OData.UriParser.InNode inNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) protected virtual System.Linq.Expressions.Expression BindIsOf (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) protected virtual System.Linq.Expressions.Expression BindLength (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) + protected virtual System.Linq.Expressions.Expression BindMatchesPattern (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) public virtual System.Linq.Expressions.Expression BindNavigationPropertyNode (Microsoft.OData.UriParser.QueryNode sourceNode, Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, string propertyPath, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) protected virtual System.Linq.Expressions.Expression BindNow (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) public virtual System.Linq.Expressions.Expression BindPropertyAccessQueryNode (Microsoft.OData.UriParser.SingleValuePropertyAccessNode propertyAccessNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl index a8e204cae..41ec65407 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl @@ -1110,9 +1110,9 @@ FlagsAttribute(), public enum Microsoft.AspNetCore.OData.Query.AllowedFunctions : int { All = 268435456 AllDateTimeFunctions = 7010304 - AllFunctions = 535494655 + AllFunctions = 1072365567 AllMathFunctions = 58720256 - AllStringFunctions = 1023 + AllStringFunctions = 536871935 Any = 134217728 Cast = 1024 Ceiling = 33554432 @@ -1127,6 +1127,7 @@ public enum Microsoft.AspNetCore.OData.Query.AllowedFunctions : int { IndexOf = 16 IsOf = 67108864 Length = 8 + MatchesPattern = 536870912 Minute = 524288 Month = 8192 None = 0 @@ -2712,6 +2713,7 @@ public abstract class Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder { public virtual System.Linq.Expressions.Expression BindInNode (Microsoft.OData.UriParser.InNode inNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) protected virtual System.Linq.Expressions.Expression BindIsOf (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) protected virtual System.Linq.Expressions.Expression BindLength (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) + protected virtual System.Linq.Expressions.Expression BindMatchesPattern (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) public virtual System.Linq.Expressions.Expression BindNavigationPropertyNode (Microsoft.OData.UriParser.QueryNode sourceNode, Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, string propertyPath, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) protected virtual System.Linq.Expressions.Expression BindNow (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) public virtual System.Linq.Expressions.Expression BindPropertyAccessQueryNode (Microsoft.OData.UriParser.SingleValuePropertyAccessNode propertyAccessNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl index a8e204cae..41ec65407 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl @@ -1110,9 +1110,9 @@ FlagsAttribute(), public enum Microsoft.AspNetCore.OData.Query.AllowedFunctions : int { All = 268435456 AllDateTimeFunctions = 7010304 - AllFunctions = 535494655 + AllFunctions = 1072365567 AllMathFunctions = 58720256 - AllStringFunctions = 1023 + AllStringFunctions = 536871935 Any = 134217728 Cast = 1024 Ceiling = 33554432 @@ -1127,6 +1127,7 @@ public enum Microsoft.AspNetCore.OData.Query.AllowedFunctions : int { IndexOf = 16 IsOf = 67108864 Length = 8 + MatchesPattern = 536870912 Minute = 524288 Month = 8192 None = 0 @@ -2712,6 +2713,7 @@ public abstract class Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder { public virtual System.Linq.Expressions.Expression BindInNode (Microsoft.OData.UriParser.InNode inNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) protected virtual System.Linq.Expressions.Expression BindIsOf (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) protected virtual System.Linq.Expressions.Expression BindLength (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) + protected virtual System.Linq.Expressions.Expression BindMatchesPattern (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) public virtual System.Linq.Expressions.Expression BindNavigationPropertyNode (Microsoft.OData.UriParser.QueryNode sourceNode, Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, string propertyPath, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) protected virtual System.Linq.Expressions.Expression BindNow (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) public virtual System.Linq.Expressions.Expression BindPropertyAccessQueryNode (Microsoft.OData.UriParser.SingleValuePropertyAccessNode propertyAccessNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedFunctionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedFunctionsTests.cs index 616457d6c..02e43df86 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedFunctionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedFunctionsTests.cs @@ -32,6 +32,7 @@ public static TheoryDataSet AllStringFunctionsData AllowedFunctions.ToLower, AllowedFunctions.ToUpper, AllowedFunctions.Trim, + AllowedFunctions.MatchesPattern, }; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs index 887d42cd0..a23bbf1d5 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs @@ -1145,6 +1145,25 @@ public void StringFunctions_StringConcat() InvokeFiltersAndVerify(filters, new Product { ProductName = "Food" }, (true, true)); } + [Fact] + public void StringFunctions_StringMatchesPattern() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "matchesPattern(ProductName, 'A\\wc')", + "$it => $it.ProductName.IsMatch(\"A\\wc\", ECMAScript)", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(ArgumentNullException), false)); + + InvokeFiltersAndVerify(filters, new Product { ProductName = "Abcd" }, (true, true)); + + InvokeFiltersAndVerify(filters, new Product { ProductName = "Abd" }, (false, false)); + + InvokeFiltersAndVerify(filters, new Product { ProductName = "Aθd" }, (false, false)); // ECMAScript has strict matching of \w + } + [Fact] public void StringFunctions_RecursiveMethodCall() { diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterQueryValidatorTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterQueryValidatorTests.cs index 2297ffed8..dd5999074 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterQueryValidatorTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterQueryValidatorTests.cs @@ -556,6 +556,10 @@ public static TheoryDataSet StringFunctions { AllowedFunctions.StartsWith, "startswith(null, 'Name')", "startswith" }, { AllowedFunctions.StartsWith, "startswith(ProductName,'Name')", "startswith" }, { AllowedFunctions.StartsWith, "startswith(ProductName, 'Name')", "startswith" }, + { AllowedFunctions.MatchesPattern, "matchesPattern(null,'Name')", "matchesPattern" }, + { AllowedFunctions.MatchesPattern, "matchesPattern(null, 'Name')", "matchesPattern" }, + { AllowedFunctions.MatchesPattern, "matchesPattern(ProductName,'Name')", "matchesPattern" }, + { AllowedFunctions.MatchesPattern, "matchesPattern(ProductName, 'Name')", "matchesPattern" }, { AllowedFunctions.Substring, "substring(null,1) eq 'Name'", "substring" }, { AllowedFunctions.Substring, "substring(null, 1) eq 'Name'", "substring" }, { AllowedFunctions.Substring, "substring(ProductName,1) eq 'Name'", "substring" },