SSDT SQLCLR integration : a case of the cart before the horse

I’ve very recently needed to delete an XEvent session’s asynchronous file target as part of tearing down the session. Native support is non-existent. What better than a simple SQLCLR stored proc. Having first tried to accurately derive the file name from the session created time based on details by Jonathan Kehayias’ (blog) post an-xevent-a-day-6-of-31-targets-week-asynchronous_file_target, I eventually gave up for a number of reasons given later and opted for the simpler wildcard search:

1
2
3
4
5
6
7
8
[Microsoft.SqlServer.Server.SqlProcedure]
public static void DeleteXEventSessionFile(SqlString targetFilePath, SqlString sessionName)
{
   foreach (var f in Directory.GetFiles((string)targetFilePath, String.Format("{0}*.*", sessionName)))
   {
      File.Delete(f);
   }
}

Great, job done now to deploy via VS 2013 (SSDT) and things start to get tricky as expected.

(156,1): SQL72014: .Net SqlClient Data Provider: Msg 10327, Level 14, State 1, Line 1 
CREATE ASSEMBLY for assembly 'TheCartBeforeTheHorseClr' failed because assembly 'TheCartBeforeTheHorseClr' is not authorized for PERMISSION_SET = EXTERNAL_ACCESS.  
The assembly is authorized when either of the following is true: the database owner (DBO) has EXTERNAL ACCESS ASSEMBLY permission and the database has the TRUSTWORTHY database property on; 
or the assembly is signed with a certificate or an asymmetric key that has a corresponding login with EXTERNAL ACCESS ASSEMBLY permission.
(156,0): SQL72045: Script execution error.  The executed script:
CREATE ASSEMBLY [TheCartBeforeTheHorseClr]
    AUTHORIZATION [dbo]
    FROM 0x...
An error occurred while the batch was being executed.

One could set the target database to trustworthy and problem solved! The requirement however is for this work to be injected into other database deployments and setting these to trustworthy is not going to wash. A quick search gives the manual deployment steps required Deploying SQL CLR assembly using Asymmetric key.

The cart before the horse

a_cart_and_a_horse

SSDT only supports safe access, and to load an assembly requiring as in this case external access, one must first create an asymmetric key from the assembly, then a login from the asymmetric key and then assign external access rights to that login. Oh, and all these actions must be performed in master. Furthermore, the examples all show an asymmetric key being created from an assembly file available from the server instance – that is going to be interesting across all existing CI environments, not to mention future ones.

To be blunt – this sucks.

But wait a minute, how does SSDT deploy an assembly? What exactly is that binary string, can it be generated, and moreover used to create an asymmetric key? It turns out that binary looking string is a hex string and an asymmetric key can be created from a loaded assembly. The steps then are:

  1. create a hex string from an assembly dll
  2. inject the hex string into a SSDT pre-deploy script implementing the required configuration in master
  3. build the project
  4. deploy

Lets go through these steps in slightly more detail.

Create a hex string from an assembly dll

This is surprisingly straightforward. Another search led to this stackoverflow byte array to hex string answer and the resulting powershell script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[CmdletBinding()]
param 
(
    [parameter(Mandatory=$true,ValueFromPipeline=$true)]
    [string]$TheCartBeforeTheHorseDll,
    [parameter(Mandatory=$true,ValueFromPipeline=$true)]
    [string]$preDeployTemplate,
    [parameter(Mandatory=$true,ValueFromPipeline=$true)]
    [string]$preDeployOutput
)
 
# load pre-deploy template
$template = [System.IO.File]::ReadAllText($preDeployTemplate);
 
# get dll as byte array, convert to HEX string and output a pre deploy script based on the loaded template.
$bytes = [System.IO.File]::ReadAllBytes($TheCartBeforeTheHorseDll);
$sb = new-object "System.Text.StringBuilder" ($bytes.Length *2 +2);
$sb.Append("0x") | Out-Null;
 
foreach ($b in $bytes)
{
   $sb.AppendFormat("{0:X2}", $b) | Out-Null;
}
# template's placeholder is %assembly_hex% 
[System.IO.File]::WriteAllText($preDeployOutput, ($template -replace "%assembly_hex%", $sb.ToString()));

Inject the hex string into a SSDT pre-deploy script implementing the required configuration in master

With a powershell script to hand it, how to get around SSDT? Heading in the direction of more C#, my colleague Jagjit Thind (LinkedIn) reminded me of build events and that a powershell script is transparent and easier to maintain.

The problem however is since SSDT’s release, a CLR code file such as C# can simply be dropped in the project and an assembly automatically generated with default SQLCLR stored procs / functions created in the dbo schema. While this is very neat, it is problematic for a post-build event to inject a pre-deploy script, as the output dacpac has been created when the event fires. The solution is to revert to VS 2010 style integration and export the CLR code to a separate project and reference. In this way the referenced project’s post-build event is executed before the referencing project’s dacpac is created.

The IncludeCompositeProject property must be set TRUE and of course don’t forget the SQLCLR stored procs / functions must now be created manually in the referencing project.

Build the project

With all the above in place, build is just a right click away. To recap,

  1. The referenced SQLCLR# project is built first and its post-build event fired
  2. The post-build event using a template injects the assembly hex string and creates a pre-deploy script in the referencing project
  3. The referencing project build consumes the generated pre-deploy script for inclusion in its output dacpac

Deploy

As with build, publish is just a right click away. The results:

(master) loading assembly [TheCartBeforeTheHorseClr]
(master) creating asymmetric key [TheCartBeforeTheHorseClr_Key]
(master) creating login [TheCartBeforeTheHorseClr_Login]
(master) cleanup: removing assembly [TheCartBeforeTheHorseClr]
Creating [TheCartBeforeTheHorseClr]...
Creating [HorseAndCart].[DeleteXEventTargetFile]...
Update complete.

A quick test shows the procedure can be executed and in this example failing as expected since the target file does not exist:

sqlclr_proc_execution_results

And for the curious…

Why give up on accurately generating the long integer value appended to the asynchronous file target by the Extended Events Engine. The calculation is:

1
(Get-Date "ccyy-mm-dd hh:mm:ss.4").ToUniversalTime().Ticks - (Get-Date "1601-01-01").ToUniversalTime().Ticks;

The hard part is nailing the file creation time which as far as I can tell is not exposed. One can get close by a getdate():

1
2
3
alter event session YourXEventSessionName on server
      state = start;
select getdate();

In Summary

The solution presented here is about removing the manual steps required to deploy an assembly requiring elevated permissions into a SQL Server database. As my colleague Natalia Bastishcheva (LinkedIn) pointed out this should be used carefully and not as a means to get around security constraints. CLR strong names are not ideal and should be at minimum protected by a strong password, probably one that is known only to the DBA / Security Admin. Of course it goes without saying the underlying code is well tested and reviewed.

All the code presented is available here in GitHub.

Enjoy.