In a series of 3 parts I will walk you through how to create the basic scope class, a nestable scope class, and finally the generics based nestable scope class.
What is a Scope class? Glossary
So before we get started, here's some term definitions that will dramatically help clarify the purpose of a scope and how it behaves.
- Thread Context Storage : A property bag that is associated with the current thread context. Think of it as having a backpack on the thread in which you can put or get objects in & out of at any time without having to pass them as parameters through method calls
- On the Thread Context : The process of putting data in the current Thread Context Storage using some key and value
- Scope class : A design pattern in which while the object is alive stores itself on the thread context until the class is disposed of (via IDisposable.Dispose(), not to be confused with a deconstructor).
- Nestable Scope class : Same as a Scope class however behaves differently depending upon whether or not a scope of the same type is already existent on the thread context.
The Basic Scope Class
I'm hoping that you actually read those above, and not just glanced over them. It is crucial that you understand all of the terms above. Now, did you actually read it this time?
In this basic scope class, let's set out an example to code against. Let's say we want to identify the total amount of times a particular method (Doodle) is called within an function (Foo). Here's the example code using a DoodleCounterScope class.
1: public void Foo()
2: {
3: using(DoodleCounterScope scope = new DoodleCounterScope())
4: {
5: Brainstorm(5);
6: Brainstorm(10);
7: Console.WriteLine(scope.Count); // Output: 15
8: }
9: }
10: private void Brainstorm(int doodleCount)
11: {
12: for (int i = 0; i < doodleCount; i++)
13: Doodle();
14: }
15: private void Doodle()
16: {
17: // Increment method counter
18: DoodleCounterScope.Current.Count += 1;
19: }
Ok, now we're going to jump into creating a scope class. Notice that a scope class works by putting data on the thread context as soon as it becomes alive and only removes once it disposes. So if we were to look at that, we can extrapolate the following code to start with.
1: public class Scope : IDisposable
2: {
3: public Scope(){ /* Put [this] on the thread context */ }
4: public void Dispose(){ /* Remove [this] from the thread context */ }
5: }
The example above begins tracking at line 3 and finishes at line 8. So in line 3 we put data on the thread context and line 8 we remove data off the thread context. ( an end of a using block executes IDisposable.Dispose() on the object referenced in the using statement. We use a 'using' block to ensure that regardless of an exception once the thread leaves the scope of the 'using' block, the dispose method is called (hence scope) )
Using the CallContext class in the System.Runtime.Remoting.Messaging namespace we can put and remove data on and off the thread. There are two sets of static functions seperating the behavior of the storage. The plain SetData and GetData works with an object on the current thread only. The more sophisticated LogicalSetData and LogicalGetData works with data on the current thread, carries over to any spawned threads, and through remoting channels. To be able to use the logical call methods, LogicalSetData and LogicalGetData, the object must be serializable and implement the marker interface ILogicalThreadAffinative in the System.Runtime.Remoting.Messaging namespace.
The CallContext stores and retrieves objects by a string name identifier. To remove data off of the thread you must call CallContext.FreeNamedDataSlot. The GetData & LogicalGetData methods only retrieve a reference to the object on the thread with the specified name (not clearing the name). If you do not free the named data slot on the thread your object will not dispose until the thread is abandoned and disposed of.
Knowing all of that, let's move forward. Let's rename the Scope class to match our example DoodleCounterScope
1: public class DoodleCounterScope : IDisposable
2: {
3: public DoodleCounterScope(){ /* Put [this] on the thread context */ }
4: public void Dispose(){ /* Remove [this] from the thread context */ }
5: }
Now let's mark the class as serializable and implement the marker interface ILogicalThreadAffinative
1: [Serializable]
2: public class DoodleCounterScope : IDisposable, ILogicalThreadAffinative
3: {
4: public DoodleCounterScope(){ /* Put [this] on the thread context */ }
5: public void Dispose(){ /* Remove [this] from the thread context */ }
6: }
Next let's put the constructor and disposing logic in place for lines 4 and 5
1: [Serializable]
2: public class DoodleCounterScope : IDisposable, ILogicalThreadAffinative
3: {
4: public DoodleCounterScope()
5: {
6: // Put [this] on the thread context
7: CallContext.LogicalSetData("DoodleCounterScope", this);
8: }
9:
10: public void Dispose()
11: {
12: // Remove [this] from the thread context
13: CallContext.FreeNamedDataSlot("DoodleCounterScope");
14: }
15: }
Look back at our example use of this DoodleCounterScope at line 18. The Doodle method gets a reference to our scope object through a DoodleCounterScope.Current static property and then increments a Count property. Now it's important to note that the Current property is static. The key differrence of using a scope class is that the static property will be returning a thread specific variable not a shared static variable. Let's add that Current property.
1: [Serializable]
2: public class DoodleCounterScope : IDisposable, ILogicalThreadAffinative
3: {
4: public static DoodleCounterScope Current
5: {
6: get
7: {
8: return CallContext.LogicalGetData("DoodleCounterScope") as DoodleCounterScope;
9: }
10: }
11:
12: public DoodleCounterScope()
13: {
14: // Put [this] on the thread context
15: CallContext.LogicalSetData("DoodleCounterScope", this);
16: }
17:
18: public void Dispose()
19: {
20: // Remove [this] from the thread context
21: CallContext.FreeNamedDataSlot("DoodleCounterScope");
22: }
23: }
Finally let's add the counter property that's being incremented on line 18 of our example above.
1: [Serializable]
2: public class DoodleCounterScope : IDisposable, ILogicalThreadAffinative
3: {
4: public static DoodleCounterScope Current
5: {
6: get
7: {
8: return CallContext.LogicalGetData("DoodleCounterScope") as DoodleCounterScope;
9: }
10: }
11:
12: public DoodleCounterScope()
13: {
14: // Put [this] on the thread context
15: CallContext.LogicalSetData("DoodleCounterScope", this);
16: }
17:
18: /// <summary>
19: /// Gets or sets the total amount of times the Doodle method was called during the existence of this Scope
20: /// </summary>
21: public int Count { get; set; }
22:
23: public void Dispose()
24: {
25: // Remove [this] from the thread context
26: CallContext.FreeNamedDataSlot("DoodleCounterScope");
27: }
28: }
Basic Scope Class Complete
We have successfully created a basic scope class that puts a reference of itself onto the thread, has a static property to get a reference of that scope off the thread later in execution, and a property of which we can modify and retrieve later.