Tải bản đầy đủ (.pdf) (36 trang)

SQLCLR - Architecture and Design Considerations

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (8.74 MB, 36 trang )

C H A P T E R 7

  

SQLCLR: Architecture and
Design Considerations
When Microsoft first announced that SQL Server would host the .NET Common Language Runtime
(CLR) back in SQL Server 2005, it created a lot of excitement in the database world. Some of that
excitement was enthusiastic support voiced by developers who envisaged lots of database scenarios that
could potentially benefit from the methods provided by the .NET Base Class Library. However, there was
also considerable nervousness and resistance from DBAs concerned about the threats posed by the new
technology and the rumors that rogue developers would be able to create vast worlds of DBA-
impenetrable, compiled in-process data access code.
When it came to it, SQLCLR integration turned out to be neither such a scary nor such a useful idea
as many thought. Those hoping to use the SQLCLR features as a wholesale replacement for T-SQL were
quickly put off by the fact that writing CLR routines generally requires more code, and performance and
reliability suffer due to the continual cost of marshaling data across the CLR boundaries. And for the
DBAs who were not .NET developers to begin with, there was a somewhat steep learning curve involved
for a feature that really didn’t have a whole lot of uses.
We’ve been living with SQLCLR for over four years now, and although it appears that CLR
integration features are still not being used that heavily, their adoption is certainly growing. SQL Server
2008 lifts the previous restriction that constrained CLR User-Defined Types (UDTs) to hold a maximum
of only 8KB of data, which seriously crippled many potential usage scenarios; all CLR UDTs may now
hold up to a maximum 2GB of data in a single item. This opens up lots of potential avenues for new
types of complex object-based data to be stored in the database, for which SQLCLR is better suited than
the predominantly set-based T-SQL engine. Indeed, SQL Server 2008 introduces three new system-
defined datatypes (geometry, geography, and hierarchyid) that provide an excellent demonstration of the
ways in which SQLCLR can extend SQL Server to efficiently store and query types of data beyond the
standard numeric and character-based data typically associated with SQL databases.
I will cover the system-defined CLR datatypes in detail in Chapters 10 and 12, which discuss spatial
data and hierarchical data, respectively. This chapter, however, concentrates on design and


performance considerations for exploiting user-defined functions based on managed code in SQL
Server, and discussion of when you should consider using SQLCLR over more traditional T-SQL
methods. It is my opinion that the primary strength of SQLCLR integration is in the ability to both move
and share code between tiers—so this chapter’s primary focus is on maintainability and reuse scenarios.
 Note This chapter assumes that you are already familiar with basic SQLCLR topics, including how to create and
