ptg 1854 CHAPTER 46 SQLCLR: Developing SQL Server Objects in .NET . IsNullIfEmpty—Tells SQL Server that the UDA will return null if its aggregated value is empty (that is, if its value is 0, or the empty string ””, and so on). . Name—Tells the deployment routine what to call the UDA when it is created in the database. . MaxByteSize—Tells SQL Server not to allow more than the specified number of bytes to be held in an instance of the UDA. You must specify this when using Format.UserDefined. For this example, you implement a very simple UDA that sums values in an integer column, but only if they are prime. Listing 46.8 shows the code to do this. LISTING 46.8 A UDA That Sums Prime Numbers using System; using System.Data; using System.Data.Sql; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; [Serializable] [Microsoft.SqlServer.Server.SqlUserDefinedAggregate( Format.Native, IsInvariantToDuplicates=false, IsInvariantToNulls=true, IsInvariantToOrder=true, IsNullIfEmpty=true )] public struct SumPrime { SqlInt64 Sum; private bool IsPrime(SqlInt64 Number) { for (int i = 2; i < Number; i++) { if (Number % i == 0) { return false; } } return true; } public void Init() { Sum = 0; ptg 1855 Developing Custom Managed Database Objects } public void Accumulate(SqlInt64 Value) { if (!Value.IsNull && IsPrime(Value) && Value > 1) Sum += Value; } public void Merge(SumPrime Prime) { Sum += Prime.Sum; } public SqlInt64 Terminate() { return Sum; } } In this code, SQL Server first calls Init(), initializing the private Sum data field to 0. For each column value passed to the aggregate, the Accumulate() method is called, wherein Sum is increased by the value of the column, if it is prime. When multiple threads converge, Merge() is called, adding the values stored in each instance (as the Prime parameter) to Sum. When the rowset has been completely parsed, SQL Server calls Terminate(), wherein the accumulated value Sum is returned. Following are the results of testing SumPrime on Production.Product (an existing AdventureWorks2008 table): SELECT TOP 10 dbo.SumPrime(p.ProductId) AS PrimeSum, p.Name FROM Production.Product p JOIN Production.WorkOrder o ON o.ProductId = p.ProductId WHERE Name LIKE ‘%Frame%’ GROUP BY p.ProductId, p.Name ORDER BY PrimeSum DESC go PrimeSum Name 360355 HL Mountain Frame - Black, 42 338462 HL Mountain Frame - Silver, 42 266030 HL Road Frame - Red, 48 214784 HL Road Frame - Black, 48 ptg 1856 CHAPTER 46 SQLCLR: Developing SQL Server Objects in .NET 133937 HL Touring Frame - Yellow, 46 68338 LL Road Frame - Red, 52 54221 LL Mountain Frame - Silver, 48 15393 ML Road Frame - Red, 52 0 HL Mountain Frame - Black, 38 0 HL Road Frame - Black, 44 (10 row(s) affected.) Following is the DDL syntax for this UDA: CREATE AGGREGATE SumPrime(@Number bigint) RETURNS bigint EXTERNAL NAME SQLCLR.SumPrime As with UDTs, with UDAs there is no ALTER AGGREGATE, but you can use DROP AGGREGATE to drop them. Developing Managed Triggers Managed triggers are static methods of a .NET class decorated with the SqlTrigger attribute. SqlTrigger has three named parameters: . Event—A required string-valued parameter that tells SQL Server which type of trigger you’re defining, as is done when defining T-SQL triggers. . Target—A required string-valued parameter that tells SQL Server which schema and table you’re attaching the trigger to. . Name—An optional string parameter that tells the deployment routine what to call the trigger when it is created in the database. The implementation contract for a managed trigger is only that it be a static method that returns void. Inside the method body of a managed trigger, you need to get a reference to the execu- tion context of the trigger so you can find out what Data Manipulation Language (DML) statement the trigger is responding to and which columns have been updated. You do this by using the SqlContext.TriggerContext object of type SqlTriggerContext. (Note that this object is null when used in nontrigger contexts.) It has the following members: . ColumnCount—An integer property that indicates how many columns were affected by the operation. . IsUpdatedColumn—A Boolean method that indicates whether the column at a specific position was updated during the operation. . TriggerAction—An enum that indicates which operation caused the trigger to fire. For DML triggers, this is either TriggerAction.Insert, TriggerAction.Update, or TriggerAction.Delete. For DDL triggers, the list is quite a bit longer. Refer to MSDN to see all the possible values of the TriggerAction enumeration. ptg 1857 Developing Custom Managed Database Objects . EventData—In the case of a DDL trigger, an object of type SqlXml that contains an XML document whose content explains the DDL that just fired. (The XML content model for this object is the same as that returned by the EVENTDATA() built-in function.) Have you ever wanted to be notified by email that some important column value in your tables has been created or updated? There are many ways to do this, including using Query Notifications. You can also accomplish this by writing a managed trigger that calls a web service, which in turn sends an email. Up until now, you haven’t had to decrease the runtime safety of your assembly. But because certain aspects of web services use the Synchronized attribute (which means they do thread synchronization), we have to change our SQLCLR assembly’s permission set to UNSAFE. CAUTION Only the sysadmin role can upload an UNSAFE assembly to SQL Server. You should allow this uploading only when you know the code being uploaded doesn’t do anything that might compromise the integrity of the data, the server, or your job. First, you need to create a simple web service routine that sends your email. To do this using Visual Studio 2008, you create a new local IIS website called photoserve and add to it a new web service called PhotoService.asmx. Then you replace the entire body of PhotoService.cs with the following C# code: using System; using System.Web.Services; using System.Net.Mail; using System.Configuration; [WebService(Namespace = “urn:www-samspublishing-com:examples:sqlclr:triggers”)] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class PhotoService : System.Web.Services.WebService { [WebMethod] public void PhotoUpdateNotify(int ProductPhotoId) { MailMessage m = new MailMessage(); m.Subject = “New Photo: “ + ProductPhotoId.ToString(); m.From = new MailAddress(“ProductPhotoService@localservername”); m.Body = “http://localhost:1347/photoserve/getphoto.aspx?ppid=” + ProductPhotoId.ToString(); m.To.Add(new MailAddress(“PhotoAdmin@ localservername “)); SmtpClient s = new SmtpClient(“localservername”, 25); s.Send(m); } } ptg 1858 CHAPTER 46 SQLCLR: Developing SQL Server Objects in .NET Of course, you need to have SMTP and IIS correctly configured on your server for this example to work completely. You also need to replace localhost and localservername and the email account names shown in the code with values that work for you. Next, you should add a new web form called getphoto.aspx to the site. You replace the entire contents of getphoto.aspx.cs with the code in Listing 46.9. LISTING 46.9 A Web Form That Retrieves Photos from SQL Server using System; using System.Data; using System.Configuration; using System.Web; using System.Data.SqlClient; using System.IO; public partial class getphoto : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (Request.QueryString[“ppid”] != null) { string ppid = Request.QueryString[“ppid”].ToString(); string FileName = “photos/” + ppid + “.jpeg”; string MappedFileName = Server.MapPath(FileName); using (SqlConnection c = new SqlConnection( “Data Source=(local);Initial Catalog=AdventureWorks2008; Integrated Security=True” ) ) { using (SqlCommand s = new SqlCommand( @”SELECT LargePhoto FROM Production.ProductPhoto WHERE ProductPhotoId = “ + ppid, c)) { c.Open(); using (SqlDataAdapter a = new SqlDataAdapter(s)) { using (DataSet d = new DataSet()) { a.Fill(d); if (d.Tables.Count == 1 && d.Tables[0].Rows.Count == 1) { ptg 1859 Developing Custom Managed Database Objects byte[] BigImg = (byte[])d.Tables[0].Rows[0][“LargePhoto”]; FileStream f = new FileStream( MappedFileName, FileMode.Create, FileAccess.Write); f.Write(BigImg, 0, BigImg.GetUpperBound(0)); f.Close(); Response.Redirect(FileName, false); } else { Response.Write(“<H2>Sorry, ProductPhotoId “ + ppid + “ was not found.</H2>”); } } } } } } else { Response.Write(“<H2>A querystring value for ppid is required.</H2>”); } } } Next, you add a subfolder to the site called photos. This is the place where the web form will save product photos as JPEG files and redirect the email recipient. The main body of the code in Listing 46.9 illustrates how to save LOB values to file in a succinct manner, so it may prove useful for your other applications. You either need to give your ASP.NET user file I/O permissions on photos or have the web application impersonate a user who has those permissions. To recap, the website code so far consists of the following: a web service ( PhotoService.asmx) that generates notification emails containing URLs. These URLs in turn point to a web form ( getphoto.aspx) that saves the varbinary value of Production.ProductPhoto.LargePhoto (given a particular ProductPhotoId) to the photos folder as [ProductPhotoId].jpeg. The last item you need is the reason you’re writing this code in the first place: a managed trigger that invokes the web service to kick off the whole process. To add this, you right- click the SQLCLR project and then select Add, Trigger. Name this new trigger class Triggers.cs (the default). Then replace the entire content of Triggers.cs with the code in Listing 46.10. ptg 1860 CHAPTER 46 SQLCLR: Developing SQL Server Objects in .NET LISTING 46.10 A Managed Trigger That Invokes a Web Service using System; using System.Data; using Microsoft.SqlServer.Server; using System.Data.SqlClient; using SQLCLR.photoserve; public partial class Triggers { [Microsoft.SqlServer.Server.SqlTrigger( Event = “FOR UPDATE”, Name = “Production.PhotoUpdateTrigger”, Target = “Production.ProductPhoto” )] public static void PhotoUpdateTrigger() { SqlTriggerContext stc = SqlContext.TriggerContext; if (stc.TriggerAction == TriggerAction.Update) { if (stc.IsUpdatedColumn(3)) //The LargePhoto varbinary(max) column { using (SqlCommand s = new SqlCommand( “SELECT DISTINCT ProductPhotoId FROM INSERTED”, new SqlConnection(“context connection=true”))) { s.Connection.Open(); using (SqlDataReader r = s.ExecuteReader(CommandBehavior.CloseConnection)) { PhotoService p = new PhotoService(); while (r.Read()) { SqlContext.Pipe.Send( “Notifying Web Service of Update for PPID: “ + r.GetInt32(0).ToString()); p.PhotoUpdateNotify(r.GetInt32(0)); } } } } } } } ptg 1861 Developing Custom Managed Database Objects Now that all the code is in place, all that’s left is an explanation of the code of PhotoUpdateTrigger() and a test case. In the code in Listing 46.10, you check to see whether the current TriggerAction is TriggerAction.Update, meaning that the trigger is firing due to an update. You declare this to be true by using the Event named parameter of the SqlTrigger attribute. Next, you select the ProductPhotoId of the updated row from the INSERTED table and connect to the database by using the context connection. You execute the command and get your SqlDataReader (r); then you instantiate the PhotoService web service. Using the overloaded method of the Pipe object, you send a string literal informational message (equivalent to T-SQL’s print function), which tells any clients what is about to happen. You call the PhotoUpdateNotify method of the web service and pass in the ProductPhotoId, which in turn sends the email containing the link back to getphoto.aspx, which generates the photo JPEG for that ProductPhotoId. To make the test case work, you need to make your local machine’s Network Service user a SQL Server login and a user in AdventureWorks2008 with at least db_datareader access. In addition, you might need to use the Visual Studio sgen.exe tool to create a serialization assembly for SQL2008SQLCLR.dll (which sgen.exe would, by default, name SQL2008SQLCLR.XmlSerializers.dll). You need to load this serialization assembly into AdventureWorks2008 before loading the main assembly (using CREATE ASSEMBLY). (At the time of this writing, it was necessary to also load System.Web.dll and its dependencies into AdventureWorks2008 before loading the application assemblies.) To test the trigger, you simply update a value of Production.ProductPhoto.LargePhoto: UPDATE Production.ProductPhoto SET LargePhoto = LargePhoto WHERE ProductPhotoId = 69 go Notifying Web Service of Update for PPID: 69 (1 row(s) affected.) If you get an email in your test inbox, you’ve done everything right. If not, don’t fret; this is a challenging example developed mainly to show the power of managed code. Using Transactions When you are writing managed objects, just as with T-SQL, it’s important to be aware of the current transaction context under which your code may be running. Managed database objects have the option of making use of the classes in the new System.Transactions namespace to control transactions. Following are the main objects you use to do this: . Transaction.Current—This is a static object of type Transaction that represents the current transaction. You use this object to explicitly roll back the current transaction ptg 1862 CHAPTER 46 SQLCLR: Developing SQL Server Objects in .NET (using Rollback()). It contains an IsolationLevel property that indicates the current transaction isolation level, as well as a TransactionCompleted event that your objects may subscribe to and a TransactionInformation property that indicates TransactionStatus and other attributes of the transaction. You can also use this object to manually enlist additional objects in the current transaction. . TransactionScope—This object represents a transactional scope that is used to wrap managed code. Note that transactions automatically roll back unless they are explic- itly committed using this object’s Complete() method. It is enough to merely instantiate this object at the beginning of the managed code: If a current transaction is active, the instantiated object assumes that transaction; if not, a new transaction is initiated. Note that it is not necessary to explicitly declare or even use transactions: if your managed code is already running in the scope of a transaction, it automatically participates in that transaction. (To turn off this behavior, you append ”enlist=false” to your connection string.) In fact, even if your code opens additional connections on additional servers, the transaction context is not only preserved but is automatically promoted to a distributed transaction that enlists all the connections involved. (The MSDTC service must be running for distributed transactions to work.) One thing you cannot do with managed transactions that you can with T-SQL is begin a new transaction and then just leave it open. The code example in Listing 46.11 illustrates the use of the System.Transactions objects in a managed stored procedure. You need to add a new managed stored procedure to the SQLCLR project and call it SPTrans. Then you need to add the using statement using System.Transactions; and replace the autogenerated method with the code from Listing 46.11. LISTING 46.11 Using Transactions in a Managed Stored Procedure [SqlProcedure] public static void SpTrans() { TransactionScope ts = null; try { SqlContext.Pipe.Send(“Proc Started”); if (Transaction.Current != null) { SqlContext.Pipe.Send(“A) Current tran is not null.”); SqlContext.Pipe.Send(“A) About to rollback current tran ”); Transaction.Current.Rollback( new ApplicationException(“I wanted to do this.”)); SqlContext.Pipe.Send(“A) Rollback Complete.”); } else ptg 1863 Developing Custom Managed Database Objects { SqlContext.Pipe.Send(“A) Current tran is null.”); } ts = new System.Transactions.TransactionScope(); SqlContext.Pipe.Send(“New Tran Started”); if (Transaction.Current != null) SqlContext.Pipe.Send(“B) Current tran is not null.”); else SqlContext.Pipe.Send(“B) Current tran is null.”); if (ts != null) ts.Complete(); SqlContext.Pipe.Send(“B) Complete() is Complete.”); } finally { if (ts != null) ts.Dispose(); SqlContext.Pipe.Send(“Proc Complete”); } } To test this code, you simply run the stored procedure from a query window (or use sqlcmd.exe) inside and outside a transactional scope and watch the results. Here’s an example: BEGIN TRAN EXEC dbo.SpTrans ROLLBACK TRAN EXEC dbo.SPTrans Using the Related System Catalogs As with other database objects, SQL Server provides catalog views that enable you to view loaded managed assemblies, routines, and types. The base view for finding these objects is sys.assemblies. To see which assemblies have been loaded (including the one you created in this chapter), you use the following query: SELECT TOP 5 name, assembly_id, permission_set_desc as permission_set FROM sys.assemblies . System; using System.Data; using System.Data .Sql; using System.Data.SqlTypes; using Microsoft. SqlServer .Server; [Serializable] [Microsoft. SqlServer .Server. SqlUserDefinedAggregate( Format.Native, IsInvariantToDuplicates=false, IsInvariantToNulls=true, IsInvariantToOrder=true, IsNullIfEmpty=true )] public. SQLCLR: Developing SQL Server Objects in .NET LISTING 46.10 A Managed Trigger That Invokes a Web Service using System; using System.Data; using Microsoft. SqlServer .Server; using System.Data.SqlClient; using. serialization assembly for SQL2 008SQLCLR.dll (which sgen.exe would, by default, name SQL2 008SQLCLR.XmlSerializers.dll). You need to load this serialization assembly into AdventureWorks2008 before loading