diff --git a/.editorconfig b/.editorconfig
index 2f9b7bab56c..1138db12d78 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -277,3 +277,8 @@ dotnet_diagnostic.CA1851.severity = warning
# Override code quality rules for test projects
[{src/Bicep.Core.Samples/*.cs,src/*Test*/*.cs}]
dotnet_diagnostic.CA1851.severity = suggestion
+
+# This library is intended to be consumed by other .NET applications, so re-enable best-practice for threading
+[src/Bicep.RpcClient/**/*.cs]
+# Do not directly await a Task
+dotnet_diagnostic.CA2007.severity = warning
\ No newline at end of file
diff --git a/Bicep.sln b/Bicep.sln
index dd961b8c050..5f03a909010 100644
--- a/Bicep.sln
+++ b/Bicep.sln
@@ -113,6 +113,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bicep.McpServer.UnitTests",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bicep.Local.Rpc", "src\Bicep.Local.Rpc\Bicep.Local.Rpc.csproj", "{8585C44C-5093-4A32-AABA-9EC7B8A6118C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bicep.RpcClient", "src\Bicep.RpcClient\Bicep.RpcClient.csproj", "{EFC27293-4DBB-4D58-85E4-4B94A8F50C8B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bicep.RpcClient.Tests", "src\Bicep.RpcClient.Tests\Bicep.RpcClient.Tests.csproj", "{930BA3F9-160A-4EB6-80DC-AABFDE3BB919}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -243,6 +247,14 @@ Global
{8585C44C-5093-4A32-AABA-9EC7B8A6118C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8585C44C-5093-4A32-AABA-9EC7B8A6118C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8585C44C-5093-4A32-AABA-9EC7B8A6118C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EFC27293-4DBB-4D58-85E4-4B94A8F50C8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EFC27293-4DBB-4D58-85E4-4B94A8F50C8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EFC27293-4DBB-4D58-85E4-4B94A8F50C8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EFC27293-4DBB-4D58-85E4-4B94A8F50C8B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {930BA3F9-160A-4EB6-80DC-AABFDE3BB919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {930BA3F9-160A-4EB6-80DC-AABFDE3BB919}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {930BA3F9-160A-4EB6-80DC-AABFDE3BB919}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {930BA3F9-160A-4EB6-80DC-AABFDE3BB919}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -279,6 +291,8 @@ Global
{83AC12EE-E6B5-45FA-AADF-68AB652CC804} = {013E98B4-42CE-4009-BC62-2588ED9028CD}
{46780AD4-62F3-4AB4-9B3C-6A8AB305C260} = {013E98B4-42CE-4009-BC62-2588ED9028CD}
{8585C44C-5093-4A32-AABA-9EC7B8A6118C} = {013E98B4-42CE-4009-BC62-2588ED9028CD}
+ {EFC27293-4DBB-4D58-85E4-4B94A8F50C8B} = {013E98B4-42CE-4009-BC62-2588ED9028CD}
+ {930BA3F9-160A-4EB6-80DC-AABFDE3BB919} = {013E98B4-42CE-4009-BC62-2588ED9028CD}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {21F77282-91E7-4304-B1EF-FADFA4F39E37}
diff --git a/src/Bicep.Core.UnitTests/Utils/FileHelper.cs b/src/Bicep.Core.UnitTests/Utils/FileHelper.cs
index 2663b3b20c5..fd7c99624f2 100644
--- a/src/Bicep.Core.UnitTests/Utils/FileHelper.cs
+++ b/src/Bicep.Core.UnitTests/Utils/FileHelper.cs
@@ -29,18 +29,32 @@ public static string GetResultFilePath(TestContext testContext, string fileName,
public static string SaveResultFile(TestContext testContext, string fileName, string contents, string? testOutputPath = null, Encoding? encoding = null)
{
- var filePath = GetResultFilePath(testContext, fileName, testOutputPath);
+ var outputPath = SaveResultFiles(testContext, [new ResultFile(fileName, contents, encoding)], testOutputPath);
- if (encoding is not null)
- {
- File.WriteAllText(filePath, contents, encoding);
- }
- else
+ return Path.Combine(outputPath, fileName);
+ }
+
+ public record ResultFile(string FileName, string Contents, Encoding? Encoding = null);
+
+ public static string SaveResultFiles(TestContext testContext, ResultFile[] files, string? testOutputPath = null)
+ {
+ var outputPath = testOutputPath ?? GetUniqueTestOutputPath(testContext);
+
+ foreach (var (fileName, contents, encoding) in files)
{
- File.WriteAllText(filePath, contents);
+ var filePath = GetResultFilePath(testContext, fileName, outputPath);
+
+ if (encoding is not null)
+ {
+ File.WriteAllText(filePath, contents, encoding);
+ }
+ else
+ {
+ File.WriteAllText(filePath, contents);
+ }
}
- return filePath;
+ return outputPath;
}
public static string SaveEmbeddedResourcesWithPathPrefix(TestContext testContext, Assembly containingAssembly, string manifestFilePrefix)
diff --git a/src/Bicep.RpcClient.Tests/Bicep.RpcClient.Tests.csproj b/src/Bicep.RpcClient.Tests/Bicep.RpcClient.Tests.csproj
new file mode 100644
index 00000000000..3f867ab8316
--- /dev/null
+++ b/src/Bicep.RpcClient.Tests/Bicep.RpcClient.Tests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Bicep.RpcClient.Tests/BicepClientTests.cs b/src/Bicep.RpcClient.Tests/BicepClientTests.cs
new file mode 100644
index 00000000000..67728a458d0
--- /dev/null
+++ b/src/Bicep.RpcClient.Tests/BicepClientTests.cs
@@ -0,0 +1,193 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Runtime.InteropServices;
+using FluentAssertions;
+using Bicep.RpcClient.Helpers;
+using Bicep.Core.UnitTests.Utils;
+using Bicep.Core.FileSystem;
+using RichardSzalay.MockHttp;
+using System.Threading.Tasks;
+
+namespace Bicep.RpcClient.Tests;
+
+[TestClass]
+public class BicepClientTests
+{
+ public TestContext TestContext { get; set; } = null!;
+
+ public required IBicepClient Bicep { get; set; }
+
+ [TestInitialize]
+ public async Task TestInitialize()
+ {
+ MockHttpMessageHandler mockHandler = new();
+
+ mockHandler.When(HttpMethod.Get, "https://downloads.bicep.azure.com/releases/latest")
+ .Respond("application/json", """
+ {
+ "tag_name": "v0.0.0-dev"
+ }
+ """);
+
+ mockHandler.When(HttpMethod.Get, "https://downloads.bicep.azure.com/v0.0.0-dev/bicep-*-*")
+ .Respond(req => {
+ var cliName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "bicep.exe" : "bicep";
+ var cliPath = Path.GetFullPath(Path.Combine(typeof(BicepClientTests).Assembly.Location, $"../PublishedCli/{cliName}"));
+
+ return new(System.Net.HttpStatusCode.OK)
+ {
+ Content = new StreamContent(File.OpenRead(cliPath))
+ };
+ });
+
+ var clientFactory = new BicepClientFactory(new(mockHandler));
+
+ Bicep = await clientFactory.DownloadAndInitialize(
+ new() { InstallPath = FileHelper.GetUniqueTestOutputPath(TestContext) },
+ TestContext.CancellationTokenSource.Token);
+ }
+
+ [TestCleanup]
+ public void TestCleanup()
+ {
+ Bicep.Dispose();
+ if (Directory.Exists(TestContext.TestResultsDirectory))
+ {
+ // This test creates a new Bicep CLI install for each test run, so we need to clean it up afterwards
+ Directory.Delete(TestContext.TestResultsDirectory, true);
+ }
+ }
+
+ [TestMethod]
+ public async Task DownloadAndInitialize_validates_version_number_format()
+ {
+ var clientFactory = new BicepClientFactory(new());
+ await FluentActions.Invoking(() => clientFactory.DownloadAndInitialize(new() { BicepVersion = "v0.1.1" }, default))
+ .Should().ThrowAsync().WithMessage("Invalid Bicep version format 'v0.1.1'. Expected format: 'x.y.z' where x, y, and z are integers.");
+ }
+
+ [TestMethod]
+ public async Task DownloadAndInitialize_validates_path_existence()
+ {
+ var nonExistentPath = FileHelper.GetUniqueTestOutputPath(TestContext);
+ var clientFactory = new BicepClientFactory(new());
+ await FluentActions.Invoking(() => clientFactory.InitializeFromPath(nonExistentPath, default))
+ .Should().ThrowAsync().WithMessage($"The specified Bicep CLI path does not exist: '{nonExistentPath}'.");
+ }
+
+ [TestMethod]
+ public void BuildDownloadUrlForTag_returns_correct_url()
+ {
+ BicepInstaller.BuildDownloadUrlForTag(OSPlatform.Linux, Architecture.X64, "v0.24.24")
+ .Should().Be("https://downloads.bicep.azure.com/v0.24.24/bicep-linux-x64");
+ }
+
+ [TestMethod]
+ public async Task GetVersion_runs_successfully()
+ {
+ var result = await Bicep.GetVersion();
+
+ result.Should().MatchRegex(@"^\d+\.\d+\.\d+(-.+)?$");
+ }
+
+ [TestMethod]
+ public async Task Compile_runs_successfully()
+ {
+ var bicepFile = FileHelper.SaveResultFile(TestContext, "main.bicep", """
+ param location string
+ """);
+
+ var result = await Bicep.Compile(new(bicepFile));
+
+ result.Success.Should().BeTrue();
+ result.Contents.Should().NotBeNullOrEmpty();
+ result.Diagnostics.Should().Contain(x => x.Code == "no-unused-params");
+ }
+
+ [TestMethod]
+ public async Task CompileParams_runs_successfully()
+ {
+ var outputPath = FileHelper.SaveResultFiles(TestContext, [
+ new("main.bicep", """
+ param location string
+ """),
+ new("main.bicepparam", """
+ using 'main.bicep'
+
+ param location = 'westus'
+ """),
+ ]);
+
+ var result = await Bicep.CompileParams(new(Path.Combine(outputPath, "main.bicepparam"), []));
+
+ result.Success.Should().BeTrue();
+ result.Parameters.Should().NotBeNullOrEmpty();
+ result.Template.Should().NotBeNullOrEmpty();
+ result.Diagnostics.Should().Contain(x => x.Code == "no-unused-params");
+ }
+
+ [TestMethod]
+ public async Task Format_runs_successfully()
+ {
+ var bicepFile = FileHelper.SaveResultFile(TestContext, "main.bicep", """
+ param location string
+ """);
+
+ var result = await Bicep.Format(new(bicepFile));
+
+ result.Contents.Should().Be("""
+ param location string
+
+ """);
+ }
+
+ [TestMethod]
+ public async Task GetSnapshot_runs_successfully()
+ {
+ var outputPath = FileHelper.SaveResultFiles(TestContext, [
+ new("main.bicep", """
+ param sku string
+
+ resource storageaccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {
+ name: 'myStgAct'
+ location: resourceGroup().location
+ kind: 'StorageV2'
+ sku: {
+ name: sku
+ }
+ }
+ """),
+ new("main.bicepparam", """
+ using 'main.bicep'
+
+ param sku = 'Premium_LRS'
+ """),
+ ]);
+
+ var result = await Bicep.GetSnapshot(new(Path.Combine(outputPath, "main.bicepparam"), new(
+ TenantId: null,
+ SubscriptionId: "0910bc80-1614-479b-a3f4-07178d3ea77b",
+ ResourceGroup: "ant-test",
+ Location: "West US",
+ DeploymentName: "main"), []));
+
+ result.Snapshot.Should().Contain("/subscriptions/0910bc80-1614-479b-a3f4-07178d3ea77b/resourceGroups/ant-test/providers/Microsoft.Storage/storageAccounts/myStgAct");
+ }
+
+ [TestMethod]
+ public async Task GetMetadataResponse_runs_successfully()
+ {
+ var bicepFile = FileHelper.SaveResultFile(TestContext, "main.bicep", """
+ @export()
+ @description('A foo object')
+ type foo = {
+ bar: string
+ }
+ """);
+
+ var result = await Bicep.GetMetadata(new(bicepFile));
+
+ result.Exports[0].Description.Should().Be("A foo object");
+ }
+}
\ No newline at end of file
diff --git a/src/Bicep.RpcClient.Tests/Files/PublicApis/Azure.Bicep.RpcClient.txt b/src/Bicep.RpcClient.Tests/Files/PublicApis/Azure.Bicep.RpcClient.txt
new file mode 100644
index 00000000000..856b4658281
--- /dev/null
+++ b/src/Bicep.RpcClient.Tests/Files/PublicApis/Azure.Bicep.RpcClient.txt
@@ -0,0 +1,212 @@
+[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Azure/bicep")]
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Bicep.RpcClient.Tests")]
+[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")]
+namespace Bicep.RpcClient
+{
+ public class BicepClientConfiguration : System.IEquatable
+ {
+ public BicepClientConfiguration() { }
+ public System.Runtime.InteropServices.Architecture? Architecture { get; init; }
+ public string? BicepVersion { get; init; }
+ public string? InstallPath { get; init; }
+ public System.Runtime.InteropServices.OSPlatform? OsPlatform { get; init; }
+ public static Bicep.RpcClient.BicepClientConfiguration Default { get; }
+ }
+ public class BicepClientFactory : Bicep.RpcClient.IBicepClientFactory
+ {
+ public BicepClientFactory(System.Net.Http.HttpClient httpClient) { }
+ public System.Threading.Tasks.Task DownloadAndInitialize(Bicep.RpcClient.BicepClientConfiguration configuration, System.Threading.CancellationToken cancellationToken) { }
+ public System.Threading.Tasks.Task InitializeFromPath(string bicepCliPath, System.Threading.CancellationToken cancellationToken) { }
+ }
+ public interface IBicepClient : System.IDisposable
+ {
+ System.Threading.Tasks.Task Compile(Bicep.RpcClient.Models.CompileRequest request, System.Threading.CancellationToken cancellationToken = default);
+ System.Threading.Tasks.Task CompileParams(Bicep.RpcClient.Models.CompileParamsRequest request, System.Threading.CancellationToken cancellationToken = default);
+ System.Threading.Tasks.Task Format(Bicep.RpcClient.Models.FormatRequest request, System.Threading.CancellationToken cancellationToken = default);
+ System.Threading.Tasks.Task GetDeploymentGraph(Bicep.RpcClient.Models.GetDeploymentGraphRequest request, System.Threading.CancellationToken cancellationToken = default);
+ System.Threading.Tasks.Task GetFileReferences(Bicep.RpcClient.Models.GetFileReferencesRequest request, System.Threading.CancellationToken cancellationToken = default);
+ System.Threading.Tasks.Task GetMetadata(Bicep.RpcClient.Models.GetMetadataRequest request, System.Threading.CancellationToken cancellationToken = default);
+ System.Threading.Tasks.Task GetSnapshot(Bicep.RpcClient.Models.GetSnapshotRequest request, System.Threading.CancellationToken cancellationToken = default);
+ System.Threading.Tasks.Task GetVersion(System.Threading.CancellationToken cancellationToken = default);
+ }
+ public interface IBicepClientFactory
+ {
+ System.Threading.Tasks.Task DownloadAndInitialize(Bicep.RpcClient.BicepClientConfiguration configuration, System.Threading.CancellationToken cancellationToken = default);
+ System.Threading.Tasks.Task InitializeFromPath(string bicepCliPath, System.Threading.CancellationToken cancellationToken = default);
+ }
+}
+namespace Bicep.RpcClient.Models
+{
+ public class CompileParamsRequest : System.IEquatable
+ {
+ public CompileParamsRequest(string Path, System.Collections.Generic.Dictionary ParameterOverrides) { }
+ public System.Collections.Generic.Dictionary ParameterOverrides { get; init; }
+ public string Path { get; init; }
+ }
+ public class CompileParamsResponse : System.IEquatable
+ {
+ public CompileParamsResponse(bool Success, System.Collections.Immutable.ImmutableArray Diagnostics, string? Parameters, string? Template, string? TemplateSpecId) { }
+ public System.Collections.Immutable.ImmutableArray Diagnostics { get; init; }
+ public string? Parameters { get; init; }
+ public bool Success { get; init; }
+ public string? Template { get; init; }
+ public string? TemplateSpecId { get; init; }
+ }
+ public class CompileRequest : System.IEquatable
+ {
+ public CompileRequest(string Path) { }
+ public string Path { get; init; }
+ }
+ public class CompileResponse : System.IEquatable
+ {
+ public CompileResponse(bool Success, System.Collections.Immutable.ImmutableArray Diagnostics, string? Contents) { }
+ public string? Contents { get; init; }
+ public System.Collections.Immutable.ImmutableArray Diagnostics { get; init; }
+ public bool Success { get; init; }
+ }
+ public class DiagnosticDefinition : System.IEquatable
+ {
+ public DiagnosticDefinition(string Source, Bicep.RpcClient.Models.Range Range, string Level, string Code, string Message) { }
+ public string Code { get; init; }
+ public string Level { get; init; }
+ public string Message { get; init; }
+ public Bicep.RpcClient.Models.Range Range { get; init; }
+ public string Source { get; init; }
+ }
+ public class FormatRequest : System.IEquatable
+ {
+ public FormatRequest(string Path) { }
+ public string Path { get; init; }
+ }
+ public class FormatResponse : System.IEquatable
+ {
+ public FormatResponse(string Contents) { }
+ public string Contents { get; init; }
+ }
+ public class GetDeploymentGraphRequest : System.IEquatable
+ {
+ public GetDeploymentGraphRequest(string Path) { }
+ public string Path { get; init; }
+ }
+ public class GetDeploymentGraphResponse : System.IEquatable
+ {
+ public GetDeploymentGraphResponse(System.Collections.Immutable.ImmutableArray Nodes, System.Collections.Immutable.ImmutableArray Edges) { }
+ public System.Collections.Immutable.ImmutableArray Edges { get; init; }
+ public System.Collections.Immutable.ImmutableArray Nodes { get; init; }
+ public class Edge : System.IEquatable
+ {
+ public Edge(string Source, string Target) { }
+ public string Source { get; init; }
+ public string Target { get; init; }
+ }
+ public class Node : System.IEquatable
+ {
+ public Node(Bicep.RpcClient.Models.Range Range, string Name, string Type, bool IsExisting, string? RelativePath) { }
+ public bool IsExisting { get; init; }
+ public string Name { get; init; }
+ public Bicep.RpcClient.Models.Range Range { get; init; }
+ public string? RelativePath { get; init; }
+ public string Type { get; init; }
+ }
+ }
+ public class GetFileReferencesRequest : System.IEquatable
+ {
+ public GetFileReferencesRequest(string Path) { }
+ public string Path { get; init; }
+ }
+ public class GetFileReferencesResponse : System.IEquatable
+ {
+ public GetFileReferencesResponse(System.Collections.Immutable.ImmutableArray FilePaths) { }
+ public System.Collections.Immutable.ImmutableArray FilePaths { get; init; }
+ }
+ public class GetMetadataRequest : System.IEquatable
+ {
+ public GetMetadataRequest(string Path) { }
+ public string Path { get; init; }
+ }
+ public class GetMetadataResponse : System.IEquatable
+ {
+ public GetMetadataResponse(System.Collections.Immutable.ImmutableArray Metadata, System.Collections.Immutable.ImmutableArray Parameters, System.Collections.Immutable.ImmutableArray Outputs, System.Collections.Immutable.ImmutableArray Exports) { }
+ public System.Collections.Immutable.ImmutableArray Exports { get; init; }
+ public System.Collections.Immutable.ImmutableArray Metadata { get; init; }
+ public System.Collections.Immutable.ImmutableArray Outputs { get; init; }
+ public System.Collections.Immutable.ImmutableArray Parameters { get; init; }
+ public class ExportDefinition : System.IEquatable
+ {
+ public ExportDefinition(Bicep.RpcClient.Models.Range Range, string Name, string Kind, string? Description) { }
+ public string? Description { get; init; }
+ public string Kind { get; init; }
+ public string Name { get; init; }
+ public Bicep.RpcClient.Models.Range Range { get; init; }
+ }
+ public class MetadataDefinition : System.IEquatable
+ {
+ public MetadataDefinition(string Name, string Value) { }
+ public string Name { get; init; }
+ public string Value { get; init; }
+ }
+ public class SymbolDefinition : System.IEquatable
+ {
+ public SymbolDefinition(Bicep.RpcClient.Models.Range Range, string Name, Bicep.RpcClient.Models.GetMetadataResponse.TypeDefinition? Type, string? Description) { }
+ public string? Description { get; init; }
+ public string Name { get; init; }
+ public Bicep.RpcClient.Models.Range Range { get; init; }
+ public Bicep.RpcClient.Models.GetMetadataResponse.TypeDefinition? Type { get; init; }
+ }
+ public class TypeDefinition : System.IEquatable
+ {
+ public TypeDefinition(Bicep.RpcClient.Models.Range? Range, string Name) { }
+ public string Name { get; init; }
+ public Bicep.RpcClient.Models.Range? Range { get; init; }
+ }
+ }
+ public class GetSnapshotRequest : System.IEquatable
+ {
+ public GetSnapshotRequest(string Path, Bicep.RpcClient.Models.GetSnapshotRequest.MetadataDefinition Metadata, System.Collections.Immutable.ImmutableArray? ExternalInputs) { }
+ public System.Collections.Immutable.ImmutableArray? ExternalInputs { get; init; }
+ public Bicep.RpcClient.Models.GetSnapshotRequest.MetadataDefinition Metadata { get; init; }
+ public string Path { get; init; }
+ public class ExternalInputValue : System.IEquatable
+ {
+ public ExternalInputValue(string Kind, System.Text.Json.Nodes.JsonNode? Config, System.Text.Json.Nodes.JsonNode Value) { }
+ public System.Text.Json.Nodes.JsonNode? Config { get; init; }
+ public string Kind { get; init; }
+ public System.Text.Json.Nodes.JsonNode Value { get; init; }
+ }
+ public class MetadataDefinition : System.IEquatable
+ {
+ public MetadataDefinition(string? TenantId, string? SubscriptionId, string? ResourceGroup, string? Location, string? DeploymentName) { }
+ public string? DeploymentName { get; init; }
+ public string? Location { get; init; }
+ public string? ResourceGroup { get; init; }
+ public string? SubscriptionId { get; init; }
+ public string? TenantId { get; init; }
+ }
+ }
+ public class GetSnapshotResponse : System.IEquatable
+ {
+ public GetSnapshotResponse(string Snapshot) { }
+ public string Snapshot { get; init; }
+ }
+ public class Position : System.IEquatable
+ {
+ public Position(int Line, int Char) { }
+ public int Char { get; init; }
+ public int Line { get; init; }
+ }
+ public class Range : System.IEquatable
+ {
+ public Range(Bicep.RpcClient.Models.Position Start, Bicep.RpcClient.Models.Position End) { }
+ public Bicep.RpcClient.Models.Position End { get; init; }
+ public Bicep.RpcClient.Models.Position Start { get; init; }
+ }
+ public class VersionRequest : System.IEquatable
+ {
+ public VersionRequest() { }
+ }
+ public class VersionResponse : System.IEquatable
+ {
+ public VersionResponse(string Version) { }
+ public string Version { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/src/Bicep.RpcClient.Tests/PublicApiTests.cs b/src/Bicep.RpcClient.Tests/PublicApiTests.cs
new file mode 100644
index 00000000000..871eacddc7f
--- /dev/null
+++ b/src/Bicep.RpcClient.Tests/PublicApiTests.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Bicep.Core.UnitTests.Assertions;
+using Bicep.Core.UnitTests.Baselines;
+using FluentAssertions;
+using PublicApiGenerator;
+
+namespace Bicep.RpcClient.Tests;
+
+[TestClass]
+public class PublicApiTests
+{
+ public TestContext TestContext { get; set; } = null!;
+
+ [TestMethod]
+ [TestCategory(BaselineHelper.BaselineTestCategory)]
+ [EmbeddedFilesTestData(@"^Files\/PublicApis\/Azure.Bicep.RpcClient.txt$")]
+ public void PublicApi_should_be_up_to_date(EmbeddedFile publicApiFile)
+ {
+ // This test just asserts that the public API surface of the assembly as defined in Azure.Bicep.RpcClient.txt is up to date.
+ // This ensures that any changes to the public API are reviewed.
+ var baselineFolder = BaselineFolder.BuildOutputFolder(TestContext, publicApiFile);
+ var result = baselineFolder.GetFileOrEnsureCheckedIn(publicApiFile.FileName);
+
+ var publicApi = typeof(BicepClientConfiguration).Assembly.GeneratePublicApi();
+
+ result.WriteToOutputFolder(publicApi);
+ result.ShouldHaveExpectedValue();
+ }
+
+ [TestMethod]
+ public void Dependencies_should_be_minimal()
+ {
+ var referencedAssemblies = typeof(BicepClientConfiguration).Assembly
+ .GetReferencedAssemblies()
+ .OrderBy(x => x.Name)
+ .Select(x => x.Name);
+
+ referencedAssemblies.Except(["netstandard"]).Should().BeEquivalentTo([
+ // Be careful when adding new dependencies to the ClientTools assembly - this assembly is intentionally slim.
+ // The assembly is used in Microsoft internal tools, where dependency management is complex, so we want to avoid transitively depending on ResourceStack
+ "System.Collections.Immutable",
+ "System.Memory",
+ "System.Text.Encodings.Web",
+ "System.Text.Json",
+ "System.Threading.Tasks.Extensions"
+ ]);
+ }
+}
diff --git a/src/Bicep.RpcClient/Bicep.RpcClient.csproj b/src/Bicep.RpcClient/Bicep.RpcClient.csproj
new file mode 100644
index 00000000000..f8b9fe86ccc
--- /dev/null
+++ b/src/Bicep.RpcClient/Bicep.RpcClient.csproj
@@ -0,0 +1,28 @@
+
+
+
+ Azure.Bicep.RpcClient
+ Bicep.RpcClient
+ true
+
+ netstandard2.0
+ enable
+ Azure;ResourceManager;ARM;Deployments;Templates;Bicep
+
+ Bicep RPC Client. Available to facilitate installation and communication with the Bicep CLI via JSONRPC.
+ The Bicep team has made this NuGet package publicly available on nuget.org. While it is public, it is not a supported package. Any dependency you take on this package will be done at your own risk and we reserve the right to push breaking changes to this package at any time.
+
+
+
+
+
+
+
+
+
+
+ <_Parameter1>Bicep.RpcClient.Tests
+
+
+
+
\ No newline at end of file
diff --git a/src/Bicep.RpcClient/BicepClient.cs b/src/Bicep.RpcClient/BicepClient.cs
new file mode 100644
index 00000000000..15282a40287
--- /dev/null
+++ b/src/Bicep.RpcClient/BicepClient.cs
@@ -0,0 +1,138 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Pipes;
+using System.Threading;
+using Bicep.RpcClient.JsonRpc;
+using Bicep.RpcClient.Models;
+
+namespace Bicep.RpcClient;
+
+internal class BicepClient : IBicepClient
+{
+ private readonly Process cliProcess;
+ private readonly JsonRpcClient jsonRpcClient;
+ private readonly Task backgroundTask;
+ private readonly CancellationTokenSource cts;
+
+ private BicepClient(NamedPipeServerStream pipeStream, Process cliProcess, JsonRpcClient jsonRpcClient, CancellationTokenSource cts)
+ {
+ this.cliProcess = cliProcess;
+ this.jsonRpcClient = jsonRpcClient;
+ this.backgroundTask = CreateBackgroundTask(pipeStream, cliProcess, jsonRpcClient, cts.Token);
+ this.cts = cts;
+ }
+
+ ///
+ /// Initializes the Bicep CLI by starting the process and establishing a JSON-RPC connection.
+ ///
+ public static async Task Initialize(string bicepCliPath, CancellationToken cancellationToken)
+ {
+ if (!File.Exists(bicepCliPath))
+ {
+ throw new FileNotFoundException($"The specified Bicep CLI path does not exist: '{bicepCliPath}'.");
+ }
+
+ var pipeName = Guid.NewGuid().ToString();
+ var pipeStream = new NamedPipeServerStream(pipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
+
+ var psi = new ProcessStartInfo
+ {
+ FileName = bicepCliPath,
+ Arguments = $"jsonrpc --pipe {pipeName}",
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardError = true,
+ RedirectStandardOutput = true
+ };
+
+ var cliProcess = Process.Start(psi)
+ ?? throw new InvalidOperationException("Failed to start Bicep CLI process");
+
+ await pipeStream.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false);
+ var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ var client = new JsonRpcClient(pipeStream, pipeStream);
+
+ return new BicepClient(pipeStream, cliProcess, client, cts);
+ }
+
+ private static Task CreateBackgroundTask(NamedPipeServerStream pipeStream, Process cliProcess, JsonRpcClient jsonRpcClient, CancellationToken cancellationToken)
+ => Task.Run(async () =>
+ {
+ try
+ {
+ await jsonRpcClient.ReadLoop(cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when disposing
+ }
+ finally
+ {
+ pipeStream.Dispose();
+ cliProcess.Dispose();
+ }
+ }, cancellationToken);
+
+ ///
+ public Task Compile(CompileRequest request, CancellationToken cancellationToken)
+ => jsonRpcClient.SendRequest("bicep/compile", request, cancellationToken);
+
+ ///
+ public Task CompileParams(CompileParamsRequest request, CancellationToken cancellationToken)
+ => jsonRpcClient.SendRequest("bicep/compileParams", request, cancellationToken);
+
+ ///
+ public async Task Format(FormatRequest request, CancellationToken cancellationToken)
+ {
+ await EnsureMinimumVersion("0.37.1", nameof(Format), cancellationToken).ConfigureAwait(false);
+ return await jsonRpcClient.SendRequest("bicep/format", request, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public Task GetDeploymentGraph(GetDeploymentGraphRequest request, CancellationToken cancellationToken)
+ => jsonRpcClient.SendRequest("bicep/getDeploymentGraph", request, cancellationToken);
+
+ ///
+ public Task GetFileReferences(GetFileReferencesRequest request, CancellationToken cancellationToken)
+ => jsonRpcClient.SendRequest("bicep/getFileReferences", request, cancellationToken);
+
+ ///
+ public Task GetMetadata(GetMetadataRequest request, CancellationToken cancellationToken)
+ => jsonRpcClient.SendRequest("bicep/getMetadata", request, cancellationToken);
+
+ ///
+ public async Task GetSnapshot(GetSnapshotRequest request, CancellationToken cancellationToken)
+ {
+ await EnsureMinimumVersion("0.36.1", nameof(GetSnapshot), cancellationToken).ConfigureAwait(false);
+ return await jsonRpcClient.SendRequest("bicep/getSnapshot", request, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task GetVersion(CancellationToken cancellationToken)
+ {
+ var response = await jsonRpcClient.SendRequest("bicep/version", new(), cancellationToken).ConfigureAwait(false);
+ return response.Version;
+ }
+
+ private async Task EnsureMinimumVersion(string requiredVersion, string operationName, CancellationToken cancellationToken)
+ {
+ var actualVersion = await GetVersion(cancellationToken).ConfigureAwait(false);
+ if (Version.Parse(actualVersion) < Version.Parse(requiredVersion))
+ {
+ throw new InvalidOperationException($"Operation '{operationName}' requires Bicep CLI version '{requiredVersion}' or later, whereas '{actualVersion}' is currently installed.");
+ }
+ }
+
+ public void Dispose()
+ {
+ cts.Cancel();
+ if (!cliProcess.HasExited)
+ {
+ cliProcess.WaitForExit();
+ }
+ }
+}
diff --git a/src/Bicep.RpcClient/BicepClientConfiguration.cs b/src/Bicep.RpcClient/BicepClientConfiguration.cs
new file mode 100644
index 00000000000..0ece9e60de0
--- /dev/null
+++ b/src/Bicep.RpcClient/BicepClientConfiguration.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Runtime.InteropServices;
+
+namespace Bicep.RpcClient;
+
+public record BicepClientConfiguration
+{
+ public string? InstallPath { get; init; }
+
+ public OSPlatform? OsPlatform { get; init; }
+
+ public Architecture? Architecture { get; init; }
+
+ public string? BicepVersion { get; init; }
+
+ public static BicepClientConfiguration Default
+ => new();
+}
\ No newline at end of file
diff --git a/src/Bicep.RpcClient/BicepClientFactory.cs b/src/Bicep.RpcClient/BicepClientFactory.cs
new file mode 100644
index 00000000000..3463e10ce79
--- /dev/null
+++ b/src/Bicep.RpcClient/BicepClientFactory.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Bicep.RpcClient.Helpers;
+
+namespace Bicep.RpcClient;
+
+public class BicepClientFactory(HttpClient httpClient) : IBicepClientFactory
+{
+ private static readonly Regex VersionRegex = new(@"^\d+\.\d+\.\d+$");
+ private static readonly string DefaultInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".bicep", "bin");
+ private static readonly OSPlatform CurrentOsPlatform = BicepInstaller.DetectCurrentOSPlatform();
+ private static readonly Architecture CurrentArchitecture = RuntimeInformation.OSArchitecture;
+
+ public async Task DownloadAndInitialize(BicepClientConfiguration configuration, CancellationToken cancellationToken)
+ {
+ if (configuration.BicepVersion is { } version && !VersionRegex.IsMatch(version))
+ {
+ throw new ArgumentException($"Invalid Bicep version format '{version}'. Expected format: 'x.y.z' where x, y, and z are integers.");
+ }
+
+ var osPlatform = configuration.OsPlatform ?? CurrentOsPlatform;
+ var architecture = configuration.Architecture ?? CurrentArchitecture;
+ var baseDownloadPath = configuration.InstallPath ?? DefaultInstallPath;
+ var versionTag = configuration.BicepVersion is { } ?
+ $"v{configuration.BicepVersion}" :
+ await BicepInstaller.GetLatestBicepVersion(httpClient, cancellationToken).ConfigureAwait(false);
+
+ var bicepCliName = osPlatform.Equals(OSPlatform.Windows) ? "bicep.exe" : "bicep";
+ // E.g. ~/.bicep/bin/v0.37.4/bicep
+ var bicepCliPath = Path.Combine(baseDownloadPath, versionTag, bicepCliName);
+
+ if (!File.Exists(bicepCliPath))
+ {
+ await BicepInstaller.DownloadAndInstallBicepCliAsync(
+ httpClient: httpClient,
+ bicepCliPath: bicepCliPath,
+ targetOSPlatform: osPlatform,
+ targetOSArchitecture: architecture,
+ versionTag: versionTag,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ return await InitializeFromPath(bicepCliPath, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task InitializeFromPath(string bicepCliPath, CancellationToken cancellationToken)
+ {
+ return await BicepClient.Initialize(bicepCliPath, cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/src/Bicep.RpcClient/Helpers/BicepInstaller.cs b/src/Bicep.RpcClient/Helpers/BicepInstaller.cs
new file mode 100644
index 00000000000..fb89136d929
--- /dev/null
+++ b/src/Bicep.RpcClient/Helpers/BicepInstaller.cs
@@ -0,0 +1,141 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Bicep.RpcClient.Helpers;
+
+internal static class BicepInstaller
+{
+ private const string DownloadBaseUrl = "https://downloads.bicep.azure.com";
+ private const string LatestReleaseUrl = $"{DownloadBaseUrl}/releases/latest";
+ private const string TagNameProperty = "tag_name";
+
+ public static async Task DownloadAndInstallBicepCliAsync(
+ HttpClient httpClient,
+ string bicepCliPath,
+ OSPlatform targetOSPlatform,
+ Architecture targetOSArchitecture,
+ string versionTag,
+ CancellationToken cancellationToken)
+ {
+ var downloadUrl = BuildDownloadUrlForTag(
+ targetOSPlatform: targetOSPlatform,
+ targetOSArchitecture: targetOSArchitecture,
+ versionTag: versionTag);
+
+ if (Path.GetDirectoryName(bicepCliPath) is { } parentPath && !Directory.Exists(parentPath))
+ {
+ Directory.CreateDirectory(parentPath);
+ }
+
+ var response = await httpClient.GetAsync(downloadUrl, cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ response.EnsureSuccessStatusCode();
+
+ using var fileStream = File.Create(path: bicepCliPath);
+ await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
+
+ // Set executable permissions on non-Windows platforms
+ if (!targetOSPlatform.Equals(OSPlatform.Windows))
+ {
+ SetExecutablePermissions(bicepCliPath, cancellationToken);
+ }
+ }
+
+ public static async Task GetLatestBicepVersion(
+ HttpClient httpClient,
+ CancellationToken cancellationToken)
+ {
+ var response = await httpClient.GetAsync(LatestReleaseUrl, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ var json = JsonSerializer.Deserialize(responseBody);
+
+ return json.GetProperty(TagNameProperty).GetString()
+ ?? throw new InvalidOperationException("Failed to determine the latest Bicep CLI version from the releases API.");
+ }
+
+ internal static string BuildDownloadUrlForTag(
+ OSPlatform targetOSPlatform,
+ Architecture targetOSArchitecture,
+ string versionTag)
+ {
+ var osPlatformArchitectureMap = new Dictionary<(OSPlatform, Architecture), string>
+ {
+ { (OSPlatform.Windows, Architecture.X64), "bicep-win-x64.exe" },
+ { (OSPlatform.Windows, Architecture.Arm64), "bicep-win-arm64.exe" },
+ { (OSPlatform.Linux, Architecture.X64), "bicep-linux-x64" },
+ { (OSPlatform.Linux, Architecture.Arm64), "bicep-linux-arm64" },
+ { (OSPlatform.OSX, Architecture.X64), "bicep-osx-x64" },
+ { (OSPlatform.OSX, Architecture.Arm64), "bicep-osx-arm64" }
+ };
+
+ if (osPlatformArchitectureMap.TryGetValue((targetOSPlatform, targetOSArchitecture), out var bicepCliArtifactName))
+ {
+ return $"{DownloadBaseUrl}/{versionTag}/{bicepCliArtifactName}";
+ }
+
+ throw new PlatformNotSupportedException(
+ $"The Bicep CLI is not available for the specified platform '{targetOSPlatform}' and architecture '{targetOSArchitecture}'. " +
+ "Supported combinations include Windows (x64, Arm64), Linux (x64, Arm64), and macOS (x64, Arm64).");
+ }
+
+ private static void SetExecutablePermissions(
+ string filePath,
+ CancellationToken cancellationToken)
+ {
+ var chmod = new System.Diagnostics.Process
+ {
+ StartInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = "chmod",
+ Arguments = $"+x \"{filePath}\"",
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ }
+ };
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ chmod.Start();
+ chmod.WaitForExit();
+
+ if (chmod.ExitCode != 0)
+ {
+ throw new UnauthorizedAccessException(
+ $"Failed to set executable permissions for the file at '{filePath}'. " +
+ $"The 'chmod' process exited with code {chmod.ExitCode}. Ensure you have sufficient permissions.");
+ }
+ }
+
+ public static OSPlatform DetectCurrentOSPlatform()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return OSPlatform.Windows;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ return OSPlatform.Linux;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return OSPlatform.OSX;
+ }
+ else
+ {
+ throw new PlatformNotSupportedException(
+ "The current operating system platform is not supported. Supported platforms are Windows, Linux, and macOS.");
+ }
+ }
+}
diff --git a/src/Bicep.RpcClient/IBicepClient.cs b/src/Bicep.RpcClient/IBicepClient.cs
new file mode 100644
index 00000000000..b132fe5370e
--- /dev/null
+++ b/src/Bicep.RpcClient/IBicepClient.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Threading;
+using Bicep.RpcClient.Models;
+
+namespace Bicep.RpcClient;
+
+public interface IBicepClient : IDisposable
+{
+ ///
+ /// Compiles a Bicep file into an ARM template.
+ ///
+ Task Compile(CompileRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Compiles a Bicepparam file into an ARM parameters file.
+ ///
+ Task CompileParams(CompileParamsRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Formats a Bicep file.
+ ///
+ Task Format(FormatRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Returns the deployment graph for a Bicep file.
+ ///
+ Task GetDeploymentGraph(GetDeploymentGraphRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Returns file references for a Bicep file.
+ ///
+ Task GetFileReferences(GetFileReferencesRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Returns metadata for a Bicep file.
+ ///
+ Task GetMetadata(GetMetadataRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets a snapshot of a Bicep parameters file.
+ ///
+ Task GetSnapshot(GetSnapshotRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves the version of the Bicep CLI.
+ ///
+ Task GetVersion(CancellationToken cancellationToken = default);
+}
diff --git a/src/Bicep.RpcClient/IBicepClientFactory.cs b/src/Bicep.RpcClient/IBicepClientFactory.cs
new file mode 100644
index 00000000000..f337e42c5ff
--- /dev/null
+++ b/src/Bicep.RpcClient/IBicepClientFactory.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Bicep.RpcClient;
+
+public interface IBicepClientFactory
+{
+ ///
+ /// Initializes a Bicep client by downloading the specified version of the Bicep CLI.
+ ///
+ /// The configuration for the Bicep client.
+ /// The cancellation token.
+ Task DownloadAndInitialize(BicepClientConfiguration configuration, CancellationToken cancellationToken = default);
+
+ ///
+ /// Initializes a Bicep client from a file system path.
+ ///
+ /// The file system path to the Bicep CLI.
+ /// The cancellation token.
+ Task InitializeFromPath(string bicepCliPath, CancellationToken cancellationToken = default);
+}
diff --git a/src/Bicep.RpcClient/JsonRpc/JsonRpcClient.cs b/src/Bicep.RpcClient/JsonRpc/JsonRpcClient.cs
new file mode 100644
index 00000000000..49e0bf1150e
--- /dev/null
+++ b/src/Bicep.RpcClient/JsonRpc/JsonRpcClient.cs
@@ -0,0 +1,181 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Concurrent;
+using System.Net.Security;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+
+namespace Bicep.RpcClient.JsonRpc;
+
+internal class JsonRpcClient(Stream reader, Stream writer) : IDisposable
+{
+ private record JsonRpcRequest(
+ string Jsonrpc,
+ string Method,
+ T Params,
+ int Id);
+
+ private record MinimalJsonRpcResponse(
+ string Jsonrpc,
+ int Id);
+
+ private record JsonRpcResponse(
+ string Jsonrpc,
+ T? Result,
+ JsonRpcError? Error,
+ int Id);
+
+ private record JsonRpcError(
+ int Code,
+ string Message,
+ JsonNode? Data);
+
+ private readonly byte[] terminator = "\r\n\r\n"u8.ToArray();
+ private int nextId = 0;
+ private readonly SemaphoreSlim writeSemaphore = new(1, 1);
+ private readonly ConcurrentDictionary> pendingResponses = new();
+
+ private readonly JsonSerializerOptions jsonSerializerOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ };
+
+ public async Task SendRequest(string method, TRequest request, CancellationToken cancellationToken)
+ {
+ var currentId = Interlocked.Increment(ref nextId);
+
+ var jsonRpcRequest = new JsonRpcRequest(Jsonrpc: "2.0", Method: method, Params: request, Id: currentId);
+ var requestContent = JsonSerializer.Serialize(jsonRpcRequest, jsonSerializerOptions);
+ var requestLength = Encoding.UTF8.GetByteCount(requestContent);
+ var rawRequest = $"Content-Length: {requestLength}\r\n\r\n{requestContent}";
+ var requestBytes = Encoding.UTF8.GetBytes(rawRequest);
+
+ await writeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await writer.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken).ConfigureAwait(false);
+ await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ writeSemaphore.Release();
+ }
+
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ if (!pendingResponses.TryAdd(currentId, tcs))
+ {
+ throw new InvalidOperationException($"A request with ID {currentId} is already pending.");
+ }
+
+ var responseContent = await tcs.Task.ConfigureAwait(false);
+ var jsonRpcResponse = JsonSerializer.Deserialize>(responseContent, jsonSerializerOptions)
+ ?? throw new InvalidOperationException("Failed to deserialize JSON-RPC response");
+
+ if (jsonRpcResponse.Result is null)
+ {
+ var error = jsonRpcResponse.Error ?? throw new InvalidDataException("Failed to retrieve JSONRPC error");
+ throw new InvalidOperationException(error.Message);
+ }
+
+ return jsonRpcResponse.Result;
+ }
+
+ public async Task ReadLoop(CancellationToken cancellationToken)
+ {
+ while (true)
+ {
+ try
+ {
+ var message = await ReadMessage(cancellationToken).ConfigureAwait(false);
+
+ var response = JsonSerializer.Deserialize(message, jsonSerializerOptions)
+ ?? throw new InvalidOperationException("Failed to deserialize JSON-RPC response");
+
+ if (pendingResponses.TryRemove(response.Id, out var tcs))
+ {
+ tcs.SetResult(message);
+ }
+ }
+ catch (Exception) when (cancellationToken.IsCancellationRequested)
+ {
+ reader.Dispose();
+ writer.Dispose();
+ break;
+ }
+ }
+ }
+
+ private async Task ReadUntilTerminator(CancellationToken cancellationToken)
+ {
+ using var outputStream = new MemoryStream();
+ var patternIndex = 0;
+ var byteBuffer = new byte[1];
+
+ while (true)
+ {
+ await ReadExactly(byteBuffer, byteBuffer.Length, cancellationToken).ConfigureAwait(false);
+
+ await outputStream.WriteAsync(byteBuffer, 0, byteBuffer.Length, cancellationToken).ConfigureAwait(false);
+ patternIndex = terminator[patternIndex] == byteBuffer[0] ? patternIndex + 1 : 0;
+ if (patternIndex == terminator.Length)
+ {
+ outputStream.Position = 0;
+ outputStream.SetLength(outputStream.Length - terminator.Length);
+ // return stream as string
+ return Encoding.UTF8.GetString(outputStream.ToArray());
+ }
+ }
+ }
+
+ private async Task ReadContent(int length, CancellationToken cancellationToken)
+ {
+ var byteBuffer = new byte[length];
+ await ReadExactly(byteBuffer, length, cancellationToken).ConfigureAwait(false);
+
+ return Encoding.UTF8.GetString(byteBuffer);
+ }
+
+ private async Task ReadMessage(CancellationToken cancellationToken)
+ {
+ var header = await ReadUntilTerminator(cancellationToken).ConfigureAwait(false);
+ var parsed = header.Split(':').Select(x => x.Trim()).ToArray();
+
+ if (parsed.Length != 2 ||
+ !parsed[0].Equals("Content-Length", StringComparison.OrdinalIgnoreCase) ||
+ !int.TryParse(parsed[1], out var contentLength) ||
+ contentLength <= 0)
+ {
+ throw new InvalidOperationException($"Invalid header: {header}");
+ }
+
+ return await ReadContent(contentLength, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async ValueTask ReadExactly(byte[] buffer, int minimumBytes, CancellationToken cancellationToken)
+ {
+ var totalRead = 0;
+ while (totalRead < minimumBytes)
+ {
+ var read = await reader.ReadAsync(buffer, totalRead, minimumBytes - totalRead, cancellationToken).ConfigureAwait(false);
+ if (read == 0)
+ {
+ throw new EndOfStreamException("Stream closed before reading expected number of bytes");
+ }
+
+ totalRead += read;
+ }
+ }
+
+ public void Dispose()
+ {
+ writer.Dispose();
+ reader.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/src/Bicep.RpcClient/Models/Models.cs b/src/Bicep.RpcClient/Models/Models.cs
new file mode 100644
index 00000000000..767ff04af0d
--- /dev/null
+++ b/src/Bicep.RpcClient/Models/Models.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Immutable;
+using System.Text.Json.Nodes;
+
+namespace Bicep.RpcClient.Models;
+
+// Data models for all RPC requests and responses.
+// These models should be kept in sync with the corresponding models in the Bicep CLI, but must also be backwards-compatible,
+// as the JSONRPC client is able to communicate with multiple versions of the Bicep CLI.
+
+public record Position(
+ int Line,
+ int Char);
+
+public record Range(
+ Position Start,
+ Position End);
+
+public record VersionRequest();
+
+public record VersionResponse(
+ string Version);
+
+public record CompileRequest(
+ string Path);
+
+public record CompileResponse(
+ bool Success,
+ ImmutableArray Diagnostics,
+ string? Contents);
+
+public record CompileParamsRequest(
+ string Path,
+ Dictionary ParameterOverrides);
+
+public record CompileParamsResponse(
+ bool Success,
+ ImmutableArray Diagnostics,
+ string? Parameters,
+ string? Template,
+ string? TemplateSpecId);
+
+public record DiagnosticDefinition(
+ string Source,
+ Range Range,
+ string Level,
+ string Code,
+ string Message);
+
+public record GetFileReferencesRequest(
+ string Path);
+
+public record GetFileReferencesResponse(
+ ImmutableArray FilePaths);
+
+public record GetMetadataRequest(
+ string Path);
+
+public record GetSnapshotRequest(
+ string Path,
+ GetSnapshotRequest.MetadataDefinition Metadata,
+ ImmutableArray? ExternalInputs)
+{
+ public record MetadataDefinition(
+ string? TenantId,
+ string? SubscriptionId,
+ string? ResourceGroup,
+ string? Location,
+ string? DeploymentName);
+
+ public record ExternalInputValue(
+ string Kind,
+ JsonNode? Config,
+ JsonNode Value);
+}
+
+public record GetSnapshotResponse(
+ string Snapshot);
+
+public record GetMetadataResponse(
+ ImmutableArray Metadata,
+ ImmutableArray Parameters,
+ ImmutableArray Outputs,
+ ImmutableArray Exports)
+{
+ public record SymbolDefinition(
+ Range Range,
+ string Name,
+ TypeDefinition? Type,
+ string? Description);
+
+ public record ExportDefinition(
+ Range Range,
+ string Name,
+ string Kind,
+ string? Description);
+
+ public record TypeDefinition(
+ Range? Range,
+ string Name);
+
+ public record MetadataDefinition(
+ string Name,
+ string Value);
+}
+
+public record GetDeploymentGraphRequest(
+ string Path);
+
+public record GetDeploymentGraphResponse(
+ ImmutableArray Nodes,
+ ImmutableArray Edges)
+{
+ public record Node(
+ Range Range,
+ string Name,
+ string Type,
+ bool IsExisting,
+ string? RelativePath);
+
+ public record Edge(
+ string Source,
+ string Target);
+}
+
+public record FormatRequest(
+ string Path);
+
+public record FormatResponse(
+ string Contents);
\ No newline at end of file
diff --git a/src/Bicep.RpcClient/Polyfills/IsExternalInit.cs b/src/Bicep.RpcClient/Polyfills/IsExternalInit.cs
new file mode 100644
index 00000000000..2e9254c3716
--- /dev/null
+++ b/src/Bicep.RpcClient/Polyfills/IsExternalInit.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#if !NET5_0_OR_GREATER
+namespace System.Runtime.CompilerServices
+{
+ ///
+ /// Class required for C# 9.0 record types.
+ ///
+ internal static class IsExternalInit
+ {
+ }
+}
+#endif
diff --git a/src/Bicep.RpcClient/local-tpn.txt b/src/Bicep.RpcClient/local-tpn.txt
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index bbe9188861e..db4961d4790 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -5,14 +5,14 @@
-
-
-
+
+
+
-
-
+
+
@@ -82,6 +82,7 @@
+
@@ -96,6 +97,7 @@
+
@@ -105,4 +107,4 @@
-
+
\ No newline at end of file