Thursday, January 15, 2009

Exploring Live Framework Triggers

The Live Framework has the ability to add triggers to resources.  There is some documentation on triggers here and here (pgs. 14-15), but after reading it I was left with more questions than answers.  So I took a deep dive exploring the nooks and crannies of triggers and this blog post is the result.

Overview of triggers

Triggers are scripts that can be executed before and after resources are created, updated, and deleted.  The scripts are written using Resource Scripts (AKA MeshScripts), a tiny DSL for working with AtomPub and FeedSync in Live Mesh.  Think of it as the T-SQL of Live Mesh.  MeshScripts be used as sprocs as well as triggers, but I’ll be focusing on triggers in this post.  See my previous posts for examples of sproc-style usage.

There are six triggers that can be attached to each resource:

  • PreCreateTrigger
  • PostCreateTrigger
  • PreUpdateTrigger
  • PostUpdateTrigger
  • PreDeleteTrigger
  • PostDeleteTrigger

The Create triggers run before and after each HTTP POST of a resource, the Update triggers run before and after each HTTP PUT of a resource, and the Delete triggers run before and after each HTTP DELETE of a resource.  This enables you to pack quite a bit of custom business logic inside a single call to the server.

Trigger parameters

The resource that you’re creating, updating, or deleting is accessible from inside each trigger as a script parameter.  For Create and Update triggers, the parameter is the actual resource sent from the client to the server in the POST or PUT request.  For Delete triggers, the parameter is the server’s version of the resource being deleted since a resource isn’t sent from the client to the server for delete requests (the client simply specifies the URL of the resource to delete).

Three steps are necessary to use a script parameter:

  1. Define the parameter
  2. Bind to the parameter from one or more statements
  3. Add the parameter to the script’s root statement

Here’s what this looks like using the syntax I created in my helper library.  I’ve bolded the three steps.

var param = 
    S.ResourceParameter<MeshObjectResource>();
 
mo.Resource.Triggers.PostCreateTrigger = 
    S.Sequence(
        S.CreateResource(news)
            .Bind(s => s.CollectionUrl, 
                param, p => p.NewsFeedLink)
            .Bind(s => s.Request.Title, 
                param, p => p.Title)
    )
    .AddParameters(param)
    .Compile();


The script snippet above adds a news entry to the news feed of the MeshObject you are creating (after it has been created, of course).  You can see this code in the context of a working sample in the download at the end of this post.  The sample also shows the equivalent “classic” syntax for the same trigger script.

Parameters are optional.  If you don’t need to access the original resource from your trigger script then you can safely omit all three steps and simply create a trigger script without any parameters.

There is only one actual resource parameter per script.  If you add more than one to the script, they are all treated as the same parameter.  This makes sense since all resource parameters are named “$Resource” under the hood.

There is another kind of script parameter called the ConstantParameter that lets you specify a name for the parameter, thus letting you to have more than one of them per script, but I have been unable to get ConstantParameters to work so we’ll ignore them for now.  I’m guessing they are used for looping statements which aren’t available in the current CTP.

Create/Update triggers

Create and Update triggers share many similarities, so I will cover them together.

Create and Update triggers are a one-shot deal.  You must attach new Create or Update triggers each time you Add() or Update() the resource.  Only the triggers appropriate for the HTTP verb are used.  So for POST, the Create triggers are executed but the Update triggers are silently tossed, and for PUT, the Update triggers are executed and the Create triggers are tossed.  By “tossed” I mean they aren’t executed, and the trigger is set to null in the response you get back.

In case it’s not clear, Create and Update triggers are not persisted on the server.  They only exist for the duration of the HTTP request/response.

Unlike sproc-style MeshScripts, the trigger script’s Source property becomes null after the script has executed.  At first I thought this was a bug, but then I realized that this was necessary so that if you then proceeded to call Update() on the item it wouldn’t re-run the same trigger again.

Just like sproc-style scripts, Create and Update triggers return the results of script execution in the Result property of the trigger script which you can inspect for details.  Use them immediately or lose them because they won’t stick around for subsequent requests.

Original vs. updated values

The script parameter for Update triggers contains the updated resource being PUT by the client.  If you need access to the original value that will be replaced by the PUT, you can access it in the PreUpdateTrigger using the following code, replacing MeshObjectResource with the appropriate resource type:

originalValue = S.ReadResource<MeshObjectResource>()
    .Bind(s => s.EntryUrl, param, p => p.SelfLink)


You can then bind to originalValue in subsequent statements.  Note that “param” in the sample is the trigger script’s resource parameter.

Delete triggers

Only Delete triggers have a non-null Source property after a POST or a PUT.  This is because only Delete triggers are persisted along with the resource on the server.  Delete triggers can be added to a resource using either POST or PUT.  Since Delete triggers are round-tripped (the Source doesn’t become null in the response), you don’t need to remember to re-add them on subsequent updates, unlike Update triggers.  However, they are re-persisted each time you do an update.  This means that you can remove a Delete trigger by setting it to null and calling Update().