deploy functions and catalog new assemblies, in addition to the C# programming language.
159
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
Bridging the SQL/CLR Gap: The SqlTypes Library
The native datatypes exposed by the .NET Framework and by SQL Server are in many cases similar, but
generally incompatible. A few major issues come up when dealing with SQL Server and .NET
interoperability from the perspective of data types:
• First and foremost, all native SQL Server data types are nullable—that is, an
instance of any given type can either hold a valid value in the domain of the type
or represent an unknown (NULL). Types in .NET generally do not support this idea
(note that C#’s null and VB .NET’s nothing are not the same as SQL Server’s NULL).
Even though the .NET Framework supports nullable types for value type variables,
these do not behave in the same way as their SQL Server equivalents.
• The second difference between the type systems has to do with implementation.
Format, precision, and scale of the types involved in each system differ
dramatically. For example, .NET’s DateTime type supports a much larger range and
much greater precision than does SQL Server’s datetime type.
• The third major difference has to do with runtime behavior of types in
conjunction with operators. For example, in SQL Server, virtually all operations
involving at least one NULL instance of a type results in NULL. However, this is not
the same behavior as that of an operation acting on a null value in .NET. Consider
the following T-SQL:
DECLARE @a int = 10;
DECLARE @b int = null;
IF (@a != @b)

PRINT 'test is true';
ELSE
PRINT 'test is false';
The result of any comparison to a NULL value in T-SQL is undefined, so the
preceding code will print “test is false.” However, consider the equivalent function
implemented using nullable int types in C# (denoted by the ? character after the
type declaration):
int? a = 10;
int? b = null;
if (a != b)
Console.Write("test is true");
else
Console.Write("test is false");
In .NET, the comparison between 10 and null takes place, resulting in the code
printing “test is true.” In addition to nullability, differences may result from
handling overflows, underflows, and other potential errors inconsistently. For
instance, adding 1 to a 32-bit integer with the value of 2147483647 (the maximum
32-bit integer value) in a .NET language may result in the value “wrapping
around,” producing -2147483648. In SQL Server, this behavior will never occur—
instead, an overflow exception will result.
In order to provide a layer of abstraction between the two type paradigms, the .NET Framework
ships with a namespace called System.Data.SqlTypes. This namespace includes a series of structures
160
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
that map SQL Server types and behaviors into .NET. Each of these structures implements nullability
through the INullable interface, which exposes an IsNull property that allows callers to determine
whether a given instance of the type is NULL. Furthermore, these types conform to the same range,
precision, and operator rules as SQL Server’s native types.
Properly using the SqlTypes types is, simply put, the most effective way of ensuring that data
marshaled into and out of SQLCLR routines is handled correctly by each type system. It is my

recommendation that, whenever possible, all methods exposed as SQLCLR objects use SqlTypes types as
both input and output parameters, rather than standard .NET types. This will require a bit more
development work up front, but it should future-proof your code to some degree and help avoid type
incompatibility issues.
Wrapping Code to Promote Cross-Tier Reuse
One of the primary selling points for SQLCLR integration, especially for shops that use the .NET
Framework for application development, is the ability to move or share code easily between tiers when it
makes sense to do so. It’s not so easy, however, to realize that objective.
The Problem
Unfortunately, some of the design necessities of working in the SQLCLR environment do not translate
well to the application tier, and vice versa. One such example is use of the SqlTypes described in the
preceding section; although it is recommended that they be used for all interfaces in SQLCLR routines,
that prescription does not make sense in the application tier, because the SqlTypes do not support the
full range of operators and options that the native .NET types support. Using them in every case might
make data access simple, but would rob you of the ability to do many complex data manipulation tasks,
and would therefore be more of a hindrance than a helpful change.
Rewriting code or creating multiple versions customized for different tiers simply does not promote
maintainability. In the best-case scenario, any given piece of logic used by an application should be
coded in exactly one place—regardless of how many different components use the logic or where it’s
deployed. This is one of the central design goals of object-oriented programming, and it’s important to
remember that it also applies to code being reused inside of SQL Server.
One Reasonable Solution
Instead of rewriting routines and types to make them compatible with the SqlTypes and implement
other database-specific logic, I recommend that you get into the habit of designing wrapper methods
and classes. These wrappers should map the SqlTypes inputs and outputs to the .NET types actually
used by the original code, and call into the underlying routines via assembly references. Wrappers are
also a good place to implement database-specific logic that may not exist in routines originally designed
for the application tier.
In addition to the maintainability benefits for the code itself, creating wrappers has a couple of
other advantages. First of all, unit tests will not need to be rewritten—the same tests that work in the

