Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@
<Project Path="samples/02-agents/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj" />
<Project Path="samples/02-agents/A2A/A2AAgent_ProtocolSelection/A2AAgent_ProtocolSelection.csproj" />
<Project Path="samples/02-agents/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj" />
</Folder>
</Folder>
<Folder Name="/Samples/05-end-to-end/">
<Project Path="samples/05-end-to-end/AgentWithPurview/AgentWithPurview.csproj" />
<Project Path="samples/05-end-to-end/M365Agent/M365Agent.csproj" />
Expand Down Expand Up @@ -578,6 +578,7 @@
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hyperlight/Microsoft.Agents.AI.Hyperlight.csproj" />
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
<RootNamespace>Microsoft.Agents.AI.Hosting.AspNetCore</RootNamespace>
<VersionSuffix>preview</VersionSuffix>
<NoWarn>$(NoWarn)</NoWarn>
</PropertyGroup>

<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />

<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
</ItemGroup>

<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework Hosting ASP.NET Core</Title>
<Description>Provides Microsoft Agent Framework support for hosting agents in an ASP.NET Core context.</Description>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI.Hosting;

/// <summary>
/// A delegating <see cref="AgentSessionStore"/> that scopes session keys by a claim value
/// extracted from the current user's identity, ensuring that sessions are isolated per user.
/// The current user is extracted from the ambient ASP.NET <see cref="HttpContext"/>.
/// </summary>
/// <remarks>
/// This relies on <see cref="IHttpContextAccessor"/>, which uses <see cref="AsyncLocal{T}"/>
/// to provide access to the current <see cref="HttpContext"/>.
/// </remarks>
public class UserIdentityScopedSessionStore : DelegatingAgentSessionStore
{
private readonly IHttpContextAccessor? _httpContextAccessor;
private readonly string _claimType;
private readonly bool _strict;

/// <summary>
/// Initializes a new instance of the <see cref="UserIdentityScopedSessionStore"/> class.
/// </summary>
/// <param name="innerStore">The underlying <see cref="AgentSessionStore"/> to delegate to.</param>
/// <param name="contextAccessor">
/// The <see cref="IHttpContextAccessor"/> used to retrieve the current user's claims.
/// </param>
/// <param name="options">The options for configuring the session store. If null, defaults are used.</param>
public UserIdentityScopedSessionStore(
AgentSessionStore innerStore,
IHttpContextAccessor? contextAccessor,
UserIdentityScopedSessionStoreOptions? options = null) : base(innerStore)
{
Comment thread
lokitoth marked this conversation as resolved.
options ??= new UserIdentityScopedSessionStoreOptions();

this._httpContextAccessor = contextAccessor;
this._claimType = Throw.IfNullOrWhitespace(options.ClaimType);
this._strict = options.Strict;
}

private string? GetScopeFromIdentity()
{
Claim? claim = this._httpContextAccessor?
.HttpContext?
.User?.Claims.FirstOrDefault(c => c.Type == this._claimType);

if (this._strict && claim == null)
{
throw new InvalidOperationException($"No claim of type '{this._claimType}' found in principal.");
}

return claim?.Value;
}

private string? ScopeId => this.GetScopeFromIdentity();

private static string EscapeScopeId(string scopeId) => scopeId.Replace("\\", "\\\\").Replace(":", "\\:");

private string GetScopedConversationId(string bareConversationId)
{
string? scopeId = this.ScopeId;
if (scopeId == null)
{
return bareConversationId;
}

return $"{EscapeScopeId(scopeId)}::{bareConversationId}";
}

/// <inheritdoc />
public override ValueTask<AgentSession> GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
=> this.InnerStore.GetSessionAsync(agent, this.GetScopedConversationId(conversationId), cancellationToken);

/// <inheritdoc />
public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default)
=> this.InnerStore.SaveSessionAsync(agent, this.GetScopedConversationId(conversationId), session, cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Security.Claims;

namespace Microsoft.Agents.AI.Hosting;

/// <summary>
/// Options for configuring <see cref="UserIdentityScopedSessionStore"/>.
/// </summary>
public class UserIdentityScopedSessionStoreOptions
{
/// <summary>
/// Gets or sets the claim type to extract from the user's identity for scoping.
/// </summary>
/// <remarks>
/// Defaults to <see cref="ClaimsIdentity.DefaultNameClaimType"/>.
/// </remarks>
public string ClaimType { get; set; } = ClaimsIdentity.DefaultNameClaimType;

/// <summary>
/// Gets or sets a value indicating whether an exception should be thrown when the specified claim is not found.
/// </summary>
/// <remarks>
/// If <see langword="true"/>, an exception is thrown when the specified claim is not found.
/// If <see langword="false"/>, the conversation ID is passed through unmodified when the claim is absent.
/// Defaults to <see langword="true"/>.
/// </remarks>
public bool Strict { get; set; } = true;
}
Original file line number Diff line number Diff line change
@@ -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 pipelines 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 abstract class DelegatingAgentSessionStore : AgentSessionStore
{
Comment thread
lokitoth marked this conversation as resolved.
/// <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);
}
Loading
Loading