diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4b750cac..d33761cf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,9 +23,13 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - uses: actions/setup-dotnet@v1 + + # Install .NET Core SDK + # TOOO: Remove this workaround once https://github.com/actions/setup-dotnet/issues/25 gets fixed + - name: Setup .NET Core + uses: coderpatros/setup-dotnet@sxs with: - dotnet-version: '3.1.102' + dotnet-version: 2.1.808,3.1.302 # Install Nerdbank.GitVersioning - name: install nbgv @@ -36,15 +40,15 @@ jobs: run: ./nbgv cloud -p ./src/MSBuild.Sdk.SqlProj/ --all-vars # Build command line tool - - name: dotnet build - run: dotnet build ./src/BuildDacpac/BuildDacpac.csproj -c Release + - name: dotnet build DacpacTool + run: dotnet build ./src/DacpacTool/DacpacTool.csproj -c Release # Run tests for command line tool - name: dotnet test - run: dotnet test ./test/BuildDacpac.Tests/BuildDacpac.Tests.csproj -c Release + run: dotnet test ./test/DacpacTool.Tests/DacpacTool.Tests.csproj -c Release # Run build for SDK package - - name: dotnet build + - name: dotnet build SDK run: dotnet build ./src/MSBuild.Sdk.SqlProj/MSBuild.Sdk.SqlProj.csproj -c Release # Ensure that test project builds @@ -80,7 +84,7 @@ jobs: strategy: matrix: os: [ "ubuntu-18.04", "macos-10.15", "windows-2019" ] - dotnet: [ '2.1.804', '2.2.207', '3.1.102' ] + dotnet: [ '2.1.808', '3.1.302' ] fail-fast: false steps: @@ -138,8 +142,8 @@ jobs: name: binary-log-${{ matrix.os }}-${{ matrix.dotnet }} path: ./msbuild.binlog - # Attempt to deploy the resulting dacpac's to a SQL Server instance running in a container - deploy: + # Attempt to deploy the resulting dacpac's to a SQL Server instance running in a container using SqlPackage.exe + deploy-sqlpackage: runs-on: ubuntu-18.04 needs: test services: @@ -150,6 +154,12 @@ jobs: SA_PASSWORD: JdMsKZPBBA8kVFXVrj8d ports: - 1433:1433 + options: >- + --health-cmd "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'JdMsKZPBBA8kVFXVrj8d' -Q 'SELECT 1' || exit 1" + --health-interval 10s + --health-timeout 3s + --health-retries 10 + --health-start-period 10s steps: # Download artifacts - name: download-artifact @@ -182,16 +192,85 @@ jobs: if: failure() uses: jwalton/gh-docker-logs@v1 + # Attempt to deploy a project to a SQL Server instance running in a container using dotnet publish + deploy-publish: + runs-on: ubuntu-18.04 + needs: test + services: + sqlserver: + image: mcr.microsoft.com/mssql/server:2019-latest + env: + ACCEPT_EULA: Y + SA_PASSWORD: JdMsKZPBBA8kVFXVrj8d + ports: + - 1433:1433 + options: >- + --health-cmd "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'JdMsKZPBBA8kVFXVrj8d' -Q 'SELECT 1' || exit 1" + --health-interval 10s + --health-timeout 3s + --health-retries 10 + --health-start-period 10s + steps: + # Fetch sources + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + + # Download artifact + - name: download-artifact + uses: actions/download-artifact@v1 + with: + name: nuget-packages + path: test/TestProjectWithSDKRef/nuget-packages + + # Setup .NET SDK + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.302' + + # Install Nerdbank.GitVersioning + - name: install nbgv + run: dotnet tool install --tool-path . nbgv + + # Set version + - name: set version + run: ./nbgv cloud -p ./src/MSBuild.Sdk.SqlProj/ --all-vars + id: nbgv + + # Replace tokens + - uses: cschleiden/replace-tokens@v1 + name: replace tokens + with: + files: 'test/TestProjectWithSDKRef/TestProjectWithSDKRef.csproj' + + # Publish the project + - name: publish project + run: dotnet publish ./test/TestProjectWithSDKRef/TestProjectWithSDKRef.csproj /p:TargetUser=sa /p:TargetPassword=JdMsKZPBBA8kVFXVrj8d /bl + + # Upload binary log + - name: upload + uses: actions/upload-artifact@v1 + with: + name: binary-log-publish + path: ./msbuild.binlog + + # Dump logs of the container if something failed + - name: Dump docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v1 + # Publish the NuGet package to NuGet.org when building master branch publish: runs-on: ubuntu-18.04 if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release/') - needs: deploy + needs: + - deploy-sqlpackage + - deploy-publish steps: # Setup .NET SDK - uses: actions/setup-dotnet@v1 with: - dotnet-version: '3.1.102' + dotnet-version: '3.1.302' # Download artifacts - name: download-artifact diff --git a/.vscode/launch.json b/.vscode/launch.json index 53f76959..b23a5fce 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/src/BuildDacpac/bin/Debug/netcoreapp3.1/BuildDacpac.dll", + "program": "${workspaceFolder}/src/Dacpactool/bin/Debug/netcoreapp3.1/Dacpactool.dll", "args": [ "-n", "MyPackage", @@ -24,7 +24,7 @@ "-p", "AnsiNullsOn=true" ], - "cwd": "${workspaceFolder}/src/BuildDacpac", + "cwd": "${workspaceFolder}/src/Dacpactool", "console": "internalConsole", "stopAtEntry": false }, diff --git a/.vscode/settings.json b/.vscode/settings.json index eb56df2f..ff63d225 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,13 @@ "markdown", "latex", "plaintext" - ] + ], + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/obj": true + } } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 88f9c5b0..4d049192 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/src/BuildDacpac/BuildDacpac.csproj", + "${workspaceFolder}/src/DacpacTool/DacpacTool.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], @@ -19,7 +19,7 @@ "type": "process", "args": [ "publish", - "${workspaceFolder}/src/BuildDacpac/BuildDacpac.csproj", + "${workspaceFolder}/src/DacpacTool/DacpacTool.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], @@ -32,7 +32,7 @@ "args": [ "watch", "run", - "${workspaceFolder}/src/BuildDacpac/BuildDacpac.csproj", + "${workspaceFolder}/src/DacpacTool/DacpacTool.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], diff --git a/Directory.Build.props b/Directory.Build.props index faca85fc..48de6c6e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ - 3.1.91 + 3.2.31 all diff --git a/MSBuild.Sdk.SqlProj.sln b/MSBuild.Sdk.SqlProj.sln index 768ef7bf..9dc24b9e 100644 --- a/MSBuild.Sdk.SqlProj.sln +++ b/MSBuild.Sdk.SqlProj.sln @@ -7,16 +7,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B0420F9B-A90 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSBuild.Sdk.SqlProj", "src\MSBuild.Sdk.SqlProj\MSBuild.Sdk.SqlProj.csproj", "{F4A31183-CB24-4D39-B498-8FE4F1A0CCA2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildDacpac", "src\BuildDacpac\BuildDacpac.csproj", "{94A9E682-EF0D-4B51-8589-30F241CF5837}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D25C8812-D92E-41D5-9BC9-30AFCF45063B}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{FD3F6B38-77EB-444F-B9B9-353F26EE8252}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dacpactool", "src\DacpacTool\Dacpactool.csproj", "{78CA944A-928F-48FA-B29F-C5DC96E67684}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A3A4D55B-4484-4501-894F-0B07708A9A1D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildDacpac.Tests", "test\BuildDacpac.Tests\BuildDacpac.Tests.csproj", "{E09D484E-594E-4614-8497-47B01D39087D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DacpacTool.Tests", "test\DacpacTool.Tests\DacpacTool.Tests.csproj", "{2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -40,38 +40,38 @@ Global {F4A31183-CB24-4D39-B498-8FE4F1A0CCA2}.Release|x64.Build.0 = Release|Any CPU {F4A31183-CB24-4D39-B498-8FE4F1A0CCA2}.Release|x86.ActiveCfg = Release|Any CPU {F4A31183-CB24-4D39-B498-8FE4F1A0CCA2}.Release|x86.Build.0 = Release|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Debug|Any CPU.Build.0 = Debug|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Debug|x64.ActiveCfg = Debug|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Debug|x64.Build.0 = Debug|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Debug|x86.ActiveCfg = Debug|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Debug|x86.Build.0 = Debug|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Release|Any CPU.ActiveCfg = Release|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Release|Any CPU.Build.0 = Release|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Release|x64.ActiveCfg = Release|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Release|x64.Build.0 = Release|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Release|x86.ActiveCfg = Release|Any CPU - {94A9E682-EF0D-4B51-8589-30F241CF5837}.Release|x86.Build.0 = Release|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Debug|x64.ActiveCfg = Debug|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Debug|x64.Build.0 = Debug|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Debug|x86.ActiveCfg = Debug|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Debug|x86.Build.0 = Debug|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Release|Any CPU.Build.0 = Release|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Release|x64.ActiveCfg = Release|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Release|x64.Build.0 = Release|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Release|x86.ActiveCfg = Release|Any CPU - {E09D484E-594E-4614-8497-47B01D39087D}.Release|x86.Build.0 = Release|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Debug|x64.ActiveCfg = Debug|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Debug|x64.Build.0 = Debug|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Debug|x86.ActiveCfg = Debug|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Debug|x86.Build.0 = Debug|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Release|Any CPU.Build.0 = Release|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Release|x64.ActiveCfg = Release|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Release|x64.Build.0 = Release|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Release|x86.ActiveCfg = Release|Any CPU + {78CA944A-928F-48FA-B29F-C5DC96E67684}.Release|x86.Build.0 = Release|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Debug|x64.Build.0 = Debug|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Debug|x86.Build.0 = Debug|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Release|Any CPU.Build.0 = Release|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Release|x64.ActiveCfg = Release|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Release|x64.Build.0 = Release|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Release|x86.ActiveCfg = Release|Any CPU + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {F4A31183-CB24-4D39-B498-8FE4F1A0CCA2} = {B0420F9B-A902-4E9D-8B29-55B4F626483B} - {94A9E682-EF0D-4B51-8589-30F241CF5837} = {B0420F9B-A902-4E9D-8B29-55B4F626483B} - {E09D484E-594E-4614-8497-47B01D39087D} = {FD3F6B38-77EB-444F-B9B9-353F26EE8252} + {78CA944A-928F-48FA-B29F-C5DC96E67684} = {B0420F9B-A902-4E9D-8B29-55B4F626483B} + {2EC1BB4C-C8A1-4C7E-AAE9-24E22EB0A8C4} = {A3A4D55B-4484-4501-894F-0B07708A9A1D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {832DCF12-9633-4AEB-A9D8-444EDF649047} diff --git a/README.md b/README.md index 3319e485..6da6b6fb 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ ## Introduction -An MSBuild SDK that is capable of producing a SQL Server Data-Tier Application package (.dacpac) from a set of SQL scripts that can be subsequently deployed using SqlPackage.exe. It provides much of the same functionality as the SQL Server Data Tools .sqlproj project format, but is build on top of the new SDK-style projects that were first introduced in Visual Studio 2017. +An MSBuild SDK that is capable of producing a SQL Server Data-Tier Application package (.dacpac) from a set of SQL scripts that can be subsequently deployed using either `SqlPackage.exe` or `dotnet publish`. It provides much of the same functionality as the SQL Server Data Tools .sqlproj project format, but is built on top of the new SDK-style projects that were first introduced in Visual Studio 2017. ## Usage The simplest usage is to create a new project file with the following contents: ```xml - + netstandard2.0 @@ -25,7 +25,7 @@ Then run a `dotnet build` and you'll find a .dacpac file in the `bin\Debug\netst There are a lot of properties that can be set on the model in the resulting `.dacpac` file which can be influenced by setting those properties in the project file using the same name. For example, the snippet below sets the `RecoveryMode` property to `Simple`: ```xml - + netstandard2.0 Simple @@ -46,7 +46,7 @@ Support for pre- and post deployment scripts has been added in version 1.1.0. Th To include these scripts into your `.dacpac` add the following to your `.csproj`: ```xml - + ... @@ -62,7 +62,7 @@ To include these scripts into your `.dacpac` add the following to your `.csproj` Especially when using pre- and post deployment scripts, but also in other scenario's, it might be useful to define variables that can be controlled at deployment time. This is supported through the use of SQLCMD variables, added in version 1.1.0. These variables can be defined in your project file using the following syntax: ```xml - + ... @@ -84,7 +84,7 @@ Especially when using pre- and post deployment scripts, but also in other scenar `MSBuild.Sdk.SqlProj` supports referencing NuGet packages that contain `.dacpac` packages. These can be referenced by using the `PackageReference` format familiar to .NET developers. They can also be installed through the NuGet Package Manager in Visual Studio. ```xml - + netstandard2.0 @@ -141,7 +141,7 @@ sqlpackage Additionally you'll need to set the `PackageProjectUrl` property inside of the `.csproj` like this: ```xml - + ... your-project-url @@ -151,6 +151,47 @@ Additionally you'll need to set the `PackageProjectUrl` property inside of the ` Other metadata for the package can be controlled by using the [documented](https://docs.microsoft.com/en-us/dotnet/core/tools/csproj#nuget-metadata-properties) properties in your project file. +### Publishing support +Starting with version 1.2.0 of MSBuild.Sdk.SqlProj there is support for publishing a project to a SQL Server using the `dotnet publish` command. There are a couple of properties that control the deployment process which have some defaults to make the experience as smooth as possible for local development. For example, on Windows if you have a default SQL Server instance running on your local machine running `dotnet publish` creates a database with the same name as the project. Unfortunately on Mac and Linux we cannot use Windows authentication, so you'll need to specify a username and password: + +``` +dotnet publish /p:TargetUser= /p:TargetPassword= +``` + +To further customize the deployment process, you can use the following properties which can either be set in the project file or specified on the command line (using the `/p:=` syntax shown above). + +| Property | Default Value | Description | +| --- | --- | --- | +| TargetServerName | (local) | Controls the name of the server to which the project is published | +| TargetDatabaseName | Name of the project | Controls the name of the database that is created | +| TargetUser | | Username used to connect to the server. If empty, Windows authentication is used | +| TargetPassword | | Password used to connect to the server. If empty, but TargetUser is set you will be prompted for the password | +| IncludeCompositeObjects | True | Controls whether objects from referenced packages are deployed to the same database | + +> IMPORTANT: Although you can set the username and password in your project file we don't recommend doing so since you'll be committing credentials to version control. Instead you should specify these at the command line when needed. + +In addition to these properties, you can also set any of the [documented](https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.dac.dacdeployoptions?view=sql-dacfx-150) deployment options. These are typically set in the project file, for example: + +```xml + + + ... + True + True + ... + + +``` + +Most of those properties are simple values (like booleans, strings and integers), but there are a couple of properties that require more complex values: + +| Property | Example value | Description | +| --- | --- | --- | +| DatabaseSpecification | Hyperscale;1024;P15 | This property is specified in the format [Edition](https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.dac.dacazureedition?view=sql-dacfx-150);[Maximum Size](https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.dac.dacazuredatabasespecification.maximumsize?view=sql-dacfx-150);[Service Objective](https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.dac.dacazuredatabasespecification.serviceobjective?view=sql-dacfx-150) | +| DoNotDropObjectTypes | Aggregates;Assemblies | A semi-colon separated list of [Object Types](https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.dac.objecttype?view=sql-dacfx-150) that should not be dropped as part of the deployment | +| ExcludeObjectTypes | Contracts;Endpoints | A semi-colon separated list of [Object Types](https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.dac.objecttype?view=sql-dacfx-150) that should not be part of the deployment | +| SqlCommandVariableValues | | These should not be set as a Property, but instead as an ItemGroup as described [here](#SQLCMD-Variables) + ## Workaround for parser errors (SQL46010) This project relies on the publicly available T-SQL parser which may not support all T-SQL syntax constructions. Therefore you might encounter a SQL46010 error if you have a script file that contains unsupported syntax. If that happens, there's a couple of workarounds you can try: diff --git a/src/DacpacTool/ActualConsole.cs b/src/DacpacTool/ActualConsole.cs new file mode 100644 index 00000000..df457e37 --- /dev/null +++ b/src/DacpacTool/ActualConsole.cs @@ -0,0 +1,17 @@ +using System; + +namespace MSBuild.Sdk.SqlProj.DacpacTool +{ + public class ActualConsole : IConsole + { + public string ReadLine() + { + return Console.ReadLine(); + } + + public void WriteLine(string value) + { + Console.WriteLine(value); + } + } +} diff --git a/src/BuildDacpac/PackageBuilderOptions.cs b/src/DacpacTool/BuildOptions.cs similarity index 87% rename from src/BuildDacpac/PackageBuilderOptions.cs rename to src/DacpacTool/BuildOptions.cs index 013b4d9d..0bb8718f 100644 --- a/src/BuildDacpac/PackageBuilderOptions.cs +++ b/src/DacpacTool/BuildOptions.cs @@ -1,9 +1,9 @@ using System.IO; using Microsoft.SqlServer.Dac.Model; -namespace MSBuild.Sdk.SqlProj.BuildDacpac +namespace MSBuild.Sdk.SqlProj.DacpacTool { - public class PackageBuilderOptions + public class BuildOptions { public string Name { get; set; } public string Version { get; set; } diff --git a/src/BuildDacpac/BuildDacpac.csproj b/src/DacpacTool/DacpacTool.csproj similarity index 79% rename from src/BuildDacpac/BuildDacpac.csproj rename to src/DacpacTool/DacpacTool.csproj index 6532dcae..1494e5e9 100644 --- a/src/BuildDacpac/BuildDacpac.csproj +++ b/src/DacpacTool/DacpacTool.csproj @@ -2,13 +2,13 @@ Exe - netcoreapp2.1;netcoreapp2.2;netcoreapp3.1 + netcoreapp2.1;netcoreapp3.1 8.0 - + diff --git a/src/DacpacTool/DeployOptions.cs b/src/DacpacTool/DeployOptions.cs new file mode 100644 index 00000000..66f57e79 --- /dev/null +++ b/src/DacpacTool/DeployOptions.cs @@ -0,0 +1,15 @@ +using System.IO; + +namespace MSBuild.Sdk.SqlProj.DacpacTool +{ + public class DeployOptions + { + public FileInfo Input { get; set; } + public string TargetServerName { get; set; } + public string TargetDatabaseName { get; set; } + public string TargetUser { get; set; } + public string TargetPassword { get; set; } + public string[] Property { get; set; } + public string[] SqlCmdVar { get; set; } + } +} diff --git a/src/BuildDacpac/Extensions.cs b/src/DacpacTool/Extensions.cs similarity index 98% rename from src/BuildDacpac/Extensions.cs rename to src/DacpacTool/Extensions.cs index fbc81768..306ca143 100644 --- a/src/BuildDacpac/Extensions.cs +++ b/src/DacpacTool/Extensions.cs @@ -3,7 +3,7 @@ using System.Reflection; using Microsoft.SqlServer.Dac.Model; -namespace MSBuild.Sdk.SqlProj.BuildDacpac +namespace MSBuild.Sdk.SqlProj.DacpacTool { public static class Extensions { diff --git a/src/DacpacTool/IConsole.cs b/src/DacpacTool/IConsole.cs new file mode 100644 index 00000000..4e2b0223 --- /dev/null +++ b/src/DacpacTool/IConsole.cs @@ -0,0 +1,8 @@ +namespace MSBuild.Sdk.SqlProj.DacpacTool +{ + public interface IConsole + { + void WriteLine(string value); + string ReadLine(); + } +} diff --git a/src/BuildDacpac/PackageBuilder.cs b/src/DacpacTool/PackageBuilder.cs similarity index 99% rename from src/BuildDacpac/PackageBuilder.cs rename to src/DacpacTool/PackageBuilder.cs index b7285f10..4fe972b5 100644 --- a/src/BuildDacpac/PackageBuilder.cs +++ b/src/DacpacTool/PackageBuilder.cs @@ -6,7 +6,7 @@ using Microsoft.SqlServer.Dac; using Microsoft.SqlServer.Dac.Model; -namespace MSBuild.Sdk.SqlProj.BuildDacpac +namespace MSBuild.Sdk.SqlProj.DacpacTool { public sealed class PackageBuilder : IDisposable { diff --git a/src/DacpacTool/PackageDeployer.cs b/src/DacpacTool/PackageDeployer.cs new file mode 100644 index 00000000..6b9e9efa --- /dev/null +++ b/src/DacpacTool/PackageDeployer.cs @@ -0,0 +1,283 @@ +using System; +using System.Globalization; +using System.IO; +using System.Reflection; +using Microsoft.Data.SqlClient; +using Microsoft.SqlServer.Dac; + +namespace MSBuild.Sdk.SqlProj.DacpacTool +{ + public sealed class PackageDeployer : IDisposable + { + private readonly IConsole _console; + + public PackageDeployer(IConsole console) + { + _console = console ?? throw new ArgumentNullException(nameof(console)); + } + + public SqlConnectionStringBuilder ConnectionStringBuilder { get; private set; } = new SqlConnectionStringBuilder(); + public DacDeployOptions DeployOptions { get; private set; } = new DacDeployOptions(); + public DacPackage Package { get; private set; } + + public void LoadPackage(FileInfo dacpacPackage) + { + if (!dacpacPackage.Exists) + { + throw new ArgumentException($"File {dacpacPackage.FullName} does not exist.", nameof(dacpacPackage)); + } + + Package = DacPackage.Load(dacpacPackage.FullName); + _console.WriteLine($"Loaded package '{Package.Name}' version '{Package.Version}' from '{dacpacPackage.FullName}'"); + } + + public void UseTargetServer(string targetServer) + { + EnsurePackageLoaded(); + + _console.WriteLine($"Using target server '{targetServer}'"); + ConnectionStringBuilder.DataSource = targetServer; + } + + public void UseSqlAuthentication(string username, string password) + { + EnsurePackageLoaded(); + + ConnectionStringBuilder.UserID = username; + if (string.IsNullOrWhiteSpace(password)) + { + _console.WriteLine("Enter password:"); + ConnectionStringBuilder.Password = _console.ReadLine(); + } + else + { + ConnectionStringBuilder.Password = password; + } + + _console.WriteLine("Using SQL Server Authentication"); + } + + public void UseWindowsAuthentication() + { + EnsurePackageLoaded(); + + ConnectionStringBuilder.IntegratedSecurity = true; + _console.WriteLine("Using Windows Authentication"); + } + + public void SetSqlCmdVariable(string key, string value) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentException("Key must have a value.", nameof(key)); + } + else if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException($"SQLCMD variable '{key}' has no value. Specify a value in the project file or on the command line.", nameof(value)); + } + + EnsurePackageLoaded(); + + DeployOptions.SqlCommandVariableValues.Add(key, value); + _console.WriteLine($"Adding SQLCMD variable '{key}' with value '{value}'"); + } + + public void Deploy(string targetDatabaseName) + { + EnsurePackageLoaded(); + EnsureConnectionStringComplete(); + + _console.WriteLine($"Deploying to database '{targetDatabaseName}'"); + + try + { + var services = new DacServices(ConnectionStringBuilder.ConnectionString); + services.Deploy(Package, targetDatabaseName, true, DeployOptions); + _console.WriteLine($"Successfully deployed database '{targetDatabaseName}'"); + } + catch (Exception ex) + { + _console.WriteLine($"ERROR: Deployment of database '{targetDatabaseName}' failed: {ex.Message}"); + } + } + + public void SetProperty(string key, string value) + { + try + { + // Convert value into the appropriate type depending on the key + object propertyValue = key switch + { + "AdditionalDeploymentContributorArguments" => value, + "AdditionalDeploymentContributorPaths" => value, + "AdditionalDeploymentContributors" => value, + "AllowDropBlockingAssemblies" => bool.Parse(value), + "AllowIncompatiblePlatform" => bool.Parse(value), + "AllowUnsafeRowLevelSecurityDataMovement" => bool.Parse(value), + "BackupDatabaseBeforeChanges" => bool.Parse(value), + "BlockOnPossibleDataLoss" => bool.Parse(value), + "BlockWhenDriftDetected" => bool.Parse(value), + "CommandTimeout" => int.Parse(value), + "CommentOutSetVarDeclarations" => bool.Parse(value), + "CompareUsingTargetCollation" => bool.Parse(value), + "CreateNewDatabase" => bool.Parse(value), + "DatabaseLockTimeout" => int.Parse(value), + "DatabaseSpecification" => ParseDatabaseSpecification(value), + "DeployDatabaseInSingleUserMode" => bool.Parse(value), + "DisableAndReenableDdlTriggers" => bool.Parse(value), + "DoNotAlterChangeDataCaptureObjects" => bool.Parse(value), + "DoNotAlterReplicatedObjects" => bool.Parse(value), + "DoNotDropObjectTypes" => ParseObjectTypes(value), + "DropConstraintsNotInSource" => bool.Parse(value), + "DropDmlTriggersNotInSource" => bool.Parse(value), + "DropExtendedPropertiesNotInSource" => bool.Parse(value), + "DropIndexesNotInSource" => bool.Parse(value), + "DropObjectsNotInSource" => bool.Parse(value), + "DropPermissionsNotInSource" => bool.Parse(value), + "DropRoleMembersNotInSource" => bool.Parse(value), + "DropStatisticsNotInSource" => bool.Parse(value), + "ExcludeObjectTypes" => ParseObjectTypes(value), + "GenerateSmartDefaults" => bool.Parse(value), + "IgnoreAnsiNulls" => bool.Parse(value), + "IgnoreAuthorizer" => bool.Parse(value), + "IgnoreColumnCollation" => bool.Parse(value), + "IgnoreColumnOrder" => bool.Parse(value), + "IgnoreComments" => bool.Parse(value), + "IgnoreCryptographicProviderFilePath" => bool.Parse(value), + "IgnoreDdlTriggerOrder" => bool.Parse(value), + "IgnoreDdlTriggerState" => bool.Parse(value), + "IgnoreDefaultSchema" => bool.Parse(value), + "IgnoreDmlTriggerOrder" => bool.Parse(value), + "IgnoreDmlTriggerState" => bool.Parse(value), + "IgnoreExtendedProperties" => bool.Parse(value), + "IgnoreFileAndLogFilePath" => bool.Parse(value), + "IgnoreFilegroupPlacement" => bool.Parse(value), + "IgnoreFileSize" => bool.Parse(value), + "IgnoreFillFactor" => bool.Parse(value), + "IgnoreFullTextCatalogFilePath" => bool.Parse(value), + "IgnoreIdentitySeed" => bool.Parse(value), + "IgnoreIncrement" => bool.Parse(value), + "IgnoreIndexOptions" => bool.Parse(value), + "IgnoreIndexPadding" => bool.Parse(value), + "IgnoreKeywordCasing" => bool.Parse(value), + "IgnoreLockHintsOnIndexes" => bool.Parse(value), + "IgnoreLoginSids" => bool.Parse(value), + "IgnoreNotForReplication" => bool.Parse(value), + "IgnoreObjectPlacementOnPartitionScheme" => bool.Parse(value), + "IgnorePartitionSchemes" => bool.Parse(value), + "IgnorePermissions" => bool.Parse(value), + "IgnoreQuotedIdentifiers" => bool.Parse(value), + "IgnoreRoleMembership" => bool.Parse(value), + "IgnoreRouteLifetime" => bool.Parse(value), + "IgnoreSemicolonBetweenStatements" => bool.Parse(value), + "IgnoreTableOptions" => bool.Parse(value), + "IgnoreTablePartitionOptions" => bool.Parse(value), + "IgnoreUserSettingsObjects" => bool.Parse(value), + "IgnoreWhitespace" => bool.Parse(value), + "IgnoreWithNocheckOnCheckConstraints" => bool.Parse(value), + "IgnoreWithNocheckOnForeignKeys" => bool.Parse(value), + "IncludeCompositeObjects" => bool.Parse(value), + "IncludeTransactionalScripts" => bool.Parse(value), + "LongRunningCommandTimeout" => int.Parse(value), + "NoAlterStatementsToChangeClrTypes" => bool.Parse(value), + "PopulateFilesOnFileGroups" => bool.Parse(value), + "RegisterDataTierApplication" => bool.Parse(value), + "RunDeploymentPlanExecutors" => bool.Parse(value), + "ScriptDatabaseCollation" => bool.Parse(value), + "ScriptDatabaseCompatibility" => bool.Parse(value), + "ScriptDatabaseOptions" => bool.Parse(value), + "ScriptDeployStateChecks" => bool.Parse(value), + "ScriptFileSize" => bool.Parse(value), + "ScriptNewConstraintValidation" => bool.Parse(value), + "ScriptRefreshModule" => bool.Parse(value), + "SqlCommandVariableValues" => throw new ArgumentException("SQLCMD variables should be set using the --sqlcmdvar command line argument and not as a property."), + "TreatVerificationErrorsAsWarnings" => bool.Parse(value), + "UnmodifiableObjectWarnings" => bool.Parse(value), + "VerifyCollationCompatibility" => bool.Parse(value), + "VerifyDeployment" => bool.Parse(value), + _ => throw new ArgumentException($"Unknown property with name {key}", nameof(key)) + }; + + PropertyInfo property = typeof(DacDeployOptions).GetProperty(key, BindingFlags.Public | BindingFlags.Instance); + property.SetValue(DeployOptions, propertyValue); + + _console.WriteLine($"Setting property {key} to value {value}"); + } + catch (FormatException) + { + throw new ArgumentException($"Unable to parse value for property with name {key}: {value}", nameof(value)); + } + } + + public void Dispose() + { + Package?.Dispose(); + Package = null; + } + + private void EnsurePackageLoaded() + { + if (Package == null) + { + throw new InvalidOperationException("Package has not been loaded. Call LoadPackage first."); + } + } + + private void EnsureConnectionStringComplete() + { + if (string.IsNullOrWhiteSpace(ConnectionStringBuilder.DataSource)) + { + throw new InvalidOperationException("A target server has not been set. Call UseTargetServer first."); + } + + if (string.IsNullOrWhiteSpace(ConnectionStringBuilder.UserID) && ConnectionStringBuilder.IntegratedSecurity == false) + { + throw new InvalidOperationException("No authentication information has been set. Call UseSqlServerAuthentication or UseWindowsAuthentication first."); + } + } + + private ObjectType[] ParseObjectTypes(string value) + { + var objectTypes = value.Split(';'); + var result = new ObjectType[objectTypes.Length]; + + for (int i = 0; i < objectTypes.Length; i++) + { + if (!Enum.TryParse(objectTypes[i], false, out ObjectType objectType)) + { + throw new ArgumentException($"Unknown object type {objectTypes[i]} specified.", nameof(value)); + } + + result[i] = objectType; + } + + return result; + } + + private DacAzureDatabaseSpecification ParseDatabaseSpecification(string value) + { + var specification = value.Split(";", 3); + if (specification.Length != 3) + { + throw new ArgumentException("Expected at least 3 parameters for DatabaseSpecification; Edition, MaximumSize and ServiceObjective", nameof(value)); + } + + if (!Enum.TryParse(specification[0], false, out DacAzureEdition edition)) + { + throw new ArgumentException($"Unknown edition '{specification[0]}' specified.", nameof(value)); + } + + if (!int.TryParse(specification[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int maximumSize)) + { + throw new ArgumentException($"Unable to parse maximum size '{specification[1]}' as an integer.", nameof(value)); + } + + return new DacAzureDatabaseSpecification + { + Edition = edition, + MaximumSize = maximumSize, + ServiceObjective = specification[2] + }; + } + } +} diff --git a/src/BuildDacpac/Program.cs b/src/DacpacTool/Program.cs similarity index 51% rename from src/BuildDacpac/Program.cs rename to src/DacpacTool/Program.cs index fb6fb8da..630e7d10 100644 --- a/src/BuildDacpac/Program.cs +++ b/src/DacpacTool/Program.cs @@ -1,16 +1,17 @@ -using System.CommandLine; +using System; +using System.CommandLine; using System.CommandLine.Invocation; using System.IO; using System.Threading.Tasks; using Microsoft.SqlServer.Dac.Model; -namespace MSBuild.Sdk.SqlProj.BuildDacpac +namespace MSBuild.Sdk.SqlProj.DacpacTool { class Program { static async Task Main(string[] args) { - var rootCommand = new RootCommand + var buildCommand = new Command("build") { new Option(new string[] { "--name", "-n" }, "Name of the package"), new Option(new string[] { "--version", "-v" }, "Version of the package"), @@ -23,14 +24,27 @@ static async Task Main(string[] args) new Option(new string[] { "--property", "-p" }, "Properties to be set on the model"), new Option(new string[] { "--sqlcmdvar", "-sc" }, "SqlCmdVariable(s) to include"), }; + buildCommand.Handler = CommandHandler.Create(BuildDacpac); + var deployCommand = new Command("deploy") + { + new Option(new string[] { "--input", "-i" }, "Path to the .dacpac package to deploy"), + new Option(new string[] { "--targetServerName", "-tsn" }, "Name of the server to deploy the package to"), + new Option(new string[] { "--targetDatabaseName", "-tdn" }, "Name of the database to deploy the package to"), + new Option(new string[] { "--targetUser", "-tu" }, "Username used to connect to the target server, using SQL Server authentication"), + new Option(new string[] { "--targetPassword", "-tp" }, "Password used to connect to the target server, using SQL Server authentication"), + new Option(new string[] { "--property", "-p" }, "Properties used to control the deployment"), + new Option(new string[] { "--sqlcmdvar", "-sc" }, "SqlCmdVariable(s) and their associated values, separated by an equals sign.") + }; + deployCommand.Handler = CommandHandler.Create(DeployDacpac); + + var rootCommand = new RootCommand { buildCommand, deployCommand }; rootCommand.Description = "Command line tool for generating a SQL Server Data-Tier Application Framework package (dacpac)"; - rootCommand.Handler = CommandHandler.Create(BuildDacpac); return await rootCommand.InvokeAsync(args); } - private static int BuildDacpac(PackageBuilderOptions options) + private static int BuildDacpac(BuildOptions options) { // Set metadata for the package using var packageBuilder = new PackageBuilder(); @@ -89,5 +103,55 @@ private static int BuildDacpac(PackageBuilderOptions options) return 0; } + private static int DeployDacpac(DeployOptions options) + { + try + { + using var deployer = new PackageDeployer(new ActualConsole()); + deployer.LoadPackage(options.Input); + + if (options.Property != null) + { + foreach (var propertyValue in options.Property) + { + string[] keyValuePair = propertyValue.Split('=', 2); + deployer.SetProperty(keyValuePair[0], keyValuePair[1]); + } + } + + if (options.SqlCmdVar != null) + { + foreach (var sqlCmdVar in options.SqlCmdVar) + { + string[] keyValuePair = sqlCmdVar.Split('=', 2); + deployer.SetSqlCmdVariable(keyValuePair[0], keyValuePair[1]); + } + } + + deployer.UseTargetServer(options.TargetServerName); + + if (!string.IsNullOrWhiteSpace(options.TargetUser)) + { + deployer.UseSqlAuthentication(options.TargetUser, options.TargetPassword); + } + else + { + deployer.UseWindowsAuthentication(); + } + + deployer.Deploy(options.TargetDatabaseName); + return 0; + } + catch (ArgumentException ex) + { + Console.WriteLine($"ERROR: An error occured while validating arguments: {ex.Message}"); + return 1; + } + catch (Exception ex) + { + Console.WriteLine($"ERROR: An error ocurred during deployment: {ex.Message}"); + return 1; + } + } } } diff --git a/src/MSBuild.Sdk.SqlProj/MSBuild.Sdk.SqlProj.csproj b/src/MSBuild.Sdk.SqlProj/MSBuild.Sdk.SqlProj.csproj index 12ea88d9..445cf9dc 100644 --- a/src/MSBuild.Sdk.SqlProj/MSBuild.Sdk.SqlProj.csproj +++ b/src/MSBuild.Sdk.SqlProj/MSBuild.Sdk.SqlProj.csproj @@ -29,14 +29,14 @@ - + - <_BuildDacpacSupportedTfms>netcoreapp2.1;netcoreapp2.2;netcoreapp3.1 + <_DacpacToolSupportedTfms>netcoreapp2.1;netcoreapp3.1 - + - + \ No newline at end of file diff --git a/src/MSBuild.Sdk.SqlProj/Sdk/Sdk.props b/src/MSBuild.Sdk.SqlProj/Sdk/Sdk.props index b2953eb2..189e551e 100644 --- a/src/MSBuild.Sdk.SqlProj/Sdk/Sdk.props +++ b/src/MSBuild.Sdk.SqlProj/Sdk/Sdk.props @@ -115,5 +115,101 @@ UserAccessOption; WithEncryption + + AdditionalDeploymentContributorArguments; + AdditionalDeploymentContributorPaths; + AdditionalDeploymentContributors; + AllowDropBlockingAssemblies; + AllowIncompatiblePlatform; + AllowUnsafeRowLevelSecurityDataMovement; + BackupDatabaseBeforeChanges; + BlockOnPossibleDataLoss; + BlockWhenDriftDetected; + CommandTimeout; + CommentOutSetVarDeclarations; + CompareUsingTargetCollation; + CreateNewDatabase; + DatabaseLockTimeout; + DatabaseSpecification; + DeployDatabaseInSingleUserMode; + DisableAndReenableDdlTriggers; + DoNotAlterChangeDataCaptureObjects; + DoNotAlterReplicatedObjects; + DoNotDropObjectTypes; + DropConstraintsNotInSource; + DropDmlTriggersNotInSource; + DropExtendedPropertiesNotInSource; + DropIndexesNotInSource; + DropObjectsNotInSource; + DropPermissionsNotInSource; + DropRoleMembersNotInSource; + DropStatisticsNotInSource; + ExcludeObjectTypes; + GenerateSmartDefaults; + IgnoreAnsiNulls; + IgnoreAuthorizer; + IgnoreColumnCollation; + IgnoreColumnOrder; + IgnoreComments; + IgnoreCryptographicProviderFilePath; + IgnoreDdlTriggerOrder; + IgnoreDdlTriggerState; + IgnoreDefaultSchema; + IgnoreDmlTriggerOrder; + IgnoreDmlTriggerState; + IgnoreExtendedProperties; + IgnoreFileAndLogFilePath; + IgnoreFilegroupPlacement; + IgnoreFileSize; + IgnoreFillFactor; + IgnoreFullTextCatalogFilePath; + IgnoreIdentitySeed; + IgnoreIncrement; + IgnoreIndexOptions; + IgnoreIndexPadding; + IgnoreKeywordCasing; + IgnoreLockHintsOnIndexes; + IgnoreLoginSids; + IgnoreNotForReplication; + IgnoreObjectPlacementOnPartitionScheme; + IgnorePartitionSchemes; + IgnorePermissions; + IgnoreQuotedIdentifiers; + IgnoreRoleMembership; + IgnoreRouteLifetime; + IgnoreSemicolonBetweenStatements; + IgnoreTableOptions; + IgnoreTablePartitionOptions; + IgnoreUserSettingsObjects; + IgnoreWhitespace; + IgnoreWithNocheckOnCheckConstraints; + IgnoreWithNocheckOnForeignKeys; + IncludeCompositeObjects; + IncludeTransactionalScripts; + LongRunningCommandTimeout; + NoAlterStatementsToChangeClrTypes; + PopulateFilesOnFileGroups; + RegisterDataTierApplication; + RunDeploymentPlanExecutors; + ScriptDatabaseCollation; + ScriptDatabaseCompatibility; + ScriptDatabaseOptions; + ScriptDeployStateChecks; + ScriptFileSize; + ScriptNewConstraintValidation; + ScriptRefreshModule; + SqlCommandVariableValues; + TreatVerificationErrorsAsWarnings; + UnmodifiableObjectWarnings; + VerifyCollationCompatibility; + VerifyDeployment; + + + + + + (local) + $(MSBuildProjectName) + true \ No newline at end of file diff --git a/src/MSBuild.Sdk.SqlProj/Sdk/Sdk.targets b/src/MSBuild.Sdk.SqlProj/Sdk/Sdk.targets index 5f9cc4c3..b0e636e6 100644 --- a/src/MSBuild.Sdk.SqlProj/Sdk/Sdk.targets +++ b/src/MSBuild.Sdk.SqlProj/Sdk/Sdk.targets @@ -90,18 +90,18 @@ - netcoreapp$(BundledNETCoreAppTargetFrameworkVersion) - $(MSBuildThisFileDirectory)../tools/$(BuildDacpacTfm)/BuildDacpac.dll + netcoreapp$(BundledNETCoreAppTargetFrameworkVersion) + $(MSBuildThisFileDirectory)../tools/$(DacpacToolTfm)/DacpacTool.dll - - + @(IntermediateAssembly->'-o "%(Identity)"', ' ') - -n $(MSBuildProjectName) -v $(PackageVersion) + -n "$(MSBuildProjectName)" -v "$(PackageVersion)" -sv $(SqlServerVersion) @(DacpacReference->'-r "%(DacpacFile)"', ' ') @(Content->'-i "%(FullPath)"', ' ') @@ -145,16 +145,37 @@ @(SqlCmdVariable->'-sc %(Identity)', ' ') @(PreDeploy->'--predeploy %(Identity)', ' ') @(PostDeploy->'--postdeploy %(Identity)', ' ') - netcoreapp$(BundledNETCoreAppTargetFrameworkVersion) - dotnet $(MSBuildThisFileDirectory)../tools/$(BuildDacpacTfm)/BuildDacpac.dll $(OutputPathArgument) $(MetadataArguments) $(SqlServerVersionArgument) $(InputFileArguments) $(ReferenceArguments) $(SqlCmdVariableArguments) $(PropertyArguments) $(PreDeploymentScriptArgument) $(PostDeploymentScriptArgument) + dotnet $(DacpacToolExe) build $(OutputPathArgument) $(MetadataArguments) $(SqlServerVersionArgument) $(InputFileArguments) $(ReferenceArguments) $(SqlCmdVariableArguments) $(PropertyArguments) $(PreDeploymentScriptArgument) $(PostDeploymentScriptArgument) - - + + + + + + <_DeployPropertyNames Include="$(KnownDeployProperties)" /> + + $(%(_DeployPropertyNames.Identity)) + + + + -i "$(TargetPath)" + -tsn "$(TargetServerName)" + -tdn "$(TargetDatabaseName)" + -tu "$(TargetUser)" + -tp "$(TargetPassword)" + @(DeployPropertyNames->'-p %(Identity)=%(PropertyValue)', ' ') + @(SqlCmdVariable->'-sc %(Identity)=%(Value)', ' ') + dotnet $(DacpacToolExe) deploy $(InputArgument) $(TargetServerNameArgument) $(TargetDatabaseNameArgument) $(TargetUserArgument) $(TargetPasswordArgument) $(PropertyArguments) $(SqlCmdVariableArguments) + + + + \ No newline at end of file diff --git a/test/BuildDacpac.Tests/DacpacHeaderParser/CustomData.cs b/test/DacpacTool.Tests/DacpacHeaderParser/CustomData.cs similarity index 90% rename from test/BuildDacpac.Tests/DacpacHeaderParser/CustomData.cs rename to test/DacpacTool.Tests/DacpacHeaderParser/CustomData.cs index 86e80cba..7e0a0b4b 100644 --- a/test/BuildDacpac.Tests/DacpacHeaderParser/CustomData.cs +++ b/test/DacpacTool.Tests/DacpacHeaderParser/CustomData.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace MSBuild.Sdk.SqlProj.BuildDacpac.Tests.DacpacHeaderParser +namespace MSBuild.Sdk.SqlProj.DacpacTool.Tests.DacpacHeaderParser { public class CustomData { diff --git a/test/BuildDacpac.Tests/DacpacHeaderParser/DacpacXml.cs b/test/DacpacTool.Tests/DacpacHeaderParser/DacpacXml.cs similarity index 91% rename from test/BuildDacpac.Tests/DacpacHeaderParser/DacpacXml.cs rename to test/DacpacTool.Tests/DacpacHeaderParser/DacpacXml.cs index 8fae04fd..101cf359 100644 --- a/test/BuildDacpac.Tests/DacpacHeaderParser/DacpacXml.cs +++ b/test/DacpacTool.Tests/DacpacHeaderParser/DacpacXml.cs @@ -2,7 +2,7 @@ using System.IO; using Microsoft.Data.Tools.Schema.Sql.Packaging; -namespace MSBuild.Sdk.SqlProj.BuildDacpac.Tests.DacpacHeaderParser +namespace MSBuild.Sdk.SqlProj.DacpacTool.Tests.DacpacHeaderParser { public class DacPacXml : IDisposable { diff --git a/test/BuildDacpac.Tests/DacpacHeaderParser/HeaderParser.cs b/test/DacpacTool.Tests/DacpacHeaderParser/HeaderParser.cs similarity index 96% rename from test/BuildDacpac.Tests/DacpacHeaderParser/HeaderParser.cs rename to test/DacpacTool.Tests/DacpacHeaderParser/HeaderParser.cs index 779340eb..65c41968 100644 --- a/test/BuildDacpac.Tests/DacpacHeaderParser/HeaderParser.cs +++ b/test/DacpacTool.Tests/DacpacHeaderParser/HeaderParser.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Xml; -namespace MSBuild.Sdk.SqlProj.BuildDacpac.Tests.DacpacHeaderParser +namespace MSBuild.Sdk.SqlProj.DacpacTool.Tests.DacpacHeaderParser { public class HeaderParser { diff --git a/test/BuildDacpac.Tests/DacpacHeaderParser/MetaData.cs b/test/DacpacTool.Tests/DacpacHeaderParser/MetaData.cs similarity index 76% rename from test/BuildDacpac.Tests/DacpacHeaderParser/MetaData.cs rename to test/DacpacTool.Tests/DacpacHeaderParser/MetaData.cs index 05602f3e..5cc89ac1 100644 --- a/test/BuildDacpac.Tests/DacpacHeaderParser/MetaData.cs +++ b/test/DacpacTool.Tests/DacpacHeaderParser/MetaData.cs @@ -1,4 +1,4 @@ -namespace MSBuild.Sdk.SqlProj.BuildDacpac.Tests.DacpacHeaderParser +namespace MSBuild.Sdk.SqlProj.DacpacTool.Tests.DacpacHeaderParser { public class Metadata { diff --git a/test/BuildDacpac.Tests/BuildDacpac.Tests.csproj b/test/DacpacTool.Tests/DacpacTool.Tests.csproj similarity index 52% rename from test/BuildDacpac.Tests/BuildDacpac.Tests.csproj rename to test/DacpacTool.Tests/DacpacTool.Tests.csproj index 447dee09..258c5b12 100644 --- a/test/BuildDacpac.Tests/BuildDacpac.Tests.csproj +++ b/test/DacpacTool.Tests/DacpacTool.Tests.csproj @@ -1,28 +1,31 @@ - netcoreapp3.1 + netcoreapp2.1;netcoreapp3.1 false - MSBuild.Sdk.SqlProj.BuildDacpac.Tests + MSBuild.Sdk.SqlProj.DacpacTool.Tests - MSBuild.Sdk.SqlProj.BuildDacpac.Tests + MSBuild.Sdk.SqlProj.DacpacTool.Tests + 8.0 - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + - + diff --git a/test/BuildDacpac.Tests/ExtensionsTest.cs b/test/DacpacTool.Tests/ExtensionsTest.cs similarity index 95% rename from test/BuildDacpac.Tests/ExtensionsTest.cs rename to test/DacpacTool.Tests/ExtensionsTest.cs index 2cf3f405..65910d04 100644 --- a/test/BuildDacpac.Tests/ExtensionsTest.cs +++ b/test/DacpacTool.Tests/ExtensionsTest.cs @@ -2,7 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; -namespace MSBuild.Sdk.SqlProj.BuildDacpac.Tests +namespace MSBuild.Sdk.SqlProj.DacpacTool.Tests { /// /// Contains tests for the class. diff --git a/test/BuildDacpac.Tests/PackageBuilderTests.cs b/test/DacpacTool.Tests/PackageBuilderTests.cs similarity index 99% rename from test/BuildDacpac.Tests/PackageBuilderTests.cs rename to test/DacpacTool.Tests/PackageBuilderTests.cs index 9145999e..250cad6e 100644 --- a/test/BuildDacpac.Tests/PackageBuilderTests.cs +++ b/test/DacpacTool.Tests/PackageBuilderTests.cs @@ -8,7 +8,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; -namespace MSBuild.Sdk.SqlProj.BuildDacpac.Tests +namespace MSBuild.Sdk.SqlProj.DacpacTool.Tests { [TestClass] public class PackageBuilderTest diff --git a/test/DacpacTool.Tests/PackageDeployerTests.cs b/test/DacpacTool.Tests/PackageDeployerTests.cs new file mode 100644 index 00000000..762da9c5 --- /dev/null +++ b/test/DacpacTool.Tests/PackageDeployerTests.cs @@ -0,0 +1,347 @@ +using System; +using System.IO; +using Microsoft.SqlServer.Dac; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using Shouldly; + +namespace MSBuild.Sdk.SqlProj.DacpacTool.Tests +{ + [TestClass] + public class PackageDeployerTests + { + private IConsole _console = Substitute.For(); + + [TestMethod] + public void LoadPackage() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + + // Assert + packageDeployer.Package.ShouldNotBeNull(); + } + + [TestMethod] + public void LoadPackageFileDoesNotExist() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = "SomeDummyFile.dacpac"; + + // Act + Should.Throw(() => packageDeployer.LoadPackage(new FileInfo(packagePath))); + + // Assert + packageDeployer.Package.ShouldBeNull(); + } + + [TestMethod] + public void UseTargetServer() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + packageDeployer.UseTargetServer("localhost"); + + // Assert + packageDeployer.ConnectionStringBuilder.DataSource.ShouldNotBeNull(); + packageDeployer.ConnectionStringBuilder.DataSource.ShouldBe("localhost"); + } + + [TestMethod] + public void UseTargetServerWithoutLoadPackage() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + Should.Throw(() => packageDeployer.UseTargetServer("localhost")); + + // Assert + packageDeployer.ConnectionStringBuilder.DataSource.ShouldBeEmpty(); + } + + [TestMethod] + public void UseWindowsAuthentication() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + packageDeployer.UseWindowsAuthentication(); + + // Assert + packageDeployer.ConnectionStringBuilder.IntegratedSecurity.ShouldBeTrue(); + } + + [TestMethod] + public void UseWindowsAuthenticationWithoutLoadPackage() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + Should.Throw(() => packageDeployer.UseWindowsAuthentication()); + + // Assert + packageDeployer.ConnectionStringBuilder.IntegratedSecurity.ShouldBeFalse(); + } + + [TestMethod] + public void UseSqlServerAuthenticationNoPasswordPrompts() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + packageDeployer.UseSqlAuthentication("testuser", null); + + // Assert + packageDeployer.ConnectionStringBuilder.IntegratedSecurity.ShouldBeFalse(); + packageDeployer.ConnectionStringBuilder.UserID.ShouldBe("testuser"); + _console.Received().ReadLine(); + } + + [TestMethod] + public void UseSqlServerAuthenticationNoPasswordSetsPassword() + { + // Arrange + _console.ReadLine().Returns("testpassword"); + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + packageDeployer.UseSqlAuthentication("testuser", null); + + // Assert + packageDeployer.ConnectionStringBuilder.IntegratedSecurity.ShouldBeFalse(); + packageDeployer.ConnectionStringBuilder.UserID.ShouldBe("testuser"); + packageDeployer.ConnectionStringBuilder.Password.ShouldBe("testpassword"); + } + + [TestMethod] + public void UseSqlAuthenticationWithPasswordDoesNotPrompt() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + packageDeployer.UseSqlAuthentication("testuser", "testpassword"); + + // Assert + packageDeployer.ConnectionStringBuilder.IntegratedSecurity.ShouldBeFalse(); + packageDeployer.ConnectionStringBuilder.UserID.ShouldBe("testuser"); + packageDeployer.ConnectionStringBuilder.Password.ShouldBe("testpassword"); + _console.DidNotReceive().ReadLine(); + } + + [TestMethod] + public void SetSqlCmdVariable() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + packageDeployer.SetSqlCmdVariable("MySqlCmdVariable", "SomeValue"); + + // Assert + packageDeployer.DeployOptions.SqlCommandVariableValues.ContainsKey("MySqlCmdVariable").ShouldBeTrue(); + packageDeployer.DeployOptions.SqlCommandVariableValues["MySqlCmdVariable"].ShouldBe("SomeValue"); + } + + [TestMethod] + public void SetSqlCmdVariableNoValue() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + Should.Throw(() => packageDeployer.SetSqlCmdVariable("MySqlCmdVariable", string.Empty)); + + // Assert + packageDeployer.DeployOptions.SqlCommandVariableValues.ContainsKey("MySqlCmdVariable").ShouldBeFalse(); + } + + [TestMethod] + public void DeployNoPackageLoaded() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + Should.Throw(() => packageDeployer.Deploy("TestDatabase")); + + // Assert + // Should throw + } + + [TestMethod] + public void DeployNoAuthentication() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + Should.Throw(() => packageDeployer.Deploy("TestDatabase")); + } + + [TestMethod] + public void SetPropertySimpleValue() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + packageDeployer.SetProperty("AllowDropBlockingAssemblies", "true"); + + // Assert + packageDeployer.DeployOptions.AllowDropBlockingAssemblies.ShouldBeTrue(); + } + + [TestMethod] + public void SetPropertyInvalidFormat() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + Should.Throw(() => packageDeployer.SetProperty("AllowDropBlockingAssemblies", "ARandomString")); + } + + [TestMethod] + public void SetPropertyDoNotDropObjectTypes() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + packageDeployer.SetProperty("DoNotDropObjectTypes", "Aggregates;Assemblies"); + + // Assert + packageDeployer.DeployOptions.DoNotDropObjectTypes.ShouldBe(new ObjectType[] { ObjectType.Aggregates, ObjectType.Assemblies }); + } + + [TestMethod] + public void SetPropertyExcludeObjectTypes() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + packageDeployer.SetProperty("ExcludeObjectTypes", "Contracts;Endpoints"); + + // Assert + packageDeployer.DeployOptions.ExcludeObjectTypes.ShouldBe(new ObjectType[] { ObjectType.Contracts, ObjectType.Endpoints }); + } + + [TestMethod] + public void SetPropertyDatabaseSpecification() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + packageDeployer.SetProperty("DatabaseSpecification", "Hyperscale;1024;P15"); + + // Assert + packageDeployer.DeployOptions.DatabaseSpecification.Edition.ShouldBe(DacAzureEdition.Hyperscale); + packageDeployer.DeployOptions.DatabaseSpecification.MaximumSize.ShouldBe(1024); + packageDeployer.DeployOptions.DatabaseSpecification.ServiceObjective.ShouldBe("P15"); + } + + [TestMethod] + public void SetPropertyDatabaseSpecificationInvalidEdition() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + Should.Throw(() => packageDeployer.SetProperty("DatabaseSpecification", "MyFancyEdition;1024;P15")); + + // Assert + packageDeployer.DeployOptions.DatabaseSpecification.Edition.ShouldBe(DacAzureEdition.Default); + packageDeployer.DeployOptions.DatabaseSpecification.MaximumSize.ShouldBe(default); + packageDeployer.DeployOptions.DatabaseSpecification.ServiceObjective.ShouldBeNull(); + } + + [TestMethod] + public void SetPropertyDatabaseSpecificationInvalidMaximumSize() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + Should.Throw(() => packageDeployer.SetProperty("DatabaseSpecification", "hyperscale;NotAnInteger;P15")); + + // Assert + packageDeployer.DeployOptions.DatabaseSpecification.Edition.ShouldBe(DacAzureEdition.Default); + packageDeployer.DeployOptions.DatabaseSpecification.MaximumSize.ShouldBe(default); + packageDeployer.DeployOptions.DatabaseSpecification.ServiceObjective.ShouldBeNull(); + } + + [TestMethod] + public void SetPropertyDatabaseSpecificationTooFewParameters() + { + // Arrange + using var packageDeployer = new PackageDeployer(_console); + var packagePath = BuildSimpleModel(); + + // Act + packageDeployer.LoadPackage(packagePath); + Should.Throw(() => packageDeployer.SetProperty("DatabaseSpecification", "hyperscale")); + + // Assert + packageDeployer.DeployOptions.DatabaseSpecification.Edition.ShouldBe(DacAzureEdition.Default); + packageDeployer.DeployOptions.DatabaseSpecification.MaximumSize.ShouldBe(default); + packageDeployer.DeployOptions.DatabaseSpecification.ServiceObjective.ShouldBeNull(); + } + + private FileInfo BuildSimpleModel() + { + var packagePath = new TestModelBuilder() + .AddTable("TestTable", ("Column1", "nvarchar(100)")) + .AddStoredProcedure("csp_GetData", "SELECT * FROM dbo.TestTable") + .SaveAsPackage(); + + return new FileInfo(packagePath); + } + } +} diff --git a/test/BuildDacpac.Tests/TestModelBuilder.cs b/test/DacpacTool.Tests/TestModelBuilder.cs similarity index 97% rename from test/BuildDacpac.Tests/TestModelBuilder.cs rename to test/DacpacTool.Tests/TestModelBuilder.cs index 69ff80cb..f12c3cbf 100644 --- a/test/BuildDacpac.Tests/TestModelBuilder.cs +++ b/test/DacpacTool.Tests/TestModelBuilder.cs @@ -3,7 +3,7 @@ using Microsoft.SqlServer.Dac; using Microsoft.SqlServer.Dac.Model; -namespace MSBuild.Sdk.SqlProj.BuildDacpac.Tests +namespace MSBuild.Sdk.SqlProj.DacpacTool.Tests { internal class TestModelBuilder { diff --git a/test/TestProjectWithPackageReference/TestProjectWithPackageReference.csproj b/test/TestProjectWithPackageReference/TestProjectWithPackageReference.csproj index 85b46ba9..1bd5433c 100644 --- a/test/TestProjectWithPackageReference/TestProjectWithPackageReference.csproj +++ b/test/TestProjectWithPackageReference/TestProjectWithPackageReference.csproj @@ -1,14 +1,22 @@ - + netstandard2.0 Sql110 + ../TestProject/bin/Debug - + + DefaultValue + SomeValue + - + + + + + \ No newline at end of file