application tier will still apply in the data tier (although you may want to write secondary unit tests for
the wrapper routines). Secondly—and perhaps more importantly—wrapping your original assemblies
can help maintain a least-privileged coding model and enhance security, as is discussed later in this
chapter in the sections “Working with Code Access Security Privileges” and “Working with Host
Protection Privileges.”
161
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
A Simple Example: E-Mail Address Format Validation
It is quite common for web forms to ask for your e-mail address, and you’ve no doubt encountered
forms that tell you if you’ve entered an e-mail address that does not comply with the standard format
expected. This sort of validation provides a quicker—but less effective—way to test an e-mail address
than actually sending an e-mail and waiting for a response, and it gives the user immediate feedback if
something is obviously incorrect.
In addition to using this logic for front-end validation, it makes sense to implement the same
approach in the database in order to drive a CHECK constraint. That way, any data that makes its way to
the database—regardless of whether it already went through the check in the application—will be
double-checked for correctness.
Following is a simple .NET method that uses a regular expression to validate the format of an e-mail
address:
public static bool IsValidEmailAddress(string emailAddress)
{
//Validate the e-mail address
Regex r =
new Regex(@"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*");

return (r.IsMatch(emailAddress));
}
This code could, of course, be used as-is in both SQL Server and the application tier—using it in SQL
Server would simply require loading the assembly and registering the function. But this has some issues,
the most obvious of which is the lack of proper NULL handling. As-is, this method will return an

ArgumentException when a NULL is passed in. Depending on your business requirements, a better choice
would probably be either NULL or false. Another potential issue occurs in methods that require slightly
different logic in the database vs. the application tier. In the case of e-mail validation, it’s difficult to
imagine how you might enhance the logic for use in a different tier, but for other methods, such
modification would present a maintainability challenge.
The solution is to catalog the assembly containing this method in SQL Server, but not directly
expose the method as a SQLCLR UDF. Instead, create a wrapper method that uses the SqlTypes and
internally calls the initial method. This means that the underlying method will not have to be modified
in order to create a version that properly interfaces with the database, and the same assembly can be
deployed in any tier. Following is a sample that shows a wrapper method created over the
IsValidEmailAddress method, in order to expose a SQLCLR UDF version that properly supports NULL
inputs and outputs. Assume that I’ve created the inner method in a class called UtilityMethods and have
also included a using statement for the namespace used in the UtilityMethods assembly.
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlBoolean IsValidEmailAddress(
SqlString emailAddress)
{
// Return NULL on NULL input
if (emailAddress.IsNull)
return (SqlBoolean.Null);

bool isValid = UtilityMethods.IsValidEmailAddress(emailAddress.Value);
return (new SqlBoolean(isValid));
}
162
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
Note that this technique can be used not only for loading assemblies from the application tier into
SQL Server, but also for going the other way—migrating logic back out of the data tier. Given the nature
of SQLCLR, the potential for code mobility should always be considered, and developers should consider
designing methods using wrappers even when creating code specifically for use in the database—this

will maximize the potential for reuse later, when or if the same logic needs to be migrated to another tier,
or even if the logic needs to be reused more than once inside of the data tier itself.
Cross-assembly references have other benefits as well, when working in the SQLCLR environment.
By properly leveraging references, it is possible to create a much more robust, secure SQLCLR solution.
The following sections introduce the security and reliability features that are used by SQLCLR, and show
how to create assembly references that exploit these features to manage security on a granular level.
SQLCLR Security and Reliability Features
Unlike stored procedures, triggers, UDFs, and other types of code modules that can be exposed within
SQL Server, a given SQLCLR routine is not directly related to a database, but rather to an assembly
cataloged within the database. Cataloging of an assembly is done using SQL Server’s CREATE ASSEMBLY
statement, and unlike their T-SQL equivalents, SQLCLR modules get their first security restrictions not
via grants, but rather at the same time their assemblies are cataloged. The CREATE ASSEMBLY statement
allows the DBA or database developer to specify one of three security and reliability permission sets that
dictate what the code in the assembly is allowed to do.
The allowed permission sets are SAFE, EXTERNAL_ACCESS, and UNSAFE. Each increasingly permissive
level includes and extends permissions granted by lower permission sets. The restricted set of
permissions allowed for SAFE assemblies includes limited access to math and string functions, along with
data access to the host database via the context connection. The EXTERNAL_ACCESS permission set adds
the ability to communicate outside of the SQL Server instance, to other database servers, file servers,
web servers, and so on. And the UNSAFE permission set gives the assembly the ability to do pretty much
anything—including running unmanaged code.
Although exposed as only a single user-controllable setting, internally each permission set’s rights
are actually enforced by two distinct methods:
• Assemblies assigned to each permission set are granted access to perform certain
operations via .NET’s Code Access Security (CAS) technology.
• At the same time, access is denied to certain operations based on checks against
a.NET 3.5 attribute called HostProtectionAttribute (HPA).
On the surface, the difference between HPA and CAS is that they are opposites: CAS permissions
dictate what an assembly can do, whereas HPA permissions dictate what an assembly cannot do. The
combination of everything granted by CAS and everything denied by HPA makes up each of the three

