How To: Register CLR Assembly To SQL Server incl. Certificate creation

Following things are important to notice:

  • The VS Project should be set to Build | Optimize code
  • The VS Project must be set to Signning | Sign the assembly with Strong Name Key File ….snk
  • The VS Project must NOT set Signing | Delay sign only
    (this would lead to Msg 10314 Strong Name validation error)
  • Windows SDK with the Strong Name Components must be installed
  • The VS Project must be set to Build events  | Post-build event:
    “C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64\signtool.exe” sign /f “$(ProjectDir)YourPrivateCertificate.pfx” /p “YourPfxPassword” $(TargetDir)$(TargetFileName)

    Change the Version number according to your installed SDK^^.
-- Steps:
--	1. Replace Placeholders in this Script
--	2. Create Certificate/Private Key/PFX File
--	3. Adapt your C#/VB .NET Project with the SQL CLR Assembly to use the Certificate (PFX file).
--	4. Make changes to Server Instance to enable Common Language Runtime. Server-Instanz so einstellen, dass die strikte Security deaktiviert wird für CLR.
--	5. Create CLR Assembly in Sql Server.
--  6. (optionally) Drop old CLR Functions.
--	7. Create new CLR Functions.

/* This Article is based on input from: 
	https://github.com/MicrosoftDocs/sql-docs/issues/2052 (Comment of Solomon Rutzky)
	https://nielsberglund.com/2017/07/01/sqlclr-and-certificates/ (Niels Berglund)
	https://nielsberglund.com/2017/07/23/sql-server-2017-sqlclr---whitelisting-assemblies/ (Niels Berglund)
	Regards to all of you - thank you so much for your publishment!
*/

-- 1... 
		/* Replace each of the following Placeholders using Ctrl+H with your own values:
		DWH					->	Database name where the Assembly should be deployed to (excluding [])
		Win10Sdk64bitPath	->	Path to the installation of Windows 10 SDK 64bit:															C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64
		CertFolderPath		->	Path to the folder where the Certificate, Private Key and PFX files will be saved (excluding "").	e.g.:	C:\DWH\Certificates
		AssemblyFullPath	->	Full Path to the DLL file to create the CRL Assembly from (excluding "").							e.g.:	C:\DWH\SqlServerClrFunctionsLib.dll
		SqlClrAssemblyName	->	Name of Assembly under which it should be registered in your Database.								e.g.:	ClrFunctions
		CertAuthority		->	Canonical Name of the Certificate's Authority (excluding "")										e.g.:	CompanyName Authority
		CertName			->	Name of the Certificate file (excluding .cer, folders and excluding "")								e.g.:	Assemblies.CompanyName.Public
		CertPvkName			->	Name of the PVK file (excluding .pvk, folders and excluding "")										e.g.:	Assemblies.CompanyName.PrivateKey
		CertPfxName			->	Name of the PFX file (excluding .pfx, folders and excluding "")										e.g.:	Assemblies.CompanyName.PrivateKey
		CertPvkPassword		->	Your strong Private Key Password (excluding "")
		CertPfxPassword		->	Another (less-strong) Password to protect the PFX file (excluding "")
		*/

-- 2....
		-- Use Windows 10 SDK makecert.exe and pvk2pfx.exe to create a (public) Certificate, Private Key and export a PFX file:
		-- https://developer.microsoft.com/de-de/windows/downloads/windows-10-sdk/
		-- 1. "Win10Sdk64bitPath\makecert.exe" -r -pe -n "CN=CertAuthority" -a sha256 -sky signature -cy authority -sv "CertFolderPath\CertPvkName.pvk" -len 2048 -m 144 "CertFolderPath\CertName.cer"
		--		you will be prompted for a Private Key Password - enter: CertPvkName
		--
		-- 2. "Win10Sdk64bitPath\pvk2pfx.exe" -pvk "CertFolderPath\CertPvkName.pvk" -spc "CertFolderPath\CertName.cer" -pfx "CertFolderPath\CertPfxName.pfx" -pi "CertPvkName" -po "CertPfxPassword"

