diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs new file mode 100644 index 0000000000..a9c3d436df --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Hosting; + +/// +/// Provides an abstract base class for agent session stores that delegate operations to an inner store +/// instance while allowing for extensibility and customization. +/// +/// +/// +/// implements the decorator pattern for s, +/// enabling the creation of pipeliens where each layer can add functionality while delegating core operations to an +/// underlying store. +/// +/// +/// The default implementation provides transparent pass-through behavior, forwarding all operations to the inner store. +/// Derived classes can override specific methods to add custom behavior while maintaining compatibility with the store +/// interface. +/// +/// +public class DelegatingAgentSessionStore : AgentSessionStore +{ + /// + /// Initializes a new instance of the class with the specified inner + /// store. + /// + /// The underlying session store instance that will handle the core operations. + /// is . + /// + /// The inner session store serves as the foundation of the delegation chain. All operations not overridden by + /// derived classes will be forwarded to this store. + /// + protected DelegatingAgentSessionStore(AgentSessionStore innerStore) + { + this.InnerStore = Throw.IfNull(innerStore); + } + + /// + /// Gets the inner session store instance that receives delegated operations. + /// + /// + /// The underlying instance that handles core storage operations. + /// + /// + /// Derived classes can use this property to access the inner session store for custom delegation scenarios + /// or to forward operations with additional processing. + /// + protected AgentSessionStore InnerStore { get; } + + /// + public override ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) + => this.InnerStore.GetSessionAsync(agent, conversationId, cancellationToken); + + /// + public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) + => this.InnerStore.SaveSessionAsync(agent, conversationId, session, cancellationToken); +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs new file mode 100644 index 0000000000..5a55b6e67b --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/DelegatingAgentSessionStoreTests.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Moq; + +namespace Microsoft.Agents.AI.Hosting.UnitTests; + +/// +/// Unit tests for the class. +/// +public class DelegatingAgentSessionStoreTests +{ + private readonly Mock _innerStoreMock; + private readonly Mock _agentMock; + private readonly TestDelegatingAgentSessionStore _delegatingStore; + private readonly AgentSession _testSession; + + /// + /// Initializes a new instance of the class. + /// + public DelegatingAgentSessionStoreTests() + { + this._innerStoreMock = new Mock(); + this._agentMock = new Mock(); + this._testSession = new TestAgentSession(); + + // Setup inner store mock + this._innerStoreMock + .Setup(x => x.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(this._testSession); + + this._innerStoreMock + .Setup(x => x.SaveSessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(ValueTask.CompletedTask); + + this._delegatingStore = new TestDelegatingAgentSessionStore(this._innerStoreMock.Object); + } + + #region Constructor Tests + + /// + /// Verify that constructor throws ArgumentNullException when innerStore is null. + /// + [Fact] + public void RequiresInnerStore() => + // Act & Assert + Assert.Throws("innerStore", () => new TestDelegatingAgentSessionStore(null!)); + + /// + /// Verify that constructor sets the inner store correctly. + /// + [Fact] + public void Constructor_WithValidInnerStore_SetsInnerStore() + { + // Act + var delegatingStore = new TestDelegatingAgentSessionStore(this._innerStoreMock.Object); + + // Assert + Assert.Same(this._innerStoreMock.Object, delegatingStore.InnerStore); + } + + #endregion + + #region Method Delegation Tests + + /// + /// Verify that GetSessionAsync delegates to inner store with correct parameters. + /// + [Fact] + public async Task GetSessionAsyncDelegatesToInnerStoreAsync() + { + // Arrange + const string expectedConversationId = "test-conversation-id"; + var expectedCancellationToken = new CancellationToken(); + + this._innerStoreMock + .Setup(x => x.GetSessionAsync( + It.Is(a => a == this._agentMock.Object), + It.Is(c => c == expectedConversationId), + It.Is(ct => ct == expectedCancellationToken))) + .ReturnsAsync(this._testSession); + + // Act + var session = await this._delegatingStore.GetSessionAsync( + this._agentMock.Object, + expectedConversationId, + expectedCancellationToken); + + // Assert + Assert.Same(this._testSession, session); + this._innerStoreMock.Verify( + x => x.GetSessionAsync( + this._agentMock.Object, + expectedConversationId, + expectedCancellationToken), + Times.Once); + } + + /// + /// Verify that SaveSessionAsync delegates to inner store with correct parameters. + /// + [Fact] + public async Task SaveSessionAsyncDelegatesToInnerStoreAsync() + { + // Arrange + const string expectedConversationId = "test-conversation-id"; + var expectedCancellationToken = new CancellationToken(); + var expectedSession = new TestAgentSession(); + + this._innerStoreMock + .Setup(x => x.SaveSessionAsync( + It.Is(a => a == this._agentMock.Object), + It.Is(c => c == expectedConversationId), + It.Is(s => s == expectedSession), + It.Is(ct => ct == expectedCancellationToken))) + .Returns(ValueTask.CompletedTask); + + // Act + await this._delegatingStore.SaveSessionAsync( + this._agentMock.Object, + expectedConversationId, + expectedSession, + expectedCancellationToken); + + // Assert + this._innerStoreMock.Verify( + x => x.SaveSessionAsync( + this._agentMock.Object, + expectedConversationId, + expectedSession, + expectedCancellationToken), + Times.Once); + } + + /// + /// Verify that GetSessionAsync awaits the inner store's result before returning. + /// + [Fact] + public async Task GetSessionAsyncAwaitsInnerStoreResultAsync() + { + // Arrange + const string expectedConversationId = "test-conversation-id"; + var taskCompletionSource = new TaskCompletionSource(); + + var innerStoreMock = new Mock(); + innerStoreMock + .Setup(x => x.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new ValueTask(taskCompletionSource.Task)); + + var delegatingStore = new TestDelegatingAgentSessionStore(innerStoreMock.Object); + + // Act + var resultTask = delegatingStore.GetSessionAsync(this._agentMock.Object, expectedConversationId); + + // Assert + Assert.False(resultTask.IsCompleted); + taskCompletionSource.SetResult(this._testSession); + Assert.True(resultTask.IsCompleted); + Assert.Same(this._testSession, await resultTask); + } + + /// + /// Verify that SaveSessionAsync awaits the inner store's completion before returning. + /// + [Fact] + public async Task SaveSessionAsyncAwaitsInnerStoreCompletionAsync() + { + // Arrange + const string expectedConversationId = "test-conversation-id"; + var expectedSession = new TestAgentSession(); + var taskCompletionSource = new TaskCompletionSource(); + + var innerStoreMock = new Mock(); + innerStoreMock + .Setup(x => x.SaveSessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new ValueTask(taskCompletionSource.Task)); + + var delegatingStore = new TestDelegatingAgentSessionStore(innerStoreMock.Object); + + // Act + var resultTask = delegatingStore.SaveSessionAsync(this._agentMock.Object, expectedConversationId, expectedSession); + + // Assert + Assert.False(resultTask.IsCompleted); + taskCompletionSource.SetResult(); + Assert.True(resultTask.IsCompleted); + await resultTask; + } + + #endregion + + #region Test Implementation + + /// + /// Test implementation of DelegatingAgentSessionStore for testing purposes. + /// + private sealed class TestDelegatingAgentSessionStore(AgentSessionStore innerStore) : DelegatingAgentSessionStore(innerStore) + { + public new AgentSessionStore InnerStore => base.InnerStore; + } + + private sealed class TestAgentSession : AgentSession; + + #endregion +}