diff --git a/src/System.Linq.Dynamic.Core/Config/StringLiteralParsingType.cs b/src/System.Linq.Dynamic.Core/Config/StringLiteralParsingType.cs
new file mode 100644
index 00000000..09867a8b
--- /dev/null
+++ b/src/System.Linq.Dynamic.Core/Config/StringLiteralParsingType.cs
@@ -0,0 +1,25 @@
+namespace System.Linq.Dynamic.Core.Config;
+
+///
+/// Defines the types of string literal parsing that can be performed.
+///
+public enum StringLiteralParsingType : byte
+{
+ ///
+ /// Represents the default string literal parsing type. Double quotes should be escaped using the default escape character (a \).
+ /// To check if a Value equals a double quote, use this c# code:
+ ///
+ /// var expression = "Value == \"\\\"\"";
+ ///
+ ///
+ Default = 0,
+
+ ///
+ /// Represents a string literal parsing type where a double quote should be escaped by an extra double quote (").
+ /// To check if a Value equals a double quote, use this c# code:
+ ///
+ /// var expression = "Value == \"\"\"\"";
+ ///
+ ///
+ EscapeDoubleQuoteByTwoDoubleQuotes = 1
+}
\ No newline at end of file
diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
index a5154e23..50ae95c6 100644
--- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
+++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
@@ -3,6 +3,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using System.Linq.Dynamic.Core.Config;
using System.Linq.Dynamic.Core.Exceptions;
using System.Linq.Dynamic.Core.Extensions;
using System.Linq.Dynamic.Core.Parser.SupportedMethods;
@@ -884,7 +885,7 @@ private AnyOf ParseStringLiteral(bool forceParseAsString)
_textParser.ValidateToken(TokenId.StringLiteral);
var text = _textParser.CurrentToken.Text;
- var parsedStringValue = StringParser.ParseString(_textParser.CurrentToken.Text);
+ var parsedStringValue = ParseStringAndEscape(text);
if (_textParser.CurrentToken.Text[0] == '\'')
{
@@ -916,11 +917,18 @@ private AnyOf ParseStringLiteral(bool forceParseAsString)
_textParser.NextToken();
}
- parsedStringValue = StringParser.ParseStringAndReplaceDoubleQuotes(text, _textParser.CurrentToken.Pos);
+ parsedStringValue = ParseStringAndEscape(text);
return _constantExpressionHelper.CreateLiteral(parsedStringValue, parsedStringValue);
}
+ private string ParseStringAndEscape(string text)
+ {
+ return _parsingConfig.StringLiteralParsing == StringLiteralParsingType.EscapeDoubleQuoteByTwoDoubleQuotes ?
+ StringParser.ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(text, _textParser.CurrentToken.Pos) :
+ StringParser.ParseStringAndUnescape(text, _textParser.CurrentToken.Pos);
+ }
+
private Expression ParseIntegerLiteral()
{
_textParser.ValidateToken(TokenId.IntegerLiteral);
diff --git a/src/System.Linq.Dynamic.Core/Parser/StringParser.cs b/src/System.Linq.Dynamic.Core/Parser/StringParser.cs
index 18dcb2b2..99aea158 100644
--- a/src/System.Linq.Dynamic.Core/Parser/StringParser.cs
+++ b/src/System.Linq.Dynamic.Core/Parser/StringParser.cs
@@ -10,10 +10,10 @@ namespace System.Linq.Dynamic.Core.Parser;
///
internal static class StringParser
{
- private const string Pattern = @"""""";
- private const string Replacement = "\"";
+ private const string TwoDoubleQuotes = "\"\"";
+ private const string SingleDoubleQuote = "\"";
- public static string ParseString(string s, int pos = default)
+ internal static string ParseStringAndUnescape(string s, int pos = default)
{
if (s == null || s.Length < 2)
{
@@ -41,20 +41,20 @@ public static string ParseString(string s, int pos = default)
}
}
- public static string ParseStringAndReplaceDoubleQuotes(string s, int pos)
+ internal static string ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(string input, int position = default)
{
- return ReplaceDoubleQuotes(ParseString(s, pos), pos);
+ return ReplaceTwoDoubleQuotesByASingleDoubleQuote(ParseStringAndUnescape(input, position), position);
}
- private static string ReplaceDoubleQuotes(string s, int pos)
+ private static string ReplaceTwoDoubleQuotesByASingleDoubleQuote(string input, int position)
{
try
{
- return Regex.Replace(s, Pattern, Replacement);
+ return Regex.Replace(input, TwoDoubleQuotes, SingleDoubleQuote);
}
catch (Exception ex)
{
- throw new ParseException(ex.Message, pos, ex);
+ throw new ParseException(ex.Message, position, ex);
}
}
}
\ No newline at end of file
diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs
index 883c71ea..3c80b7bb 100644
--- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs
+++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
+using System.Linq.Dynamic.Core.Config;
using System.Linq.Dynamic.Core.CustomTypeProviders;
using System.Linq.Dynamic.Core.Parser;
using System.Linq.Dynamic.Core.Util.Cache;
@@ -273,4 +274,10 @@ public IQueryableAnalyzer QueryableAnalyzer
///
///
public bool ConvertObjectToSupportComparison { get; set; }
+
+ ///
+ /// Defines the type of string literal parsing that will be performed.
+ /// Default value is StringLiteralParsingType.Default.
+ ///
+ public StringLiteralParsingType StringLiteralParsing { get; set; } = StringLiteralParsingType.Default;
}
\ No newline at end of file
diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs
index 4c4a9ee0..055216a1 100644
--- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs
+++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Globalization;
+using System.Linq.Dynamic.Core.Config;
using System.Linq.Dynamic.Core.CustomTypeProviders;
using System.Linq.Dynamic.Core.Exceptions;
using System.Linq.Dynamic.Core.Tests.Helpers;
@@ -975,6 +976,21 @@ public void DynamicExpressionParser_ParseLambda_StringLiteralStartEmbeddedQuote_
Assert.Equal("\"\"test\"", rightValue);
}
+ [Theory] // #786
+ [InlineData("Escaped", "\"{\\\"PropertyA\\\":\\\"\\\"}\"")]
+ [InlineData("Verbatim", @"""{\""PropertyA\"":\""\""}""")]
+ // [InlineData("Raw", """"{\"PropertyA\":\"\"}"""")] // TODO : does not work ???
+ public void DynamicExpressionParser_ParseLambda_StringLiteral_EscapedJson(string _, string expression)
+ {
+ // Act
+ var result = DynamicExpressionParser
+ .ParseLambda(typeof(object), expression)
+ .Compile()
+ .DynamicInvoke();
+
+ result.Should().Be("{\"PropertyA\":\"\"}");
+ }
+
[Fact]
public void DynamicExpressionParser_ParseLambda_StringLiteral_MissingClosingQuote()
{
@@ -1549,7 +1565,10 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio
resultIncome.Should().Be("Income == 5");
// Act : string
- var expressionTextUserName = "StaticHelper.Filter(\"UserName == \"\"x\"\"\")";
+ // Replace " with \"
+ // Replace \" with \\\"
+ StaticHelper.Filter("UserName == \"x\"");
+ var expressionTextUserName = "StaticHelper.Filter(\"UserName == \\\"x\\\"\")";
var lambdaUserName = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionTextUserName, user);
var funcUserName = (Expression>)lambdaUserName;
@@ -1558,33 +1577,28 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_Expressio
// Assert : string
resultUserName.Should().Be(@"UserName == ""x""");
- }
- [Fact]
- public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpression1String()
- {
- // Arrange
- var config = new ParsingConfig
+ // Act : string
+ // Replace " with \"
+ // Replace \" with \"\"
+ var configNonDefault = new ParsingConfig
{
- CustomTypeProvider = new TestCustomTypeProvider()
+ CustomTypeProvider = new TestCustomTypeProvider(),
+ StringLiteralParsing = StringLiteralParsingType.EscapeDoubleQuoteByTwoDoubleQuotes
};
+ expressionTextUserName = "StaticHelper.Filter(\"UserName == \"\"x\"\"\")";
+ lambdaUserName = DynamicExpressionParser.ParseLambda(configNonDefault, typeof(User), null, expressionTextUserName, user);
+ funcUserName = (Expression>)lambdaUserName;
- var user = new User();
+ delegateUserName = funcUserName.Compile();
+ resultUserName = (string?)delegateUserName.DynamicInvoke(user);
- // Act
- var expressionText = @"StaticHelper.In(Id, StaticHelper.SubSelect(""Identity"", ""LegalPerson"", ""StaticHelper.In(ParentId, StaticHelper.SubSelect(""""LegalPersonId"""", """"PointSiteTD"""", """"Identity = 5"""", """"""""))"", """"))";
- var lambda = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionText, user);
- var func = (Expression>)lambda;
-
- var compile = func.Compile();
- var result = (bool?)compile.DynamicInvoke(user);
-
- // Assert
- result.Should().Be(false);
+ // Assert : string
+ resultUserName.Should().Be(@"UserName == ""x""");
}
[Fact]
- public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpression2String()
+ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexExpressionString()
{
// Arrange
var config = new ParsingConfig
@@ -1594,8 +1608,12 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexEx
var user = new User();
+ // Replace " with \"
+ // Replace \" with \\\"
+ var _ = StaticHelper.In(Guid.NewGuid(), StaticHelper.SubSelect("Identity", "LegalPerson", "StaticHelper.In(ParentId, StaticHelper.SubSelect( \"LegalPersonId\", \"PointSiteTD\", \"Identity = 5\", \"\")) ", ""));
+ var expressionText = "StaticHelper.In(Id, StaticHelper.SubSelect(\"Identity\", \"LegalPerson\", \"StaticHelper.In(ParentId, StaticHelper.SubSelect(\\\"LegalPersonId\\\", \\\"PointSiteTD\\\", \\\"Identity = 5\\\", \\\"\\\"))\", \"\"))";
+
// Act
- var expressionText = @"StaticHelper.In(Id, StaticHelper.SubSelect(""Identity"", ""LegalPerson"", ""StaticHelper.In(ParentId, StaticHelper.SubSelect(""""LegalPersonId"""", """"PointSiteTD"""", """"Identity = "" + StaticHelper.ToExpressionString(StaticHelper.Get(""CurrentPlace""), 2) + """""", """"""""))"", """"))";
var lambda = DynamicExpressionParser.ParseLambda(config, typeof(User), null, expressionText, user);
var func = (Expression>)lambda;
diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs
index b39a2349..0272f026 100644
--- a/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs
+++ b/test/System.Linq.Dynamic.Core.Tests/Parser/StringParserTests.cs
@@ -10,10 +10,10 @@ public class StringParserTests
[Theory]
[InlineData("'s")]
[InlineData("\"s")]
- public void StringParser_With_UnexpectedUnclosedString_ThrowsException(string input)
+ public void StringParser_ParseStringAndUnescape_With_UnexpectedUnclosedString_ThrowsException(string input)
{
// Act
- var exception = Assert.Throws(() => StringParser.ParseString(input));
+ var exception = Assert.Throws(() => StringParser.ParseStringAndUnescape(input));
// Assert
Assert.Equal($"Unexpected end of string with unclosed string at position 2 near '{input}'.", exception.Message);
@@ -23,10 +23,10 @@ public void StringParser_With_UnexpectedUnclosedString_ThrowsException(string in
[InlineData("")]
[InlineData(null)]
[InlineData("x")]
- public void StringParser_With_InvalidStringLength_ThrowsException(string input)
+ public void StringParser_ParseStringAndUnescape_With_InvalidStringLength_ThrowsException(string input)
{
// Act
- Action action = () => StringParser.ParseString(input);
+ Action action = () => StringParser.ParseStringAndUnescape(input);
// Assert
action.Should().Throw().WithMessage($"String '{input}' should have at least 2 characters.");
@@ -35,30 +35,30 @@ public void StringParser_With_InvalidStringLength_ThrowsException(string input)
[Theory]
[InlineData("xx")]
[InlineData(" ")]
- public void StringParser_With_InvalidStringQuoteCharacter_ThrowsException(string input)
+ public void StringParser_ParseStringAndUnescape_With_InvalidStringQuoteCharacter_ThrowsException(string input)
{
// Act
- Action action = () => StringParser.ParseString(input);
+ Action action = () => StringParser.ParseStringAndUnescape(input);
// Assert
action.Should().Throw().WithMessage("An escaped string should start with a double (\") or a single (') quote.");
}
[Fact]
- public void StringParser_With_UnexpectedUnrecognizedEscapeSequence_ThrowsException()
+ public void StringParser_ParseStringAndUnescape_With_UnexpectedUnrecognizedEscapeSequence_ThrowsException()
{
// Arrange
var input = new string(new[] { '"', '\\', 'u', '?', '"' });
// Act
- Action action = () => StringParser.ParseString(input);
+ Action action = () => StringParser.ParseStringAndUnescape(input);
// Assert
var parseException = action.Should().Throw();
parseException.Which.InnerException!.Message.Should().Contain("hexadecimal digits");
- parseException.Which.StackTrace.Should().Contain("at System.Linq.Dynamic.Core.Parser.StringParser.ParseString(String s, Int32 pos) in ").And.Contain("StringParser.cs:line ");
+ parseException.Which.StackTrace.Should().Contain("at System.Linq.Dynamic.Core.Parser.StringParser.ParseStringAndUnescape(String s, Int32 pos) in ").And.Contain("StringParser.cs:line ");
}
[Theory]
@@ -66,10 +66,10 @@ public void StringParser_With_UnexpectedUnrecognizedEscapeSequence_ThrowsExcepti
[InlineData("'s'", "s")]
[InlineData("'\\\\'", "\\")]
[InlineData("'\\n'", "\n")]
- public void StringParser_Parse_SingleQuotedString(string input, string expectedResult)
+ public void StringParser_ParseStringAndUnescape_SingleQuotedString(string input, string expectedResult)
{
// Act
- var result = StringParser.ParseString(input);
+ var result = StringParser.ParseStringAndUnescape(input);
// Assert
result.Should().Be(expectedResult);
@@ -93,12 +93,39 @@ public void StringParser_Parse_SingleQuotedString(string input, string expectedR
[InlineData("\"\\\"\\\"\"", "\"\"")]
[InlineData("\"AB YZ 19 \uD800\udc05 \u00e4\"", "AB YZ 19 \uD800\udc05 \u00e4")]
[InlineData("\"\\\\\\\\192.168.1.1\\\\audio\\\\new\"", "\\\\192.168.1.1\\audio\\new")]
- public void StringParser_Parse_DoubleQuotedString(string input, string expectedResult)
+ [InlineData("\"{\\\"PropertyA\\\":\\\"\\\"}\"", @"{""PropertyA"":""""}")] // #786
+ public void StringParser_ParseStringAndUnescape_DoubleQuotedString(string input, string expectedResult)
{
// Act
- var result = StringParser.ParseString(input);
+ var result = StringParser.ParseStringAndUnescape(input);
// Assert
result.Should().Be(expectedResult);
}
+
+ [Fact]
+ public void StringParser_ParseStringAndUnescape()
+ {
+ // Arrange
+ var test = "\"x\\\"X\"";
+
+ // Act
+ var result = StringParser.ParseStringAndUnescape(test);
+
+ // Assert
+ result.Should().Be("x\"X");
+ }
+
+ [Fact]
+ public void StringParser_ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote()
+ {
+ // Arrange
+ var test = "\"x\"\"X\"";
+
+ // Act
+ var result = StringParser.ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(test);
+
+ // Assert
+ result.Should().Be("x\"X");
+ }
}
\ No newline at end of file
diff --git a/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs b/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs
index 7bfd3b46..69fc5ce3 100644
--- a/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs
+++ b/test/System.Linq.Dynamic.Core.Tests/TestClasses/StaticHelper.cs
@@ -37,7 +37,7 @@ public static StaticHelperSqlExpression SubSelect(string columnName, string obje
CustomTypeProvider = new TestCustomTypeProvider()
};
- expFilter = DynamicExpressionParser.ParseLambda(config, true, filter); // Failed Here!
+ expFilter = DynamicExpressionParser.ParseLambda(config, true, filter);
}
return new StaticHelperSqlExpression