-- 3....

		-- 1. Copy previously created PFX file to the root of your C# .Net Project so that $(ProjectDir) matches the directory of the PFX file.
		-- 2. Sign DLL in Visual Studio using the created PFX file in step 2 by adding the following command to ---->>>> Post Build Event: <<<<<----
		--	"Win10Sdk64bitPath\signtool.exe" sign /f "$(ProjectDir)CertPfxName.pfx" /p "CertPfxPassword" $(TargetDir)$(TargetFileName)
		-- 3. Build Project
		--
		-- 4. Copy your signed DLL to "AssemblyFullPath" of the SQL Server.

-- 4....
		USE MASTER
		GO
		EXEC sp_configure 'show advanced options', 1;
		RECONFIGURE;
		GO
		EXEC sp_configure 'clr_enabled', 1
		RECONFIGURE
		GO	
		EXEC sp_configure 'clr strict security', 0;  
		RECONFIGURE;  
		GO
		-- Re-visit current server configuration (displays a list of all server settings):
		EXEC sp_configure	
		GO  

		/* Optional: Sometimes it might be necessary to change the Owner of the Database to the 'SA' (or 'dbo' or another Server Admin). 
		  -- This might be the case if your Database has been imported/restored from another server (or by another user).

			USE [DWH];
			--SELECT owner_sid FROM sys.databases WHERE database_id = DB_ID('DWH')
			SELECT name FROM sys.database_principals WHERE sid = (SELECT TOP 1 owner_sid FROM sys.databases WHERE database_id = DB_ID('DWH'));
		
			EXEC sp_changedbowner @loginame = 'sa';
		*/
-- 5....

		-- a.) Ensure, the SQL Server Service User (see services.msc and look out for your SQL Server Instance) 
		--		has Read & Execute Permissions to the Assembly File

	USE [DWH]; -- Important to set context to the Database where you want to register your DLL!
	GO
	CREATE ASSEMBLY SqlClrAssemblyName from 'AssemblyFullPath' WITH PERMISSION_SET = UNSAFE	-- use UNSAFE to be aligned with the Permission granted to the Certificate Login previsouly created
	GO
	-- In case of SQL Server Error Message 10134: It might be necessary to add your Assembly to the trusted_assemblies if your Functions won't execute...	
	/*
		SELECT * FROM sys.assemblies a WHERE a.name LIKE '%SqlClrAssemblyName%';
		-- Grab the BINARY of the Assembly file: SELECT content FROM sys.assembly_files WHERE name LIKE '%SqlClrAssemblyName%'
		DECLARE @hash varbinary(64), @clrName nvarchar(4000) 
			= (SELECT name FROM sys.assemblies a WHERE a.name LIKE '%SqlClrAssemblyName%');
		SELECT @hash = HASHBYTES('SHA2_512', (SELECT content FROM sys.assembly_files WHERE name LIKE '%SqlClrAssemblyName%'));
		SELECT @hash as Hash, @clrName as Name;
		EXEC  sp_add_trusted_assembly @hash = @hash, @description = @clrName;
		SELECT * FROM sys.trusted_assemblies
	*/

-- 6.....
	USE [DWH];
	GO
	BEGIN TRAN

		DROP FUNCTION IF EXISTS common.ToBit;
		-- tbd... DROP FUNCTION IF EXISTS ...

		-- in case, you run into trouble and need to re-create the Assembly, execute this after Dropping previously created functions...
		-- DROP ASSEMBLY SqlClrAssemblyName;

-- 7.....		
		GO
		CREATE FUNCTION common.ToBit(@value sql_variant,
			@valueIfNull bit,
			@valueIfConvertError bit) RETURNS bit
			EXTERNAL NAME SqlClrAssemblyName.Scalar.ToBit;
		GO

	COMMIT TRAN

/* After modifying the DLL it might be necessary to apply the following:
	ALTER ASSEMBLY SqlClrAssemblyName from 'AssemblyFullPath' WITH PERMISSION_SET = UNSAFE , UNCHECKED DATA;
	
	-- !! After changing the Certificate of the DLL, you might be required to update the HASH in the trusted_assemblies by re-invoking the Step 5. !!
	-- tbd... Create or Drop & re-create Functions...
*/

Following example C# Scalar.cs file within in Class Library Project and .NET 4.5.2 Target Framework

using System;
using System.Text;
using System.Data.SqlTypes;
using System.Collections;
using System.Globalization;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Common;
using SqlServerClrFunctionsLib;
using Microsoft.SqlServer.Server;

