Programming SQL Server CLR Integration
Programming SQL Server CLR Integration
Jurgen Postelmans
U2U nv/sa
March 3, 2004
Applies to:
Summary:
This article will give you an overview how you can write stored procedures, triggers and user-
defined functions in SQL Server "Yukon" using C# as a programming language.
Contents:
• Introduction
• Starting from a .NET Assembly
• Registering Class Libraries in SQL Server
• Registering User-Defined Functions in SQL Server
• .NET User-Defined Functions and the SqlFunction Attribute
• Registering .NET Stored Procedures in SQL Server
• Conclusion
• About the author
Introduction
One of the major new features of SQL Server "Yukon" is the integration of the .NET Framework
Common Language Runtime (CLR) into the SQL Server database engine. This means that you now
have the possibility to write your stored procedures, triggers and user-defined functions in managed
code. The languages currently supported are C#, Visual Basic .NET, managed C++ and JavaScript
.NET. Furthermore, SQL Server "Yukon" can only host the "Whidbey" version of the .NET
Framework. Previous versions of the .NET Framework are not supported. For performance reasons,
the .NET runtime is lazy loaded by SQL Server. This means that the .NET runtime will only be loaded
when it is really necessary, such as when you would execute a managed stored procedure for the
first time.
Whether or not you should implement your stored procedures and user-defined functions using .NET
code depends largely on what you do in those functions. If they contain a lot of procedural code
then writing them in .NET will typically make them faster and easier to implement. If, on the other
hand, you do a lot of data access in your functions, implementing them in T-SQL will typically yield
the best performance.
In the MathTutor project we will implement one class called Math with 3 simple functions. The initial
version of this class is shown below.
namespace MathTutor
{
public class Math
{
public static SqlInt32 AddNumbers(SqlInt32 i, SqlInt32 j)
{
return i + j;
}
try
{
number += by;
}
catch (Exception ex)
{
retValue = 1;
}
return retValue;
}
}
}
Note that to declare the parameters of the methods, you can either use the standard types provided
by the .NET Framework or their corresponding SqlTypes. If you know that your methods will only be
used in T-SQL it is preferable to use the SqlTypes which are defined in the System.Data.SqlTypes
namespace. The SqlTypes behave in the same way as the built-in SQL Server data types, especially
when you're working with NULL values.
Registering Class Libraries in SQL Server
Once the Class Library is compiled we need to perform two steps in SQL Server "Yukon" to expose
the methods in our MathTutor class library:
These steps are mandatory because SQL Server "Yukon" cannot execute any arbitrary managed
code that you might have in your assemblies.
The first step is accomplished using the CREATE ASSEMBLY statement as shown below.
The CREATE ASSEMBLY statement registers and loads an assembly in SQL Server. In the FROM
clause you specify the location of the assembly you want to register. If this assembly depends on
other assemblies you will also need to register these. Once the assembly is registered the original
assemblies on the file system are not used anymore. All the registration information is stored in 3
SQL Server system tables. These are called sys.assemblies, sys.assembly_files and
sys.assembly_modules.
When you load an assembly in SQL Server you have the possibility to specify a security level for you
managed code. This is done using the WITH PERMISSION_SET parameter as shown below.
When the permission set is set to safe, which is the default, the managed code in our assembly
cannot access any external resources like the file system, registry, network...
Once the user-defined functions are registered we can call them using the same syntax as you
would use for T-SQL user-defined functions.
SELECT dbo.AddNumbers(10,20)
-----------
30
(1 row(s) affected)
If your user-defined functions do not execute as expected you can debug them by attaching the
Visual Studio .NET debugger to the SQL Server process. This process is called SqlServr.exe as show
in the picture below.
Figure 1: Attaching the Visual Studio .NET debugger to the SQL Server process
Once the debugger is attached to the SQL Server process, execute your user-defined function in the
SQL Workbench. The breakpoint in your Visual Studio .NET project will be hit and you can debug
your source code.
As explained previously, you can either use standard types or SqlTypes for the declaration of the
parameters or return value of your user-defined functions. The main difference between them lies in
the way NULL values are handled. If you pass a NULL value to a standard type parameter an
exception is generated. This is due to the fact that standard types like Int32 are not nullable. If you
execute the following line of code
SELECT dbo.SubtractNumbers(10,null)
namespace MathTutor
{
public class Math
{
[SqlFunction(DataAccess = DataAccessKind.None,
SystemDataAccess = SystemDataAccessKind.None,
IsDeterministic = true, IsPrecise = true)]
public static SqlInt32 AddNumbers(SqlInt32 i, SqlInt32 j)
{
return i + j;
}
[SqlFunction(DataAccess = DataAccessKind.None,
SystemDataAccess = SystemDataAccessKind.None,
IsDeterministic = true,IsPrecise = true)]
public static int SubtractNumbers(int i, int j)
{
return i - j;
}
try
{
number += by;
}
catch (Exception ex)
{
retValue = 1;
}
return retValue;
}
}
}
Also note that the IsDeterministic property is used on the SqlFunction attribute to mark the function
as being deterministic. Deterministic functions always return the same result any time they are
called with a specific set of input values.
User-defined functions can not only be used in SELECT statements but also for the definition of
computed fields in a table. In the following table there is a field called Addition that contains the
result of the execution on the AddNumbers user-defined function.
The PERSISTED keyword tells SQL Server to physically store the computed values in the field of the
table. The value in the computed field will automatically be updated whenever one of the dependent
fields change.
Since the Addition field is persisted and since the user-defined function is marked as deterministic
you could create an index on this computed field.
This would not be possible is AddNumbers was not deterministic since the return value of the
AddNumbers function would be different every time it's called wilh a specific set of input values.
To execute the stored procedure "IncrementBy" the following T-SQL code can be used.
Conclusion
In this first article we covered the integration between the .NET Framework and SQL Server
"Yukon". You saw how stored procedures, user-defined functions and triggers can be created in .NET
and used within SQL Server "Yukon".
Jurgen Postelmans
U2U nv/sa
April 7, 2004
Applies to:
Summary:
Learn how you can access database objects from within stored procedures or functions using
managed code and the SqlServer Data Provider in SQL Server 2005.
Contents:
• Introduction
• The SqlContext object
• Sending data to the client using the SqlPipe object
• Server-side cursors and the SqlResultSet object
• .NET User-defined functions and Security
• Conclusion
• About the author
Introduction
In the previous article I showed how you can write stored procedures and functions in managed
code and how to use them in T-SQL. What I didn't cover was how you can access database objects
in managed stored procedures or functions. For this purpose SQL Server 2005 ships with a new
managed ADO.NET provider called the SqlServer Data Provider (as opposed to the SqlClient Data
Provider, which shipped since the .NET Framework v1.0).
This managed provider is an in-process provider that is used to directly communicate from the
Common Language Runtime (CLR) to SQL Server. The SQL Server in-process provider can only
connect to the SQL Server that hosts the CLR. It cannot be used to connect to any other SQL Server
you might have running on the network. The SqlServer Data Provider is implemented in the
System.Data.SqlServer namespace.
A first example of the usage of the SqlContext object is shown in the code below. This code is part
of a .NET Class Library called NorthwindDAL.
using System;
using System.Data.SqlTypes;
using System.Data.SqlServer;
using System.Data.Sql;
namespace NorthwindDAL
{
public class Northwind
{
[SqlFunction(DataAccess = DataAccessKind.Read)]
public static SqlString GetProductNameByID(SqlInt32
productdID)
{
SqlCommand cmd = SqlContext.GetCommand();
cmd.CommandText = "select ProductName " +
"from Products where " +
"ProductID = '" + productdID.ToString() +
"'";
return (string) cmd.ExecuteScalar();
}
}
}
The static GetCommand function of the SqlContext object is used to create a SqlCommand object.
This SqlCommand object is initialized with the select statement we want to execute. On the last line
the select statement is executed using the ExecuteScalar function and the result is returned to the
caller.
Before the user-defined function can be made available, the assembly in which it resides must be
registered in SQL Server. This is done using the CREATE ASSEMBLY statement as explained in the
previous article.
Next we need to register the user-defined function using the CREATE FUNCTION statement.
----------------------------------------
Chai
(1 row(s) affected)
Once the stored procedure is registered, it can be executed from the client and the resulting string
'Hello world from .NET' will be returned.
EXEC HelloWorld
To get an SqlPipe object, the static GetPipe method is first called on the SqlContext object. Once the
SqlPipe object is obtained, the Send method can be used to transmit data to the calling client. The
Send method has 4 overloaded versions that can be used to either send a String, SqlError,
SqlDataReader or a SqlResultSet to the client.
GO
EXEC GetProductsByCategoryDataReader 1
ProductID ProductName
----------- ----------------------------------------
17 Alice Mutton
3 Aniseed Syrup
40 Boston Crab Meat
60 Camembert Pierrot
...
SqlResultSet rs = cmd.ExecuteResultSet(
ResultSetOptions.Scrollable | ResultSetOptions.Updatable);
SqlPipe pipe = SqlContext.GetPipe();
pipe.Send(rs);
}
The ExecuteResultSet method returns a fully scrollable and updateable cursor to the client.
In the next example a stored procedure called UpdatePrices is created. This stored procedure will
update the prices of certain products with an specific amount that is passed in as a parameter.
while(rs.Read())
{
//retrieve the ProductID column and
//if it is 1 update the price
if (rs.GetInt32(3) == 1)
{
//Update the UnitPrice field
rs.SetSqlMoney(5, rs.GetSqlMoney(5) + amount);
//Update the database
rs.Update();
}
}
rs.Close();
}
Again we create a SqlResultSet that represent a scrollable and updatable cursor. We loop over every
record that is in the resultset, and if necessary we use the SetSqlMoney method to update the
UnitPrice field if necessary.
[SqlFunction(DataAccess=DataAccessKind.None)]
public static SqlString ReadFromFile(SqlString filename)
{
StreamReader reader = File.OpenText(filename.ToString());
string content = reader.ReadToEnd();
reader.Close();
return (SqlString)content;
}
If the assembly in which this user-defined function resides is registered using the default CREATE
ASSEMBLY statement you will receive a security exception upon execution of the user-defined
function.
GO
.Net SqlClient Data Provider: Msg 6522, Level 16, State 1, Procedure
ReadFromFile, Line 0
A CLR error occurred during execution of 'ReadFromFile':
System.Security.SecurityException: Request failed.
at NorthwindDAL.Northwind.ReadFromFile(SqlString filename) +0.
This exception is generated because the default permission set under which all .NET code runs
inside SQL Server is set to SAFE. This means that no external resources like files can be accessed.
In order to correctly register our .NET assembly we need to execute the following T-SQL statement:
The EXTERNAL_ACCESS permission set grants the NorthwindDAL assembly access to external
resources like the file system.
GO
SELECT dbo.ReadFromFile('c:\data.txt')
Execution of the user-defined function returns the content of the file specified in the ReadFromFile
call.
----------------------------------------------------------------------
Contents of a simple file...
(1 row(s) affected)
Conclusion
In this second article you saw how you can use the SQLServer Data Provider to access database
objects in your managed user-defined functions and stored procedures. The SqlServer Data Provider
runs inside the SQL Server 2005 database engine and provides direct access to all database objects.