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));
        }

    }
	
}

Performance IDENTITY vs SEQUENCE vs GUID

ich habe noch ein wenig nachgeforscht und nun eine gute Erklärung gefunden, warum GUIDs schneller sind als IDENTITYs…

Die Methode NEWSEQUENTIALID() ist ein Wrapper einer Windows API Methode (UuidCreateSequential).

Siehe hier: https://msdn.microsoft.com/de-de/library/ms189786%28v=sql.120%29.aspx und https://msdn.microsoft.com/de-de/library/aa379322%28VS.85%29.aspx

Daher wird auch innerhalb des SQL Servers kein Caching (= Logging) der zuletzt benutzten ID vorgenommen.

Bei IDENTITY ist hard kodiert ein Caching für jeden 10. Wert vorgesehen.

Daher ist hier ein großes Caching-Aufkommen = Logging vorhanden.

Bei einem Restore der DB prüft die Engine den letzten eingetragenen Wert und nimmt den nächsten verfügbaren (erzeugt kein Gap in den IDs).

Zumindest in der Theorie. In der Praxis gibt es ja den Bug mit dem Reseed, bei dem Sie sogar einen Kommentar im connect hinterlassen haben J.

Bei SEQUENCEs ist der Default-Wert fürs Caching 50.

Daher ist jede Sequence bei 1 Mio Zeilen schon mal etwa 1-2 Sekunden schneller als IDENTITY.

Wenn der Wert fürs Caching maximiert wird, dürften Sequences genau so schnell wie GUIDs werden.

Bei einem Restore der DB ist ein (großer) Gap bei den IDs zu erwarten (und zwar entsprechend der nicht genutzten Werte des noch offenen Caches).

Ein guter Artikel der die Performance der SEQUENCEs im Vergleich zu IDENTITYs beleuchtet ist hier:

http://sqlmag.com/sql-server/sequences-part-2

Hier ist noch ein weiterer interssanter Artikel: http://blogs.msdn.com/b/sqlserverfaq/archive/2010/05/27/guid-vs-int-debate.aspx

Test scenario for GUIDs vs. IDENTITY vs. SEQUENCE

See also…

http://www.sqlskills.com/blogs/kimberly/disk-space-is-cheap/

http://www.codeproject.com/Articles/32597/Performance-Comparison-Identity-x-NewId-x-NewSeque

RML Utility for parallel queries… https://support.microsoft.com/en-us/kb/944837

Set up a Table with 6.7 Mio Rows with Sales data (real scenario table).

Add Clustered Index with:

          ID (UUID/GUID/SEQUENCE/IDENTITY)

Add 2 Non-Cl. Indexes for other columns.

Use Foreign Keys to Customer

Foreign Key for SalesDetails to new Sales table.

Set Database and Log File Size to 5GB.

Try different Fill Factors (100 vs. 90 vs. 80 vs. 70 vs. 60 vs. 50 vs. 40 vs. 30 vs. 20)

          – Insert 100.000 Rows using one Batch

          – Insert 1.000 Rows in 100 concurrent sessions at once

  • Measure inserting
  • Measure Selecting using an INNER JOIN on SalesDetails and Customer

Measure IOs using SQL Server Profiler (TSQL_Duration):