I am trying to create a custom scope class called AuditScope
, whereby the current scope can be accessed via AuditScope.Current
.
If there are nested scopes, the current scope is the most nested scope.
I want this to be thread safe, so I am using AsyncLocal
to ensure the current scope belongs to the current async context and there are no clashes with other requests. This is similar to the TransactionScope
class if any of you have come across it.
Here is my scope class:
public sealed class AuditScope : IDisposable
{
private static readonly AsyncLocal<Stack<AuditScope>> ScopeStack = new();
public int ExecutedByUserId { get; }
public AuditScope(int executedByUserId)
{
ExecutedByUserId = executedByUserId;
if (ScopeStack.Value == null)
{
ScopeStack.Value = new Stack<AuditScope>();
}
ScopeStack.Value.Push(this);
}
public static AuditScope? Current
{
get
{
if (ScopeStack.Value == null || ScopeStack.Value.Count == 0)
{
return null;
}
return ScopeStack.Value.Peek();
}
}
public void Dispose()
{
ScopeStack.Value?.Pop();
}
}
All my tests are individually passing, however if I run them all at the same time, one test consistently fails:
[Test]
public async Task GivenThreadCreatesScope_AndSecondThreadCreatesScope_WhenCurrentScopeAccessedOnBothThreads_ThenCorrectScopeReturned()
{
// Arrange
static async Task createScopeWithLifespan(int lifespanInMilliseconds)
{
// This line throws the error, saying it is not null (for the 2000ms scope)
// No scope has been created yet for this async context, so current should be null
Assert.IsNull(AuditScope.Current);
using (var scope = new AuditScope(1))
{
// Scope has been created, so current should match
Assert.AreEqual(scope, AuditScope.Current);
await Task.Delay(lifespanInMilliseconds);
// Scope has not been disposed, so current should still match
Assert.AreEqual(scope, AuditScope.Current);
}
// Scope has been disposed, so current should be null
Assert.IsNull(AuditScope.Current);
}
// Act & Assert
await Task.WhenAll(
createScopeWithLifespan(1000),
createScopeWithLifespan(2000));
}
Surely since the using
statements are in different contexts, this should work? Why does pass when run on its own, but not when run alongside the other tests?
For completeness, see below for the other tests I am running it alongside, but I seriously doubt they are directly impacting them:
[Test]
public void GivenNoCurrentScope_WhenCurrentScopeAccessed_ThenNull()
{
// Act
var result = AuditScope.Current;
// Arrange
Assert.Null(result);
}
[Test]
public void GivenScope_WhenScopeDisposed_ThenNull()
{
// Arrange
using (var scope = new AuditScope(1))
{
}
// Act
var result = AuditScope.Current;
// Arrange
Assert.Null(result);
}
[Test]
public void GivenScopeCreated_WhenCurrentScopeAccessed_ThenScopeReturned()
{
// Arrange
using (var scope = new AuditScope(1))
{
// Act
var result = AuditScope.Current;
// Arrange
Assert.NotNull(result);
Assert.AreEqual(scope, result);
}
}
[Test]
public void GivenNestedScopeCreated_WhenCurrentScopeAccessed_ThenNestedScopeReturned()
{
// Arrange
using (var scope = new AuditScope(1))
{
using (var nestedScope = new AuditScope(2))
{
// Act
var result = AuditScope.Current;
// Arrange
Assert.NotNull(result);
Assert.AreEqual(nestedScope, result);
}
}
}
[Test]
public void GivenNestedScopeCreated_WhenNestedScopeDisposed_ThenCurrentScopeRevertsToParent()
{
// Arrange
using (var scope = new AuditScope(1))
{
using (var nestedScope = new AuditScope(2))
{
}
// Act
var result = AuditScope.Current;
// Arrange
Assert.NotNull(result);
Assert.AreEqual(scope, result);
}
}
事实证明,某处肯定存在参考问题,因为将Stack<AuditScope>
替换为ImmutableStack<AuditScope>
修复了该问题。
I had the same problem. Like mine, I believe your issue is in your Dispose:
public void Dispose()
{
ScopeStack.Value?.Pop();
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.