Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions src/Contextually/InfoBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ internal class InfoBlock : IDisposable
{
private bool IsDisposed { get; set; }

/// <summary>
/// Create a new Info block with new values and potentially a parent block.
/// </summary>
/// <param name="parent">Optional parent block.</param>
/// <param name="info">Required values for this block.</param>
internal InfoBlock(RelevantInfoContainer container, NameValueCollection info)
{
ParentBlock = null;

Container = container;

CurrentInfo = new NameValueCollection(info);
}

/// <summary>
/// Create a new Info block with new values and potentially a parent block.
/// </summary>
Expand All @@ -20,18 +34,22 @@ internal InfoBlock(InfoBlock parent, NameValueCollection info)
{
ParentBlock = parent;

Container = parent.Container;

CurrentInfo = new NameValueCollection(info);
}

internal RelevantInfoContainer Container { get; }

/// <summary>
/// A reference to the parent enclosing Info block.
/// </summary>
internal InfoBlock ParentBlock { get; private set; }
internal InfoBlock ParentBlock { get; }

/// <summary>
/// The values assigned in the nearest enclosing Info block.
/// </summary>
internal NameValueCollection CurrentInfo { get; private set; }
internal NameValueCollection CurrentInfo { get; }

/// <summary>
/// Disposes of the context and sets the active context to the parent, if any.
Expand All @@ -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;
}
Expand Down
64 changes: 60 additions & 4 deletions src/Contextually/Relevant.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading;

Expand All @@ -8,12 +9,64 @@ namespace Contextually
/// Allows the creation of Info blocks and reading the current context.
/// </summary>
public static class Relevant
{
private static RelevantInfoContainer RootContainer { get; } = new RelevantInfoContainer();

private static IDictionary<string, RelevantInfoContainer> NamedContainers { get; } = new Dictionary<string, RelevantInfoContainer>();

/// <summary>
/// Retrieve the full set of values from all enclosing Info blocks.
/// </summary>
/// <returns>Full set of values from all enclosing Info blocks.</returns>
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();
}
}

/// <summary>
/// Starts a new Info block with a set of values.
/// </summary>
/// <param name="info">The values this Info block represents.</param>
/// <returns>An <see cref="IDisposable"/> object for use with a using block.</returns>
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);
}
}
}

/// <summary>
/// Allows the creation of Info blocks and reading the current context.
/// Used for including context in libraries without exposing publicly.
/// </summary>
public class RelevantInfoContainer
{
/// <summary>
/// Retrieve the full set of values from all enclosing Info blocks.
/// </summary>
/// <returns>Full set of values from all enclosing Info blocks.</returns>
public static NameValueCollection Info()
public NameValueCollection Info()
{
var current = Head.Value;
var info = new NameValueCollection();
Expand All @@ -31,16 +84,19 @@ public static NameValueCollection Info()
/// <summary>
/// The head node which is an Info block (may point to parent blocks).
/// </summary>
internal static AsyncLocal<InfoBlock> Head { get; } = new AsyncLocal<InfoBlock>();
internal AsyncLocal<InfoBlock> Head { get; } = new AsyncLocal<InfoBlock>();

/// <summary>
/// Starts a new Info block with a set of values.
/// </summary>
/// <param name="info">The values this Info block represents.</param>
/// <returns>An <see cref="IDisposable"/> object for use with a using block.</returns>
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;
}
Expand Down
137 changes: 137 additions & 0 deletions test/Contextually.Tests/SynchronyousTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down