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() {