Search code examples
c#sql-serverdac

DacServices Rollback if new constraint validation fails


We are updating our databases using the DacServices. However sometimes new constraints get added that cause Constraint Violations, which require us to write a migration script to make sure the Data in the column is valid.

However with the DacServices.Deploy if the ScriptNewConstraintValidation option is true and the new constraint errors, I cannot figure out how to rollback the changes made by the DacServices.

We are using the AlwaysOn Availability so performing a backup/restore would be extremely difficult. Below is one way I have tried with no success.

var dacServices = new DacServices(dbConnection.ConnectionString);
var deploymentOptions = new DacDeployOptions
{
    CreateNewDatabase = createNewDatabase,
    ScriptDatabaseCompatibility = false,
    ScriptDatabaseCollation = false,
    IgnoreUserSettingsObjects = true,
    DropPermissionsNotInSource = false,
    DropRoleMembersNotInSource = false,
    IgnorePermissions = true,
    IgnoreRoleMembership = true,
    GenerateSmartDefaults = true,
    IncludeTransactionalScripts = true,
    ScriptNewConstraintValidation = true
};

Microsoft.SqlServer.Dac.DacPackage dacPackage = DacPackage.Load(dacPath);

Console.WriteLine("Deploying DacPac");
CancellationTokenSource tokenSource = new CancellationTokenSource();
try
{
    dacServices.Deploy(dacPackage, databaseName, true, deploymentOptions, tokenSource.Token);
}
catch(Exception)
{
    tokenSource.Cancel();
}

Solution

  • The problem here, as the OP points out, is that the step to validate the new constraints happens outside of the transactional statements that are included when IncludeTransactionalScripts is set to true. This means that the changes to the database will be committed, even though the deployment specifies constraints that are inconsistent with the data in the database. This seems like a bug that Microsoft should fix.

    Nonetheless, my suggestion was to use the DacServices framework to generate a deployment script, as opposed to just deploying directly to the database. Once the script has been generated, you can add transactional statements to ensure that the entirety of the script executes as an atomic transaction. You would insert a BEGIN TRANSACTION statement right after the USE [$(DatabaseName)]; GO statement and then a COMMIT TRANSACTION statement at the very end of the script.

    Two things to note, though: 1) the generated script must be executed from SQLCMD (you can't just execute it using a C# SqlConnection or even an SMO ServerConnection for instance), 2) I would suggest that if you manually wrap the script in a transaction, you skip the step of setting IncludeTransactionalScripts to true.