// TODO: For Unit Testing: Set the following options in this Project's Properties:
//      - Project | Build | Optimize Code => 0
//
// TODO: For Deployment to SQL Server: Set the following options in this Project's Properties:
//      - Project | Build | Optimize Code => 1

/// <summary>
/// Constraints for CLR Functions:
/// https://docs.microsoft.com/en-us/sql/relational-databases/user-defined-functions/create-user-defined-functions-database-engine?view=sql-server-2017
/// 
/// Data Type Mapping:
/// https://docs.microsoft.com/en-us/sql/relational-databases/clr-integration-database-objects-types-net-framework/mapping-clr-parameter-data?view=sql-server-2017
/// Extract: (SQL Server -> System.Data.SqlTypes.* -> .NET CLR Data Type
///     bigint -> SqlInt64 -> Int64, Nullable{Int64}
///     bit -> SqlBoolean -> Boolean, Nullable{Boolean}
///     date -> SqlDateTime -> DateTime, Nullable{DateTime}
///     decimal -> SqlDecimal -> Decimal, Nullable{Decimal}
///     float -> SqlDouble -> Double, Nullable{Double}
///     nvarchar -> SqlChars | SqlString -> String | Char[]         // SQLChars is a better match for data transfer and access, and SQLString is a better match for performing String operations.
///     nvarchar(1) -> SqlChars | SqlString -> Char | Nullable{char}
///     sql_variant -> None -> Object
/// </summary>
public class Scalar
{


    [SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic = true)]
    public static string LTrim(string value, string charsToTrim)
    {
        if (value == null || value == "")
            return value;
        else
        {
            return value.TrimStart(charsToTrim.ToCharArray());
        }
    }

    /// <summary>
    /// Description:	Konvertiert den übergebenen Wert @value in den entsprechenden Ja/Nein (Bit)-Wert (1 Bit).
    /// 				Es werden auch Strings mit Ja/Nein, J/N, Yes/No, Y/N, Si/No, True/False erkannt, sowie X/- erkannt.
    /// 
    /// 				Wenn @value NULL ist, wird @valueIfNull zurückgegeben.
    /// 				Wenn ein Konvertierungsfehler auftritt, wird @valueIfConvertError zurückgegeben.
    /// </summary>
    /// <param name="value"></param>
    /// <param name="valueIfNull"></param>
    /// <param name="valueIfConvertError"></param>
    /// <returns></returns>
    [SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic = true)]
    public static bool? ToBit(Object value, bool? valueIfNull, bool? valueIfConvertError)
    {
    convertStringAgain:
        try
        {
            long lngValue;

            if (value == DBNull.Value)
                return valueIfNull;
            else if (value.GetType() == typeof(SqlBoolean))
                return (bool?)((SqlBoolean)value).Value;
            else if (value is bool)
                return (bool)value;
            else if (value.GetType() == typeof(SqlString))
            {
                string s = ((SqlString)value).Value;
                if (s.StartsWithAny(StringComparison.CurrentCultureIgnoreCase, "J", "Y", "S", "T", "W", "1", "X"))
                    return true;
                else if (s.Length == 0 || s.StartsWithAny(StringComparison.CurrentCultureIgnoreCase, "N", "F", "0", "-", " "))
                    return false;
            }
            else if (value.GetType() == typeof(SqlChars))
            {
                char[] val = ((SqlChars)value).Value;
                char c = val.Length > 0 ? val[0] : ' ';
                if (c.In('J', 'Y', 'S', 'T', 'W', '1', 'j', 'y', 's', 't', 'w', 'X', 'x'))
                    return true;
                else if (c.In('N', 'F', '0', 'n', 'f', '-', ' '))
                    return false;
            }
            else if (value.GetType() == typeof(string))
            {
                value = new SqlString((string)value);
                goto convertStringAgain;
            }
            else if (long.TryParse(Convert.ToString(value), out lngValue))
                return lngValue != 0;

            return valueIfConvertError;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(string.Format("Exception when converting the value '{0}' of type '{1}' to Bool! Message: {2}", value, value.GetType().FullName, ex.Message));
        }

    }
	
}

Leave a Reply

Your email address will not be published. Required fields are marked *