diff --git a/Packages.props b/Packages.props
index 576642f1..ea0ac32b 100644
--- a/Packages.props
+++ b/Packages.props
@@ -20,6 +20,7 @@
+
diff --git a/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8056.0-alpha.nupkg b/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8056.0-alpha.nupkg
new file mode 100644
index 00000000..467c2e00
Binary files /dev/null and b/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8056.0-alpha.nupkg differ
diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs
index cbd556db..aa46a8d9 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs
@@ -41,6 +41,7 @@ using Microsoft.SqlTools.ServiceLayer.AzureBlob;
using Microsoft.SqlTools.ServiceLayer.ExecutionPlan;
using Microsoft.SqlTools.ServiceLayer.ObjectManagement;
using System.IO;
+using Microsoft.SqlTools.ServiceLayer.SqlProjects;
namespace Microsoft.SqlTools.ServiceLayer
{
@@ -179,6 +180,9 @@ namespace Microsoft.SqlTools.ServiceLayer
ObjectManagementService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(ObjectManagementService.Instance);
+ SqlProjectsService.Instance.InitializeService(serviceHost);
+ serviceProvider.RegisterSingleService(SqlProjectsService.Instance);
+
serviceHost.InitializeRequestHandlers();
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj
index bee3cbc5..e9eca682 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj
+++ b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj
@@ -50,6 +50,7 @@
+
diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/CloseSqlProject.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/CloseSqlProject.cs
new file mode 100644
index 00000000..2b08003d
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/CloseSqlProject.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 CloseSqlProjectRequest
+ {
+ public static readonly RequestType Type = RequestType.Create("sqlprojects/closeProject");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/NewSqlProject.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/NewSqlProject.cs
new file mode 100644
index 00000000..069e3c7f
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/NewSqlProject.cs
@@ -0,0 +1,39 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.SqlServer.Dac.Projects;
+using Microsoft.SqlTools.Hosting.Protocol.Contracts;
+using Microsoft.SqlTools.ServiceLayer.Utility;
+
+namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts
+{
+ ///
+ /// Parameters for creating a new SQL Project
+ ///
+ public class NewSqlProjectParams : SqlProjectParams
+ {
+ ///
+ /// Type of SQL Project: SDK-style or Legacy
+ ///
+ public ProjectType SqlProjectType { get; set; }
+
+ ///
+ /// Database schema provider for the project, in the format
+ /// "Microsoft.Data.Tools.Schema.Sql.SqlXYZDatabaseSchemaProvider".
+ /// Case sensitive.
+ ///
+ public string? DatabaseSchemaProvider { get; set; }
+
+ ///
+ /// Version of the Microsoft.Build.Sql SDK for the project, if overriding the default
+ ///
+ public string? BuildSdkVersion { get; set; }
+ }
+
+ public class NewSqlProjectRequest
+ {
+ public static readonly RequestType Type = RequestType.Create("sqlprojects/newProject");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/OpenSqlProject.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/OpenSqlProject.cs
new file mode 100644
index 00000000..33c77aea
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/OpenSqlProject.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 OpenSqlProjectRequest
+ {
+ public static readonly RequestType Type = RequestType.Create("sqlprojects/openProject");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/AddSqlObjectScript.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/AddSqlObjectScript.cs
new file mode 100644
index 00000000..5f6ea9df
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/AddSqlObjectScript.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 AddSqlObjectScriptRequest
+ {
+ public static readonly RequestType Type = RequestType.Create("sqlprojects/addSqlObjectScript");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/DeleteSqlObjectScript.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/DeleteSqlObjectScript.cs
new file mode 100644
index 00000000..409e0422
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/DeleteSqlObjectScript.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 DeleteSqlObjectScriptRequest
+ {
+ public static readonly RequestType Type = RequestType.Create("sqlprojects/deleteSqlObjectScript");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/ExcludeSqlObjectScript.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/ExcludeSqlObjectScript.cs
new file mode 100644
index 00000000..d75aa75d
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/ExcludeSqlObjectScript.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 ExcludeSqlObjectScriptRequest
+ {
+ public static readonly RequestType Type = RequestType.Create("sqlprojects/excludeSqlObjectScript");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlProjectParams.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlProjectParams.cs
new file mode 100644
index 00000000..22a60e2d
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlProjectParams.cs
@@ -0,0 +1,31 @@
+//
+// 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.Utility;
+
+namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts
+{
+ ///
+ /// Parameters for a generic SQL Project operation
+ ///
+ public class SqlProjectParams : GeneralRequestDetails
+ {
+ ///
+ /// Absolute path of the project, including .sqlproj
+ ///
+ public string ProjectUri { get; set; }
+ }
+
+ ///
+ /// Parameters for a SQL Project operation that targets a script
+ ///
+ public class SqlProjectScriptParams : SqlProjectParams
+ {
+ ///
+ /// Path of the script, including .sql, relative to the .sqlproj
+ ///
+ public string Path { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs
new file mode 100644
index 00000000..4e6b661c
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs
@@ -0,0 +1,140 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+using System;
+using System.Collections.Concurrent;
+using System.Threading.Tasks;
+using Microsoft.SqlServer.Dac.Projects;
+using Microsoft.SqlTools.Hosting.Protocol;
+using Microsoft.SqlTools.ServiceLayer.Hosting;
+using Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts;
+using Microsoft.SqlTools.ServiceLayer.Utility;
+
+namespace Microsoft.SqlTools.ServiceLayer.SqlProjects
+{
+ ///
+ /// Main class for SqlProjects service
+ ///
+ class SqlProjectsService
+ {
+ private static readonly Lazy instance = new Lazy(() => new SqlProjectsService());
+
+ ///
+ /// Gets the singleton instance object
+ ///
+ public static SqlProjectsService Instance => instance.Value;
+
+ private Lazy> projects = new Lazy>(() => new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase));
+
+ ///
+ /// that maps Project URI to Project
+ ///
+ public ConcurrentDictionary Projects => projects.Value;
+
+ ///
+ /// Initializes the service instance
+ ///
+ ///
+ public void InitializeService(ServiceHost serviceHost)
+ {
+ // Project-level functions
+ serviceHost.SetRequestHandler(OpenSqlProjectRequest.Type, HandleOpenSqlProjectRequest, isParallelProcessingSupported: true);
+ serviceHost.SetRequestHandler(CloseSqlProjectRequest.Type, HandleCloseSqlProjectRequest, isParallelProcessingSupported: true);
+ serviceHost.SetRequestHandler(NewSqlProjectRequest.Type, HandleNewSqlProjectRequest, isParallelProcessingSupported: true);
+
+ // SQL object script calls
+ serviceHost.SetRequestHandler(AddSqlObjectScriptRequest.Type, HandleAddSqlObjectScriptRequest, isParallelProcessingSupported: false);
+ serviceHost.SetRequestHandler(DeleteSqlObjectScriptRequest.Type, HandleDeleteSqlObjectScriptRequest, isParallelProcessingSupported: false);
+ serviceHost.SetRequestHandler(ExcludeSqlObjectScriptRequest.Type, HandleExcludeSqlObjectScriptRequest, isParallelProcessingSupported: false);
+ }
+
+ #region Handlers
+
+ #region Project-level functions
+
+ internal async Task HandleOpenSqlProjectRequest(SqlProjectParams requestParams, RequestContext requestContext)
+ {
+ await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri), requestContext);
+ }
+
+ internal async Task HandleCloseSqlProjectRequest(SqlProjectParams requestParams, RequestContext requestContext)
+ {
+ await RunWithErrorHandling(() => Projects.TryRemove(requestParams.ProjectUri, out _), requestContext);
+ }
+
+ internal async Task HandleNewSqlProjectRequest(NewSqlProjectParams requestParams, RequestContext requestContext)
+ {
+ await RunWithErrorHandling(async () =>
+ {
+ await SqlProject.CreateProjectAsync(requestParams.ProjectUri, requestParams.SqlProjectType, requestParams.DatabaseSchemaProvider);
+ GetProject(requestParams.ProjectUri); // load into the cache
+
+ }, requestContext);
+ }
+
+ #endregion
+
+ #region Sql object script calls
+
+ internal async Task HandleAddSqlObjectScriptRequest(SqlProjectScriptParams requestParams, RequestContext requestContext)
+ {
+ await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri).SqlObjectScripts.Add(new SqlObjectScript(requestParams.Path)), requestContext);
+ }
+
+ internal async Task HandleDeleteSqlObjectScriptRequest(SqlProjectScriptParams requestParams, RequestContext requestContext)
+ {
+ await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri).SqlObjectScripts.Delete(requestParams.Path), requestContext);
+ }
+
+ internal async Task HandleExcludeSqlObjectScriptRequest(SqlProjectScriptParams requestParams, RequestContext requestContext)
+ {
+ await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri).SqlObjectScripts.Exclude(requestParams.Path), requestContext);
+ }
+
+ #endregion
+
+ #endregion
+
+ #region Helper methods
+
+ private async Task RunWithErrorHandling(Action action, RequestContext requestContext)
+ {
+ await RunWithErrorHandling(async () => await Task.Run(action), requestContext);
+ }
+
+ private async Task RunWithErrorHandling(Func action, RequestContext requestContext)
+ {
+ try
+ {
+ await action();
+
+ await requestContext.SendResult(new ResultStatus()
+ {
+ Success = true,
+ ErrorMessage = null
+ });
+ }
+ catch (Exception ex)
+ {
+ await requestContext.SendResult(new ResultStatus()
+ {
+ Success = false,
+ ErrorMessage = ex.Message
+ });
+ }
+ }
+
+ private SqlProject GetProject(string projectUri)
+ {
+ if (!Projects.ContainsKey(projectUri))
+ {
+ Projects[projectUri] = new SqlProject(projectUri);
+ }
+
+ return Projects[projectUri];
+ }
+
+ #endregion
+ }
+}
diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs
new file mode 100644
index 00000000..00cad9a2
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs
@@ -0,0 +1,182 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.SqlServer.Dac.Projects;
+using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility;
+using Microsoft.SqlTools.ServiceLayer.SqlProjects;
+using Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts;
+using Microsoft.SqlTools.ServiceLayer.Test.Common.RequestContextMocking;
+using Microsoft.SqlTools.ServiceLayer.Utility;
+using NUnit.Framework;
+
+namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SqlProjects
+{
+ public class SqlProjectsServiceTests : TestBase
+ {
+ [Test]
+ public async Task TestErrorDuringExecution()
+ {
+ SqlProjectsService service = new();
+ string projectUri = await service.CreateSqlProject(); // validates result.Success == true
+
+ // Validate that result indicates failure when there's an exception
+ MockRequest requestMock = new();
+ await service.HandleNewSqlProjectRequest(new NewSqlProjectParams()
+ {
+ ProjectUri = projectUri,
+ SqlProjectType = ProjectType.SdkStyle
+
+ }, requestMock.Object);
+
+ Assert.IsFalse(requestMock.Result.Success);
+ Assert.IsTrue(requestMock.Result.ErrorMessage!.Contains("Cannot create a new SQL project"));
+ }
+
+ [Test]
+ public async Task TestOpenCloseProject()
+ {
+ // Setup
+ string sdkProjectUri = TestContext.CurrentContext.GetTestProjectPath(nameof(TestOpenCloseProject) + "Sdk");
+ string legacyProjectUri = TestContext.CurrentContext.GetTestProjectPath(nameof(TestOpenCloseProject) + "Legacy");
+
+ if (File.Exists(sdkProjectUri)) File.Delete(sdkProjectUri);
+ if (File.Exists(legacyProjectUri)) File.Delete(legacyProjectUri);
+
+ SqlProjectsService service = new();
+
+ Assert.AreEqual(0, service.Projects.Count);
+
+ // Validate creating SDK-style project
+ MockRequest requestMock = new();
+ await service.HandleNewSqlProjectRequest(new NewSqlProjectParams()
+ {
+ ProjectUri = sdkProjectUri,
+ SqlProjectType = ProjectType.SdkStyle
+
+ }, requestMock.Object);
+
+ Assert.IsTrue(requestMock.Result.Success);
+ Assert.AreEqual(1, service.Projects.Count);
+ Assert.IsTrue(service.Projects.ContainsKey(sdkProjectUri));
+ Assert.AreEqual(service.Projects[sdkProjectUri].SqlProjStyle, ProjectType.SdkStyle);
+
+ // Validate creating Legacy-style project
+ requestMock = new();
+ await service.HandleNewSqlProjectRequest(new NewSqlProjectParams()
+ {
+ ProjectUri = legacyProjectUri,
+ SqlProjectType = ProjectType.LegacyStyle
+ }, requestMock.Object);
+
+ Assert.IsTrue(requestMock.Result.Success);
+ Assert.AreEqual(2, service.Projects.Count);
+ Assert.IsTrue(service.Projects.ContainsKey(legacyProjectUri));
+ Assert.AreEqual(service.Projects[legacyProjectUri].SqlProjStyle, ProjectType.LegacyStyle);
+
+ // Validate closing a project
+ requestMock = new();
+ await service.HandleCloseSqlProjectRequest(new SqlProjectParams() { ProjectUri = sdkProjectUri }, requestMock.Object);
+
+ Assert.IsTrue(requestMock.Result.Success);
+ Assert.AreEqual(1, service.Projects.Count);
+ Assert.IsTrue(!service.Projects.ContainsKey(sdkProjectUri));
+
+ // Validate opening a project
+ requestMock = new();
+ await service.HandleOpenSqlProjectRequest(new SqlProjectParams() { ProjectUri = sdkProjectUri }, requestMock.Object);
+
+ Assert.IsTrue(requestMock.Result.Success);
+ Assert.AreEqual(2, service.Projects.Count);
+ Assert.IsTrue(service.Projects.ContainsKey(sdkProjectUri));
+ }
+
+ [Test]
+ public async Task TestSqlObjectScriptAddDeleteExclude()
+ {
+ // Setup
+ SqlProjectsService service = new();
+ string projectUri = await service.CreateSqlProject();
+ Assert.AreEqual(0, service.Projects[projectUri].SqlObjectScripts.Count);
+
+ // Validate adding a SQL object script
+ MockRequest requestMock = new();
+ string scriptRelativePath = "MyTable.sql";
+ string scriptFullPath = Path.Join(Path.GetDirectoryName(projectUri), scriptRelativePath);
+ await File.WriteAllTextAsync(scriptFullPath, "CREATE TABLE [MyTable] ([Id] INT)");
+
+ await service.HandleAddSqlObjectScriptRequest(new SqlProjectScriptParams()
+ {
+ ProjectUri = projectUri,
+ Path = scriptRelativePath
+ }, requestMock.Object);
+
+ Assert.IsTrue(requestMock.Result.Success);
+ Assert.AreEqual(1, service.Projects[projectUri].SqlObjectScripts.Count);
+ Assert.IsTrue(service.Projects[projectUri].SqlObjectScripts.Contains(scriptRelativePath));
+
+ // Validate excluding a SQL object script
+ requestMock = new();
+ await service.HandleExcludeSqlObjectScriptRequest(new SqlProjectScriptParams()
+ {
+ ProjectUri = projectUri,
+ Path = scriptRelativePath
+ }, requestMock.Object);
+
+ Assert.IsTrue(requestMock.Result.Success);
+ Assert.AreEqual(0, service.Projects[projectUri].SqlObjectScripts.Count);
+ Assert.IsTrue(File.Exists(scriptFullPath));
+
+ // Re-add to set up for Delete
+ requestMock = new();
+ await service.HandleAddSqlObjectScriptRequest(new SqlProjectScriptParams()
+ {
+ ProjectUri = projectUri,
+ Path = scriptRelativePath
+ }, requestMock.Object);
+
+ Assert.IsTrue(requestMock.Result.Success);
+ Assert.AreEqual(1, service.Projects[projectUri].SqlObjectScripts.Count);
+
+ // Validate deleting a SQL object script
+ requestMock = new();
+ await service.HandleDeleteSqlObjectScriptRequest(new SqlProjectScriptParams()
+ {
+ ProjectUri = projectUri,
+ Path = scriptRelativePath
+ }, requestMock.Object);
+
+ Assert.IsTrue(requestMock.Result.Success);
+ Assert.AreEqual(0, service.Projects[projectUri].SqlObjectScripts.Count);
+ Assert.IsFalse(File.Exists(scriptFullPath));
+ }
+ }
+
+ internal static class SqlProjectsExtensions
+ {
+ ///
+ /// Uses the service to create a new SQL project
+ ///
+ ///
+ ///
+ public async static Task CreateSqlProject(this SqlProjectsService service)
+ {
+ string projectUri = TestContext.CurrentContext.GetTestProjectPath();
+
+ MockRequest requestMock = new();
+ await service.HandleNewSqlProjectRequest(new NewSqlProjectParams()
+ {
+ ProjectUri = projectUri,
+ SqlProjectType = ProjectType.SdkStyle
+
+ }, requestMock.Object);
+
+ Assert.IsTrue(requestMock.Result.Success);
+
+ return projectUri;
+ }
+ }
+}
diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestBase.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestBase.cs
new file mode 100644
index 00000000..a4b51a5a
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestBase.cs
@@ -0,0 +1,47 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+using System.IO;
+using NUnit.Framework;
+
+namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility
+{
+ [TestFixture]
+ public abstract class TestBase
+ {
+ static TestBase()
+ {
+ RunTimestamp = DateTime.Now.ToString("yyyyMMdd-HHmmssffff");
+ }
+
+ public static string RunTimestamp
+ {
+ get;
+ private set;
+ }
+
+ public static string TestRunFolder => Path.Join(TestContext.CurrentContext.WorkDirectory, "SqlToolsServiceTestRuns", $"Run{RunTimestamp}");
+
+
+ [OneTimeSetUp]
+ public void SetUp()
+ {
+ if (!Directory.Exists(TestRunFolder))
+ {
+ Directory.CreateDirectory(TestRunFolder);
+ }
+ }
+
+ [OneTimeTearDown]
+ public void TearDown()
+ {
+ if (Directory.Exists(TestRunFolder))
+ {
+ Directory.Delete(TestRunFolder, recursive: true);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestContextHelpers.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestContextHelpers.cs
new file mode 100644
index 00000000..85e8845c
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestContextHelpers.cs
@@ -0,0 +1,19 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System.IO;
+using NUnit.Framework;
+
+namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility
+{
+ public static class TestContextHelpers
+ {
+ private static string TestName => TestContext.CurrentContext.Test.Name;
+
+ public static string GetTestWorkingFolder(this TestContext context) => Path.Join(TestBase.TestRunFolder, TestName);
+
+ public static string GetTestProjectPath(this TestContext context, string? projectName = null) => Path.Join(context.GetTestWorkingFolder(), $"{projectName ?? TestName}.sqlproj");
+ }
+}
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/RequestContextMocking/RequestContextMocks.cs b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/RequestContextMocking/RequestContextMocks.cs
index 68f2bfad..ea102224 100644
--- a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/RequestContextMocking/RequestContextMocks.cs
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/RequestContextMocking/RequestContextMocks.cs
@@ -13,7 +13,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common.RequestContextMocking
{
public static class RequestContextMocks
{
-
public static Mock> Create(Action resultCallback)
{
var requestContext = new Mock>();
@@ -61,4 +60,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common.RequestContextMocking
return mock;
}
}
+
+ public class MockRequest
+ {
+ private T? result;
+ public T Result => result ?? throw new InvalidOperationException("No result has been sent for the request");
+
+ public Mock> Mock;
+ public RequestContext Object => Mock.Object;
+
+ public MockRequest()
+ {
+ Mock = RequestContextMocks.Create(actual => result = actual);
+ }
+ }
}