Commit Hooks

Introduction

Commit hook is a logic flow control pattern similar to trigger in relational databases. It enables to hook the CRUD events per objects of particular class. For cases when an object is being created (with a new operator), updated (by writing to a field) and deleted (when Deleteis called, and after the committed delete), additional event handlers of code might be added for execution.

Example

1
using System;
2
using Starcounter;
3
4
namespace TestHooks
5
{
6
[Database]
7
public class Hooked
8
{
9
public string state { get; set; }
10
}
11
12
[Database]
13
public class YetAnotherClass
14
{
15
public int Stock { get; set; }
16
}
17
18
class Program
19
{
20
static void Main()
21
{
22
Hook<Hooked>.BeforeDelete += (s, obj) =>
23
{
24
obj.state = "is about to be deleted";
25
Console.WriteLine("Hooked: Object {0} is to be deleted", obj.GetObjectNo());
26
};
27
28
Hook<Hooked>.CommitInsert += (s, obj) =>
29
{
30
obj.state = "is created";
31
Console.WriteLine("Hooked: Object {0} is created", obj.GetObjectNo());
32
var nobj = new YetAnotherClass() { Stock = 42 };
33
};
34
35
Hook<Hooked>.CommitUpdate += (s, obj) =>
36
{
37
obj.state = "is updated";
38
Console.WriteLine("Hooked: Object {0} is updated", obj.GetObjectNo());
39
};
40
41
Hook<Hooked>.CommitUpdate += (s, obj) => // a second callback
42
{
43
Console.WriteLine("Hooked: We promise you, object {0} is updated", obj.GetObjectNo());
44
};
45
46
Hook<Hooked>.CommitDelete += (s, onum) =>
47
{
48
Console.WriteLine("Hooked: Object {0} is deleted", onum);
49
Hooked rp = (Hooked)DbHelper.FromID(onum); // returns null here
50
// the following will cause an exception
51
// Console.WriteLine("We cannot do like this: {0}", rp.state);
52
};
53
54
Hook<YetAnotherClass>.CommitInsert += (s, obj) =>
55
{
56
Console.WriteLine("Never triggered in this app, since it happens to get invoked inside another hook");
57
};
58
59
Hooked p = null;
60
Db.Transact(() =>
61
{
62
p = new Hooked() { state = "created" };
63
});
64
65
Db.Transact(() =>
66
{
67
p.state = "property changed";
68
Console.WriteLine("01: The changed object isn't yet commited", p.GetObjectNo());
69
});
70
71
Console.WriteLine("02: Change for property of {0} is committed", p.GetObjectNo());
72
73
Db.Transact(() =>
74
{
75
Console.WriteLine("03: We have entered the transaction scope");
76
Console.WriteLine("04: We are about to delete an object {0}, yet it still exists", p.GetObjectNo());
77
p.state = "deleted";
78
p.Delete();
79
Console.WriteLine("05: The deleted object {0} is no longer be available", p.GetObjectNo());
80
Console.WriteLine("06: Were are about to commit the deletion");
81
});
82
Console.WriteLine("07: Deletion is committed");
83
}
84
}
85
}
Copied!
The output produced is as follows (accurate to ObjectNo):
1
Hooked: Object 29 is created
2
01: The changed object isn't yet commited
3
Hooked: Object 29 is updated
4
Hooked: We promise you, object 29 is updated
5
02: Change for property of 29 is committed
6
03: We have entered the transaction scope
7
04: We are about to delete an object 29, yet it still exists
8
Hooked: Object 29 is to be deleted
9
05: The deleted object 29 is no longer be available
10
06: Were are about to commit the deletion
11
Hooked: Object 29 is deleted
12
07: Deletion is committed
Copied!
Those familiar with .NET recognize Starcounter follows a convention of .NET EventHandler for commit hooks. Currently, the first argument of the callback isn't used. The second argument is a reference to an object being transacted (for create, update and pre-delete events) or an ObjectNo of the object which itself is already deleted (for post-delete event). As in the .NET convention one can have an arbitrary number of event handlers registered per event, which will be triggered in the order of registration on the event occurrence.

Q&A

Why there are separate pre-delete (BeforeDelete) and post-delete (CommitDelete) hooks?
Remember that after object is physically deleted in the end of a successful transaction scope, you can no longer access it in a post-delete commit hook delegate. However you might still want to do something meaningful with it just around the moment of deletion. That is why the pre-delete hook is introduced. Note that a pre-delete hook triggers callback inside the transaction scope, but not in the end of transaction. It means that, in case a transaction has been retried N times, any pre-delete hook for any object deleted inside this transaction will also be executed N times, while all other hooks will be executed exactly once, right after a successful transaction commit. Thus, consider pre-delete hook behaving as a transaction side-effect.
How much should commit hooks be used?
In general, in situations where you can choose, we recommend to avoid using commit hooks. They introduce non-linear flows in the logic, hence producing more complicated and less maintainable code. Commit hooks is a powerful tool that should only be used in situations where benefits of using them overweight the drawbacks. One popular example is separate logging of changes in objects of selected classes.
Can I do DB operations inside commit hooks?
The answer is "Yes", since all commit hooks relate to write operations (create/update/delete), thus there must always be a transaction spanning these operations, and all event handlers are run inside this transaction. For example, in TestHooks we create an instance of a class YetAnotherClass inside CommitInsert, but do not introduce a transaction scope around this line. The reason being for it is that there is already a transaction from Main which spans this call.
Notes.
  1. 1.
    It is currently not possible to detach commit hook event handlers.
  2. 2.
    CRUD operations introduced inside a hook are not triggering additional hooks. For instance, in TestHooks the insert hook for YetAnotherClass is never invoked, because the only place for it triggered is in CommitInsert, which is itself a commit hook.
  3. 3.
    It is recommended to avoid sync tasks in commit hooks. Instead, wrap the tasks in Session.ScheduleTask or Scheduling.ScheduleTask. In essence, when doing anything more than updating database objects, an asynchronous task should be scheduled for it. Otherwise, unexpected behavior might occur, such as Self.GET calls returning null.
Last modified 3yr ago
Copy link