feat: Add DelegatingAgentSessionStore

Add helper for decorator pattern for AgentSessionStore
This commit is contained in:
Jacob Alber
2026-05-07 09:48:47 -04:00
Unverified
parent a84ad42f6d
commit 3f9ae8334c
2 changed files with 269 additions and 0 deletions
@@ -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;
/// <summary>
/// Provides an abstract base class for agent session stores that delegate operations to an inner store
/// instance while allowing for extensibility and customization.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="DelegatingAgentSessionStore"/> implements the decorator pattern for <see cref="AgentSessionStore"/>s,
/// enabling the creation of pipeliens where each layer can add functionality while delegating core operations to an
/// underlying store.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public class DelegatingAgentSessionStore : AgentSessionStore
{
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingAgentSessionStore"/> class with the specified inner
/// store.
/// </summary>
/// <param name="innerStore">The underlying session store instance that will handle the core operations.</param>
/// <exception cref="ArgumentNullException"><paramref name="innerStore"/> is <see langword="null"/>.</exception>
/// <remarks>
/// 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.
/// </remarks>
protected DelegatingAgentSessionStore(AgentSessionStore innerStore)
{
this.InnerStore = Throw.IfNull(innerStore);
}
/// <summary>
/// Gets the inner session store instance that receives delegated operations.
/// </summary>
/// <value>
/// The underlying <see cref="AgentSessionStore"/> instance that handles core storage operations.
/// </value>
/// <remarks>
/// Derived classes can use this property to access the inner session store for custom delegation scenarios
/// or to forward operations with additional processing.
/// </remarks>
protected AgentSessionStore InnerStore { get; }
/// <inheritdoc/>
public override ValueTask<AgentSession> GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
=> this.InnerStore.GetSessionAsync(agent, conversationId, cancellationToken);
/// <inheritdoc/>
public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default)
=> this.InnerStore.SaveSessionAsync(agent, conversationId, session, cancellationToken);
}
@@ -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;
/// <summary>
/// Unit tests for the <see cref="DelegatingAgentSessionStore"/> class.
/// </summary>
public class DelegatingAgentSessionStoreTests
{
private readonly Mock<AgentSessionStore> _innerStoreMock;
private readonly Mock<AIAgent> _agentMock;
private readonly TestDelegatingAgentSessionStore _delegatingStore;
private readonly AgentSession _testSession;
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingAgentSessionStoreTests"/> class.
/// </summary>
public DelegatingAgentSessionStoreTests()
{
this._innerStoreMock = new Mock<AgentSessionStore>();
this._agentMock = new Mock<AIAgent>();
this._testSession = new TestAgentSession();
// Setup inner store mock
this._innerStoreMock
.Setup(x => x.GetSessionAsync(It.IsAny<AIAgent>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(this._testSession);
this._innerStoreMock
.Setup(x => x.SaveSessionAsync(It.IsAny<AIAgent>(), It.IsAny<string>(), It.IsAny<AgentSession>(), It.IsAny<CancellationToken>()))
.Returns(ValueTask.CompletedTask);
this._delegatingStore = new TestDelegatingAgentSessionStore(this._innerStoreMock.Object);
}
#region Constructor Tests
/// <summary>
/// Verify that constructor throws ArgumentNullException when innerStore is null.
/// </summary>
[Fact]
public void RequiresInnerStore() =>
// Act & Assert
Assert.Throws<ArgumentNullException>("innerStore", () => new TestDelegatingAgentSessionStore(null!));
/// <summary>
/// Verify that constructor sets the inner store correctly.
/// </summary>
[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
/// <summary>
/// Verify that GetSessionAsync delegates to inner store with correct parameters.
/// </summary>
[Fact]
public async Task GetSessionAsyncDelegatesToInnerStoreAsync()
{
// Arrange
const string expectedConversationId = "test-conversation-id";
var expectedCancellationToken = new CancellationToken();
this._innerStoreMock
.Setup(x => x.GetSessionAsync(
It.Is<AIAgent>(a => a == this._agentMock.Object),
It.Is<string>(c => c == expectedConversationId),
It.Is<CancellationToken>(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);
}
/// <summary>
/// Verify that SaveSessionAsync delegates to inner store with correct parameters.
/// </summary>
[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<AIAgent>(a => a == this._agentMock.Object),
It.Is<string>(c => c == expectedConversationId),
It.Is<AgentSession>(s => s == expectedSession),
It.Is<CancellationToken>(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);
}
/// <summary>
/// Verify that GetSessionAsync awaits the inner store's result before returning.
/// </summary>
[Fact]
public async Task GetSessionAsyncAwaitsInnerStoreResultAsync()
{
// Arrange
const string expectedConversationId = "test-conversation-id";
var taskCompletionSource = new TaskCompletionSource<AgentSession>();
var innerStoreMock = new Mock<AgentSessionStore>();
innerStoreMock
.Setup(x => x.GetSessionAsync(It.IsAny<AIAgent>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask<AgentSession>(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);
}
/// <summary>
/// Verify that SaveSessionAsync awaits the inner store's completion before returning.
/// </summary>
[Fact]
public async Task SaveSessionAsyncAwaitsInnerStoreCompletionAsync()
{
// Arrange
const string expectedConversationId = "test-conversation-id";
var expectedSession = new TestAgentSession();
var taskCompletionSource = new TaskCompletionSource();
var innerStoreMock = new Mock<AgentSessionStore>();
innerStoreMock
.Setup(x => x.SaveSessionAsync(It.IsAny<AIAgent>(), It.IsAny<string>(), It.IsAny<AgentSession>(), It.IsAny<CancellationToken>()))
.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
/// <summary>
/// Test implementation of DelegatingAgentSessionStore for testing purposes.
/// </summary>
private sealed class TestDelegatingAgentSessionStore(AgentSessionStore innerStore) : DelegatingAgentSessionStore(innerStore)
{
public new AgentSessionStore InnerStore => base.InnerStore;
}
private sealed class TestAgentSession : AgentSession;
#endregion
}