How to store per thread contextual data in c#?

208 views Asked by At

My concrete need is to store remote IP so it can be printed while logging, without explicitly passing it down every method call. A sort of per thread environment variable scope.

I thought of having a per thread Singleton so logger can access it. Is there a better/safer way?

Make it work with async is a plus.

My ideal API should look like this:

using (new LogScope("key1", "value1"))
{
   Call1();
}

using (new LogScope("key1", "value1"))
{
    Call1();
}

void Call1()
{
    using (new LogScope("key2", "value2"))
    {
        Call2();     // key1:value1 key2:value2
        using (new LogScope("key1", "value3"))
        {
            Call2(); // key1:value3 key2:value2
        }
    }
    using (new LogScope("key1", "value3"))
    {
        Call2();     // key1:value3
    }
}

void Call2()
{
    foreach (var kv in LogScope.Context) Console.Write("'{0}':'{1}' ");
    Console.WriteLine();
}
2

There are 2 answers

2
g.pickardou On

a) If you would like to use a per thread singleton, you definitely should not write your own, there are out of the box solutions. Mainly all modern DI framework supports instantiate option like singletons, and singletons per thread. Just an example from Ninject:

kernel.Bind(typeof (IAnyThing)).To(typeof (AnyThing)).InThreadScope();

or if you would not like to use / implement interface:

kernel.Bind(typeof (AnyThing)).To(typeof (AnyThing)).InThreadScope();

After that binding whereever you ask for your DI container an instance of AnyThing you got a per thread singleton. AnyThing could be really anything: say a POCO.

EDIT: In case you would like any custom scoping, then just implement your scope definition, leave the rest of the container look for Custom in this page

b) You could use ThreadLocal<T>.

EDIT: Before suggesting ThreadLocal one note about async expectation you mentioned in your question. Yes, ThreadLocal is not "compatible" with async.

However this is not the root of the problem: Thread affinity is not "compatible" with most of the concurrent programming design patterns (CDP), because many CDP uses the concept of thread "unaffinity", like pool, serializing to a message queue, marshaling to other thread etc. So having a thread affinity as an expectation, and having async work as an expectation seems to be not a good idea together, one should be dropped or replaced. Note this is not about the async keyword, this is about the async controlflow. END EDIT

Please refer to sample code in msdn doc

    // Demonstrates: 
    //      ThreadLocal(T) constructor 
    //      ThreadLocal(T).Value 
    //      One usage of ThreadLocal(T) 
    static void Main()
    {
        // Thread-Local variable that yields a name for a thread
        ThreadLocal<string> ThreadName = new ThreadLocal<string>(() =>
        {
            return "Thread" + Thread.CurrentThread.ManagedThreadId;
        });

        // Action that prints out ThreadName for the current thread
        Action action = () =>
        {
            // If ThreadName.IsValueCreated is true, it means that we are not the 
            // first action to run on this thread. 
            bool repeat = ThreadName.IsValueCreated;

            Console.WriteLine("ThreadName = {0} {1}", ThreadName.Value, repeat ? "(repeat)" : "");
        };

        // Launch eight of them.  On 4 cores or less, you should see some repeat ThreadNames
        Parallel.Invoke(action, action, action, action, action, action, action, action);

        // Dispose when you are done
        ThreadName.Dispose();
    }
3
sstan On

Not the most optimal implementation. But it should do what you are looking for. And it should at least demonstrate some principles you can reuse.

public class LogScope : IDisposable
{
    private static readonly ThreadLocal<Stack<Dictionary<string, string>>> currentDictionary =
        new ThreadLocal<Stack<Dictionary<string, string>>>(() => new Stack<Dictionary<string, string>>());

    public LogScope(string key, string value)
    {
        var stack = currentDictionary.Value;
        Dictionary<string, string> newDictionary = null;
        if (stack.Count == 0)
        {
            newDictionary = new Dictionary<string, string>();
        }
        else
        {
            newDictionary = new Dictionary<string, string>(stack.Peek());
        }

        newDictionary[key] = value;

        stack.Push(newDictionary);
    }

    public void Dispose()
    {
        currentDictionary.Value.Pop();
    }

    public static IEnumerable<KeyValuePair<string, string>> Context
    {
        get
        {
            var stack = currentDictionary.Value;
            if (stack.Count == 0)
            {
                return Enumerable.Empty<KeyValuePair<string, string>>();
            }
            else
            {
                return stack.Peek();
            }
        }
    }
}

EDIT: I should specify that in my implementation of LogScope, an instance of LogScope will only work properly if it is instantiated and disposed on the same thread. If you create a LogScope in one thread, and then dispose it in another thread, then the results are undefined. Likewise, while a LogScope instance is in effect, its key/values will only be visible within that thread.