Delete triggers are executed when you perform an HTTP DELETE on the URL of a resource that already has a Delete trigger added to it by a previous operation.  Since no actual resource is posted or returned by the DELETE operation, there is no way to examine the script results or learn about errors.

How triggers deal with errors

They don’t. :-)  To be more precise, errors are simply ignored.  They don’t cancel the POST/PUT/DELETE operation.  Similar to sproc-style scripts, no script Result is returned to the client if an error occurs.  Unlike sproc-style scripts, the error is not returned to the client.

Transactions

While we’re on the subject of sproc-style scripts, it should be noted that sproc-style scripts are not transactional, and trigger-style scripts aren’t transactional either.  Sure, they may execute within the scope of a single HTTP request/response “transaction” but there is no rollback on failure.  Future releases are expected to include compensation/undo support.

Comparison to SQL triggers

Various databases support statement-level triggers and row-level triggers.  Statement-level triggers are executed once for a batch of rows resulting from a single statement, while row-level triggers are executed once for each row.  Statement-level triggers and row-level triggers attached to each table in the database.

While Live Framework triggers can inspect data “per row,” the triggers are actually attached to each “row,” not to each “table.”  And as you already know, only Delete triggers actually remain attached to the “row.”

This means that it isn’t possible to put triggers on feeds (the equivalent of tables) that fire when entries are added, updated, or removed from the feed.

And as I explain in the next section, you can’t currently modify the incoming data before it is added or updated, unlike with SQL triggers.

Parameters are read-only (I think…)

At first I was under the impression that the incoming POST/PUT data exposed in the parameter to the PreCreate and PreUpdate triggers could be modified and the modified values would be passed along to the actual POST or PUT operation.  I made this assumption based on the following quote from page 15 of this document:

"The output of the PreCreateTrigger can be data-bound to the actual POST request entity and the data is propagated dynamically in the request pipeline. Similarly, the response entity of the POST operation can be data bound to the PostCreateTrigger. A similar binding can be done using the PreUpdateTrigger to the request entity of the PUT operation and the response of the PUT operation and the PostUpdateTrigger. Note that such a model to flow the data dynamically between the PostDeleteTrigger script and the response entity is not applicable to the DELETE operation since we do not return response entity in the DELETE operation."

This sounds promising, but unfortunately I have been unable to find a way to update the script parameter.

The problem is that I can’t find a way to bind to the resource parameter.  The resource parameter is exposed as a StatementParameter, not as a Statement.  All of the Bind() methods that take a StatementParameter have the parameter on the right-hand-side.  This means that you can assign from a resource parameter, but you can’t assign to it.

So I tried binding to “Parameters[0].Value” on the root statement of the script, but that didn’t work.  Then I tried binding to the parameter using its secret “$Resource” name, but that didn’t work either.

Perhaps someone forgot to add the appropriate Bind() overload, or perhaps there’s another way to get at the parameter that I’m not thinking of.  But until this is sorted out, parameters are read-only, at least on my box.

Once parameters can be modified, it will be interesting to see if you can completely replace the parameter (even set it to null?), or only update properties on it.  It will also be interesting to see if you can delete the resource in the PostCreate trigger and return a completely different resource to the client.  This could be a useful technique for creating singleton Mesh objects.

Triggers and the local LOE

Triggers don’t work at all if you’re connecting to the local client LOE.  If you add triggers to a resource and then Add() or Update() it, the resource comes back with all its triggers set to null.  This makes sense because the ability to execute scripts inside the client LOE is expected to be added in a later release.

But not even the Delete triggers are persisted and propagated up to the server.  It turns out that Delete triggers also don’t propagate from the server down to the client.  This made me nervous, wondering what will happen if I update a client-side resource that has a server-side Delete trigger.  Will the absence of a client-side trigger clobber the server-side trigger?  Thankfully the server properly merges the client-side update with the server-side resource’s Delete triggers.  Must be some FeedSync magic.

Then I tried deleting a resource on the client that had server-side Delete triggers.  The resource was successfully removed on the server, but the server-side triggers failed to execute!  So synchronization bypasses triggers.

Speculation regarding client script execution

Once client script execution is added in a future release, how will this probably change the situation?

Create/Update triggers will run on the client if you connect via ConnectLocal().

Assuming synchronization of Delete triggers is fixed, you will be able to add Delete triggers on either the client or the server.  If you delete the resource via Connect(), the trigger will run on the server.  If you delete via ConnectLocal(), the trigger will run on the client.

But what if you want a trigger to always run on the server?  Perhaps the trigger accesses external resources that you are unable to access while the client is offline.  Or perhaps the trigger accesses resources that aren’t synced to the client such as Contacts, Profiles, or MeshObjects that aren’t mapped to that particular device.  Perhaps there could be a client-side queue of pending triggers that are synchronized up to the server?

Creating triggers inside of scripts

