diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/UserIdentityScopedSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/UserIdentityScopedSessionStore.cs
index c1d6136239..8d9416f002 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/UserIdentityScopedSessionStore.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/UserIdentityScopedSessionStore.cs
@@ -32,21 +32,17 @@ public class UserIdentityScopedSessionStore : DelegatingAgentSessionStore
///
/// The used to retrieve the current user's claims.
///
- ///
- /// The claim type to extract from the user's identity for scoping. Defaults to .
- ///
- ///
- /// If , an exception is thrown when the specified claim is not found.
- /// If , the conversation ID is passed through unmodified when the claim is absent.
- ///
- public UserIdentityScopedSessionStore(AgentSessionStore innerStore,
- IHttpContextAccessor? contextAccessor,
- string claimType = ClaimsIdentity.DefaultNameClaimType,
- bool strict = true) : base(innerStore)
+ /// The options for configuring the session store. If null, defaults are used.
+ public UserIdentityScopedSessionStore(
+ AgentSessionStore innerStore,
+ IHttpContextAccessor? contextAccessor,
+ UserIdentityScopedSessionStoreOptions? options = null) : base(innerStore)
{
+ options ??= new UserIdentityScopedSessionStoreOptions();
+
this._httpContextAccessor = contextAccessor;
- this._claimType = Throw.IfNullOrWhitespace(claimType);
- this._strict = strict;
+ this._claimType = Throw.IfNullOrWhitespace(options.ClaimType);
+ this._strict = options.Strict;
}
private string? GetScopeFromIdentity()
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/UserIdentityScopedSessionStoreOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/UserIdentityScopedSessionStoreOptions.cs
new file mode 100644
index 0000000000..40c8b08b1b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/UserIdentityScopedSessionStoreOptions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Security.Claims;
+
+namespace Microsoft.Agents.AI.Hosting;
+
+///
+/// Options for configuring .
+///
+public class UserIdentityScopedSessionStoreOptions
+{
+ ///
+ /// Gets or sets the claim type to extract from the user's identity for scoping.
+ ///
+ ///
+ /// Defaults to .
+ ///
+ public string ClaimType { get; set; } = ClaimsIdentity.DefaultNameClaimType;
+
+ ///
+ /// Gets or sets a value indicating whether an exception should be thrown when the specified claim is not found.
+ ///
+ ///
+ /// If , an exception is thrown when the specified claim is not found.
+ /// If , the conversation ID is passed through unmodified when the claim is absent.
+ /// Defaults to .
+ ///
+ public bool Strict { get; set; } = true;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs
index a9c3d436df..6e8314a74c 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs
@@ -14,7 +14,7 @@ namespace Microsoft.Agents.AI.Hosting;
///
///
/// implements the decorator pattern for s,
-/// enabling the creation of pipeliens where each layer can add functionality while delegating core operations to an
+/// enabling the creation of pipelines where each layer can add functionality while delegating core operations to an
/// underlying store.
///
///
@@ -23,7 +23,7 @@ namespace Microsoft.Agents.AI.Hosting;
/// interface.
///
///
-public class DelegatingAgentSessionStore : AgentSessionStore
+public abstract class DelegatingAgentSessionStore : AgentSessionStore
{
///
/// Initializes a new instance of the class with the specified inner
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/UserIdentityScopedSessionStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/UserIdentityScopedSessionStoreTests.cs
index 504c4177f7..3098ad9021 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/UserIdentityScopedSessionStoreTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/UserIdentityScopedSessionStoreTests.cs
@@ -52,7 +52,23 @@ public class UserIdentityScopedSessionStoreTests
///
[Fact]
public void RequiresInnerStore() =>
- Assert.Throws("innerStore", () => new UserIdentityScopedSessionStore(null!, this._httpContextAccessorMock.Object));
+ Assert.Throws("innerStore", () => new UserIdentityScopedSessionStore(null!, this._httpContextAccessorMock.Object, CreateOptions()));
+
+ ///
+ /// Verify that constructor uses default options when options is null.
+ ///
+ [Fact]
+ public void UsesDefaultOptionsWhenNull()
+ {
+ // Arrange
+ this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId);
+
+ // Act - should not throw and use default claim type
+ var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, options: null);
+
+ // Assert
+ Assert.NotNull(store);
+ }
///
/// Verify that constructor accepts null IHttpContextAccessor.
@@ -61,7 +77,7 @@ public class UserIdentityScopedSessionStoreTests
public void Constructor_WithNullHttpContextAccessor_DoesNotThrow()
{
// Act & Assert - should not throw
- var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, contextAccessor: null, strict: false);
+ var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, contextAccessor: null, CreateOptions(strict: false));
Assert.NotNull(store);
}
@@ -70,21 +86,21 @@ public class UserIdentityScopedSessionStoreTests
///
[Fact]
public void RequiresClaimType_NotNull() =>
- Assert.Throws("claimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, claimType: null!));
+ Assert.Throws("options.ClaimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions(claimType: null!)));
///
/// Verify that constructor throws ArgumentException when claimType is empty.
///
[Fact]
public void RequiresClaimType_NotEmpty() =>
- Assert.Throws("claimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, claimType: string.Empty));
+ Assert.Throws("options.ClaimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions(claimType: string.Empty)));
///
/// Verify that constructor throws ArgumentException when claimType is whitespace.
///
[Fact]
public void RequiresClaimType_NotWhitespace() =>
- Assert.Throws("claimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, claimType: " "));
+ Assert.Throws("options.ClaimType", () => new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions(claimType: " ")));
#endregion
@@ -98,7 +114,7 @@ public class UserIdentityScopedSessionStoreTests
{
// Arrange
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId);
- var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
+ var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
// Act
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
@@ -123,7 +139,7 @@ public class UserIdentityScopedSessionStoreTests
var store = new UserIdentityScopedSessionStore(
this._innerStoreMock.Object,
this._httpContextAccessorMock.Object,
- claimType: CustomClaimType);
+ CreateOptions(claimType: CustomClaimType));
// Act
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
@@ -148,7 +164,7 @@ public class UserIdentityScopedSessionStoreTests
var store = new UserIdentityScopedSessionStore(
this._innerStoreMock.Object,
this._httpContextAccessorMock.Object,
- strict: true);
+ CreateOptions(strict: true));
// Act & Assert
var exception = await Assert.ThrowsAsync(
@@ -168,7 +184,7 @@ public class UserIdentityScopedSessionStoreTests
var store = new UserIdentityScopedSessionStore(
this._innerStoreMock.Object,
this._httpContextAccessorMock.Object,
- strict: false);
+ CreateOptions(strict: false));
// Act - should not throw
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
@@ -190,7 +206,7 @@ public class UserIdentityScopedSessionStoreTests
{
// Arrange
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId);
- var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
+ var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
// Act
var result = await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
@@ -211,7 +227,7 @@ public class UserIdentityScopedSessionStoreTests
{
// Arrange
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, TestUserId);
- var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
+ var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
var sessionToSave = new TestAgentSession();
// Act
@@ -238,7 +254,7 @@ public class UserIdentityScopedSessionStoreTests
var store = new UserIdentityScopedSessionStore(
this._innerStoreMock.Object,
this._httpContextAccessorMock.Object,
- claimType: CustomClaimType);
+ CreateOptions(claimType: CustomClaimType));
var sessionToSave = new TestAgentSession();
// Act
@@ -265,7 +281,7 @@ public class UserIdentityScopedSessionStoreTests
var store = new UserIdentityScopedSessionStore(
this._innerStoreMock.Object,
this._httpContextAccessorMock.Object,
- strict: true);
+ CreateOptions(strict: true));
var sessionToSave = new TestAgentSession();
// Act & Assert
@@ -286,7 +302,7 @@ public class UserIdentityScopedSessionStoreTests
var store = new UserIdentityScopedSessionStore(
this._innerStoreMock.Object,
this._httpContextAccessorMock.Object,
- strict: false);
+ CreateOptions(strict: false));
var sessionToSave = new TestAgentSession();
// Act - should not throw
@@ -317,7 +333,7 @@ public class UserIdentityScopedSessionStoreTests
var store = new UserIdentityScopedSessionStore(
this._innerStoreMock.Object,
this._httpContextAccessorMock.Object,
- strict: true);
+ CreateOptions(strict: true));
// Act & Assert
await Assert.ThrowsAsync(
@@ -335,7 +351,7 @@ public class UserIdentityScopedSessionStoreTests
var store = new UserIdentityScopedSessionStore(
this._innerStoreMock.Object,
this._httpContextAccessorMock.Object,
- strict: false);
+ CreateOptions(strict: false));
// Act - should not throw
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
@@ -376,12 +392,12 @@ public class UserIdentityScopedSessionStoreTests
// Act - User 1
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, User1);
- var store1 = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
+ var store1 = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
await store1.GetSessionAsync(this._agentMock.Object, TestConversationId);
// Act - User 2
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, User2);
- var store2 = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
+ var store2 = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
await store2.GetSessionAsync(this._agentMock.Object, TestConversationId);
// Assert
@@ -399,7 +415,7 @@ public class UserIdentityScopedSessionStoreTests
// Arrange
const string UserIdWithColon = "user:with:colons";
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, UserIdWithColon);
- var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
+ var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
// Act
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
@@ -422,7 +438,7 @@ public class UserIdentityScopedSessionStoreTests
// Arrange
const string UserIdWithBackslash = @"domain\user";
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, UserIdWithBackslash);
- var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
+ var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
// Act
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
@@ -445,7 +461,7 @@ public class UserIdentityScopedSessionStoreTests
// Arrange
const string UserIdWithBoth = @"domain\user:role";
this.SetupHttpContextWithClaim(ClaimsIdentity.DefaultNameClaimType, UserIdWithBoth);
- var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object);
+ var store = new UserIdentityScopedSessionStore(this._innerStoreMock.Object, this._httpContextAccessorMock.Object, CreateOptions());
// Act
await store.GetSessionAsync(this._agentMock.Object, TestConversationId);
@@ -463,6 +479,17 @@ public class UserIdentityScopedSessionStoreTests
#region Helper Methods
+ private static UserIdentityScopedSessionStoreOptions CreateOptions(
+ string? claimType = ClaimsIdentity.DefaultNameClaimType,
+ bool strict = true)
+ {
+ return new UserIdentityScopedSessionStoreOptions
+ {
+ ClaimType = claimType!,
+ Strict = strict
+ };
+ }
+
private void SetupHttpContextWithClaim(string claimType, string claimValue)
{
var claims = new[] { new Claim(claimType, claimValue) };