2

I'm creating a relatively simple SQL CLR in C#.

The job of this CLR is to publish events to an Azure Event Grid Topic.

I would like to be able to call this particular CLR from any number of triggers for any number of tables (in actuality there will be few).

Given the core code below how does one accurately retrieve the name of the table that caused/fired the trigger? I have little clue where to start.

I would like to be able to remove the SqlTrigger attribute and somehow access the table name for the var table property.

// I Would like to leave the attribute commented out
//
//[Microsoft.SqlServer.Server.SqlTrigger(Name = "PublishToEventGridTrigger",
//     Target = "NamesOfThings", Event = "FOR UPDATE, INSERT, DELETE")]
public static void PublishToEventGridTrigger()
{
    // TODO - How should these settings be handled?     
    //        Not sure if config files are accessible... To test..
    // ***************************************************************

    string topicHost = "https://####.eventgrid.azure.net/api/events";
    string topicKey = "####";

    // TODO - Get Table name for this
    var table = "How Do I set This";
    string eventType = $"##.{table}.{SqlContext.TriggerContext.TriggerAction.ToString()}";

    // ***************************************************************

    try
    {
        // extract data involved in trigger
        // Create the Event object
        EventGridEvent evt = new EventGridEvent("QWDBEvent", eventType, new
                      TriggerData(SqlContext.TriggerContext, SqlContext.Pipe));

        // Publish the event
        Event.Publish(topicHost, topicKey, evt);
    }
    catch (Exception ex)
    {
        //TODO - how do we handle these through SQL CLR? 
        // Going with a fire-and-forget for now    
        SqlContext.Pipe.Send($"Failure firing {ex.Message}");        
    }
    SqlContext.Pipe.Send($"Trigger fired");
}
Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
Darren Wainwright
  • 30,247
  • 21
  • 76
  • 127
  • 1
    This is not natively supported, and not easy to accomplish. I actually have a mock-up that does it but haven't had time to publish it. Certainly not straight-forward. Also, this question is a duplicate of at least 1 or 2 others, but I need to search for them as it has been a while since I have seen them. – Solomon Rutzky Feb 04 '20 at 15:17
  • @SolomonRutzky- thanks so much. I did see a couple other Q's on here but nothing that defined any actual answer. Any other links would be fab. If it's a pain to accomplish then I'll re-think and go for a CLR per trigger where needed. Was just hoping to have a generic event dispatcher. – Darren Wainwright Feb 04 '20 at 15:31
  • 1
    You should probably just make this a CLR Stored Procedure and invoke it from TSQL Triggers passing in any required arguments. – David Browne - Microsoft Feb 04 '20 at 15:46
  • @DavidBrowne-Microsoft - oh. That seems like a reasonable idea. Thanks. I'll give it a whirl. – Darren Wainwright Feb 04 '20 at 15:48
  • @DarrenWainwright Do you need access to the `INSERTED` and `DELETED` tables for this? Or just to know that the event happened and which table it was? – Solomon Rutzky Feb 04 '20 at 16:34
  • @SolomonRutzky - I'm currently accessing those to retrieve the PK of whatever entity was inserted/updated or deleted. – Darren Wainwright Feb 04 '20 at 16:47

1 Answers1

1

This is not natively supported, and not super easy to accomplish. I actually have a mock-up that does it but haven't had time to publish it. Certainly not straight-forward. For the moment, you can take a look at both answers to this question:

SQL CLR Trigger - get source table

And I will update this answer when I get my solution cleaned up and posted.

