diff --git a/src/Contextually/InfoBlock.cs b/src/Contextually/InfoBlock.cs
index 2dab64e..7131cc5 100644
--- a/src/Contextually/InfoBlock.cs
+++ b/src/Contextually/InfoBlock.cs
@@ -11,6 +11,20 @@ internal class InfoBlock : IDisposable
{
private bool IsDisposed { get; set; }
+ ///
+ /// Create a new Info block with new values and potentially a parent block.
+ ///
+ /// Optional parent block.
+ /// Required values for this block.
+ internal InfoBlock(RelevantInfoContainer container, NameValueCollection info)
+ {
+ ParentBlock = null;
+
+ Container = container;
+
+ CurrentInfo = new NameValueCollection(info);
+ }
+
///
/// Create a new Info block with new values and potentially a parent block.
///
@@ -20,18 +34,22 @@ internal InfoBlock(InfoBlock parent, NameValueCollection info)
{
ParentBlock = parent;
+ Container = parent.Container;
+
CurrentInfo = new NameValueCollection(info);
}
+ internal RelevantInfoContainer Container { get; }
+
///
/// A reference to the parent enclosing Info block.
///
- internal InfoBlock ParentBlock { get; private set; }
+ internal InfoBlock ParentBlock { get; }
///
/// The values assigned in the nearest enclosing Info block.
///
- internal NameValueCollection CurrentInfo { get; private set; }
+ internal NameValueCollection CurrentInfo { get; }
///
/// Disposes of the context and sets the active context to the parent, if any.
@@ -45,10 +63,10 @@ public void Dispose()
throw new ObjectDisposedException(nameof(InfoBlock),
"This Info block was already disposed, and you should already know that.");
- if (Relevant.Head.Value == this)
+ if (Container.Head.Value == this)
{
// Set the head of the context linked-list to the parent of this.
- Relevant.Head.Value = ParentBlock;
+ Container.Head.Value = ParentBlock;
IsDisposed = true;
}
diff --git a/src/Contextually/Relevant.cs b/src/Contextually/Relevant.cs
index 4442aa3..8cd5fae 100644
--- a/src/Contextually/Relevant.cs
+++ b/src/Contextually/Relevant.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading;
@@ -8,12 +9,64 @@ namespace Contextually
/// Allows the creation of Info blocks and reading the current context.
///
public static class Relevant
+ {
+ private static RelevantInfoContainer RootContainer { get; } = new RelevantInfoContainer();
+
+ private static IDictionary NamedContainers { get; } = new Dictionary();
+
+ ///
+ /// Retrieve the full set of values from all enclosing Info blocks.
+ ///
+ /// Full set of values from all enclosing Info blocks.
+ public static NameValueCollection Info(string containerName = null)
+ {
+ if(containerName == null)
+ return RootContainer.Info();
+ else
+ {
+ if(!NamedContainers.TryGetValue(containerName, out var container))
+ {
+ container = new RelevantInfoContainer();
+ NamedContainers.Add(containerName, container);
+ }
+
+ return container.Info();
+ }
+ }
+
+ ///
+ /// Starts a new Info block with a set of values.
+ ///
+ /// The values this Info block represents.
+ /// An object for use with a using block.
+ public static IDisposable Info(NameValueCollection info, string containerName = null)
+ {
+ if(containerName == null)
+ return RootContainer.Info(info);
+ else
+ {
+ if(!NamedContainers.TryGetValue(containerName, out var container))
+ {
+ container = new RelevantInfoContainer();
+ NamedContainers.Add(containerName, container);
+ }
+
+ return container.Info(info);
+ }
+ }
+ }
+
+ ///
+ /// Allows the creation of Info blocks and reading the current context.
+ /// Used for including context in libraries without exposing publicly.
+ ///
+ public class RelevantInfoContainer
{
///
/// Retrieve the full set of values from all enclosing Info blocks.
///
/// Full set of values from all enclosing Info blocks.
- public static NameValueCollection Info()
+ public NameValueCollection Info()
{
var current = Head.Value;
var info = new NameValueCollection();
@@ -31,16 +84,19 @@ public static NameValueCollection Info()
///
/// The head node which is an Info block (may point to parent blocks).
///
- internal static AsyncLocal Head { get; } = new AsyncLocal();
+ internal AsyncLocal Head { get; } = new AsyncLocal();
///
/// Starts a new Info block with a set of values.
///
/// The values this Info block represents.
/// An object for use with a using block.
- public static IDisposable Info(NameValueCollection info)
+ public IDisposable Info(NameValueCollection info)
{
- Head.Value = new InfoBlock(Head.Value, info);
+ if(Head.Value == null)
+ Head.Value = new InfoBlock(this, info);
+ else
+ Head.Value = new InfoBlock(Head.Value, info);
return Head.Value;
}
diff --git a/test/Contextually.Tests/SynchronyousTests.cs b/test/Contextually.Tests/SynchronyousTests.cs
index db1e933..21455d6 100644
--- a/test/Contextually.Tests/SynchronyousTests.cs
+++ b/test/Contextually.Tests/SynchronyousTests.cs
@@ -7,6 +7,143 @@ namespace Contextually.Tests
[TestCategory("Synchronyous")]
public class SynchronyousTests
{
+ [TestMethod]
+ public void AccessingNamedInfoWithoutABlockReturnsEmptyCollection()
+ {
+ // ARRANGE
+
+ // ACT
+ var info = Relevant.Info("test");
+
+ // ASSERT
+ Assert.IsNotNull(info, "should have a value");
+ Assert.IsInstanceOfType(info, typeof(NameValueCollection), "should be a NameValueCollection");
+ Assert.AreEqual(0, info.Count, "should have no pairs");
+ }
+
+ [TestMethod]
+ public void AccessingNamedInfoWithinASingleBlockReturnsExpectedValues()
+ {
+ // ARRANGE
+ var expectedValues = new NameValueCollection
+ {
+ ["Key1"] = "Value1"
+ };
+
+ NameValueCollection actualValues;
+
+ // ACT
+ using (Relevant.Info(expectedValues, "test"))
+ {
+ actualValues = Relevant.Info("test");
+ }
+
+ // ASSERT
+ Assert.IsNotNull(actualValues, "should have a value");
+ Assert.IsInstanceOfType(actualValues, typeof(NameValueCollection), "should be a NameValueCollection");
+ Assert.AreEqual(expectedValues.Count, actualValues.Count, $"should have {expectedValues.Count} pair");
+ Assert.AreEqual(expectedValues["Key1"], actualValues["Key1"], "should have same value");
+ }
+
+ [TestMethod]
+ public void AccessingNamedInfoWithinNestedBlocksReturnsExpectedValues()
+ {
+ // ARRANGE
+ var expectedValuesLevelOne = new NameValueCollection
+ {
+ ["Key1"] = "Value1"
+ };
+
+ var expectedValuesLevelTwo = new NameValueCollection
+ {
+ ["Key1"] = "Value1",
+ ["Key2"] = "Value2"
+ };
+
+ var valuesLevelTwo = new NameValueCollection
+ {
+ ["Key2"] = "Value2"
+ };
+
+ NameValueCollection actualValuesLevelOne;
+ NameValueCollection actualValuesLevelTwo;
+
+ // ACT
+ using (Relevant.Info(expectedValuesLevelOne, "test"))
+ {
+ actualValuesLevelOne = Relevant.Info("test");
+
+ using (Relevant.Info(valuesLevelTwo, "test"))
+ {
+ actualValuesLevelTwo = Relevant.Info("test");
+ }
+ }
+
+ // ASSERT
+ Assert.IsNotNull(actualValuesLevelOne, "should have a value");
+ Assert.IsInstanceOfType(actualValuesLevelOne, typeof(NameValueCollection), "should be a NameValueCollection");
+ Assert.AreEqual(expectedValuesLevelOne.Count, actualValuesLevelOne.Count, $"should have {expectedValuesLevelOne.Count} pair");
+ Assert.AreEqual(expectedValuesLevelOne["Key1"], actualValuesLevelOne["Key1"], "should have same value");
+
+ Assert.IsNotNull(actualValuesLevelTwo, "should have a value");
+ Assert.IsInstanceOfType(actualValuesLevelTwo, typeof(NameValueCollection), "should be a NameValueCollection");
+ Assert.AreEqual(expectedValuesLevelTwo.Count, actualValuesLevelTwo.Count, $"should have {expectedValuesLevelTwo.Count} pair");
+ Assert.AreEqual(expectedValuesLevelTwo["Key1"], actualValuesLevelTwo["Key1"], "should have same value");
+ Assert.AreEqual(expectedValuesLevelTwo["Key2"], actualValuesLevelTwo["Key2"], "should have same value");
+ }
+
+ public void AccessingNamedInfoWithinDifferentNestedBlocksReturnsExpectedValues()
+ {
+ // ARRANGE
+ var expectedValuesLevelOne = new NameValueCollection
+ {
+ ["Key1"] = "Value1"
+ };
+
+ var expectedValuesLevelTwo = new NameValueCollection
+ {
+ ["Key1"] = "Value1",
+ ["Key2"] = "Value2"
+ };
+
+ var valuesLevelTwo = new NameValueCollection
+ {
+ ["Key2"] = "Value2"
+ };
+
+ NameValueCollection actualValuesLevelOne;
+ NameValueCollection actualValuesLevelTwo;
+ NameValueCollection actualValuesLevelOneInner;
+
+ // ACT
+ using (Relevant.Info(expectedValuesLevelOne))
+ {
+ actualValuesLevelOne = Relevant.Info();
+
+ using (Relevant.Info(valuesLevelTwo, "test"))
+ {
+ actualValuesLevelTwo = Relevant.Info("test");
+ actualValuesLevelOneInner = Relevant.Info();
+ }
+ }
+
+ // ASSERT
+ Assert.IsNotNull(actualValuesLevelOne, "should have a value");
+ Assert.IsInstanceOfType(actualValuesLevelOne, typeof(NameValueCollection), "should be a NameValueCollection");
+ Assert.AreEqual(expectedValuesLevelOne.Count, actualValuesLevelOne.Count, $"should have {expectedValuesLevelOne.Count} pair");
+ Assert.AreEqual(expectedValuesLevelOne["Key1"], actualValuesLevelOne["Key1"], "should have same value");
+
+ Assert.IsNotNull(actualValuesLevelTwo, "should have a value");
+ Assert.IsInstanceOfType(actualValuesLevelTwo, typeof(NameValueCollection), "should be a NameValueCollection");
+ Assert.AreEqual(valuesLevelTwo.Count, actualValuesLevelTwo.Count, $"should have {valuesLevelTwo.Count} pair");
+ Assert.AreEqual(valuesLevelTwo["Key2"], actualValuesLevelTwo["Key2"], "should have same value");
+
+ Assert.IsNotNull(actualValuesLevelOne, "should have a value");
+ Assert.IsInstanceOfType(actualValuesLevelOneInner, typeof(NameValueCollection), "should be a NameValueCollection");
+ Assert.AreEqual(expectedValuesLevelOne.Count, actualValuesLevelOneInner.Count, $"should have {expectedValuesLevelOne.Count} pair");
+ Assert.AreEqual(expectedValuesLevelOne["Key1"], actualValuesLevelOneInner["Key1"], "should have same value");
+ }
+
[TestMethod]
public void AccessingInfoWithoutABlockReturnsEmptyCollection()
{