Officially, you can’t add triggers to resources from inside of scripts.  If you try, you will get the following error message: “Trigger can not be associated with a resource which is being modified using meshscripts.”  Hey, look!  They said MeshScripts!  Personally, I think that’s a far better name than Live Framework Resource Scripts, as you can tell from the titles of my previous blog posts. :-)

Anyway, it is possible to add Delete triggers to resources from inside of a script.  The trick is that you must copy them from a pre-existing resource, like so:

S.Sequence(
    originalCollection = S.ReadResourceCollection<MeshObjectResource>(ScriptHelper.MeshObjectsUrl)
    .WithQuery<MeshObjectResource, MeshObject>(
        q => q.Where(o => o.Resource.Title.StartsWith("Original"))),
    S.CreateResource(ScriptHelper.MeshObjectsUrl, 
        new MeshObjectResource("I have delete triggers"))
    .Bind(s => s.Request.Triggers.PreDeleteTrigger, 
        originalCollection, c => c.Response.Entries[0].Triggers.PreDeleteTrigger)
    .Bind(s => s.Request.Triggers.PostDeleteTrigger, 
        originalCollection, c => c.Response.Entries[0].Triggers.PostDeleteTrigger)
).Compile().RunAtServer();


Technically, you can use this technique to add Create and Update triggers too.  This can be verified by inspecting the the script result and seeing that the resource was returned with Create and Update triggers containing the Source script that you specified.  However, these triggers don’t run.  Why not?

Scripts bypass trigger execution

Just as synchronization bypasses trigger execution, scripts also bypass trigger execution.  This is why our Create and Update triggers were added but didn’t run.

What happens if we use a script to delete a resource with Delete triggers on the server?  The script deletes the resource without running its triggers.

Consequences of bypassing triggers

If you choose to use Delete triggers, you must be careful to do all of your Delete operations through direct HTTP DELETE calls to the server.  Don’t use ConnectLocal(), and don’t use MeshScripts to delete resources.

This loophole could be useful in “oops” situations where you don’t want the triggers to run.

The bigger issue here is that you can’t reliably enforce server-side business logic.  I spoke with Abolade about this after his PDC session and he mentioned that perhaps the content screening hook points (used to block enclosures containing viruses and other inappropriate content) could be exposed to users for running custom business logic that is capable of rejecting content.  This could also be used to implement table-style triggers that are guaranteed to always run.  At first I thought this would be cool to have, but now I’m starting to think that such a server-centric feature isn’t an appropriate fit with the design philosophy of Mesh.  I may elaborate why in a future post.

Triggers on non-Mesh objects

Currently the root ServiceDocument at https://user-ctp.windows.net/ exposes Profiles and Contacts in addition to Mesh.  I think these are known as Federated Storage Services, but I’m not sure.  Contacts map out of the Mesh to your actual Hotmail contacts.  Anyway, you access /Profiles and /Contacts using the same resource-based programming model as the rest of /Mesh.  Anything that is a Resource can have triggers, so what happens if we add triggers to a Contact?

I added a new Contact containing Create and Delete triggers.  The Create triggers worked, but the Delete triggers weren’t persisted and therefore didn’t run when I deleted the Contact.

I’m guessing there’s a service integration layer that translates back and forth between Mesh’s resource-based programming model and external services.  The Contacts service probably doesn’t have a place to store arbitrary data such as triggers, so they get lost in translation.  But the Create and Update triggers can still run because they don’t need to be persisted anywhere, so they can live entirely in the world of Mesh’s resource-oriented request/response pipeline that wraps the calls to the Contacts service.  Hmm, maybe there are benefits to not having to persist triggers…  But it would also be nice to have a consistent programming model for Create, Update, and Delete.

Summary of limitations

There are a number of limitations scattered throughout this blog post, so here’s a more concise list:

  • Create and Update triggers aren’t persisted
  • No row-level/statement-level triggers on feeds
  • Trigger parameters are read-only (I think)
  • Can’t add triggers from scripts
  • Synchronization bypasses triggers
  • Scripts bypass triggers
  • Delete triggers don’t work on non-Mesh objects
  • Local LOE doesn’t support triggers
  • Triggers can’t reliably enforce business logic

Download

You can download the sample code here.  The samples use my Fluent MeshScripts library with a few minor updates.

While writing this I discovered and fixed a bug in my library’s expression-to-string code when it encounters expressions such as “c => c.Response.Entries[0].Triggers.PreDeleteTrigger”.   I also created an AddParameters overload that takes an SResourceParameter<TResource>.

The code includes examples of using all the trigger types, creating a resource with triggers from a script, bypassing delete triggers with a script, triggers on Contacts, and the “can’t add triggers from meshscripts” error.

Wrap-up

Besides providing some detailed documentation and code samples for Live Framework triggers, hopefully this post has helped you think about scenarios where you might want to use them, as well as provided some pointers on when to avoid them or use them with care.  I also hope this can be used to improve the usability and functionality of this powerful feature of the Live Framework.


Update: it appears that triggers don't work on DataFeeds and DataEntries. See Raviraj's post in this forum thread for details.

No comments: