Web requests using a CLR procedure

For most things that you can’t do straigh-off in T-SQL, you have the option to write a Common Language Runtime procedure. This gives you access to a huge array of APIs, so you can create really cool stuff, way beyond what T-SQL would normally allow you to do. Here, we’ll explore how to write a web request procedure in C#, so you can download a web document (for instance, a file on a REST service) directly into a table or a string variable using just a regular function call.

This article assumes that you are familiar with HTTP concepts such as GET and POST. Also, you should be comfortable with .NET programming and working in Visual Studio. However, you don’t need any deeper networking knowledge, as the .NET Framework makes a pretty good abstraction for you in its object model. This demo is based on the .NET 3.0/3.5 framework, and all the code is written in C#. The example code will work just fine in the Express Edition of Visual C#.

Let me also start off by say that I’m no C# hero. Any obvious subjects to improvement that you spot, please let me know in the comments section.

Starting a new C# project

In Visual C#, create a new project and select “Class library”, so your code will compile to a DLL file. This may differ between versions of Visual Studio and SQL Server, but you’ll figure it out.

The C# code

Here’s the example code of a C# procedure that we’re going to use. Starting off, we’re including Microsoft.SqlServer.Server because we’re going to need this to communicate with the server – retrieving arguments and returning values or recordsets.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.Collections;
using System.Globalization;

// For the SQL Server integration
using Microsoft.SqlServer.Server;

// Other things we need for WebRequest
using System.Net;
using System.Text;
using System.IO;

We’re setting up two basic functions, GET() and POST(), corresponding to their respective HTTP equivalents. We’re going to create a class, simply called Functions, but you could call it what you like.

public partial class Functions
{

Partial, in this case means that you can split the code over several different files if you want. You will notice that both functions are prefixed with an attribute (the stuff within brackets). The attribute is there for Visual Studio, so it knows that we are building an SQL Server function. Here’s the GET() function in its entirety, with comments:

    // Function to return a web URL as a string value.
    [Microsoft.SqlServer.Server.SqlFunction(DataAccess=DataAccessKind.Read)]
    public static SqlString GET(SqlString uri, SqlString username, SqlString passwd)
    {
        // The SqlPipe is how we send data back to the caller
        SqlPipe pipe = SqlContext.Pipe;
        SqlString document;

        // Set up the request, including authentication
        WebRequest req = WebRequest.Create(Convert.ToString(uri));
        if (Convert.ToString(username) != null & Convert.ToString(username) != "")
        {
            req.Credentials = new NetworkCredential(
                Convert.ToString(username),
                Convert.ToString(passwd));
        }
        ((HttpWebRequest)req).UserAgent = "CLR web client on SQL Server";

        // Fire off the request and retrieve the response.
        // We'll put the response in the string variable "document".
        WebResponse resp = req.GetResponse();
        Stream dataStream = resp.GetResponseStream();
        StreamReader rdr = new StreamReader(dataStream);
        document = (SqlString)rdr.ReadToEnd();

        // Close up everything...
        rdr.Close();
        dataStream.Close();
        resp.Close();

        // .. and return the output to the caller.
        return (document);
    }

The POST() function looks fairly similar:

    // Function to submit a HTTP POST and return the resulting output.
    [Microsoft.SqlServer.Server.SqlFunction(DataAccess = DataAccessKind.Read)]
    public static SqlString POST(SqlString uri, SqlString postData, SqlString username, SqlString passwd)
    {
        SqlPipe pipe = SqlContext.Pipe;
        SqlString document;
        byte[] postByteArray = Encoding.UTF8.GetBytes(Convert.ToString(postData));

        // Set up the request, including authentication, 
        // method=POST and encoding:
        WebRequest req = WebRequest.Create(Convert.ToString(uri));
        ((HttpWebRequest)req).UserAgent = "CLR web client on SQL Server";
        if (Convert.ToString(username) != null & Convert.ToString(username) != "")
        {
            req.Credentials = new NetworkCredential(
                Convert.ToString(username),
                Convert.ToString(passwd));
        }
        req.Method = "POST";
        req.ContentType = "application/x-www-form-urlencoded";

        // Submit the POST data
        Stream dataStream = req.GetRequestStream();
        dataStream.Write(postByteArray, 0, postByteArray.Length);
        dataStream.Close();

        // Collect the response, put it in the string variable "document"
        WebResponse resp = req.GetResponse();
        dataStream = resp.GetResponseStream();
        StreamReader rdr = new StreamReader(dataStream);
        document = (SqlString)rdr.ReadToEnd();

        // Close up and return
        rdr.Close();
        dataStream.Close();
        resp.Close();

        return (document);
    }

You’ll notice that I haven’t bothered trapping errors in this tutorial code, but I suppose this could be a good idea in production code, or you could write a TRY-CATCH block in T-SQL when calling the function.

Finally, we just need to close the curly brackets on the class, then it’s compile time! 🙂

}

How to enable the server to run CLR code

If you haven’t enabled CLR integration on the server (it’s disabled by default), you need to do this using sp_configure:

sp_configure 'clr enabled', 1;
GO
RECONFIGURE;
GO

Before you enable CLR execution, you should be aware of what it means to server security, and you should probably check with the server’s owner.

If you haven’t enabled CLR execution, you’ll get the following error message when you try to execute your code:

Msg 6263, Level 16, State 1, Line 1
Execution of user code in the .NET Framework is disabled. Enable
        "clr enabled" configuration option.

Also, in order to run “unsafe” managed code (i.e. code that has access to stuff outside the SQL Server context), you need to mark the database as trustworthy. This is done using the ALTER DATABASE statement:

ALTER DATABASE myDatabase SET TRUSTWORTHY ON;

If you’ve forgotten this last step, you’ll get the following error:

Msg 10327, Level 14, State 1, Line 1
CREATE ASSEMBLY for assembly 'SqlWebRequest' failed because assembly
      'SqlWebRequest' is not authorized for PERMISSION_SET = UNSAFE.
      The assembly is authorized when either of the following is true:
      the database owner (DBO) has UNSAFE ASSEMBLY permission and the
      database has the TRUSTWORTHY database property on; or the assembly
      is signed with a certificate or an asymmetric key that has a
      corresponding login with UNSAFE ASSEMBLY permission.

Setting up your assembly and function in the database

You have to load the DLL (the assembly) into the database. This is done in the database you’re going to work in. The assembly is actually stored in the database, so you don’t need to keep the DLL file once you’ve registered the assembly.

USE myDatabase
GO

CREATE ASSEMBLY SqlWebRequest
FROM 'D:\Stuff\SqlWebRequest.dll'
WITH PERMISSION_SET=UNSAFE;
GO

The UNSAFE clause means that the assembly has more privileges, which is needed in this case to get “outside access”, in this case to the networking functionality. If you’re simply doing arithmetics, string parsing or something like that, PERMISSION_SET=SAFE is probably a better idea.

Finally, all you need to do is create the CLR functions that reference the functions in the assembly. This follows the basics of the CREATE FUNCTION statement, except we’re using the EXTERNAL NAME clause to point to the assembly, class and C# function name:

CREATE FUNCTION dbo.fn_get_webrequest(
     @uri        nvarchar(max),
     @user       nvarchar(255)=NULL,
     @passwd     nvarchar(255)=NULL
)
RETURNS nvarchar(max)
AS
EXTERNAL NAME SqlWebRequest.Functions.GET;

GO

CREATE FUNCTION dbo.fn_post_webrequest(
     @uri         nvarchar(max),
     @postdata    nvarchar(max),
     @user        nvarchar(255)=NULL,
     @passwd      nvarchar(255)=NULL
)
RETURNS nvarchar(max)
AS

EXTERNAL NAME SqlWebRequest.Functions.POST;

GO

How to call the function in T-SQL

Now your CLR function is ready to use! Try using it to get stock prices from Yahoo, for instance:

PRINT dbo.fn_get_webrequest('http://quote.yahoo.com/d/quotes.csv?'+
       's=AAPL+YHOO+GOOG+GE+MSFT&f=snl1t1ghc1', DEFAULT, DEFAULT);

This is what the output should look like:

"AAPL","Apple Inc.",449.07,"3:40pm",437.66,451.54,+6.27
"YHOO","Yahoo! Inc.",20.78,"3:40pm",20.575,20.85,+0.05
"GOOG","Google Inc.",789.77,"3:40pm",784.40,795.9499,-1.00
"GE","General Electric ",23.09,"3:40pm",22.91,23.15,+0.28
"MSFT","Microsoft Corpora",27.38,"3:40pm",27.34,27.60,+0.0

Hope you found this tutorial useful. Again, let me know in the comment section if there’s anything I’ve missed.

12 comments

  1. Hey there. Just wanted to mention a few things:

    1) no need to declare “SqlPipe pipe” as the pipe is not available in SQL Functions, only Procs and Triggers.

    2) No need to declare “document” as “SqlString”. I would use regular “String” as it will be converted implicitly to SqlString via the return().

    3) No need to explicitly String.Convert() any of the incoming SqlString variables as all Sql* types have a “Value” property that returns the native .Net type that you would expect. So “username.Value” is what you would want to use.

    4) You definitely need a try-catch-finally construct around the Streams and StreamReaders. If an exception occurs and the Close() methods are not called on all of the variables (assuming the Streams have been opened / populated), then those references will be orphaned and cannot be garbage collected. They will hold onto that memory until the SQL Server process is restarted. All external resources should be cleaned up in a finally block. This is not something that can be handled via the T-SQL TRY / CATCH construct.

    5) No need to mark the assembly as UNSAFE as it is not doing any UNSAFE operations. All it needs is EXTERNAL_ACCESS.

    Hope this helps :).

    Take care,
    Solomon…

  2. When I try to run the Post, this is what I get.

    Msg 6522, Level 16, State 1, Line 22
    A .NET Framework error occurred during execution of user-defined routine or aggregate “udfHTTPPost”:
    System.NullReferenceException: Object reference not set to an instance of an object.
    System.NullReferenceException:
    at Functions.udfHTTPPost(SqlString uri, SqlString postData, SqlString username, SqlString passwd)
    .

  3. I have 3 parameters, Anyone knows how to upload 3 parameters to api, location of the file, fileiD and file Type are my parameters

  4. Thanks Daniel , I succeed calling a web request from what you’ve teached…. but some problem bother me… that is when calling a web request it hang there for about 20~30 seconds, which is not acceptable for my system user. Do you have any guide about this hanging problem. Thanks for your contribution.^^

    1. To be honest, calling a web service from a CLR procedure is probably not best practice. It’s certainly not something I would do in realtime, but perhaps rather as part of an ETL process.

      If you’re having problems with delays, I would seriously consider putting this type of logic in the application layer instead.

      1. thanks for your precious suggestion~ BTW, I’m also curious about “Is there an example of a Post. Like the Get example.’ asked by Jeff, Hope for your kindness to offer this, Thank in advance..^^

  5. Using SET TRUSTWORTHY ON for a database is risky and not advised, as it potentially exposes a huge security hole through privilege escalation. Instead, you can sign the assembly with a certificate or asymmetric key that is associated with a login with the UNSAFE ASSEMBLY permission.

    https://www.red-gate.com/simple-talk/blogs/careful-trustworthy-setting/

    https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/clr-strict-security?view=sql-server-2017

    1. I completely agree that enabling `TRUSTWORTHY` should be avoided in all but quick testing / demo / trouble-shooting scenarios. Unfortunately, historically there has been _very_ little quality documentation explaining how module signing works, and showing how easy it can be. So, in order to better inform everyone of the benefits of module signing (and how to accomplish it), and the problems with Impersonation, `TRUSTWORTHY`, and cross-DB ownership chaining, I have set up https://ModuleSigning.Info/ . The link at the bottom of the homepage goes to a blog post of mine that is an overview of a presentation that I give on this topic. It has links to other posts that go into details. And I have a few more posts yet to publish (soon) that will be linked as additional examples and/or proofs.

      Also, with regards to SQLCLR and the “CLR strict security” setting introduced in SQL Server 2017, I have posted two methods of accomplishing the preferred module signing approach that result in fully self-contained deployment scripts, and will work in Visual Studio / SSDT or in a custom build / automation setup:

      * https://sqlquantumleap.com/2017/08/09/sqlclr-vs-sql-server-2017-part-2-clr-strict-security-solution-1/
      * https://sqlquantumleap.com/2017/08/16/sqlclr-vs-sql-server-2017-part-3-clr-strict-security-solution-2/

Leave a comment

Your email address will not be published. Required fields are marked *