diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln
index 57234b521..7b472ccec 100644
--- a/libraries/AWS.Lambda.Powertools.sln
+++ b/libraries/AWS.Lambda.Powertools.sln
@@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Kafka
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.ModuleInitializer.Tests", "tests\AWS.Lambda.Powertools.ModuleInitializer.Tests\AWS.Lambda.Powertools.ModuleInitializer.Tests.csproj", "{E1F2A3B4-C5D6-7E8F-9A0B-1C2D3E4F5A6B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.ConcurrencyTests", "tests\AWS.Lambda.Powertools.ConcurrencyTests\AWS.Lambda.Powertools.ConcurrencyTests.csproj", "{D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -704,6 +706,18 @@ Global
{E1F2A3B4-C5D6-7E8F-9A0B-1C2D3E4F5A6B}.Release|x86.ActiveCfg = Release|Any CPU
{E1F2A3B4-C5D6-7E8F-9A0B-1C2D3E4F5A6B}.Release|x86.Build.0 = Release|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Debug|x64.Build.0 = Debug|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Debug|x86.Build.0 = Debug|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Release|x64.ActiveCfg = Release|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Release|x64.Build.0 = Release|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Release|x86.ActiveCfg = Release|Any CPU
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
@@ -764,5 +778,6 @@ Global
{B640DB80-C982-407B-A2EC-CD29AC77DDB8} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5}
{E1F2A3B4-C5D6-7E8F-9A0B-1C2D3E4F5A6B} = {1CFF5568-8486-475F-81F6-06105C437528}
+ {D2951A1A-D0EF-4CA4-AB4D-5ABAEFD164F5} = {1CFF5568-8486-475F-81F6-06105C437528}
EndGlobalSection
EndGlobal
diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
index 051c5c339..c13710131 100644
--- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.Common.Core;
namespace AWS.Lambda.Powertools.Common;
@@ -167,7 +168,7 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue)
///
/// The X-Ray trace identifier.
public string XRayTraceId =>
- GetEnvironmentVariable(Constants.XrayTraceIdEnv);
+ LambdaTraceProvider.CurrentTraceId;
///
/// Gets a value indicating whether this instance is Lambda.
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.Logging/InternalsVisibleTo.cs
index 308debcfe..06a9c208a 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/InternalsVisibleTo.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/InternalsVisibleTo.cs
@@ -15,4 +15,5 @@
using System.Runtime.CompilerServices;
-[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Logging.Tests")]
\ No newline at end of file
+[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Logging.Tests")]
+[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.ConcurrencyTests")]
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs
index 889ac9478..ae7f40501 100644
--- a/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Logging/Logger.Scope.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using AWS.Lambda.Powertools.Logging.Internal;
using AWS.Lambda.Powertools.Logging.Internal.Helpers;
@@ -9,10 +11,25 @@ namespace AWS.Lambda.Powertools.Logging;
public static partial class Logger
{
///
- /// Gets the scope.
+ /// Thread-safe dictionary for per-thread scope storage.
+ /// Uses ManagedThreadId as key to ensure isolation when Lambda processes
+ /// multiple concurrent requests (AWS_LAMBDA_MAX_CONCURRENCY > 1).
+ ///
+ private static readonly ConcurrentDictionary> _threadScopes = new();
+
+ ///
+ /// Gets the scope for the current thread.
+ /// Creates a new dictionary if one doesn't exist for this thread.
///
/// The scope.
- private static IDictionary Scope { get; } = new Dictionary(StringComparer.Ordinal);
+ private static IDictionary Scope
+ {
+ get
+ {
+ var threadId = Environment.CurrentManagedThreadId;
+ return _threadScopes.GetOrAdd(threadId, _ => new Dictionary(StringComparer.Ordinal));
+ }
+ }
///
/// Gets the correlation identifier from the log context.
@@ -70,7 +87,7 @@ public static void AppendKeys(IEnumerable> keys)
/// Remove additional keys from the log context.
///
/// The list of keys.
- public static void RemoveKeys(params string[] keys)
+ public static void RemoveKeys(params string[] keys)
{
if (keys == null) return;
foreach (var key in keys)
@@ -88,11 +105,15 @@ public static IEnumerable> GetAllKeys()
}
///
- /// Removes all additional keys from the log context.
+ /// Removes all additional keys from the log context for the current thread.
///
internal static void RemoveAllKeys()
{
- Scope.Clear();
+ var threadId = Environment.CurrentManagedThreadId;
+ if (_threadScopes.TryGetValue(threadId, out var scope))
+ {
+ scope.Clear();
+ }
}
///
diff --git a/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj b/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj
index 246bfec47..e8cf1d244 100644
--- a/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj
+++ b/libraries/src/AWS.Lambda.Powertools.Parameters/AWS.Lambda.Powertools.Parameters.csproj
@@ -20,6 +20,7 @@
+
diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj b/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj
index 40c87a013..efe32278b 100644
--- a/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj
+++ b/libraries/src/AWS.Lambda.Powertools.Tracing/AWS.Lambda.Powertools.Tracing.csproj
@@ -16,6 +16,7 @@
+
diff --git a/libraries/src/Directory.Packages.props b/libraries/src/Directory.Packages.props
index d318e8203..cf240f82a 100644
--- a/libraries/src/Directory.Packages.props
+++ b/libraries/src/Directory.Packages.props
@@ -7,7 +7,7 @@
-
+
diff --git a/libraries/tests/AWS.Lambda.Powertools.ConcurrencyTests/AWS.Lambda.Powertools.ConcurrencyTests.csproj b/libraries/tests/AWS.Lambda.Powertools.ConcurrencyTests/AWS.Lambda.Powertools.ConcurrencyTests.csproj
new file mode 100644
index 000000000..907369e13
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.ConcurrencyTests/AWS.Lambda.Powertools.ConcurrencyTests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ default
+ enable
+ enable
+ AWS.Lambda.Powertools.ConcurrencyTests
+ AWS.Lambda.Powertools.ConcurrencyTests
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/libraries/tests/AWS.Lambda.Powertools.ConcurrencyTests/Logging/BufferIsolationTests.cs b/libraries/tests/AWS.Lambda.Powertools.ConcurrencyTests/Logging/BufferIsolationTests.cs
new file mode 100644
index 000000000..b5f62a7ab
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.ConcurrencyTests/Logging/BufferIsolationTests.cs
@@ -0,0 +1,311 @@
+using AWS.Lambda.Powertools.Logging;
+using AWS.Lambda.Powertools.Logging.Internal;
+using Microsoft.Extensions.Logging;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.ConcurrencyTests.Logging;
+
+///
+/// Tests for validating buffer isolation in Powertools Logger under concurrent execution scenarios.
+/// These tests verify that when multiple Lambda invocations run concurrently,
+/// each invocation's log buffer remains isolated from other invocations.
+///
+public class BufferIsolationTests : IDisposable
+{
+ public BufferIsolationTests()
+ {
+ LogBufferManager.ResetForTesting();
+ Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null);
+ }
+
+ public void Dispose()
+ {
+ Logger.ClearBuffer();
+ LogBufferManager.ResetForTesting();
+ Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null);
+ }
+
+ ///
+ /// Verifies that concurrent invocations maintain separate buffers.
+ ///
+ [Theory]
+ [InlineData(2, 3)]
+ [InlineData(3, 5)]
+ [InlineData(5, 2)]
+ public void BufferSeparation_ConcurrentInvocations_ShouldMaintainSeparateBuffers(int concurrencyLevel, int entriesPerInvocation)
+ {
+ var results = new BufferSeparationResult[concurrencyLevel];
+ var barrier = new Barrier(concurrencyLevel);
+ var tasks = new Task[concurrencyLevel];
+
+ LogBufferManager.ResetForTesting();
+
+ for (int i = 0; i < concurrencyLevel; i++)
+ {
+ int invocationIndex = i;
+ tasks[i] = Task.Run(() =>
+ {
+ var invocationId = $"Root=1-{Guid.NewGuid():N};Parent={invocationIndex:X16};Sampled=1";
+ var entriesAdded = new List();
+
+ Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", invocationId);
+
+ barrier.SignalAndWait();
+
+ Logger.Configure(config =>
+ {
+ config.MinimumLogLevel = LogLevel.Debug;
+ config.LogBuffering = new LogBufferingOptions
+ {
+ BufferAtLogLevel = LogLevel.Debug,
+ FlushOnErrorLog = false
+ };
+ });
+
+ for (int e = 0; e < entriesPerInvocation; e++)
+ {
+ var entryMarker = $"inv_{invocationIndex}_entry_{e}_{Guid.NewGuid():N}";
+ entriesAdded.Add(entryMarker);
+ Logger.LogDebug(entryMarker);
+ }
+
+ Thread.Sleep(Random.Shared.Next(1, 10));
+
+ results[invocationIndex] = new BufferSeparationResult
+ {
+ InvocationId = invocationId,
+ InvocationIndex = invocationIndex,
+ EntriesAdded = entriesAdded,
+ ExpectedEntryCount = entriesPerInvocation
+ };
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ foreach (var result in results)
+ {
+ Assert.Equal(result.ExpectedEntryCount, result.EntriesAdded.Count);
+
+ var foreignEntries = result.EntriesAdded
+ .Where(e => !e.StartsWith($"inv_{result.InvocationIndex}_"))
+ .ToList();
+
+ Assert.Empty(foreignEntries);
+ }
+
+ var totalEntries = results.Sum(r => r.EntriesAdded.Count);
+ var expectedTotal = concurrencyLevel * entriesPerInvocation;
+ Assert.Equal(expectedTotal, totalEntries);
+ }
+
+ ///
+ /// Verifies that flushing one invocation's buffer doesn't affect another active invocation's buffer.
+ ///
+ [Theory]
+ [InlineData(10, 3)]
+ [InlineData(30, 2)]
+ [InlineData(50, 5)]
+ public void BufferLifecycleIsolation_OverlappingInvocations_ShouldPreserveActiveBuffer(int shortDuration, int entriesPerInvocation)
+ {
+ var longDuration = shortDuration * 3;
+ var shortResult = new BufferLifecycleResult();
+ var longResult = new BufferLifecycleResult();
+ var barrier = new Barrier(2);
+
+ LogBufferManager.ResetForTesting();
+
+ var shortTask = Task.Run(() =>
+ {
+ var invocationId = $"Root=1-{Guid.NewGuid():N};Parent=SHORT;Sampled=1";
+ shortResult.InvocationId = invocationId;
+ shortResult.EntriesAdded = new List();
+
+ Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", invocationId);
+
+ barrier.SignalAndWait();
+
+ Logger.Configure(config =>
+ {
+ config.MinimumLogLevel = LogLevel.Debug;
+ config.LogBuffering = new LogBufferingOptions
+ {
+ BufferAtLogLevel = LogLevel.Debug,
+ FlushOnErrorLog = false
+ };
+ });
+
+ for (int e = 0; e < entriesPerInvocation; e++)
+ {
+ var entry = $"short_entry_{e}_{Guid.NewGuid():N}";
+ shortResult.EntriesAdded.Add(entry);
+ Logger.LogDebug(entry);
+ }
+
+ Thread.Sleep(shortDuration);
+
+ Logger.FlushBuffer();
+ shortResult.BufferFlushed = true;
+ });
+
+ var longTask = Task.Run(() =>
+ {
+ var invocationId = $"Root=1-{Guid.NewGuid():N};Parent=LONG;Sampled=1";
+ longResult.InvocationId = invocationId;
+ longResult.EntriesAdded = new List();
+
+ Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", invocationId);
+
+ barrier.SignalAndWait();
+
+ Logger.Configure(config =>
+ {
+ config.MinimumLogLevel = LogLevel.Debug;
+ config.LogBuffering = new LogBufferingOptions
+ {
+ BufferAtLogLevel = LogLevel.Debug,
+ FlushOnErrorLog = false
+ };
+ });
+
+ for (int e = 0; e < entriesPerInvocation; e++)
+ {
+ var entry = $"long_entry_{e}_{Guid.NewGuid():N}";
+ longResult.EntriesAdded.Add(entry);
+ Logger.LogDebug(entry);
+ }
+
+ Thread.Sleep(longDuration);
+
+ var postFlushEntry = $"long_post_flush_{Guid.NewGuid():N}";
+ longResult.EntriesAdded.Add(postFlushEntry);
+ Logger.LogDebug(postFlushEntry);
+
+ longResult.BufferIntactAfterOtherFlush = true;
+ longResult.TotalEntriesAfterOtherFlush = longResult.EntriesAdded.Count;
+ });
+
+ Task.WaitAll(shortTask, longTask);
+
+ Assert.True(longResult.BufferIntactAfterOtherFlush);
+
+ var expectedLongEntries = entriesPerInvocation + 1;
+ Assert.Equal(expectedLongEntries, longResult.TotalEntriesAfterOtherFlush);
+ }
+
+ ///
+ /// Verifies that buffer eviction in one invocation doesn't affect another invocation's buffer.
+ ///
+ [Theory]
+ [InlineData(5)]
+ [InlineData(8)]
+ [InlineData(10)]
+ public void BufferEvictionIsolation_SizeLimitEviction_ShouldOnlyAffectOwnBuffer(int entriesPerInvocation)
+ {
+ var smallBufferSize = 1024;
+ var largeBufferSize = 1024 * 1024;
+
+ var evictingResult = new BufferEvictionResult();
+ var normalResult = new BufferEvictionResult();
+ var barrier = new Barrier(2);
+
+ LogBufferManager.ResetForTesting();
+
+ var evictingTask = Task.Run(() =>
+ {
+ var invocationId = $"Root=1-{Guid.NewGuid():N};Parent=EVICTING;Sampled=1";
+ evictingResult.InvocationId = invocationId;
+ evictingResult.EntriesAdded = new List();
+
+ Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", invocationId);
+
+ barrier.SignalAndWait();
+
+ Logger.Configure(config =>
+ {
+ config.MinimumLogLevel = LogLevel.Debug;
+ config.LogBuffering = new LogBufferingOptions
+ {
+ BufferAtLogLevel = LogLevel.Debug,
+ FlushOnErrorLog = false,
+ MaxBytes = smallBufferSize
+ };
+ });
+
+ for (int e = 0; e < entriesPerInvocation * 3; e++)
+ {
+ var entry = $"evicting_entry_{e}_{new string('X', 200)}_{Guid.NewGuid():N}";
+ evictingResult.EntriesAdded.Add(entry);
+ Logger.LogDebug(entry);
+ }
+
+ Thread.Sleep(Random.Shared.Next(5, 15));
+ evictingResult.EvictionTriggered = true;
+ });
+
+ var normalTask = Task.Run(() =>
+ {
+ var invocationId = $"Root=1-{Guid.NewGuid():N};Parent=NORMAL;Sampled=1";
+ normalResult.InvocationId = invocationId;
+ normalResult.EntriesAdded = new List();
+
+ Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", invocationId);
+
+ barrier.SignalAndWait();
+
+ Logger.Configure(config =>
+ {
+ config.MinimumLogLevel = LogLevel.Debug;
+ config.LogBuffering = new LogBufferingOptions
+ {
+ BufferAtLogLevel = LogLevel.Debug,
+ FlushOnErrorLog = false,
+ MaxBytes = largeBufferSize
+ };
+ });
+
+ for (int e = 0; e < entriesPerInvocation; e++)
+ {
+ var entry = $"normal_entry_{e}_{Guid.NewGuid():N}";
+ normalResult.EntriesAdded.Add(entry);
+ Logger.LogDebug(entry);
+ }
+
+ Thread.Sleep(Random.Shared.Next(10, 20));
+
+ normalResult.AllEntriesRetained = normalResult.EntriesAdded.Count == entriesPerInvocation;
+ });
+
+ Task.WaitAll(evictingTask, normalTask);
+
+ Assert.True(normalResult.AllEntriesRetained,
+ $"Normal invocation lost entries. Expected {entriesPerInvocation}, had {normalResult.EntriesAdded.Count}");
+
+ Assert.True(evictingResult.EvictionTriggered);
+ }
+
+ private class BufferSeparationResult
+ {
+ public string InvocationId { get; set; } = string.Empty;
+ public int InvocationIndex { get; set; }
+ public List EntriesAdded { get; set; } = new();
+ public int ExpectedEntryCount { get; set; }
+ }
+
+ private class BufferLifecycleResult
+ {
+ public string InvocationId { get; set; } = string.Empty;
+ public List EntriesAdded { get; set; } = new();
+ public bool BufferFlushed { get; set; }
+ public bool BufferIntactAfterOtherFlush { get; set; }
+ public int TotalEntriesAfterOtherFlush { get; set; }
+ }
+
+ private class BufferEvictionResult
+ {
+ public string InvocationId { get; set; } = string.Empty;
+ public List EntriesAdded { get; set; } = new();
+ public bool EvictionTriggered { get; set; }
+ public bool AllEntriesRetained { get; set; }
+ }
+}
diff --git a/libraries/tests/AWS.Lambda.Powertools.ConcurrencyTests/Logging/KeyIsolationTests.cs b/libraries/tests/AWS.Lambda.Powertools.ConcurrencyTests/Logging/KeyIsolationTests.cs
new file mode 100644
index 000000000..b5e75244a
--- /dev/null
+++ b/libraries/tests/AWS.Lambda.Powertools.ConcurrencyTests/Logging/KeyIsolationTests.cs
@@ -0,0 +1,574 @@
+using AWS.Lambda.Powertools.Logging;
+using Xunit;
+
+namespace AWS.Lambda.Powertools.ConcurrencyTests.Logging;
+
+///
+/// Tests for validating key isolation in Powertools Logger under concurrent execution scenarios.
+/// These tests verify that when multiple Lambda invocations run concurrently (multi-instance mode),
+/// each invocation's logging keys remain isolated from other invocations.
+///
+public class KeyIsolationTests
+{
+ ///
+ /// Demonstrates that a shared static Dictionary (old implementation) would fail under concurrent access.
+ /// This proves our tests would catch the thread-safety bug.
+ ///
+ [Fact]
+ public void DemonstrateOldImplementationWouldFail_SharedStaticDictionary()
+ {
+ var sharedScope = new Dictionary();
+ var concurrencyLevel = 5;
+ var results = new (string InvocationId, string UniqueKey, Dictionary AllKeysAtEnd)[concurrencyLevel];
+ var barrier = new Barrier(concurrencyLevel);
+ var tasks = new Task[concurrencyLevel];
+ var lockObj = new object();
+
+ for (int i = 0; i < concurrencyLevel; i++)
+ {
+ int invocationIndex = i;
+ tasks[i] = Task.Run(() =>
+ {
+ var invocationId = Guid.NewGuid().ToString();
+ var uniqueKey = $"invocation_{invocationId}";
+
+ barrier.SignalAndWait();
+
+ lock (lockObj)
+ {
+ sharedScope[uniqueKey] = invocationId;
+ }
+
+ Thread.Sleep(Random.Shared.Next(1, 10));
+
+ Dictionary allKeys;
+ lock (lockObj)
+ {
+ allKeys = new Dictionary(sharedScope);
+ }
+
+ results[invocationIndex] = (invocationId, uniqueKey, allKeys);
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ var allUniqueKeys = results.Select(r => r.UniqueKey).ToHashSet();
+ var anyLeakageDetected = results.Any(result =>
+ result.AllKeysAtEnd.Keys.Any(k => allUniqueKeys.Contains(k) && k != result.UniqueKey));
+
+ Assert.True(anyLeakageDetected,
+ "Expected the OLD shared-dictionary implementation to show key leakage between concurrent invocations.");
+ }
+
+ ///
+ /// Verifies that concurrent invocations don't leak keys to each other.
+ ///
+ [Theory]
+ [InlineData(2)]
+ [InlineData(5)]
+ [InlineData(10)]
+ public void KeyIsolation_ConcurrentInvocations_ShouldNotLeakKeys(int concurrencyLevel)
+ {
+ var results = new ConcurrentInvocationResult[concurrencyLevel];
+ var barrier = new Barrier(concurrencyLevel);
+ var tasks = new Task[concurrencyLevel];
+
+ ClearAllLoggerState();
+
+ for (int i = 0; i < concurrencyLevel; i++)
+ {
+ int invocationIndex = i;
+ tasks[i] = Task.Run(() =>
+ {
+ var invocationId = Guid.NewGuid().ToString();
+ var uniqueKey = $"invocation_{invocationId}";
+
+ barrier.SignalAndWait();
+
+ Logger.AppendKey(uniqueKey, invocationId);
+
+ Thread.Sleep(Random.Shared.Next(1, 10));
+
+ var allKeys = Logger.GetAllKeys().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+
+ results[invocationIndex] = new ConcurrentInvocationResult
+ {
+ InvocationId = invocationId,
+ UniqueKey = uniqueKey,
+ AllKeysAtEnd = allKeys
+ };
+
+ Logger.RemoveKeys(uniqueKey);
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ var allUniqueKeys = results.Select(r => r.UniqueKey).ToHashSet();
+
+ foreach (var result in results)
+ {
+ var foreignKeys = result.AllKeysAtEnd.Keys
+ .Where(k => allUniqueKeys.Contains(k) && k != result.UniqueKey)
+ .ToList();
+
+ Assert.Empty(foreignKeys);
+ }
+ }
+
+ ///
+ /// Verifies that GetAllKeys returns only the calling invocation's keys.
+ ///
+ [Theory]
+ [InlineData(2, 3)]
+ [InlineData(5, 2)]
+ [InlineData(8, 5)]
+ public void GetAllKeys_ConcurrentInvocations_ShouldReturnOnlyOwnKeys(int concurrencyLevel, int keysPerInvocation)
+ {
+ var results = new GetAllKeysResult[concurrencyLevel];
+ var barrier = new Barrier(concurrencyLevel);
+ var tasks = new Task[concurrencyLevel];
+
+ ClearAllLoggerState();
+
+ for (int i = 0; i < concurrencyLevel; i++)
+ {
+ int invocationIndex = i;
+ tasks[i] = Task.Run(() =>
+ {
+ var invocationId = Guid.NewGuid().ToString();
+ var appendedKeys = new Dictionary();
+
+ barrier.SignalAndWait();
+
+ for (int k = 0; k < keysPerInvocation; k++)
+ {
+ var key = $"inv_{invocationId}_key_{k}";
+ var value = $"value_{k}_{invocationId}";
+ Logger.AppendKey(key, value);
+ appendedKeys[key] = value;
+ }
+
+ Thread.Sleep(Random.Shared.Next(1, 10));
+
+ var allKeysResult = Logger.GetAllKeys().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+
+ results[invocationIndex] = new GetAllKeysResult
+ {
+ InvocationId = invocationId,
+ AppendedKeys = appendedKeys,
+ GetAllKeysResult_Keys = allKeysResult
+ };
+
+ Logger.RemoveKeys(appendedKeys.Keys.ToArray());
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ var allAppendedKeysByInvocation = results.ToDictionary(
+ r => r.InvocationId,
+ r => r.AppendedKeys.Keys.ToHashSet());
+
+ foreach (var result in results)
+ {
+ var ownKeys = result.AppendedKeys.Keys.ToHashSet();
+ var otherInvocationKeys = allAppendedKeysByInvocation
+ .Where(kvp => kvp.Key != result.InvocationId)
+ .SelectMany(kvp => kvp.Value)
+ .ToHashSet();
+
+ var foreignKeysInResult = result.GetAllKeysResult_Keys.Keys
+ .Where(k => otherInvocationKeys.Contains(k))
+ .ToList();
+
+ Assert.Empty(foreignKeysInResult);
+
+ var missingOwnKeys = ownKeys.Where(k => !result.GetAllKeysResult_Keys.ContainsKey(k)).ToList();
+ Assert.Empty(missingOwnKeys);
+ }
+ }
+
+ ///
+ /// Verifies that concurrent invocations using the same key name maintain separate values.
+ ///
+ [Theory]
+ [InlineData(2)]
+ [InlineData(5)]
+ [InlineData(10)]
+ public void SameNameKey_ConcurrentInvocations_ShouldMaintainSeparateValues(int concurrencyLevel)
+ {
+ var sharedKeyName = "shared_test_key";
+ var results = new SameNameKeyResult[concurrencyLevel];
+ var barrier = new Barrier(concurrencyLevel);
+ var tasks = new Task[concurrencyLevel];
+
+ ClearAllLoggerState();
+
+ for (int i = 0; i < concurrencyLevel; i++)
+ {
+ int invocationIndex = i;
+ tasks[i] = Task.Run(() =>
+ {
+ var invocationId = Guid.NewGuid().ToString();
+ var uniqueValue = $"unique_value_{invocationId}";
+
+ barrier.SignalAndWait();
+
+ Logger.AppendKey(sharedKeyName, uniqueValue);
+
+ Thread.Sleep(Random.Shared.Next(1, 15));
+
+ var allKeys = Logger.GetAllKeys().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+ var retrievedValue = allKeys.TryGetValue(sharedKeyName, out var val) ? val?.ToString() : null;
+
+ results[invocationIndex] = new SameNameKeyResult
+ {
+ InvocationId = invocationId,
+ ExpectedValue = uniqueValue,
+ RetrievedValue = retrievedValue
+ };
+
+ Logger.RemoveKeys(sharedKeyName);
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ foreach (var result in results)
+ {
+ Assert.Equal(result.ExpectedValue, result.RetrievedValue);
+ }
+ }
+
+ ///
+ /// Verifies that ClearState on one invocation doesn't affect another active invocation.
+ ///
+ [Theory]
+ [InlineData(10)]
+ [InlineData(30)]
+ [InlineData(50)]
+ public void ClearState_OverlappingInvocations_ShouldNotAffectActiveInvocation(int shortDuration)
+ {
+ var longDuration = shortDuration * 3;
+ var shortInvocationResult = new ClearStateResult();
+ var longInvocationResult = new ClearStateResult();
+ var barrier = new Barrier(2);
+
+ ClearAllLoggerState();
+
+ var shortTask = Task.Run(() =>
+ {
+ var invocationId = Guid.NewGuid().ToString();
+ var uniqueKey = $"short_inv_{invocationId}";
+
+ shortInvocationResult.InvocationId = invocationId;
+ shortInvocationResult.UniqueKey = uniqueKey;
+
+ barrier.SignalAndWait();
+
+ Logger.AppendKey(uniqueKey, invocationId);
+ shortInvocationResult.KeyAppended = true;
+
+ Thread.Sleep(shortDuration);
+
+ var keysToRemove = Logger.GetAllKeys().Select(k => k.Key).ToArray();
+ Logger.RemoveKeys(keysToRemove);
+ shortInvocationResult.ClearStateCalled = true;
+ });
+
+ var longTask = Task.Run(() =>
+ {
+ var invocationId = Guid.NewGuid().ToString();
+ var uniqueKey = $"long_inv_{invocationId}";
+
+ longInvocationResult.InvocationId = invocationId;
+ longInvocationResult.UniqueKey = uniqueKey;
+
+ barrier.SignalAndWait();
+
+ Logger.AppendKey(uniqueKey, invocationId);
+ longInvocationResult.KeyAppended = true;
+
+ Thread.Sleep(longDuration);
+
+ var allKeys = Logger.GetAllKeys().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+ longInvocationResult.KeysAfterOtherClear = allKeys;
+ longInvocationResult.OwnKeyStillPresent = allKeys.ContainsKey(uniqueKey);
+
+ Logger.RemoveKeys(uniqueKey);
+ });
+
+ Task.WaitAll(shortTask, longTask);
+
+ Assert.True(longInvocationResult.OwnKeyStillPresent,
+ $"Long invocation's key was removed when short invocation called ClearState");
+
+ Assert.False(longInvocationResult.KeysAfterOtherClear.ContainsKey(shortInvocationResult.UniqueKey),
+ $"Short invocation's key leaked into long invocation's scope");
+ }
+
+ private static void ClearAllLoggerState()
+ {
+ var existingKeys = Logger.GetAllKeys()
+ .Select(k => k.Key)
+ .Where(k => !string.IsNullOrEmpty(k))
+ .ToArray();
+ if (existingKeys.Length > 0)
+ {
+ Logger.RemoveKeys(existingKeys);
+ }
+ }
+
+ private class ConcurrentInvocationResult
+ {
+ public string InvocationId { get; set; } = string.Empty;
+ public string UniqueKey { get; set; } = string.Empty;
+ public Dictionary AllKeysAtEnd { get; set; } = new();
+ }
+
+ private class GetAllKeysResult
+ {
+ public string InvocationId { get; set; } = string.Empty;
+ public Dictionary AppendedKeys { get; set; } = new();
+ public Dictionary GetAllKeysResult_Keys { get; set; } = new();
+ }
+
+ private class SameNameKeyResult
+ {
+ public string InvocationId { get; set; } = string.Empty;
+ public string ExpectedValue { get; set; } = string.Empty;
+ public string? RetrievedValue { get; set; }
+ }
+
+ private class ClearStateResult
+ {
+ public string InvocationId { get; set; } = string.Empty;
+ public string UniqueKey { get; set; } = string.Empty;
+ public bool KeyAppended { get; set; }
+ public bool ClearStateCalled { get; set; }
+ public Dictionary KeysAfterOtherClear { get; set; } = new();
+ public bool OwnKeyStillPresent { get; set; }
+ }
+
+ #region Regression Tests for Thread-Safety Bug
+
+ ///
+ /// Minimal reproduction test for the thread-safety bug.
+ ///
+ /// Before the fix, this test would throw InvalidOperationException:
+ /// "Collection was modified; enumeration operation may not execute."
+ ///
+ /// Root cause was Logger.Scope being a static Dictionary<string, object>
+ /// which is not thread-safe for concurrent read/write operations.
+ ///
+ /// The fix uses per-thread scope storage via ConcurrentDictionary<int, Dictionary>
+ /// keyed by ManagedThreadId.
+ ///
+ [Fact]
+ public async Task ConcurrentAccess_ForeachOnGetAllKeys_ShouldNotThrowException()
+ {
+ // Clear any existing keys
+ Logger.RemoveKeys(Logger.GetAllKeys()?.Select(x => x.Key).ToArray() ?? []);
+
+ var tasks = new List
+ {
+ // Thread 1: Enumerate (mimics GetLogEntry line 229)
+ Task.Run(() =>
+ {
+ for (int i = 0; i < 100; i++)
+ foreach (var kvp in Logger.GetAllKeys())
+ {
+ // Just enumerate
+ }
+ }),
+
+ // Thread 2: Log (also enumerates internally)
+ Task.Run(() =>
+ {
+ for (int i = 0; i < 100; i++)
+ Logger.LogInformation($"Iteration {i}");
+ }),
+
+ // Thread 3: Modify keys
+ Task.Run(() =>
+ {
+ for (int i = 0; i < 100; i++)
+ {
+ Logger.AppendKey($"key_{i % 10}", i);
+ Logger.RemoveKey($"key_{(i - 1) % 10}");
+ }
+ })
+ };
+
+ // With the fix, this should complete without throwing InvalidOperationException
+ await Task.WhenAll(tasks);
+ }
+
+ ///
+ /// Regression test for the thread-safety bug where concurrent GetAllKeys enumeration
+ /// during AppendKey/RemoveKey modifications would throw InvalidOperationException.
+ ///
+ /// Root cause (before fix):
+ /// Logger.Scope was a static Dictionary<string, object> which is not thread-safe.
+ /// When Thread A enumerated via GetAllKeys() while Thread B modified via AppendKey()/RemoveKey(),
+ /// it would throw "Collection was modified; enumeration operation may not execute."
+ ///
+ /// The fix uses per-thread scope storage via ConcurrentDictionary<int, Dictionary>
+ /// keyed by ManagedThreadId, ensuring each thread has its own isolated dictionary.
+ ///
+ [Theory]
+ [InlineData(100)]
+ [InlineData(500)]
+ [InlineData(1000)]
+ public async Task ConcurrentAccess_GetAllKeysDuringModification_ShouldNotThrowException(int iterations)
+ {
+ ClearAllLoggerState();
+
+ var exceptions = new List();
+ var exceptionLock = new object();
+
+ var tasks = new List
+ {
+ // Thread 1: Enumerate via GetAllKeys (mimics GetLogEntry line 229)
+ Task.Run(() =>
+ {
+ try
+ {
+ for (int i = 0; i < iterations; i++)
+ {
+ foreach (var kvp in Logger.GetAllKeys())
+ {
+ // Just enumerate - this is what GetLogEntry does internally
+ _ = kvp.Key;
+ _ = kvp.Value;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ lock (exceptionLock)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ }),
+
+ // Thread 2: Log (also enumerates internally via GetLogEntry)
+ Task.Run(() =>
+ {
+ try
+ {
+ for (int i = 0; i < iterations; i++)
+ {
+ Logger.LogInformation($"Iteration {i}");
+ }
+ }
+ catch (Exception ex)
+ {
+ lock (exceptionLock)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ }),
+
+ // Thread 3: Modify keys rapidly
+ Task.Run(() =>
+ {
+ try
+ {
+ for (int i = 0; i < iterations; i++)
+ {
+ var keyName = $"key_{i % 10}";
+ Logger.AppendKey(keyName, i);
+ Logger.RemoveKey($"key_{(i - 1) % 10}");
+ }
+ }
+ catch (Exception ex)
+ {
+ lock (exceptionLock)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ })
+ };
+
+ await Task.WhenAll(tasks);
+
+ // With the old implementation, this would throw InvalidOperationException or ArgumentException
+ // With the fix (per-thread scope), no exceptions should occur
+ Assert.Empty(exceptions);
+ }
+
+ ///
+ /// Stress test: Multiple threads simultaneously performing all Logger operations.
+ /// This validates that the thread-per-scope implementation handles high concurrency.
+ ///
+ [Theory]
+ [InlineData(3, 100)]
+ [InlineData(5, 200)]
+ [InlineData(10, 100)]
+ public async Task StressTest_MultipleThreadsAllOperations_ShouldNotThrowException(int threadCount, int operationsPerThread)
+ {
+ ClearAllLoggerState();
+
+ var exceptions = new List();
+ var exceptionLock = new object();
+ var barrier = new Barrier(threadCount);
+
+ var tasks = Enumerable.Range(0, threadCount).Select(threadIndex => Task.Run(() =>
+ {
+ try
+ {
+ barrier.SignalAndWait();
+
+ for (int i = 0; i < operationsPerThread; i++)
+ {
+ // Mix of all operations
+ var keyName = $"thread_{threadIndex}_key_{i % 5}";
+
+ // AppendKey
+ Logger.AppendKey(keyName, $"value_{i}");
+
+ // GetAllKeys with enumeration
+ var keys = Logger.GetAllKeys().ToList();
+
+ // Log (internally calls GetAllKeys)
+ Logger.LogDebug($"Thread {threadIndex} iteration {i}, keys: {keys.Count}");
+
+ // RemoveKey
+ if (i % 2 == 0)
+ {
+ Logger.RemoveKey(keyName);
+ }
+
+ // RemoveKeys (batch)
+ if (i % 10 == 0)
+ {
+ var keysToRemove = Logger.GetAllKeys()
+ .Select(k => k.Key)
+ .Where(k => k.StartsWith($"thread_{threadIndex}_"))
+ .ToArray();
+ Logger.RemoveKeys(keysToRemove);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ lock (exceptionLock)
+ {
+ exceptions.Add(new Exception($"Thread {threadIndex}: {ex.Message}", ex));
+ }
+ }
+ })).ToList();
+
+ await Task.WhenAll(tasks);
+
+ Assert.Empty(exceptions);
+ }
+
+ #endregion
+}
diff --git a/libraries/tests/Directory.Packages.props b/libraries/tests/Directory.Packages.props
index 7560fa2f3..19885ee18 100644
--- a/libraries/tests/Directory.Packages.props
+++ b/libraries/tests/Directory.Packages.props
@@ -4,7 +4,7 @@
-
+
@@ -14,8 +14,8 @@
-
-
+
+
diff --git a/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj
index 31af6d48d..7b14e9b73 100644
--- a/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj
+++ b/libraries/tests/e2e/functions/core/logging/AOT-Function-ILogger/src/AOT-Function-ILogger/AOT-Function-ILogger.csproj
@@ -17,10 +17,10 @@
partial
-
-
+
+
-
+
diff --git a/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj b/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj
index 31af6d48d..7b14e9b73 100644
--- a/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj
+++ b/libraries/tests/e2e/functions/core/logging/AOT-Function/src/AOT-Function/AOT-Function.csproj
@@ -17,10 +17,10 @@
partial
-
-
+
+
-
+
diff --git a/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.csproj b/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.csproj
index 1a86bae5a..a98d96910 100644
--- a/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.csproj
+++ b/libraries/tests/e2e/functions/core/logging/Function/src/Function/Function.csproj
@@ -11,8 +11,8 @@
true
-
-
+
+
diff --git a/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/Function.Tests.csproj b/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/Function.Tests.csproj
index 076d16cd3..363f12625 100644
--- a/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/Function.Tests.csproj
+++ b/libraries/tests/e2e/functions/core/logging/Function/test/Function.Tests/Function.Tests.csproj
@@ -7,8 +7,8 @@
Logging.E2E.Tests
-
-
+
+
diff --git a/libraries/tests/e2e/functions/core/metrics/AOT-Function/src/AOT-Function/AOT-Function.csproj b/libraries/tests/e2e/functions/core/metrics/AOT-Function/src/AOT-Function/AOT-Function.csproj
index 90c8a0813..644edfdcf 100644
--- a/libraries/tests/e2e/functions/core/metrics/AOT-Function/src/AOT-Function/AOT-Function.csproj
+++ b/libraries/tests/e2e/functions/core/metrics/AOT-Function/src/AOT-Function/AOT-Function.csproj
@@ -17,10 +17,10 @@
partial
-
-
+
+
-
+
diff --git a/libraries/tests/e2e/functions/core/metrics/Function/src/Function/Function.csproj b/libraries/tests/e2e/functions/core/metrics/Function/src/Function/Function.csproj
index 898c1d56f..b6bf92f11 100644
--- a/libraries/tests/e2e/functions/core/metrics/Function/src/Function/Function.csproj
+++ b/libraries/tests/e2e/functions/core/metrics/Function/src/Function/Function.csproj
@@ -11,8 +11,8 @@
true
-
-
+
+
diff --git a/libraries/tests/e2e/functions/core/metrics/Function/test/Function.Tests/Function.Tests.csproj b/libraries/tests/e2e/functions/core/metrics/Function/test/Function.Tests/Function.Tests.csproj
index 73fabc989..579b9beeb 100644
--- a/libraries/tests/e2e/functions/core/metrics/Function/test/Function.Tests/Function.Tests.csproj
+++ b/libraries/tests/e2e/functions/core/metrics/Function/test/Function.Tests/Function.Tests.csproj
@@ -7,8 +7,8 @@
Metrics.E2E.Tests
-
-
+
+
diff --git a/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj b/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj
index 7f7dd1646..a14d1ed87 100644
--- a/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj
+++ b/libraries/tests/e2e/functions/core/tracing/AOT-Function/src/AOT-Function/AOT-Function.csproj
@@ -17,10 +17,10 @@
partial
-
-
+
+
-
+
diff --git a/libraries/tests/e2e/functions/core/tracing/Function/src/Function/Function.csproj b/libraries/tests/e2e/functions/core/tracing/Function/src/Function/Function.csproj
index 7ee821731..fd2302b57 100644
--- a/libraries/tests/e2e/functions/core/tracing/Function/src/Function/Function.csproj
+++ b/libraries/tests/e2e/functions/core/tracing/Function/src/Function/Function.csproj
@@ -11,8 +11,8 @@
true
-
-
+
+
diff --git a/libraries/tests/e2e/functions/core/tracing/Function/test/Function.Tests/Function.Tests.csproj b/libraries/tests/e2e/functions/core/tracing/Function/test/Function.Tests/Function.Tests.csproj
index 0352e1b05..73d3537a0 100644
--- a/libraries/tests/e2e/functions/core/tracing/Function/test/Function.Tests/Function.Tests.csproj
+++ b/libraries/tests/e2e/functions/core/tracing/Function/test/Function.Tests/Function.Tests.csproj
@@ -7,8 +7,8 @@
Tracing.E2E.Tests
-
-
+
+
diff --git a/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionHandlerTest/AOT-FunctionHandlerTest.csproj b/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionHandlerTest/AOT-FunctionHandlerTest.csproj
index 6810da241..51d23e17b 100644
--- a/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionHandlerTest/AOT-FunctionHandlerTest.csproj
+++ b/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionHandlerTest/AOT-FunctionHandlerTest.csproj
@@ -18,10 +18,10 @@
AOT-Function
-
-
+
+
-
+
diff --git a/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionMethodAttributeTest/AOT-FunctionMethodAttributeTest.csproj b/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionMethodAttributeTest/AOT-FunctionMethodAttributeTest.csproj
index 6810da241..51d23e17b 100644
--- a/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionMethodAttributeTest/AOT-FunctionMethodAttributeTest.csproj
+++ b/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionMethodAttributeTest/AOT-FunctionMethodAttributeTest.csproj
@@ -18,10 +18,10 @@
AOT-Function
-
-
+
+
-
+
diff --git a/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionPayloadSubsetTest/AOT-FunctionPayloadSubsetTest.csproj b/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionPayloadSubsetTest/AOT-FunctionPayloadSubsetTest.csproj
index 546fc431b..f1eeeb576 100644
--- a/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionPayloadSubsetTest/AOT-FunctionPayloadSubsetTest.csproj
+++ b/libraries/tests/e2e/functions/idempotency/AOT-Function/src/AOT-FunctionPayloadSubsetTest/AOT-FunctionPayloadSubsetTest.csproj
@@ -18,10 +18,10 @@
AOT-Function
-
-
+
+
-
+
diff --git a/libraries/tests/e2e/functions/idempotency/Function/src/Function/Function.csproj b/libraries/tests/e2e/functions/idempotency/Function/src/Function/Function.csproj
index 1c2d28f88..6efe4f521 100644
--- a/libraries/tests/e2e/functions/idempotency/Function/src/Function/Function.csproj
+++ b/libraries/tests/e2e/functions/idempotency/Function/src/Function/Function.csproj
@@ -11,8 +11,8 @@
true
-
-
+
+
diff --git a/libraries/tests/e2e/functions/idempotency/Function/test/Function.Tests/Function.Tests.csproj b/libraries/tests/e2e/functions/idempotency/Function/test/Function.Tests/Function.Tests.csproj
index 3570eeda9..dc9877306 100644
--- a/libraries/tests/e2e/functions/idempotency/Function/test/Function.Tests/Function.Tests.csproj
+++ b/libraries/tests/e2e/functions/idempotency/Function/test/Function.Tests/Function.Tests.csproj
@@ -7,8 +7,8 @@
Logging.E2E.Tests
-
-
+
+