Post-commit hooks

Introduction

Post-commit hooks is a way to guarantee that some code executes after committing a transaction. This functionality is, for example, useful if you want to send out confirmation emails after you've created orders in the database:
1
using Starcounter;
2
3
[Database]
4
public class Order
5
{
6
// Properties
7
}
8
9
public class Program
10
{
11
static void Main()
12
{
13
Hook<Order>.AfterCommitInsert += (sender, id) =>
14
{
15
// Executes after you commit a new Order
16
var order = Db.FromId<Order>(id);
17
SendConfirmationEmail(order);
18
};
19
20
// Create a new order and trigger the hook
21
Db.Transact(() => new Order());
22
}
23
24
private static void SendConfirmationEmail(Order order)
25
{
26
// Implementation for sending a confirmation email
27
}
28
}
Copied!
With this, you're guaranteed to send out a confirmation email after someone creates a new order. The hook is triggered no matter what app creates the order, as long as the app is in the same database, the app with the hooks runs, and the app has registered the hooks.
The fact that post-commit hooks execute after the transaction differentiates it from normal commit hooks where the hook executes as a part of the transaction.

API

There are three different post-commit hooks:
1
Hook<DatabaseClass>.AfterCommitInsert += (sender, id) => { /* Implementation */ };
2
Hook<DatabaseClass>.AfterCommitUpdate += (sender, id) => { /* Implementation */ };
3
Hook<DatabaseClass>.AfterCommitDelete += (sender, id) => { /* Implementation */ };
Copied!
Each event uses the standard EventHandler<T> delegate, where T is ulong. The ID provided to the hook is the unique object ID of the triggering object. The sender is an object that you can cast to a Task that represents the asynchronous operation for the transaction that triggered the hook. Read more about how you can use this with a custom scheduler in the post-commit hooks with a custom scheduler section.
The DatabaseClass in the code sample above is the database class that should trigger the hook when an instance from the class is inserted, updated, or deleted.

Invocation

Starcounter invokes the correct hooks when an instance of the specified class is inserted, updated, or deleted. This statement assumes that the hook is registered and that the app with the hook is running.

Triggering hooks

For example, say that we have these four post-commit hooks:
1
Hook<Order>.AfterCommitInsert += (sender, id) => Debug.WriteLine("AfterCommitInsert-Order");
2
Hook<Person>.AfterCommitInsert += (sender, id) => Debug.WriteLine("AfterCommitInsert-Person");
3
Hook<Person>.AfterCommitUpdate += (sender, id) => Debug.WriteLine("AfterCommitUpdate-Person");
4
Hook<Person>.AfterCommitDelete += (sender, id) => Debug.WriteLine("AfterCommitDelete-Person");
Copied!
To trigger each of these, we write something like this:
1
// Insert order
2
Db.Transact(() => new Order());
3
4
// Insert person
5
var person = Db.Transact(() => new Person());
6
7
// Update person
8
Db.Transact(() => person.Name = "Someone");
9
10
// Delete person
11
Db.Transact(() => person.Delete());
Copied!
Debug console
1
AfterCommitInsert-Order
2
AfterCommitInsert-Person
3
AfterCommitUpdate-Person
4
AfterCommitDelete-Person
Copied!
Notice how each of these operations is in a separate transaction. If we put them in the same transaction, the result is different:
1
Db.Transact(() =>
2
{
3
// Insert order
4
new Order();
5
6
// Insert person
7
var person = new Person();
8
9
// Update person
10
person.Name = "Someone";
11
12
// Delete person
13
person.Delete();
14
});
Copied!
Debug console
1
AfterCommitInsert-Order
Copied!
This code only triggers the AfterCommitInsert hook for the Order class. The reason for this is that the hooks are triggered based on the final result of the transaction, not the individual operations themselves. The only state change after the transaction was that the app created an Order since the transaction created and then immediately deleted the Person.
Post-commit hooks trigger multiple times if there are several operations in one transaction:
1
Db.Transact(() =>
2
{
3
new Person();
4
new Person();
5
});
Copied!
Debug console
1
AfterCommitInsert-Person
2
AfterCommitInsert-Person
Copied!

Infinite loops

If you invoke a post-commit hook from inside a commit hook, it runs in an infinite loop because you're scheduling a task from inside that task:
1
// This will cause an infinite loop once invoked
2
Hook<Order>.AfterCommitInsert += (sender, id) =>
3
{
4
Db.Transact(() => new Order());
5
};
Copied!

