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