HOWEVER, while you say that there will be only a few tables that this will be applied to, please keep in mind that triggers are executed in the context of the transaction that the DML (or even DDL) statement is a part of. This has two consequences:

  1. the longer the trigger takes, the longer the DML / DDL operation takes, hence the longer that the locks on the object(s) are being held. This can have an adverse effect on concurrency / performance. It makes many folks nervous enough to tie DML / DDL statements to web service calls to local (intranet / same network) services, but across the internet is introducing a dangerous, highly variable risk into a process that should be fairly simple. Of course, if you are using an Azure VM or Azure SQL Managed Instance, then maybe the latency is low-enough for this to be safe "enough". Either way, be very cautious! (To be clear: this risk is not mitigated by using a T-SQL trigger to execute a SQLCLR stored procedure; that's all still the same transaction)
  2. if the trigger fails / throws an error, by default that will abort the transaction and rollback the DML (or DDL) operation. Is that intended behavior? Usually failure to log an event shouldn't abort the operation itself, right? So you would probably need to swallow the error (or at least write it to a text file, perhaps).

You should be able to decouple the DML/DLL operation from the logging piece by setting up Service Broker to handle calling the logging service. In this case you would:

  1. Use a T-SQL trigger
    1. get the table name via:
      SELECT tab.[name]
      FROM   sys.objects tab
      WHERE  tab.[object_id] = (
                        SELECT trg.[parent_object_id]
                        FROM sys.objects trg
                        WHERE  trg.[object_id] = @@PROCID
                               );
      
    2. Gather any other info to log, possible from the INSERTED and/or DELETED tables (you won't have access to these after this; though you could possibly repackage the data in those two tables as XML to send as part of the Service Broker message -- e.g. SELECT * FROM inserted FOR XML RAW('ins'); )
    3. Queue a message for Service Broker including that gathered info
  2. Use Service Broker
    1. will handle messages asynchronously from DML / DLL operations
    2. can execute SQLCLR stored procedure, passing in info gathered in T-SQL trigger, to call logging service (whether internal or external network)

Keep in mind (since you mentioned getting affected PK value from INSERTED and DELETED tables) that there can be multiple rows in those tables if the DML operation affected multiple rows (i.e. never assume a single row for DML operations).

Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
  • Thank you for the well thought out and described solution. The CLR assembly I'm putting together is there to deliver a basic message to Azure's Event Grid - similar to the broker here. If I built a standard TSQL trigger that called an SQL CLR Sproc and that sproc call failed would the entire transaction fail? Likewise, can the CLR sproc be called and the trigger not have to wait for any kind of response? (thinking latency in calling event grid) – Darren Wainwright Feb 04 '20 at 19:22
  • @DarrenWainwright Hi again. I already addressed that first part in the italicized final sentence of the first consequence: unless you do some special things to prevent it, YES, any failure in the trigger or anything it executes will cancel/rollback the entire DML operation / transaction. Triggers implicitly have `SET XACT_ABORT ON` such that any error is an immediate exception to the entire process. You _can_ set that to `OFF` but then you better not have any other logic that should cancel the DML operation if it errors. Hence why I suggested swallowing the error in .NET, saving to text file. – Solomon Rutzky Feb 04 '20 at 19:34
  • @DarrenWainwright For part 2: you might be able to call `HttpWebRequest` async, but that will require the assembly be set to `UNSAFE` instead of just `EXTERNAL_ACCESS`. I haven't tried this. Not sure if it matters that the calling process (i.e. the method that is that SQLCLR stored proc) will have terminated by the time the response comes back (that is how it works for a static method, right?). Not sure if that will orphan threads that have no way to GC or what. But it might be worth testing. – Solomon Rutzky Feb 04 '20 at 19:37
  • Thanks again. Clearly a little of it went a little over my head :) I'm going to give this approach a whirl. I'm also happy to mark this as answered. – Darren Wainwright Feb 04 '20 at 20:58
  • @DarrenWainwright Good luck, and let me know how it goes. – Solomon Rutzky Feb 04 '20 at 22:03
  • 1
    I certainly will do. Thank you again. You're a fantastic representation of how this community does awesome :) – Darren Wainwright Feb 05 '20 at 18:57
  • @DarrenWainwright You're welcome. And thanks, that's very kind of you . – Solomon Rutzky Feb 07 '20 at 17:25