Invocation timing

Also, post-commit hooks are not guaranteed to execute right after the transaction. It's possible that another transaction executes before executing the hook. Thus, you can't assume that the database state that existed after the transaction that triggered the hook is still the same:
1
Hook<Person>.AfterCommitInsert += (sender, id) =>
2
{
3
var person = Db.FromId<Person>(id);
4
// Here, person can be null
5
};
6
7
var person = Db.Transact(() => new Person());
8
9
// This may run before the hook
10
Db.Transact(() => person.Delete());
Copied!
If another thread deletes the person between the time the transaction completes and before the hook is invoked, the object will no longer exist.

Durability

Post-commit hooks execute after the transaction is committed to memory but before Starcounter flushes the commit to the transaction log. That the post-commit hooks execute before the transaction log flush means that the transaction is not guaranteed to be durable in the hook:
1
Hook<Order>.AfterCommitInsert += (sender, id) =>
2
{
3
// Executes after you commit a new Order
4
var order = Db.FromId<Order>(id);
5
SendConfirmationEmail(order);
6
7
// POWER OUTAGE
8
};
9
10
// Create a new order and trigger the hook
11
Db.Transact(() => new Order());
Copied!
If the transaction with the new order has not been flushed to the transaction log where the power outage happens, then the customer gets the confirmation email but the order has not been stored in the database.
You can guarantee that the transaction is durable by executing the hook after the task with ContinueWith:
1
Hook<Order>.AfterCommitInsert += (sender, id) =>
2
{
3
var task = (System.Threading.Tasks.Task)sender;
4
5
// Flush the transaction to the log
6
task.ContinueWith((t) =>
7
{
8
// The transaction is now durable
9
// Put the code for the hook here
10
});
11
};
Copied!

Registering hooks

You can almost register hooks anywhere. A common practice is to have them in a specific class with a Register method and invoke this method in the entry point:
1
public static class PostCommitHooks
2
{
3
public static void Register()
4
{
5
Hook<Order>.AfterCommitInsert += (sender, id) => { /* */ };
6
// The other post-commit hooks
7
}
8
}
9
10
public class Program
11
{
12
static void Main()
13
{
14
PostCommitHooks.Register();
15
}
16
}
Copied!
When you register post-commit hooks, avoid two things: registering the hooks in a transaction or in another hook. The reason for this is that the transaction may have to restart if there's a conflict which would register the hook several times. If you register it in another hook the inner hook will be registered every time the outer hook is called. If you register a hook many times, it's called as many times as its registered:
1
public class Program
2
{
3
static void Main()
4
{
5
for (int i = 0; i < 3; i++)
6
{
7
Hook<Order>.AfterCommitInsert += (sender, id) => Debug.WriteLine("In insert hook");
8
}
9
10
Db.Transact(() => new Order());
11
}
12
}
Copied!
Debug console
1
In insert hook
2
In insert hook
3
In insert hook
Copied!

Advanced: post-commit hooks with a custom scheduler

Starcounter uses its default scheduling mechanism to schedule tasks that the post-commit hooks execute in. Currently, that's an instance of DbTaskScheduler. A complementary API can provide a custom scheduler when you register post-commit hooks, forcing Starcounter to utilize that instead of the built-in default. The example below shows a custom task scheduler that extends the Starcounter database task scheduler, executing a callback every time the hook in which it is installed in is queued:
1
using Starcounter;
2
using System;
3
using System.Threading.Tasks;
4
5
[Database]
6
public class Order
7
{
8
// Properties that are included in an order
9
}
10
11
public class Program
12
{
13
static void Main()
14
{
15
Hook<Order>.OnAfterCommitInsert(
16
(sender, id) => { /* */ },
17
new NotifyingScheduler(() => Console.Write("A task was scheduled")));
18
19
Db.Transact(() => new Order());
20
}
21
}
22
23
class NotifyingScheduler : DbTaskScheduler
24
{
25
readonly Action callback;
26
27
public NotifyingScheduler(Action action)
28
{
29
callback = action;
30
}
31
32
protected override void QueueTask(Task task)
33
{
34
base.QueueTask(task);
35
callback?.Invoke();
36
}
37
}
Copied!
Corresponding APIs are available for all other hooks:
  • Hook<DatabaseClass>.OnAfterCommitInsert
  • Hook<DatabaseClass>.OnAfterCommitUpdate
  • Hook<DatabaseClass>.OnAfterCommitDelete
Last modified 3yr ago