How to capture PRINT statements from TSQL called by CLR stored procedure
In order to capture messages (either from PRINT or RAISERROR('', 1, 10) or like it) in .NET (regardless of calling a Stored Procedure or ad hoc SQL), you need to set up a method that will get called by the SqlConnection.InfoMessage event.

The basic implementation is as follows:

[SqlProcedure()]
public static void Print_CLR()
{
    using (SqlConnection conn = new SqlConnection("context connection=true"))   
    {
        conn.InfoMessage += new SqlInfoMessageEventHandler(CaptureMessage);
        conn.Open();

        using (SqlCommand c = new SqlCommand("exec dbo.Print_TSQL", conn))
        {   
            c.ExecuteNonQuery();    
        }
    }
}

private static void CaptureMessage(object sender, SqlInfoMessageEventArgs args)
{
  // do something with args.Message

  return;
}


At this point, you still need to do something with the Message. While there are a few options, some of them have security implications due to the environment. SQL Server's CLR host is highly constrained compared to the main CLR running on the OS that is used for Windows Apps, Console Apps, and ASP.NET apps.

If you use the static method approach as shown above, then what you do with the Message will impact the PERMISSION_SET value for the assembly containing this code:


If you simply want to print the message back to the user, then you can do something like the following which can be done in a SAFE assembly:

SqlContext.Pipe.Send(args.Message);

If you want to write the messages to a file, then you can do something like the following which can be done in an EXTERNAL_ACCESS assembly:

File.AppendAllText(FILE_PATH_CONSTANT, args.Message);


This might cause extra latency and not be worth it. But on the other hand, if the process fails in the middle, whatever was written to the file would still be there, so there is definitely some benefit to this approach.
But if you want to store the messages in a variable to do something with later, then you will need a static class variable since the method to capture the message is static. For example, you can declare private static m_Messages StringBuilder = new StringBuiler("");. Then, in your CaptureMessage method you can do the following:

m_Messages.AppendLine(args.Message);


This seems simple enough. And for most any environment outside of SQLCLR it is. However, using a static variable in SQLCLR requires that the assembly have a PERMISSION_SET of UNSAFE, and it is best to avoid that if at all possible. And the reason that the assembly is required to be marked as UNSAFE is that the AppDomain where this code is running is shared across all Sessions / SPIDs. Hence, a static class variable is also shared across Sessions and that can lead to very odd behaviour.


Thankfully, all hope is not lost if you want to collect messages to an instance variable. To do this, you need to define the handler inline using an anonymous delegate. The anonymous method is within the same scope as the rest of your code so it has access to local variables. This is quite handy as it allows you to easily store the messages in an assembly that has a PERMISSION_SET of SAFE (which is highly preferred).

[SqlProcedure()]
public static void Print_CLR()
{
    StringBuilder _Messages = new StringBuilder("");

    using (SqlConnection conn = new SqlConnection("context connection=true"))   
    {
        conn.InfoMessage += delegate(object sender, SqlInfoMessageEventArgs args)
        {
            SqlConnection _Connection = (SqlConnection)sender;
            // _Connection.DataSource is the "Server" of the ConnectionString
            // _Connection.Database is the CURRENT database of the Connection

            _Messages.Append(args.Message);

            return;
        };

        conn.Open();

        using (SqlCommand c = new SqlCommand("exec dbo.Print_TSQL", conn))
        {   
            c.ExecuteNonQuery();    
        }
    }
}