permission sets.
Beyond this basic difference is a much more important distinction between the two access control
methods. Although violation of a permission enforced by either method will result in a runtime
exception, the actual checks are done at very different times. CAS grants are checked dynamically at
runtime via a stack walk performed as code is executed. On the other hand, HPA permissions are
checked at the point of just-in-time compilation—just before calling the method being referenced.
To observe how these differences affect the way code runs, a few test cases will be necessary, which
are described in the following sections.
163
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
 Tip You can download the source code of the examples in this chapter, together with all associated project files
and libraries, from the Source Code/Download area of the Apress web site,
www.apress.com
.
Security Exceptions
To begin with, let’s take a look at how a CAS exception works. Create a new assembly containing the
following CLR stored procedure:
[SqlProcedure]
public static void CAS_Exception()
{
SqlContext.Pipe.Send("Starting...");

using (FileStream fs =
new FileStream(@"c:\b.txt", FileMode.Open))
{
//Do nothing...
}

SqlContext.Pipe.Send("Finished...");


return;
}
Catalog the assembly as SAFE and execute the stored procedure. This will result in the following
output:
Starting...
Msg 6522, Level 16, State 1, Procedure CAS_Exception, Line 0
A .NET Framework error occurred during execution of user-defined routine or
aggregate "CAS_Exception":
System.Security.SecurityException: Request for the permission of type
'System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
System.Security.SecurityException:
at System.Security.CodeAccessSecurityEngine.Check(Object demand,
164
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
StackCrawlMark& stackMark, Boolean isPermSet)
at System.Security.CodeAccessPermission.Demand()
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32
rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options,
SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy)
at System.IO.FileStream..ctor(String path, FileMode mode)
at udf_part2.CAS_Exception()
.
The exception thrown in this case is a SecurityException, indicating that this was a CAS violation (of
the FileIOPermission type). But the exception is not the only thing that happened; notice that the first
line of the output is the string “Starting...” which was output by the SqlPipe.Send method used in the
first line of the stored procedure. So before the exception was hit, the method was entered and code
execution succeeded until the actual permissions violation was attempted.
 Note File I/O is a good example of access to a resource—local or otherwise—that is not allowed within the
context connection. Avoiding this particular violation using the SQLCLR security buckets would require cataloging

the assembly using the
EXTERNAL_ACCESS
permission.
Host Protection Exceptions
To see how HPA exceptions behave, let’s repeat the same experiment described in the previous section,
this time with the following stored procedure (again, cataloged as SAFE):
[SqlProcedure]
public static void HPA_Exception()
{
SqlContext.Pipe.Send("Starting...");

//The next line will throw an HPA exception...
Monitor.Enter(SqlContext.Pipe);

//Release the lock (if the code even gets here)...
Monitor.Exit(SqlContext.Pipe);

SqlContext.Pipe.Send("Finished...");
165
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
return;
}
Just like before, an exception occurs. But this time, the output is a bit different:
Msg 6522, Level 16, State 1, Procedure HPA_Exception, Line 0
A .NET Framework error occurred during execution of user-defined routine or
aggregate "HPA_Exception":
System.Security.HostProtectionException: Attempted to perform an operation that
was forbidden by the CLR host.

The protected resources (only available with full trust) were: All

The demanded resources were: Synchronization, ExternalThreading

