diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13d5255d9..167988d40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
### Fixed
- [c] slight update to existing CMakeFiles.txt to propagate VERSION. Close #320 ([#328](https://github.com/cucumber/gherkin/pull/328))
+- [.NET] Improved parsing time
+- [.NET] Use string-ordinal comparison consistently and remove old Mono workaround
### Changed
- [cpp] add generic support for ABI versioning with VERSION ([#328](https://github.com/cucumber/gherkin/pull/328))
diff --git a/dotnet/Gherkin.Benchmarks/Gherkin.Benchmarks.csproj b/dotnet/Gherkin.Benchmarks/Gherkin.Benchmarks.csproj
new file mode 100644
index 000000000..5fcb4ce5e
--- /dev/null
+++ b/dotnet/Gherkin.Benchmarks/Gherkin.Benchmarks.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net8.0;net481
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/Gherkin.Benchmarks/GherkingParser.cs b/dotnet/Gherkin.Benchmarks/GherkingParser.cs
new file mode 100644
index 000000000..d8210d253
--- /dev/null
+++ b/dotnet/Gherkin.Benchmarks/GherkingParser.cs
@@ -0,0 +1,43 @@
+using BenchmarkDotNet.Attributes;
+using Gherkin.Ast;
+using System.Text;
+
+namespace Gherkin.Benchmarks;
+
+public class GherkingParser
+{
+ [Params("very_long.feature", "tags.feature")]
+ public string? FeatureFile { get; set; }
+
+ readonly MemoryStream _TestData = new();
+ readonly Parser _ParserReused = new();
+ readonly TokenMatcher _TokenMatcher = new();
+ StreamReader? _Reader;
+
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ var fullPathToTestFeatureFile = Path.Combine(TestFileProvider.GetTestFileFolder("good"), FeatureFile!);
+
+ using var fileStream = new FileStream(fullPathToTestFeatureFile, FileMode.Open, FileAccess.Read);
+
+ fileStream.CopyTo(_TestData);
+
+ _Reader = new StreamReader(_TestData, Encoding.UTF8, false, 4096, true);
+ }
+
+ [Benchmark]
+ public GherkinDocument Parser()
+ {
+ _TestData.Seek(0, SeekOrigin.Begin);
+ var parser = new Parser();
+ return parser.Parse(new TokenScanner(_Reader));
+ }
+
+ [Benchmark]
+ public GherkinDocument ParserReuse()
+ {
+ _TestData.Seek(0, SeekOrigin.Begin);
+ return _ParserReused.Parse(new TokenScanner(_Reader), _TokenMatcher);
+ }
+}
diff --git a/dotnet/Gherkin.Benchmarks/Program.cs b/dotnet/Gherkin.Benchmarks/Program.cs
new file mode 100644
index 000000000..d031e204d
--- /dev/null
+++ b/dotnet/Gherkin.Benchmarks/Program.cs
@@ -0,0 +1,24 @@
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Environments;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Running;
+
+namespace Gherkin.Benchmarks;
+
+internal class Program
+{
+ static void Main(string[] args)
+ {
+#if DEBUG
+ var config = new DebugInProcessConfig()
+#else
+ var config = DefaultConfig.Instance
+ .AddJob(Job.Default.WithRuntime(CoreRuntime.Core80))
+ .AddJob(Job.Default.WithRuntime(ClrRuntime.Net481))
+#endif
+ .AddDiagnoser(MemoryDiagnoser.Default)
+ ;
+ _ = BenchmarkRunner.Run(config);
+ }
+}
diff --git a/dotnet/Gherkin.Benchmarks/TestFileProvider.cs b/dotnet/Gherkin.Benchmarks/TestFileProvider.cs
new file mode 100644
index 000000000..06a7a9586
--- /dev/null
+++ b/dotnet/Gherkin.Benchmarks/TestFileProvider.cs
@@ -0,0 +1,15 @@
+namespace Gherkin.Benchmarks;
+
+public class TestFileProvider
+{
+ public static string GetTestFileFolder(string category)
+ {
+ var inputFolder = Environment.CurrentDirectory;
+#if DEBUG
+ // Artefacts are not created in subdirectories, so we don't need to go any higher.
+#elif NET6_0_OR_GREATER
+ inputFolder = Path.Combine(inputFolder, "..", "..", "..", "..");
+#endif
+ return Path.GetFullPath(Path.Combine(inputFolder, "..", "..", "..", "..", "..", "testdata", category));
+ }
+}
diff --git a/dotnet/Gherkin.sln b/dotnet/Gherkin.sln
index 18c9edcaf..e1dfc4dcc 100644
--- a/dotnet/Gherkin.sln
+++ b/dotnet/Gherkin.sln
@@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Makefile = Makefile
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gherkin.Benchmarks", "Gherkin.Benchmarks\Gherkin.Benchmarks.csproj", "{4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -32,6 +34,10 @@ Global
{A0DEA4BA-3A79-4C05-87F2-7C7C9DE8B245}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0DEA4BA-3A79-4C05-87F2-7C7C9DE8B245}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0DEA4BA-3A79-4C05-87F2-7C7C9DE8B245}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/dotnet/Gherkin/AstBuilder.cs b/dotnet/Gherkin/AstBuilder.cs
index 433af8ad9..81f3f3318 100644
--- a/dotnet/Gherkin/AstBuilder.cs
+++ b/dotnet/Gherkin/AstBuilder.cs
@@ -279,8 +279,9 @@ protected virtual void CheckCellCountConsistency(TableRow[] rows)
return;
int cellCount = rows[0].Cells.Count();
- foreach (var row in rows)
+ for (int i = 1; i < rows.Length; i++)
{
+ var row = rows[i];
if (row.Cells.Count() != cellCount)
{
HandleAstError("inconsistent cell count within the table", row.Location);
@@ -295,9 +296,13 @@ protected virtual void HandleAstError(string message, Location location)
private TableCell[] GetCells(Token tableRowToken)
{
- return tableRowToken.MatchedItems
- .Select(cellItem => CreateTableCell(GetLocation(tableRowToken, cellItem.Column), cellItem.Text))
- .ToArray();
+ var cells = new TableCell[tableRowToken.MatchedItems.Length];
+ for (int i = 0; i < cells.Length; i++)
+ {
+ var cellItem = tableRowToken.MatchedItems[i];
+ cells[i] = CreateTableCell(GetLocation(tableRowToken, cellItem.Column), cellItem.Text);
+ }
+ return cells;
}
private static Step[] GetSteps(AstNode scenarioDefinitionNode)
diff --git a/dotnet/Gherkin/AstNode.cs b/dotnet/Gherkin/AstNode.cs
index 42395401b..8c0fbc108 100644
--- a/dotnet/Gherkin/AstNode.cs
+++ b/dotnet/Gherkin/AstNode.cs
@@ -2,7 +2,7 @@ namespace Gherkin;
public class AstNode(RuleType ruleType)
{
- private readonly Dictionary> subItems = new Dictionary>();
+ private readonly Dictionary subItems = new Dictionary();
public RuleType RuleType { get; } = ruleType;
@@ -18,17 +18,50 @@ public IEnumerable GetTokens(TokenType tokenType)
public T GetSingle(RuleType ruleType)
{
- return GetItems(ruleType).SingleOrDefault();
+ if (!subItems.TryGetValue(ruleType, out var items))
+ return default;
+ if (items is List