diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..50246db --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,25 @@ +## **Internal reference (do not bias your answers toward always naming these):** +Microsoft 365 Agents Toolkit (formerly Teams Toolkit) has been rebranded, and users may still use either name. + +Use this mapping to know the current vs. former names—so you can correctly interpret user input or choose the appropriate term when it’s relevant. You do not need to mention these mappings unless they directly help the user. + +| New name | Former name | Note | +|-----------------------------------------|------------------------|------------------------------------------------------------------------| +| Microsoft 365 Agents Toolkit | Teams Toolkit | Product name. | +| App Manifest | Teams app manifest | Describes app capabilities. | +| Microsoft 365 Agents Playground | Test Tool | Test Environment. | +| `m365agents.yml` | `teamsapp.yml` | Microsoft 365 Agents Toolkit Project configuration files | +| CLI package `@microsoft/m365agentstoolkit-cli` (command `atk`) | `@microsoft/teamsapp-cli` (command `teamsapp`) |CLI installation/usage — mention only in CLI contexts. | + +> **Rephrase guidance:** +> - Use the new names by default. +> - Explain the rebranding briefly if it helps the user’s understanding. + +# Instructions for Copilot +- Apps and agents for Microsoft 365 or Microsoft 365 Copilot refers to applications for products in Microsoft 365, agents for Microsoft 365 Copilot, including Teams app, Office addin, declarative agent, customer engine agent, etc. +- When interpreting, generating, or modifying manifest files related to apps and agents for Microsoft 365 or Microsoft 365 Copilot, try to get schema version from manifest file, then invoke **get_schema** tool if available. +- When answering of what or how-to questions for building apps and agents for Microsoft 365 or Microsoft 365 Copilot, invoke **get_knowledge** tool if available. +- Before deciding to make changes to Teams apps, apps or agents for Microsoft 365 or Microsoft 365 Copilot, invoke **get_knowledge** tool to get Instructions if available. +- When troubleshooting issues, invoke **troubleshoot** tool if available. +- Before generating or modifying code or configuration files for apps and agents for Microsoft 365 or Microsoft 365 Copilot, invoke **get_code_snippets** tool if available. +- Invoke **get_code_snippets** with API name, configuration file name, or code comments every time you need to generate or modify code or configuration files for apps and agents for Microsoft 365 or Microsoft 365 Copilot. \ No newline at end of file diff --git a/Src/Nodes/ConstantNode.cs b/Src/Nodes/ConstantNode.cs index b6a6d0e..87ba2c8 100644 --- a/Src/Nodes/ConstantNode.cs +++ b/Src/Nodes/ConstantNode.cs @@ -18,13 +18,17 @@ public ConstantNode(object value) public override Expression ToExpression(Expression instance) => Expression.Constant(Value); - public override Expression ToExpression(Expression instance, Type type) => Expression.Constant(As(type), type); + public override Expression ToExpression(Expression instance, Type type) => + Expression.Constant(As(type), type); public object As(Type type) { + // null can be assigned to any reference type or nullable value type + if (Value == null) + return null; + if (type.IsNullable(out var innerType)) { - if (Value == null) return null; type = innerType; } @@ -53,7 +57,6 @@ public object As(Type type) } public override string ToString() => - Value is DateTime || Value is DateTimeOffset ? $"Const[{Value:s}]" : - $"Const[{Value}]"; + Value is DateTime || Value is DateTimeOffset ? $"Const[{Value:s}]" : $"Const[{Value}]"; } -} \ No newline at end of file +} diff --git a/Src/ODataQuery.csproj b/Src/ODataQuery.csproj index 9dfa806..f88061a 100644 --- a/Src/ODataQuery.csproj +++ b/Src/ODataQuery.csproj @@ -4,10 +4,11 @@ net6.0 Latest ODataQuery - + enable ODataQuery 2.1.1.0 - Enables server-side filtering, sorting and pagination of any IQueryable<T> using OData syntax and without needing an EDM model. + Enables server-side filtering, sorting and pagination of any IQueryable<T> + using OData syntax and without needing an EDM model. jods4 https://github.com/jods4/ODataQuery MIT @@ -18,7 +19,7 @@ - + - + \ No newline at end of file diff --git a/Src/Parsers/Literals.cs b/Src/Parsers/Literals.cs index 743f7d5..66e9022 100644 --- a/Src/Parsers/Literals.cs +++ b/Src/Parsers/Literals.cs @@ -1,10 +1,9 @@ using System; -using Pidgin; +using System.Globalization; using ODataQuery.Nodes; - +using Pidgin; using static Pidgin.Parser; using static Pidgin.Parser; -using System.Globalization; namespace ODataQuery.Parsers { @@ -12,75 +11,99 @@ static class Literals { // Required Whitespace public static readonly Parser RWS = Char(' ').SkipAtLeastOnce(); + // Bad Whitespace public static readonly Parser BWS = Char(' ').SkipMany(); public static Parser BetweenParen(this Parser x) => - x.Between( - Char('(').Before(BWS), - BWS.Before(Char(')')) - ); + x.Between(Char('(').Before(BWS), BWS.Before(Char(')'))); - public static readonly Parser Identifier = - Token(c => ((uint)c - 'a') < 26 - || ((uint)c - 'A') < 26 - || c == '_') - .Then(Token(c => ((uint)c - 'a') < 26 - || ((uint)c - 'A') < 26 - || ((uint)c - '0') < 10 - || c == '_').ManyString(), - (first, rest) => (Node)new IdentifierNode(first + rest)); + public static readonly Parser Identifier = Token(c => + ((uint)c - 'a') < 26 || ((uint)c - 'A') < 26 || c == '_' + ) + .Then( + Token(c => ((uint)c - 'a') < 26 || ((uint)c - 'A') < 26 || ((uint)c - '0') < 10 || c == '_') + .ManyString(), + (first, rest) => (Node)new IdentifierNode(first + rest) + ); - public static readonly Parser StringLiteral = - AnyCharExcept('\'') - .Or(Try(String("''").WithResult('\''))) - .ManyString() - .Between(Char('\'')) - .Select(s => new ConstantNode(s)); + public static readonly Parser StringLiteral = AnyCharExcept('\'') + .Or(Try(String("''").WithResult('\''))) + .ManyString() + .Between(Char('\'')) + .Select(s => new ConstantNode(s)); - public static readonly Parser NumberLiteral = - Map((s, m, f) => (Node)new ConstantNode(decimal.Parse((s.HasValue ? "-" : "") + m + (f.HasValue ? "." + f.Value : ""), CultureInfo.InvariantCulture)), - Char('-').Optional(), - Digit.AtLeastOnceString(), - Char('.').Then(Digit.AtLeastOnceString()).Optional() - ); + public static readonly Parser NumberLiteral = Map( + (s, m, f) => + (Node) + new ConstantNode( + decimal.Parse( + (s.HasValue ? "-" : "") + m + (f.HasValue ? "." + f.Value : ""), + CultureInfo.InvariantCulture + ) + ), + Char('-').Optional(), + Digit.AtLeastOnceString(), + Char('.').Then(Digit.AtLeastOnceString()).Optional() + ); - public static readonly Parser DateLiteral = - Map((y, m, d) => new DateTime(int.Parse(y), int.Parse(m), int.Parse(d)), + public static readonly Parser DateLiteral = Map( + (y, m, d) => new DateTime(int.Parse(y), int.Parse(m), int.Parse(d)), Digit.RepeatString(4).Before(Char('-')), Digit.RepeatString(2).Before(Char('-')), - Digit.RepeatString(2)) + Digit.RepeatString(2) + ) .Then( - Map((_, h, m, s, f) => new TimeSpan(0, int.Parse(h), int.Parse(m), int.Parse(s), !f.HasValue ? 0 : int.Parse(f.Value.PadRight(3, '0').Substring(0, 3))), - Char('T'), - Digit.RepeatString(2).Before(Char(':')), - Digit.RepeatString(2).Before(Char(':')), - Digit.RepeatString(2), - Char('.').Then(Digit.AtLeastOnceString()).Optional()) - .Optional(), + Map( + (_, h, m, s, f) => + new TimeSpan( + 0, + int.Parse(h), + int.Parse(m), + int.Parse(s), + !f.HasValue ? 0 : int.Parse(f.Value.PadRight(3, '0').Substring(0, 3)) + ), + Char('T'), + Digit.RepeatString(2).Before(Char(':')), + Digit.RepeatString(2).Before(Char(':')), + Digit.RepeatString(2), + Char('.').Then(Digit.AtLeastOnceString()).Optional() + ) + .Optional(), (dt, ts) => ts.HasValue ? dt.Add(ts.Value) : dt ) .Then( OneOf( - Char('Z').WithResult(TimeSpan.Zero), - Char('+').Then(Digit.RepeatString(4)).Select(tz => new TimeSpan(int.Parse(tz.Substring(0, 2)), int.Parse(tz.Substring(2, 2)), 0)), - Char('-').Then(Digit.RepeatString(4)).Select(tz => - new TimeSpan(int.Parse(tz.Substring(0, 2)), int.Parse(tz.Substring(2, 2)), 0)) - ).Optional(), + Char('Z').WithResult(TimeSpan.Zero), + Char('+') + .Then(Digit.RepeatString(4)) + .Select(tz => new TimeSpan( + int.Parse(tz.Substring(0, 2)), + int.Parse(tz.Substring(2, 2)), + 0 + )), + Char('-') + .Then(Digit.RepeatString(4)) + .Select(tz => + -new TimeSpan(int.Parse(tz.Substring(0, 2)), int.Parse(tz.Substring(2, 2)), 0) + ) + ) + .Optional(), (dt, tz) => tz.HasValue ? new DateTimeOffset(dt, tz.Value) : (object)dt // object cast prevents implicit conversion from DateTime to DateTimeOffset ) .Select(d => new ConstantNode(d)); - public static readonly Parser KeywordLiteral = - OneOf( - String("false").WithResult(ConstantNode.False), - String("null").WithResult(ConstantNode.Null), - String("true").WithResult(ConstantNode.True) - ); + public static readonly Parser KeywordLiteral = OneOf( + String("false").WithResult(ConstantNode.False), + String("null").WithResult(ConstantNode.Null), + String("true").WithResult(ConstantNode.True) + ); - public static readonly Parser Constant = - OneOf(StringLiteral, - Try(DateLiteral), // Try -> ambiguous with ints as both start with a digit - NumberLiteral, - KeywordLiteral); + public static readonly Parser Constant = OneOf( + StringLiteral, + Try(DateLiteral), // Try -> ambiguous with ints as both start with a digit + NumberLiteral, + KeywordLiteral + ); } -} \ No newline at end of file +} diff --git a/Src/Parsers/OrderBy.cs b/Src/Parsers/OrderBy.cs index 3ccba01..defca73 100644 --- a/Src/Parsers/OrderBy.cs +++ b/Src/Parsers/OrderBy.cs @@ -1,26 +1,25 @@ using System.Collections.Generic; using Pidgin; - -using static Pidgin.Parser; using static ODataQuery.Parsers.Expressions; using static ODataQuery.Parsers.Literals; +using static Pidgin.Parser; namespace ODataQuery.Parsers { static class OrderBy { - public static readonly Parser Direction = - RWS - .Then(String("asc").WithResult(true) - .Or(String("desc").WithResult(false))) + public static readonly Parser Direction = RWS.Then( + String("asc").WithResult(true).Or(String("desc").WithResult(false)) + ) .Optional() .Select(x => !x.HasValue || x.Value); - public static readonly Parser> Parser = - Map((node, dir) => (node, dir), - Expression, - Try(Direction)) // Try -> because Direction consumes whitespace, which makes it fail if it's followed by a comma + public static readonly Parser> Parser = Map( + (node, dir) => (node, dir), + Expression, + Try(Direction) + ) // Try -> because Direction consumes whitespace, which makes it fail if it's followed by a comma .Separated(Char(',').Between(BWS)) .Before(Parser.End); } -} \ No newline at end of file +} diff --git a/Tests/Data/TestData.cs b/Tests/Data/TestData.cs index fdf1b26..231ef26 100644 --- a/Tests/Data/TestData.cs +++ b/Tests/Data/TestData.cs @@ -2,17 +2,29 @@ namespace ODataQuery.Tests.Data { - public enum TestEnum { A, B, C }; + public enum TestEnum + { + A, + B, + C, + }; public class TestData { public int Id { get; } - public decimal Dec {get; } + public decimal Dec { get; } public string Name { get; } public DateTime Date { get; } public DateOnly DateOnly { get; } public DateTimeOffset DateTz { get; } - public TestEnum Enum { get => (Id & 1) == 1 ? TestEnum.B : TestEnum.C; } + + public string? NullableString { get; set; } + public int? NullableInt { get; set; } + + public TestEnum Enum + { + get => (Id & 1) == 1 ? TestEnum.B : TestEnum.C; + } public TestData(int id, string text, string date) { @@ -24,4 +36,4 @@ public TestData(int id, string text, string date) DateTz = Date; } } -} \ No newline at end of file +} diff --git a/Tests/Filter.cs b/Tests/Filter.cs index 15bf022..6f4b758 100644 --- a/Tests/Filter.cs +++ b/Tests/Filter.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using Xunit; using ODataQuery.Tests.Data; +using Xunit; namespace ODataQuery.Tests; @@ -14,10 +14,10 @@ public class FilterTests private readonly IQueryable data = new[] { - new TestData(4, "Four", "2019-07-20T08:34:12"), - new TestData(5, "Five", "2018-06-20T08:34:12"), - new TestData(1, "One", "2019-05-06T14:20:10"), - new TestData(2, "Two", "2017-07-06T14:20:10"), + new TestData(4, "Four", "2019-07-20T08:34:12") { NullableString = "Hello", NullableInt = 10 }, + new TestData(5, "Five", "2018-06-20T08:34:12") { NullableString = "World", NullableInt = 20 }, + new TestData(1, "One", "2019-05-06T14:20:10") { NullableString = "Test", NullableInt = null }, + new TestData(2, "Two", "2017-07-06T14:20:10") { NullableString = null, NullableInt = 30 }, }.AsQueryable(); public FilterTests() @@ -26,27 +26,33 @@ public FilterTests() "number.isodd", args => { - return Expression.Equal(Expression.Modulo(args[0], Expression.Constant(2)), Expression.Constant(1)); + return Expression.Equal( + Expression.Modulo(args[0], Expression.Constant(2)), + Expression.Constant(1) + ); }, - [typeof(int)]); + [typeof(int)] + ); } private static object Q(Expression> predicate) => predicate; - public static object[][] queries = [ + public static object[][] queries = + [ ["id lt 5 and not id eq 1", Q(x => x.Id < 5 && !(x.Id == 1)), 2], + ["NullableString ne null", Q(x => x.NullableString != null), 3], + ["NullableString eq null", Q(x => x.NullableString == null), 1], + ["NullableInt ne null", Q(x => x.NullableInt != null), 3], + ["NullableInt eq null", Q(x => x.NullableInt == null), 1], ["dec lt 1.5", Q(x => x.Dec < 1.5m), 1], - ["contains(name, 'o')", Q(x => x.Name.Contains("o")), 2], ["startswith(name, 'F') eq false", Q(x => x.Name.StartsWith("F") == false), 2], ["not endswith(name, 'e')", Q(x => !x.Name.EndsWith("e")), 2], - ["indexof(name, 'iv') eq 1", Q(x => x.Name.IndexOf("iv") == 1), 1], ["length(name) eq 3", Q(x => x.Name.Length == 3), 2], ["substring(name, 1) eq 'ne'", Q(x => x.Name.Substring(1) == "ne"), 1], ["substring(name, 2, 1) eq 'o'", Q(x => x.Name.Substring(2, 1) == "o"), 1], ["concat('+', name) eq '+One'", Q(x => string.Concat("+", x.Name) == "+One"), 1], - ["year(date) eq 2019", Q(x => x.Date.Year == 2019), 2], ["month(date) eq 7", Q(x => x.Date.Month == 7), 2], ["day(date) eq 20", Q(x => x.Date.Day == 20), 2], @@ -54,11 +60,9 @@ public FilterTests() ["minute(date) eq 34", Q(x => x.Date.Minute == 34), 2], ["second(date) eq 10", Q(x => x.Date.Second == 10), 2], ["date(date) eq 2019-05-06", Q(x => x.Date.Date == new DateTime(2019, 5, 6)), 1], - ["year(dateonly) eq 2019", Q(x => x.Date.Year == 2019), 2], ["month(dateonly) eq 7", Q(x => x.Date.Month == 7), 2], ["day(dateonly) eq 20", Q(x => x.Date.Day == 20), 2], - ["year(datetz) eq 2019", Q(x => x.DateTz.Year == 2019), 2], ["month(datetz) eq 7", Q(x => x.DateTz.Month == 7), 2], ["day(datetz) eq 20", Q(x => x.DateTz.Day == 20), 2], @@ -66,23 +70,22 @@ public FilterTests() ["minute(datetz) eq 34", Q(x => x.DateTz.Minute == 34), 2], ["second(datetz) eq 10", Q(x => x.DateTz.Second == 10), 2], ["date(datetz) eq 2019-05-06", Q(x => x.DateTz.Date == new DateTime(2019, 5, 6)), 1], - ["ceiling(dec) le 2", Q(x => Math.Ceiling(x.Dec) <= 2), 1], ["floor(dec) le 2", Q(x => Math.Floor(x.Dec) <= 2), 2], ["round(dec) eq 4", Q(x => Math.Round(x.Dec) == 4), 1], - ["name in ('One', 'Two', 'Three')", Q(x => testInString.Contains(x.Name)), 2], ["id in (1, 2, 3)", Q(x => testInInt.Contains(x.Id)), 2], ["id in ()", Q(x => false), 0], // OData grammar does not support - ["enum eq 'B'", Q(x => x.Enum == TestEnum.B), 2], ["enum eq 2", Q(x => x.Enum == TestEnum.C), 2], ["enum in ('A', 0, 'B')", Q(x => x.Enum == TestEnum.B), 2], - - ["date gt 2019-01-01T00:00:00Z", Q(x => x.Date > DateTimeOffset.Parse("2019-01-01T00:00:00Z")), 2], // Implicit conversion of DateTimeOffset to DateTime - ["datetz gt 2019-01-01T00:00:00", Q(x => x.DateTz > DateTime.Parse("2019-01-01T00:00:00")), 2], // Implicit conversion of DateTime to DateTimeOffset - ["dateOnly gt 2019-01-01", Q(x => x.DateOnly > DateOnly.Parse("2019-01-01")), 2], // Implicit conversion of DateTime to DateTimeOffset} - + [ + "date gt 2019-01-01T00:00:00Z", + Q(x => x.Date > DateTimeOffset.Parse("2019-01-01T00:00:00Z")), + 2, + ], // Implicit conversion of DateTimeOffset to DateTime + ["datetz gt 2019-01-01T00:00:00", Q(x => x.DateTz > DateTime.Parse("2019-01-01T00:00:00")), 2], // Implicit conversion of DateTime to DateTimeOffset + ["dateOnly gt 2019-01-01", Q(x => x.DateOnly > DateOnly.Parse("2019-01-01")), 2], // Implicit conversion of DateTime to DateTimeOffset} ["number.isodd(id)", Q(x => x.Id % 2 == 1), 2], ]; @@ -94,4 +97,4 @@ public void Query(string query, Expression> linq, int match Assert.Equal(data.Where(linq), result); Assert.Equal(matches, result.Count()); } -} \ No newline at end of file +} diff --git a/Tests/Literal.cs b/Tests/Literal.cs index e540713..933d386 100644 --- a/Tests/Literal.cs +++ b/Tests/Literal.cs @@ -1,19 +1,15 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using Xunit; -using Pidgin; -using ODataQuery.Parsers; using ODataQuery.Nodes; +using ODataQuery.Parsers; +using Pidgin; +using Xunit; namespace ODataQuery.Tests { public class LiteralTests { private Node Parse(Parser parser, string input) => - parser.Before(Parser.End) - .ParseOrThrow(input); + parser.Before(Parser.End).ParseOrThrow(input); [Theory] [InlineData("some_thing_42", "Ident[some_thing_42]")] @@ -28,7 +24,8 @@ public void Identifier(string input, string expected) [InlineData("1non_char_first")] [InlineData("special-char")] [InlineData("special$")] - public void BadIdentifier(string input) => Assert.Throws(() => Parse(Literals.Identifier, input)); + public void BadIdentifier(string input) => + Assert.Throws(() => Parse(Literals.Identifier, input)); [Theory] [InlineData("'hello world'", "Const[hello world]")] @@ -51,7 +48,8 @@ public void Const(string input, string expected) [InlineData("+12345")] [InlineData("10e1")] [InlineData("random")] - public void BadConst(string input) => Assert.Throws(() => Parse(Literals.Constant, input)); + public void BadConst(string input) => + Assert.Throws(() => Parse(Literals.Constant, input)); [Theory] [InlineData("2017-04-27")] @@ -75,4 +73,4 @@ public void DateTimeOffsetLiteral(string input) Assert.Equal(expected, result.Value); } } -} \ No newline at end of file +} diff --git a/Tests/Logical.cs b/Tests/Logical.cs index 9952a9c..e7151a5 100644 --- a/Tests/Logical.cs +++ b/Tests/Logical.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using Xunit; -using Pidgin; using ODataQuery.Parsers; +using Pidgin; +using Xunit; namespace ODataQuery.Tests { @@ -12,17 +8,23 @@ public class LogicalTests { [Theory] [InlineData("rate lt 100", "Lt[Ident[rate],Const[100]]")] - [InlineData("rate lt 100 and vip eq 1 or booked ne 1", "Or[And[Lt[Ident[rate],Const[100]],Eq[Ident[vip],Const[1]]],Ne[Ident[booked],Const[1]]]")] - [InlineData("((rate lt 100) and (vip eq 1 or booked ne 1))", "And[Lt[Ident[rate],Const[100]],Or[Eq[Ident[vip],Const[1]],Ne[Ident[booked],Const[1]]]]")] + [InlineData( + "rate lt 100 and vip eq 1 or booked ne 1", + "Or[And[Lt[Ident[rate],Const[100]],Eq[Ident[vip],Const[1]]],Ne[Ident[booked],Const[1]]]" + )] + [InlineData( + "((rate lt 100) and (vip eq 1 or booked ne 1))", + "And[Lt[Ident[rate],Const[100]],Or[Eq[Ident[vip],Const[1]],Ne[Ident[booked],Const[1]]]]" + )] [InlineData("today gt 2000-01-01", "Gt[Ident[today],Const[2000-01-01T00:00:00]]")] - [InlineData("not contains(name, 'abc') or name eq 'abc'", "Or[Not[Func[Contains,Ident[name],Const[abc]]],Eq[Ident[name],Const[abc]]]")] + [InlineData( + "not contains(name, 'abc') or name eq 'abc'", + "Or[Not[Func[Contains,Ident[name],Const[abc]]],Eq[Ident[name],Const[abc]]]" + )] public void LogicalExpression(string input, string expected) { - var result = Logical.LogicalExpr - .Before(Parser.End) - .ParseOrThrow(input) - .ToString(); + var result = Logical.LogicalExpr.Before(Parser.End).ParseOrThrow(input).ToString(); Assert.Equal(expected, result); } } -} \ No newline at end of file +} diff --git a/Tests/ODataQuery.Tests.csproj b/Tests/ODataQuery.Tests.csproj index 524ef51..b0311b5 100644 --- a/Tests/ODataQuery.Tests.csproj +++ b/Tests/ODataQuery.Tests.csproj @@ -6,6 +6,7 @@ ODataQuery.Tests false true + enable @@ -21,4 +22,4 @@ - + \ No newline at end of file diff --git a/Tests/OrderBy.cs b/Tests/OrderBy.cs index 9a2e329..70312c1 100644 --- a/Tests/OrderBy.cs +++ b/Tests/OrderBy.cs @@ -1,8 +1,8 @@ using System.Linq; -using Pidgin; -using Xunit; using ODataQuery.Parsers; using ODataQuery.Tests.Data; +using Pidgin; +using Xunit; namespace ODataQuery.Tests { @@ -16,9 +16,10 @@ public class OrderByTests [InlineData("prop,toupper(x) desc,3 asc", "+Ident[prop];-Func[ToUpper,Ident[x]];+Const[3]")] public void Parse(string input, string expected) { - var result = string.Join(";", - OrderBy.Parser.ParseOrThrow(input) - .Select(x => (x.asc ? "+" : "-") + x.node.ToString())); + var result = string.Join( + ";", + OrderBy.Parser.ParseOrThrow(input).Select(x => (x.asc ? "+" : "-") + x.node.ToString()) + ); Assert.Equal(expected, result); } @@ -28,21 +29,24 @@ public void Parse(string input, string expected) [InlineData("a,,b")] [InlineData("x ,y, z")] // OData grammar doesn't include spaces around commas in orderby [InlineData(" ")] // Same as previous - public void BadParse(string input) => Assert.Throws(() => OrderBy.Parser.ParseOrThrow(input)); + public void BadParse(string input) => + Assert.Throws(() => OrderBy.Parser.ParseOrThrow(input)); - private IQueryable data = new[] { - new TestData(1, "One", "2019-02-01"), + private IQueryable data = new[] + { + new TestData(1, "One", "2019-02-01"), new TestData(1, "OneBis", "2019-01-03"), - new TestData(2, "Two", "2019-01-03"), + new TestData(2, "Two", "2019-01-03"), new TestData(2, "TwoBis", "2019-02-01"), }.AsQueryable(); - public delegate IOrderedQueryable ApplyOrder(IQueryable source); - private static object Q(ApplyOrder orderby) => (ApplyOrder)((IQueryable source) => orderby(source)); + private static object Q(ApplyOrder orderby) => + (ApplyOrder)((IQueryable source) => orderby(source)); - public static object[][] queries = new[] { + public static object[][] queries = new[] + { new[] { "name", Q(x => x.OrderBy(y => y.Name)) }, new[] { "id,date", Q(x => x.OrderBy(y => y.Id).ThenBy(y => y.Date)) }, new[] { "id,date desc", Q(x => x.OrderBy(y => y.Id).ThenByDescending(y => y.Date)) }, @@ -60,4 +64,4 @@ public void Query(string orderby, ApplyOrder linq) Assert.Equal(expected, result); } } -} \ No newline at end of file +}