System.Security.HostProtectionException:
at System.Security.CodeAccessSecurityEngine.ThrowSecurityException(Assembly
asm,
PermissionSet granted, PermissionSet refused, RuntimeMethodHandle rmh,
SecurityAction action, Object demand, IPermission permThatFailed)
at System.Security.CodeAccessSecurityEngine.ThrowSecurityException(Object
assemblyOrString, PermissionSet granted, PermissionSet refused, RuntimeMethodHandle
rmh, SecurityAction action, Object demand, IPermission permThatFailed)
at System.Security.CodeAccessSecurityEngine.CheckSetHelper(PermissionSet
grants,
PermissionSet refused, PermissionSet demands, RuntimeMethodHandle rmh, Object
assemblyOrString, SecurityAction action, Boolean throwException)
166
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
at System.Security.CodeAccessSecurityEngine.CheckSetHelper(CompressedStack
cs,
PermissionSet grants, PermissionSet refused, PermissionSet demands,
RuntimeMethodHandle rmh, Assembly asm, SecurityAction action)
at udf_part2.HPA_Exception()
.
Unlike when executing the CAS_Exception stored procedure, this time we do not see the “Starting...”
message, indicating that the SqlPipe.Send method was not called before hitting the exception. As a
matter of fact, the HPA_Exception method was not ever entered at all during the code execution phase
(you can verify this by attempting to set a breakpoint inside of the function and starting a debug session
in Visual Studio). The reason that the breakpoint can’t be hit is that the permissions check was
performed and the exception thrown immediately after just-in-time compilation.
You should also note that the wording of the exception has a different tone than in the previous
case. The wording of the CAS exception is a rather benign “Request for the permission ... failed.” On the

