diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs index 2f57e26409..1d5373e38a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Hosting; @@ -43,4 +45,35 @@ public abstract class AgentSessionStore AIAgent agent, string conversationId, CancellationToken cancellationToken = default); + + /// Asks the for an object of the specified type . + /// The type of object being requested. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// is . + /// + /// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the , + /// including itself or any services it might be wrapping. This is particularly useful for inspecting delegation chains + /// to verify that specific store implementations are present. + /// + public virtual object? GetService(Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + return serviceKey is null && serviceType.IsInstanceOfType(this) + ? this + : null; + } + + /// Asks the for an object of type . + /// The type of the object to be retrieved. + /// An optional key that can be used to help identify the target service. + /// The found object, otherwise . + /// + /// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the , + /// including itself or any services it might be wrapping. This is particularly useful for inspecting delegation chains + /// to verify that specific store implementations are present. + /// + public TService? GetService(object? serviceKey = null) + => this.GetService(typeof(TService), serviceKey) is TService service ? service : default; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs index 6e8314a74c..e80d3b907a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs @@ -59,4 +59,23 @@ public abstract class DelegatingAgentSessionStore : AgentSessionStore /// public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) => this.InnerStore.SaveSessionAsync(agent, conversationId, session, cancellationToken); + + /// + /// + /// This implementation first checks if this instance satisfies the service request. + /// If not, it chains the request to the inner store, allowing services to be retrieved + /// from any store in the delegation chain. + /// + public override object? GetService(Type serviceType, object? serviceKey = null) + { + // First, check if this instance satisfies the request + object? service = base.GetService(serviceType, serviceKey); + if (service is not null) + { + return service; + } + + // Chain to the inner store + return this.InnerStore.GetService(serviceType, serviceKey); + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs index f73776f172..f04ad7c20d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs @@ -191,6 +191,182 @@ public class DelegatingAgentSessionStoreTests #endregion + #region GetService Tests + + /// + /// Verify that GetService returns itself when requesting the exact type. + /// + [Fact] + public void GetServiceReturnsItselfForExactType() + { + // Act + var result = this._delegatingStore.GetService(typeof(TestDelegatingAgentSessionStore)); + + // Assert + Assert.Same(this._delegatingStore, result); + } + + /// + /// Verify that GetService returns itself when requesting a base type. + /// + [Fact] + public void GetServiceReturnsItselfForBaseType() + { + // Act + var result = this._delegatingStore.GetService(typeof(DelegatingAgentSessionStore)); + + // Assert + Assert.Same(this._delegatingStore, result); + } + + /// + /// Verify that GetService returns itself when requesting AgentSessionStore. + /// + [Fact] + public void GetServiceReturnsItselfForAgentSessionStoreType() + { + // Act + var result = this._delegatingStore.GetService(typeof(AgentSessionStore)); + + // Assert + Assert.Same(this._delegatingStore, result); + } + + /// + /// Verify that GetService chains to inner store when type is not satisfied by outer store. + /// + [Fact] + public void GetServiceChainsToInnerStore() + { + // Arrange + var innerStore = new ConcreteAgentSessionStore(); + var delegatingStore = new TestDelegatingAgentSessionStore(innerStore); + + // Act + var result = delegatingStore.GetService(typeof(ConcreteAgentSessionStore)); + + // Assert + Assert.Same(innerStore, result); + } + + /// + /// Verify that GetService chains through multiple delegation layers. + /// + [Fact] + public void GetServiceChainsThoughMultipleDelegationLayers() + { + // Arrange - create a three-layer chain: outer -> middle -> inner + var innerStore = new ConcreteAgentSessionStore(); + var middleStore = new AnotherDelegatingAgentSessionStore(innerStore); + var outerStore = new TestDelegatingAgentSessionStore(middleStore); + + // Act - request the innermost store type + var result = outerStore.GetService(typeof(ConcreteAgentSessionStore)); + + // Assert + Assert.Same(innerStore, result); + } + + /// + /// Verify that GetService can find a store in the middle of the delegation chain. + /// + [Fact] + public void GetServiceFindsMiddleStoreInChain() + { + // Arrange - create a three-layer chain: outer -> middle -> inner + var innerStore = new ConcreteAgentSessionStore(); + var middleStore = new AnotherDelegatingAgentSessionStore(innerStore); + var outerStore = new TestDelegatingAgentSessionStore(middleStore); + + // Act - request the middle store type + var result = outerStore.GetService(typeof(AnotherDelegatingAgentSessionStore)); + + // Assert + Assert.Same(middleStore, result); + } + + /// + /// Verify that GetService returns null when the requested type is not found in the chain. + /// + [Fact] + public void GetServiceReturnsNullWhenTypeNotFound() + { + // Arrange + var innerStore = new ConcreteAgentSessionStore(); + var delegatingStore = new TestDelegatingAgentSessionStore(innerStore); + + // Act + var result = delegatingStore.GetService(typeof(string)); + + // Assert + Assert.Null(result); + } + + /// + /// Verify that GetService returns null when a service key is provided but not matched. + /// + [Fact] + public void GetServiceReturnsNullWhenServiceKeyProvided() + { + // Act + var result = this._delegatingStore.GetService(typeof(TestDelegatingAgentSessionStore), "some-key"); + + // Assert + Assert.Null(result); + } + + /// + /// Verify that GetService throws ArgumentNullException when serviceType is null. + /// + [Fact] + public void GetServiceThrowsWhenServiceTypeIsNull() => + Assert.Throws("serviceType", () => this._delegatingStore.GetService(null!)); + + /// + /// Verify that GetService generic method works correctly. + /// + [Fact] + public void GetServiceGenericReturnsItself() + { + // Act + var result = this._delegatingStore.GetService(); + + // Assert + Assert.Same(this._delegatingStore, result); + } + + /// + /// Verify that GetService generic method chains to inner store. + /// + [Fact] + public void GetServiceGenericChainsToInnerStore() + { + // Arrange + var innerStore = new ConcreteAgentSessionStore(); + var delegatingStore = new TestDelegatingAgentSessionStore(innerStore); + + // Act + var result = delegatingStore.GetService(); + + // Assert + Assert.Same(innerStore, result); + } + + /// + /// Verify that GetService generic method returns null when type not found. + /// + [Fact] + public void GetServiceGenericReturnsNullWhenTypeNotFound() + { + // Act + var result = this._delegatingStore.GetService(); + + // Assert + Assert.Null(result); + } + + #endregion + #region Test Implementation /// @@ -201,6 +377,23 @@ public class DelegatingAgentSessionStoreTests public new AgentSessionStore InnerStore => base.InnerStore; } + /// + /// Another delegating store implementation for testing multi-layer chains. + /// + private sealed class AnotherDelegatingAgentSessionStore(AgentSessionStore innerStore) : DelegatingAgentSessionStore(innerStore); + + /// + /// Concrete (non-delegating) session store for testing GetService chaining. + /// + private sealed class ConcreteAgentSessionStore : AgentSessionStore + { + public override ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) + => new(new TestAgentSession()); + + public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) + => ValueTask.CompletedTask; + } + private sealed class TestAgentSession : AgentSession; #endregion diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs index f2635162ae..18fe3095e3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/IsolationKeyScopedAgentSessionStoreTests.cs @@ -364,6 +364,45 @@ public class IsolationKeyScopedAgentSessionStoreTests #endregion + #region GetService Tests + + /// + /// Verify that GetService can retrieve IsolationKeyScopedAgentSessionStore from a delegation chain. + /// + [Fact] + public void GetServiceReturnsIsolationKeyScopedAgentSessionStore() + { + // Arrange + var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var store = new IsolationKeyScopedAgentSessionStore(this._innerStoreMock.Object, provider); + + // Act + var result = store.GetService(); + + // Assert + Assert.Same(store, result); + } + + /// + /// Verify that GetService chains through to find inner store types. + /// + [Fact] + public void GetServiceChainsToInnerStore() + { + // Arrange + var concreteInnerStore = new ConcreteAgentSessionStore(); + var provider = new TestSessionIsolationKeyProvider(TestIsolationKey); + var store = new IsolationKeyScopedAgentSessionStore(concreteInnerStore, provider); + + // Act + var result = store.GetService(); + + // Assert + Assert.Same(concreteInnerStore, result); + } + + #endregion + #region Helper Classes /// @@ -386,5 +425,17 @@ public class IsolationKeyScopedAgentSessionStoreTests private sealed class TestAgentSession : AgentSession; + /// + /// Concrete (non-delegating) session store for testing GetService chaining. + /// + private sealed class ConcreteAgentSessionStore : AgentSessionStore + { + public override ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) + => new(new TestAgentSession()); + + public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) + => ValueTask.CompletedTask; + } + #endregion }