Skip to content

Commit 5d57461

Browse files
committed
Support embedded quotes in string literal (#65)
1 parent 47db7aa commit 5d57461

File tree

9 files changed

+224
-23
lines changed

9 files changed

+224
-23
lines changed

src/System.Linq.Dynamic.Core/DynamicExpressionParser.cs

+33
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ namespace System.Linq.Dynamic.Core
99
/// </summary>
1010
public static class DynamicExpressionParser
1111
{
12+
/// <summary>
13+
/// Parses a expression into a LambdaExpression. (Also create a constructor for all the parameters.)
14+
/// </summary>
15+
/// <param name="itType">The main type from the dynamic class expression.</param>
16+
/// <param name="resultType">Type of the result. If not specified, it will be generated dynamically.</param>
17+
/// <param name="expression">The expression.</param>
18+
/// <param name="values">An object array that contains zero or more objects which are used as replacement values.</param>
19+
/// <returns>The generated <see cref="LambdaExpression"/></returns>
20+
public static LambdaExpression ParseLambda([NotNull] Type itType, [CanBeNull] Type resultType, [NotNull] string expression, params object[] values)
21+
{
22+
Check.NotNull(itType, nameof(itType));
23+
Check.NotEmpty(expression, nameof(expression));
24+
25+
return ParseLambda(true, itType, resultType, expression, values);
26+
}
27+
1228
/// <summary>
1329
/// Parses a expression into a LambdaExpression.
1430
/// </summary>
@@ -26,6 +42,23 @@ public static LambdaExpression ParseLambda(bool createParameterCtor, [NotNull] T
2642
return ParseLambda(createParameterCtor, new[] { Expression.Parameter(itType, "") }, resultType, expression, values);
2743
}
2844

45+
/// <summary>
46+
/// Parses a expression into a LambdaExpression. (Also create a constructor for all the parameters.)
47+
/// </summary>
48+
/// <param name="parameters">A array from ParameterExpressions.</param>
49+
/// <param name="resultType">Type of the result. If not specified, it will be generated dynamically.</param>
50+
/// <param name="expression">The expression.</param>
51+
/// <param name="values">An object array that contains zero or more objects which are used as replacement values.</param>
52+
/// <returns>The generated <see cref="LambdaExpression"/></returns>
53+
public static LambdaExpression ParseLambda([NotNull] ParameterExpression[] parameters, [CanBeNull] Type resultType, [NotNull] string expression, params object[] values)
54+
{
55+
Check.NotEmpty(parameters, nameof(parameters));
56+
Check.Condition(parameters, p => p.Count(x => x == null) == 0, nameof(parameters));
57+
Check.NotEmpty(expression, nameof(expression));
58+
59+
return ParseLambda(true, parameters, resultType, expression, values);
60+
}
61+
2962
/// <summary>
3063
/// Parses a expression into a LambdaExpression.
3164
/// </summary>

src/System.Linq.Dynamic.Core/ExpressionParser.cs

+5-12
Original file line numberDiff line numberDiff line change
@@ -834,14 +834,7 @@ Expression ParseStringLiteral()
834834
_textParser.ValidateToken(TokenId.StringLiteral);
835835
char quote = _textParser.CurrentToken.Text[0];
836836
string s = _textParser.CurrentToken.Text.Substring(1, _textParser.CurrentToken.Text.Length - 2);
837-
int start = 0;
838-
while (true)
839-
{
840-
int i = s.IndexOf(quote, start);
841-
if (i < 0) break;
842-
s = s.Remove(i, 1);
843-
start = i + 1;
844-
}
837+
845838
if (quote == '\'')
846839
{
847840
if (s.Length != 1)
@@ -1684,11 +1677,11 @@ int FindIndexer(Type type, Expression[] args, out MethodBase method)
16841677
{
16851678
foreach (Type t in SelfAndBaseTypes(type))
16861679
{
1687-
//#if !(NETFX_CORE || WINDOWS_APP || DOTNET5_1 || UAP10_0 || NETSTANDARD)
1680+
//#if !(NETFX_CORE || WINDOWS_APP || DOTNET5_1 || UAP10_0 || NETSTANDARD)
16881681
MemberInfo[] members = t.GetDefaultMembers();
1689-
//#else
1690-
// MemberInfo[] members = new MemberInfo[0];
1691-
//#endif
1682+
//#else
1683+
// MemberInfo[] members = new MemberInfo[0];
1684+
//#endif
16921685
if (members.Length != 0)
16931686
{
16941687
IEnumerable<MethodBase> methods = members

src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs

+17-2
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,27 @@ public void NextToken()
229229
char quote = _ch;
230230
do
231231
{
232-
NextChar();
233-
while (_textPos < _textLen && _ch != quote) NextChar();
232+
bool escaped;
233+
234+
do
235+
{
236+
escaped = false;
237+
NextChar();
238+
239+
if (_ch == '\\')
240+
{
241+
escaped = true;
242+
if (_textPos < _textLen) NextChar();
243+
}
244+
}
245+
while (_textPos < _textLen && (_ch != quote || escaped));
246+
234247
if (_textPos == _textLen)
235248
throw ParseError(_textPos, Res.UnterminatedStringLiteral);
249+
236250
NextChar();
237251
} while (_ch == quote);
252+
238253
t = TokenId.StringLiteral;
239254
break;
240255

test/EntityFramework.DynamicLinq.Tests/project.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"../System.Linq.Dynamic.Core.Tests/EntitiesTests.*.cs",
2626
"../System.Linq.Dynamic.Core.Tests/Entities/*.cs",
2727
"../System.Linq.Dynamic.Core.Tests/TestHelpers/*.cs",
28+
"../System.Linq.Dynamic.Core.Tests/Helpers/*.cs",
2829
"../System.Linq.Dynamic.Core.Tests/Helpers/*/*.cs"
2930
]
3031
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using System.Linq.Dynamic.Core.Exceptions;
2+
using System.Linq.Expressions;
3+
using Xunit;
4+
5+
namespace System.Linq.Dynamic.Core.Tests
6+
{
7+
public class DynamicExpressionParserTests
8+
{
9+
[Fact]
10+
public void Parse_ParameterExpressionMethodCall_ReturnsIntExpression()
11+
{
12+
var expression = DynamicExpressionParser.ParseLambda(true,
13+
new[] { Expression.Parameter(typeof(int), "x") },
14+
typeof(int),
15+
"x + 1");
16+
Assert.Equal(typeof(int), expression.Body.Type);
17+
}
18+
19+
[Fact]
20+
public void Parse_TupleToStringMethodCall_ReturnsStringLambdaExpression()
21+
{
22+
var expression = DynamicExpressionParser.ParseLambda(
23+
typeof(Tuple<int>),
24+
typeof(string),
25+
"it.ToString()");
26+
Assert.Equal(typeof(string), expression.ReturnType);
27+
}
28+
29+
[Fact]
30+
public void Parse_StringLiteral_ReturnsBooleanLambdaExpression()
31+
{
32+
var expression = DynamicExpressionParser.ParseLambda(new[] { Expression.Parameter(typeof(string), "Property1") }, typeof(Boolean), "Property1 == \"test\"");
33+
Assert.Equal(typeof(Boolean), expression.Body.Type);
34+
}
35+
36+
[Fact]
37+
public void Parse_StringLiteralEmpty_ReturnsBooleanLambdaExpression()
38+
{
39+
var expression = DynamicExpressionParser.ParseLambda(new[] { Expression.Parameter(typeof(string), "Property1") }, typeof(Boolean), "Property1 == \"\"");
40+
Assert.Equal(typeof(Boolean), expression.Body.Type);
41+
}
42+
43+
[Fact]
44+
public void Parse_StringLiteralEmbeddedQuote_ReturnsBooleanLambdaExpression()
45+
{
46+
string expectedRightValue = "\"test \\\"string\"";
47+
var expression = DynamicExpressionParser.ParseLambda(
48+
new[] { Expression.Parameter(typeof(string), "Property1") },
49+
typeof(Boolean),
50+
string.Format("Property1 == {0}", expectedRightValue));
51+
52+
string rightValue = ((BinaryExpression)expression.Body).Right.ToString();
53+
Assert.Equal(typeof(Boolean), expression.Body.Type);
54+
Assert.Equal(expectedRightValue, rightValue);
55+
}
56+
57+
[Fact]
58+
public void Parse_StringLiteralStartEmbeddedQuote_ReturnsBooleanLambdaExpression()
59+
{
60+
string expectedRightValue = "\"\\\"test\"";
61+
var expression = DynamicExpressionParser.ParseLambda(
62+
new[] { Expression.Parameter(typeof(string), "Property1") },
63+
typeof(Boolean),
64+
string.Format("Property1 == {0}", expectedRightValue));
65+
66+
string rightValue = ((BinaryExpression)expression.Body).Right.ToString();
67+
Assert.Equal(typeof(Boolean), expression.Body.Type);
68+
Assert.Equal(expectedRightValue, rightValue);
69+
}
70+
71+
[Fact]
72+
public void Parse_StringLiteral_MissingClosingQuote()
73+
{
74+
string expectedRightValue = "\"test\\\"";
75+
76+
Assert.Throws<ParseException>(() => DynamicExpressionParser.ParseLambda(
77+
new[] { Expression.Parameter(typeof(string), "Property1") },
78+
typeof(Boolean),
79+
string.Format("Property1 == {0}", expectedRightValue)));
80+
}
81+
82+
[Fact]
83+
public void Parse_StringLiteralEscapedBackslash_ReturnsBooleanLambdaExpression()
84+
{
85+
string expectedRightValue = "\"test\\string\"";
86+
var expression = DynamicExpressionParser.ParseLambda(
87+
new[] { Expression.Parameter(typeof(string), "Property1") },
88+
typeof(Boolean),
89+
string.Format("Property1 == {0}", expectedRightValue));
90+
91+
string rightValue = ((BinaryExpression)expression.Body).Right.ToString();
92+
Assert.Equal(typeof(Boolean), expression.Body.Type);
93+
Assert.Equal(expectedRightValue, rightValue);
94+
}
95+
96+
//[Fact]
97+
//public void ParseLambda_DelegateTypeMethodCall_ReturnsEventHandlerLambdaExpression()
98+
//{
99+
// var expression = DynamicExpressionParser.ParseLambda(true,
100+
// typeof(EventHandler),
101+
// null,
102+
// new[] { Expression.Parameter(typeof(object), "sender"), Expression.Parameter(typeof(EventArgs), "e") },
103+
// "sender.ToString()");
104+
105+
// Assert.Equal(typeof(void), expression.ReturnType);
106+
// Assert.Equal(typeof(EventHandler), expression.Type);
107+
//}
108+
109+
//[Fact] this should fail : not allowed
110+
//public void ParseLambda_VoidMethodCall_ReturnsActionDelegate()
111+
//{
112+
// var expression = DynamicExpressionParser.ParseLambda(
113+
// typeof(IO.FileStream),
114+
// null,
115+
// "it.Close()");
116+
// Assert.Equal(typeof(void), expression.ReturnType);
117+
// Assert.Equal(typeof(Action<IO.FileStream>), expression.Type);
118+
//}
119+
}
120+
}

test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs

+16
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,22 @@ public void ExpressionTests_Enum()
331331
Assert.Equal(TestEnum.Var5, result7.Single());
332332
}
333333

334+
// [Fact]
335+
public void ExpressionTests_Enum_IN()
336+
{
337+
GlobalConfig.CustomTypeProvider = new NetStandardCustomTypeProvider();
338+
339+
//Arrange
340+
var model1 = new ModelWithEnum { TestEnum = TestEnum.Var1 };
341+
var model2 = new ModelWithEnum { TestEnum = TestEnum.Var2 };
342+
var model3 = new ModelWithEnum { TestEnum = TestEnum.Var3 };
343+
var qry = new [] { model1, model2, model3 }.AsQueryable();
344+
345+
// Act
346+
//var expected = qry.Where(x => x.TestEnum )
347+
//var result = qry.Where("TestEnum IN (@0)", new[] { TestEnum.Var1, TestEnum.Var2 });
348+
}
349+
334350
[Fact]
335351
public void ExpressionTests_Enum_Nullable()
336352
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+

2+
namespace System.Linq.Dynamic.Core.Tests.Helpers.Models
3+
{
4+
public class ModelWithEnum
5+
{
6+
public string Name { get; set; }
7+
8+
public TestEnum TestEnum { get; set; }
9+
}
10+
}

test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs

+21
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,27 @@ public void Where_Dynamic()
2727
Assert.Equal(testList[1], userByFirstName.Single());
2828
}
2929

30+
[Fact]
31+
public void Where_Dynamic_StringQuoted()
32+
{
33+
//Arrange
34+
var testList = User.GenerateSampleModels(2, allowNullableProfiles: true);
35+
testList[0].UserName = @"This \""is\"" a test.";
36+
var qry = testList.AsQueryable();
37+
38+
//Act
39+
var result1a = qry.Where(@"UserName == ""This \""is\"" a test.""").ToArray();
40+
var result1b = qry.Where("UserName == \"This \\\"is\\\" a test.\"").ToArray();
41+
var result2 = qry.Where("UserName == @0", @"This \""is\"" a test.").ToArray();
42+
var expected = qry.Where(x => x.UserName == @"This \""is\"" a test.").ToArray();
43+
44+
//Assert
45+
Assert.Equal(1, expected.Length);
46+
Assert.Equal(expected, result1a);
47+
Assert.Equal(expected, result1b);
48+
Assert.Equal(expected, result2);
49+
}
50+
3051
[Fact]
3152
public void Where_Dynamic_SelectNewObjects()
3253
{

test/System.Linq.Dynamic.Core.Tests/project.json

+1-9
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,10 @@
77
"Newtonsoft.Json": "9.0.1",
88
"QueryInterceptor.Core": "1.0.5",
99
"System.Linq.Dynamic.Core": { "target": "project" },
10-
"xunit": "2.2.0-beta2-build3300"
10+
"xunit": "2.2.0"
1111
},
1212

1313
"frameworks": {
14-
"net461": {
15-
"buildOptions": { "define": [ "EF" ] },
16-
"dependencies": {
17-
"EntityFramework": "6.1.3",
18-
"EntityFramework.DynamicLinq": { "target": "project" },
19-
"Microsoft.AspNet.Identity.EntityFramework": "2.2.1"
20-
}
21-
},
2214
"netcoreapp1.0": {
2315
"buildOptions": { "define": [ "NETSTANDARD", "EFCORE" ] },
2416
"imports": [

0 commit comments

Comments
 (0)