other hand, the HPA exception carries a much sterner warning: “Attempted to perform an operation that
was forbidden.” This difference in wording is not accidental. CAS grants are concerned with security—to
keep code from being able to access something protected because it’s not supposed to have access. HPA
permissions, on the other hand, are concerned with server reliability and keeping the CLR host running
smoothly and efficiently. Threading and synchronization are considered potentially threatening to
reliability and are therefore limited to assemblies marked as UNSAFE.
 Note Using a .NET disassembler (such as Red Gate Reflector,
www.red-gate.com/products/reflector/
), it is
possible to explore the Base Class Library to see which HPA attributes are assigned to various classes and
methods. For instance, the
Monitor
class is decorated with the following attributes that control host access:
[ComVisible(true), HostProtection(SecurityAction.LinkDemand, Synchronization=true,
ExternalThreading=true)]
.
A full list of what is and is not allowed based on the CAS and HPA models is beyond the scope of this
chapter, but is well documented by Microsoft. Refer to the following MSDN topics:
• Host Protection Attributes and CLR Integration Programming
(
• CLR Integration Code Access Security ( />us/library/ms345101.aspx)
167
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
The Quest for Code Safety
You might be wondering why I’m covering the internals of the SQLCLR permission sets and how their
exceptions differ, when fixing the exceptions is so easy: simply raise the permission level of the
assemblies to EXTERNAL_ACCESS or UNSAFE and give the code access to do what it needs to do. The fact is,
raising the permission levels will certainly work, but by doing so you may be circumventing the security
policy, instead of working with it to make your system more secure.
As mentioned in the previous section, code access permissions are granted at the assembly level

rather than the method or line level. Therefore, raising the permission of a given assembly in order to
make a certain module work can actually affect many different modules contained within the assembly,
giving them all enhanced access. Granting additional permissions on several modules within an
assembly can in turn create a maintenance burden: if you want to be certain that there are no security
problems, you must review each and every line of code in every module in the assembly to make sure it’s
not doing anything it’s not supposed to do—you can no longer trust the engine to check for you.
You might now be thinking that the solution is simple: split up your methods so that each resides in
a separate assembly, and then grant permissions that way. Then each method really will have its own
permission set. But even in that case, permissions may not be granular enough to avoid code review
nightmares. Consider a complex 5,000-line module that requires a single file I/O operation to read some
lines from a text file. By giving the entire module EXTERNAL_ACCESS permissions, it can now read the lines
from that file. But of course, you still have to check all of the 4,999 remaining code lines to make sure
they’re not doing anything unauthorized.
Then there is the question of the effectiveness of manual code review. Is doing a stringent review
every time any change is made enough to ensure that the code won’t cause problems that would be
detected by the engine if the code was marked SAFE? And do you really want to have to do a stringent
review before deployment every time any change is made? In the following section, I will show you how
to eliminate many of these problems by taking advantage of assembly dependencies in your SQLCLR
environment.
Selective Privilege Escalation via Assembly References
In an ideal world, SQLCLR module permissions could be made to work like T-SQL module permissions
as described in Chapter 5: outer modules would be granted the least possible privileges, but would be
able to selectively and temporarily escalate their privileges in order to perform certain operations that
require more access. This would lessen the privileged surface area significantly, which would mean that
there would be less need to do a stringent security review on outer (less-privileged) module layers, which
undoubtedly constitute the majority of code written for a given system—the engine would make sure
they behave.
The general solution to this problem is to split up code into separate assemblies based on
permissions requirements, but not to do so without regard for both maintenance overhead and reuse.
For example, consider the 5,000-line module mentioned in the previous section, which needs to read a

few lines from a text file. The entire module could be granted a sufficiently high level of privileges to read
the file, or the code to read the file could be taken out and placed into its own assembly. This external
assembly would expose a method that takes a file name as input and returns a collection of lines. As I’ll
show in the following sections, this solution would let you catalog the bulk of the code as SAFE yet still do
the file I/O operation. Plus, future modules that need to read lines from text files could reference the
same assembly, and therefore not have to reimplement this logic.
The encapsulation story is, alas, not quite as straightforward as creating a new assembly with the
necessary logic and referencing it. Due to the different behavior of CAS and HPA exceptions, you might
have to perform some code analysis in order to properly encapsulate the permissions of the inner
168
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
modules. In the following sections, I’ll cover each of the permission types separately in order to illustrate
how to design a solution.
Working with Host Protection Privileges
A fairly common SQLCLR pattern is to create static collections that can be shared among callers.
However, as with any shared data set, proper synchronization is essential in case you need to update
some of the data after its initial load. From a SQLCLR standpoint, this gets dicey due to the fact that
threading and synchronization require UNSAFE access—granting such an open level of permission is not
something to be taken lightly.
For an example of a scenario that might make use of a static collection, consider a SQLCLR UDF
used to calculate currency conversions based on exchange rates:
[SqlFunction]
public static SqlDecimal GetConvertedAmount(
SqlDecimal InputAmount,
SqlString InCurrency,
SqlString OutCurrency)
{
//Convert the input amount to the base
decimal BaseAmount =
GetRate(InCurrency.Value) *

InputAmount.Value;

//Return the converted base amount
return (new SqlDecimal(
GetRate(OutCurrency.Value) * BaseAmount));
}
The GetConvertedAmount method internally makes use of another method, GetRate:
private static decimal GetRate(string Currency)
{
decimal theRate;
rwl.AcquireReaderLock(100);

try
{
theRate = rates[Currency];
}
finally
{
rwl.ReleaseLock();
}

return (theRate);
}
GetRate performs a lookup in a static generic instance of Dictionary<string, decimal>, called
rates. This collection contains exchange rates for the given currencies in the system. In order to protect
169
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
against problems that will occur if another thread happens to be updating the rates, synchronization is
handled using a static instance of ReaderWriterLock, called rwl. Both the dictionary and the
ReaderWriterLock are instantiated when a method on the class is first called, and both are marked

readonly in order to avoid being overwritten after instantiation:
static readonly Dictionary<string, decimal>
rates = new Dictionary<string, decimal>();
static readonly ReaderWriterLock
rwl = new ReaderWriterLock();
If cataloged using either the SAFE or EXTERNAL_ACCESS permission sets, this code fails due to its use of
synchronization (the ReaderWriterLock), and running it produces a HostProtectionException. The
solution is to move the affected code into its own assembly, cataloged as UNSAFE. Because the host
protection check is evaluated at the moment of just-in-time compilation of a method in an assembly,
rather than dynamically as the method is running, the check is done as the assembly boundary is being
crossed. This means that an outer method can be marked SAFE and temporarily escalate its permissions
by calling into an UNSAFE core.
 Note You might be wondering about the validity of this example, given the ease with which this system could
be implemented in pure T-SQL, which would eliminate the permissions problem outright. I do feel that this is a
realistic example, especially if the system needs to do a large number of currency translations on any given day.
SQLCLR code will generally outperform T-SQL for even simple mathematical work, and caching the data in a
shared collection rather than reading it from the database on every call is a huge efficiency win. I’m confident that
this solution would easily outperform any pure T-SQL equivalent.
When designing the UNSAFE assembly, it is important from a reuse point of view to carefully analyze
what functionality should be made available. In this case, it’s not the use of the dictionary that is causing
the problem—synchronization via the ReaderWriterLock is throwing the actual exception. However, a
wrapping method placed solely around a ReaderWriterLock would probably not promote very much
reuse. A better tactic, in my opinion, is to wrap the Dictionary and the ReaderWriterLock together,
creating a new ThreadSafeDictionary class. This class could be used in any scenario in which a shared
data cache is required.
Following is my implementation of the ThreadSafeDictionary; I have not implemented all of the
methods that the generic Dictionary class exposes, but rather only those I commonly use—namely, Add,
Remove, and ContainsKey:
using System;
using System.Collections.Generic;

using System.Text;
using System.Threading;

namespace SafeDictionary
{
public class ThreadSafeDictionary<K, V>
{
170
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
private readonly Dictionary<K, V> dict = new Dictionary<K,V>();
private readonly ReaderWriterLock theLock = new ReaderWriterLock();

public void Add(K key, V value)
{
theLock.AcquireWriterLock(2000);

try
{
dict.Add(key, value);
}
finally
{
theLock.ReleaseLock();
}
}

public V this[K key]
{
get
{

theLock.AcquireReaderLock(2000);
try
{
return (this.dict[key]);
}
finally
{
theLock.ReleaseLock();
}
}

set
{
theLock.AcquireWriterLock(2000);
try
{
dict[key] = value;
}
finally
{
theLock.ReleaseLock();
}
}
}

public bool Remove(K key)
{
theLock.AcquireWriterLock(2000);
try
{

return (dict.Remove(key));
171
CHAPTER 7  SQLCLR: ARCHITECTURE AND DESIGN CONSIDERATIONS
}
finally
{
theLock.ReleaseLock();
}
}

public bool ContainsKey(K key)
{
theLock.AcquireReaderLock(2000);
try
{
return (dict.ContainsKey(key));
}
finally
{
theLock.ReleaseLock();
}
}
}
}
This class should be placed into a new assembly, which should then be compiled and cataloged in
SQL Server as UNSAFE. A reference to the UNSAFE assembly should be used in the exchange rates
conversion assembly, after which a few lines of the previous example code will have to change. First of
all, the only static object that must be created is an instance of ThreadSafeDictionary:
static readonly ThreadSafeDictionary<string, decimal> rates =
new ThreadSafeDictionary<string, decimal>();

Since the ThreadSafeDictionary is already thread safe, the GetRate method no longer needs to be
concerned with synchronization. Without this requirement, its code becomes greatly simplified:
private static decimal GetRate(string Currency)
{
return (rates[Currency]);
}
The exchange rates conversion assembly can still be marked SAFE, and can now make use of the
encapsulated synchronization code without throwing a HostProtectionException. And none of the code
actually contained in the assembly will be able to use resources that violate the permissions allowed by
the SAFE bucket—quite an improvement over the initial implementation, from a security perspective.
172

×