#c# #.net #asynchronous #.net-core #scope
#c# #.net #асинхронный #.net-core #область видимости
Вопрос:
Я пытаюсь создать вызываемый пользовательский класс области AuditScope
, через который можно получить доступ к текущей области AuditScope.Current
.
Если есть вложенные области, текущая область является наиболее вложенной областью.
Я хочу, чтобы это было потокобезопасно, поэтому я использую AsyncLocal
, чтобы убедиться, что текущая область относится к текущему контексту асинхронности и нет столкновений с другими запросами. Это похоже на TransactionScope
класс, если кто-нибудь из вас с ним сталкивался.
Вот мой класс области видимости:
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();
}
}
Все мои тесты проходят по отдельности, однако, если я запускаю их все одновременно, один тест последовательно завершается неудачей:
[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 amp; Assert
await Task.WhenAll(
createScopeWithLifespan(1000),
createScopeWithLifespan(2000));
}
Конечно, поскольку using
операторы находятся в разных контекстах, это должно сработать? Почему проходит при самостоятельном запуске, но не при запуске вместе с другими тестами?
Для полноты см. Ниже Другие тесты, с которыми я его выполняю, но я серьезно сомневаюсь, что они напрямую влияют на них:
[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);
}
}
Ответ №1:
Оказывается, где-то должна была быть проблема со ссылкой, так как замена Stack<AuditScope>
ImmutableStack<AuditScope>
исправила проблему.
Ответ №2:
У меня была та же проблема. Как и у меня, я считаю, что ваша проблема в вашем распоряжении:
public void Dispose()
{
ScopeStack.Value?.Pop();
}
TLDR; это то, что я готов поспорить, если вы измените его на это…
public void Dispose()
{
ScopeStack.Value?.Pop();
if(ScopeStack.Value.Count == 0)
ScopeStack.Value = null;
}
… ваш код будет работать так, как ожидалось, даже без ImmutableStack .
AsyncLocal похож на ThreadStatic и при использовании в асинхронном контексте (например, MVC). Это дает то же разделение, которое вы ожидаете для контекста вызова, но устраняет проблему, созданную тем, что этот контекст обслуживается разными потоками.
Однако, в отличие от ThreadStatic, асинхронный локальный контекст передается дочерним потокам в том же контексте вызова.
В случае наших модульных тестов этот начальный контекст вызова является тестовым исполнителем, и он, вероятно, выполняет все эти другие тесты в том же потоке. К тому времени, когда он переходит к рассматриваемому тесту, ваш ScopeStack инициализируется, а затем передается в дочерние потоки. Вероятно, вы могли бы воспроизвести ту же ошибку, просто создав и уничтожив ScopeStack прямо перед выполнением задач.
Установка для него значения null позволяет вместо этого заново инициализировать его внутри потоков, а асинхронный локальный не выполняется (все дочерние потоки будут инициализировать свои собственные, поскольку они не увидят, что он уже установлен).
ImmutableStack исправляет это, потому что, несмотря на то, что он стекает, вы никогда не вносите изменения в оригинал. Единственная проблема здесь заключается в том, что если вы хотите выполнить какое-либо недопустимое обнаружение вложенности или не разрешить удаление от родительского элемента до дочернего, вы не сможете увидеть такие изменения, внесенные дочерними областями. Конечно, это может быть уже недоступно, если вам нужно запустить несколько братьев и сестер от одного и того же родителя (и в этот момент они не могут совместно использовать стек).