diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Folders/AddFolder.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Folders/AddFolder.cs
new file mode 100644
index 00000000..8b5de800
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Folders/AddFolder.cs
@@ -0,0 +1,26 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.SqlTools.Hosting.Protocol.Contracts;
+using Microsoft.SqlTools.ServiceLayer.Utility;
+
+namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts
+{
+ ///
+ /// Parameters for adding, deleting, or excluding a folder
+ ///
+ public class FolderParams : SqlProjectParams
+ {
+ ///
+ /// Path of the folder, typically relative to the .sqlproj file
+ ///
+ public string Path { get; set; }
+ }
+
+ public class AddFolderRequest
+ {
+ public static readonly RequestType Type = RequestType.Create("sqlProjects/addFolder");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Folders/DeleteFolder.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Folders/DeleteFolder.cs
new file mode 100644
index 00000000..00cca65b
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Folders/DeleteFolder.cs
@@ -0,0 +1,15 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.SqlTools.Hosting.Protocol.Contracts;
+using Microsoft.SqlTools.ServiceLayer.Utility;
+
+namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts
+{
+ public class DeleteFolderRequest
+ {
+ public static readonly RequestType Type = RequestType.Create("sqlProjects/deleteFolder");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs
index 2e81496a..0d8b212f 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs
@@ -50,6 +50,10 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlProjects
serviceHost.SetRequestHandler(DeleteSqlObjectScriptRequest.Type, HandleDeleteSqlObjectScriptRequest, isParallelProcessingSupported: false);
serviceHost.SetRequestHandler(ExcludeSqlObjectScriptRequest.Type, HandleExcludeSqlObjectScriptRequest, isParallelProcessingSupported: false);
+ // Folder functions
+ serviceHost.SetRequestHandler(AddFolderRequest.Type, HandleAddFolderRequest, isParallelProcessingSupported: false);
+ serviceHost.SetRequestHandler(DeleteFolderRequest.Type, HandleDeleteFolderRequest, isParallelProcessingSupported: false);
+
// SQLCMD variable functions
serviceHost.SetRequestHandler(AddSqlCmdVariableRequest.Type, HandleAddSqlCmdVariableRequest, isParallelProcessingSupported: false);
serviceHost.SetRequestHandler(DeleteSqlCmdVariableRequest.Type, HandleDeleteSqlCmdVariableRequest, isParallelProcessingSupported: false);
@@ -143,6 +147,20 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlProjects
#endregion
+ #region Folder functions
+
+ internal async Task HandleAddFolderRequest(FolderParams requestParams, RequestContext requestContext)
+ {
+ await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri).Folders.Add(new Folder(requestParams.Path)), requestContext);
+ }
+
+ internal async Task HandleDeleteFolderRequest(FolderParams requestParams, RequestContext requestContext)
+ {
+ await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri).Folders.Delete(requestParams.Path), requestContext);
+ }
+
+ #endregion
+
#region SQLCMD variable functions
internal async Task HandleAddSqlCmdVariableRequest(AddSqlCmdVariableParams requestParams, RequestContext requestContext)
diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs
index bb17dc9d..18e1e89d 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs
+++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs
@@ -252,6 +252,37 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SqlProjects
Assert.IsFalse(service.Projects[projectUri].DatabaseReferences.Any(x => x is SqlProjectReference), "Database references list expected to not contain the SQL Project reference");
}
+ [Test]
+ public async Task TestFolderAddDelete()
+ {
+ // Setup
+ SqlProjectsService service = new();
+ string projectUri = await service.CreateSqlProject();
+ Assert.AreEqual(0, service.Projects[projectUri].Folders.Count, "Baseline number of folders");
+
+ // Validate adding a folder
+ MockRequest requestMock = new();
+ FolderParams folderParams = new FolderParams()
+ {
+ ProjectUri = projectUri,
+ Path = "TestFolder"
+ };
+
+ await service.HandleAddFolderRequest(folderParams, requestMock.Object);
+
+ requestMock.AssertSuccess(nameof(service.HandleAddFolderRequest));
+ Assert.AreEqual(1, service.Projects[projectUri].Folders.Count, "Folder count after add");
+ Assert.IsTrue(Directory.Exists(Path.Join(Path.GetDirectoryName(projectUri), folderParams.Path)), $"Subfolder '{folderParams.Path}' expected to exist on disk");
+ Assert.IsTrue(service.Projects[projectUri].Folders.Contains(folderParams.Path), $"SqlObjectScripts expected to contain {folderParams.Path}");
+
+ // Validate deleting a folder
+ requestMock = new();
+ await service.HandleDeleteFolderRequest(folderParams, requestMock.Object);
+
+ requestMock.AssertSuccess(nameof(service.HandleDeleteFolderRequest));
+ Assert.AreEqual(0, service.Projects[projectUri].Folders.Count, "Folder count after delete");
+ }
+
[Test]
public async Task TestSqlCmdVariablesAddDelete()
{