diff --git a/dotnet/Selenium.sln b/dotnet/Selenium.sln
index 59907c6346ca5..49b566611dbb3 100644
--- a/dotnet/Selenium.sln
+++ b/dotnet/Selenium.sln
@@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Selenium.WebDriver.Safari.T
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Selenium.WebDriver.Support.Tests", "test\support\Selenium.WebDriver.Support.Tests.csproj", "{2136C695-2526-45E0-AE1D-68FBBC6A9DE2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selenium.WebDriver.ResourceUtilitiesGenerator", "private\Selenium.WebDriver.ResourceUtilitiesGenerator\Selenium.WebDriver.ResourceUtilitiesGenerator.csproj", "{C3650129-9310-F297-D3F8-219678D6F433}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -69,6 +71,10 @@ Global
{2136C695-2526-45E0-AE1D-68FBBC6A9DE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2136C695-2526-45E0-AE1D-68FBBC6A9DE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2136C695-2526-45E0-AE1D-68FBBC6A9DE2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C3650129-9310-F297-D3F8-219678D6F433}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C3650129-9310-F297-D3F8-219678D6F433}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C3650129-9310-F297-D3F8-219678D6F433}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C3650129-9310-F297-D3F8-219678D6F433}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/ResourceUtilitiesGenerator.cs b/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/ResourceUtilitiesGenerator.cs
new file mode 100644
index 0000000000000..b06da29bb87f0
--- /dev/null
+++ b/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/ResourceUtilitiesGenerator.cs
@@ -0,0 +1,167 @@
+//
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The SFC licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Text;
+using System.Threading;
+
+namespace Selenium.WebDriver.ResourceUtilitiesGenerator;
+
+[Generator]
+public class ResourceUtilitiesGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var jsFiles = context.AdditionalTextsProvider
+ .Where(static (text) => text.Path.EndsWith(".js") || text.Path.EndsWith(".json"))
+ .WithTrackingName("ResourceFiles")
+ .Select(static (data, token) =>
+ {
+ var name = Path.GetFileName(data.Path);
+ var code = GenerateAtom(data, token, out string propertyName, out var diagnostics);
+
+ return (name, code, propertyName, diagnostics);
+ });
+
+ context.RegisterSourceOutput(jsFiles, static (context, pair) =>
+ {
+ foreach (var diagnostic in pair.diagnostics)
+ {
+ context.ReportDiagnostic(diagnostic);
+ }
+ if (pair.code is not null)
+ {
+ context.AddSource($"ResourceUtilities.{pair.propertyName}.g.cs", SourceText.From(pair.code, Encoding.UTF8));
+ }
+ });
+ }
+
+ private static string? GenerateAtom(AdditionalText additionalText, CancellationToken token, out string propertyName, out ImmutableArray diagnostics)
+ {
+ diagnostics = [];
+ var sourceText = additionalText.GetText(token);
+ if (sourceText is null)
+ {
+ var d = Diagnostic.Create(new DiagnosticDescriptor("WRG1001", "Failed to read atom", "Atom '{0}' could not be read", "WebDriverResourceGenerator", DiagnosticSeverity.Error, true), Location.None, additionalText.Path);
+ diagnostics = [d];
+ propertyName = string.Empty;
+ return null;
+ }
+
+ propertyName = GetPropertyNameFromFilePath(additionalText.Path, out var lang, out var diag);
+ if (diag is not null)
+ {
+ diagnostics = diagnostics.Add(diag);
+ }
+
+ string contents = $$"""""""
+ //
+
+ namespace OpenQA.Selenium.Internal;
+
+ internal static partial class ResourceUtilities
+ {
+ [global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("{{lang}}")]
+ internal const string {{propertyName}} =
+ """""
+
+ """"""";
+
+ var builder = new StringBuilder(contents);
+
+ // Normalize line endings for each line
+ foreach (var line in sourceText.Lines)
+ {
+ builder.AppendLine(sourceText.GetSubText(line.Span).ToString());
+ }
+
+ builder.AppendLine("""""""
+ """"";
+ }
+ """"""");
+
+ return builder.ToString();
+ }
+
+ private static string GetPropertyNameFromFilePath(string filePath, out string language, out Diagnostic? diagnostic)
+ {
+ diagnostic = null;
+ if (filePath.EndsWith("webdriver.json"))
+ {
+ language = "json";
+ return "WebDriverPrefsJson";
+ }
+ else if (filePath.EndsWith("is-displayed.js"))
+ {
+ language = "javascript";
+ return "IsDisplayedAtom";
+ }
+ else if (filePath.EndsWith("mutation-listener.js"))
+ {
+ language = "javascript";
+ return "MutationListenerAtom";
+ }
+ else if (filePath.EndsWith("get-attribute.js"))
+ {
+ language = "javascript";
+ return "GetAttributeAtom";
+ }
+ else if (filePath.EndsWith("find-elements.js"))
+ {
+ language = "javascript";
+ return "FindElementsAtom";
+ }
+
+ diagnostic = Diagnostic.Create(new DiagnosticDescriptor("WRG1002", "Unknown resource file", "Unknown file in the resource generator '{0}'", "WebDriverResourceGenerator", DiagnosticSeverity.Warning, true), Location.None, filePath);
+
+ var suffix = filePath.EndsWith(".js") ? "Atom" : "Json";
+
+ language = string.Empty;
+ return KebabCaseToPascalCase(Path.GetFileNameWithoutExtension(filePath)) + suffix;
+ }
+
+ private static string KebabCaseToPascalCase(string v)
+ {
+ Span newValues = new char[v.Length];
+ int newValuesOffset = 0;
+ for (int i = 0; i < v.Length; i++)
+ {
+ if (i == 0)
+ {
+ newValues[i - newValuesOffset] = char.ToUpperInvariant(v[i]);
+ }
+ else if (char.IsLetter(v[i]))
+ {
+ newValues[i - newValuesOffset] = v[i];
+ }
+ else
+ {
+ i++;
+ newValuesOffset++;
+ newValues[i - newValuesOffset] = char.ToUpperInvariant(v[i]);
+ }
+ }
+
+ return newValues.Slice(0, v.Length - newValuesOffset).ToString();
+ }
+}
diff --git a/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/Selenium.WebDriver.ResourceUtilitiesGenerator.csproj b/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/Selenium.WebDriver.ResourceUtilitiesGenerator.csproj
new file mode 100644
index 0000000000000..a94a3b0bbc31a
--- /dev/null
+++ b/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/Selenium.WebDriver.ResourceUtilitiesGenerator.csproj
@@ -0,0 +1,24 @@
+
+
+
+ netstandard2.0
+ preview
+ enable
+ true
+
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/webdriver/Selenium.WebDriver.csproj b/dotnet/src/webdriver/Selenium.WebDriver.csproj
index 5d549c6342759..358d5110435ee 100644
--- a/dotnet/src/webdriver/Selenium.WebDriver.csproj
+++ b/dotnet/src/webdriver/Selenium.WebDriver.csproj
@@ -79,13 +79,20 @@
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+