diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs
index 733a7af9a7..03ec8cdadb 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs
@@ -19,9 +19,10 @@ public static class AgentHostingServiceCollectionExtensions
/// The service collection to configure.
/// The name of the agent.
/// The instructions for the agent.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// The same instance so that additional calls can be chained.
/// Thrown when or is .
- public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions)
+ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
@@ -30,7 +31,7 @@ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, s
var chatClient = sp.GetRequiredService();
var tools = sp.GetKeyedServices(name).ToList();
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
- });
+ }, lifetime);
}
///
@@ -40,9 +41,10 @@ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, s
/// The name of the agent.
/// The instructions for the agent.
/// The chat client which the agent will use for inference.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// The same instance so that additional calls can be chained.
/// Thrown when or is .
- public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, IChatClient chatClient)
+ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, IChatClient chatClient, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
@@ -50,7 +52,7 @@ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, s
{
var tools = sp.GetKeyedServices(name).ToList();
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
- });
+ }, lifetime);
}
///
@@ -60,9 +62,10 @@ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, s
/// The name of the agent.
/// The instructions for the agent.
/// The key to use when resolving the chat client from the service provider. If , a non-keyed service will be resolved.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// The same instance so that additional calls can be chained.
/// Thrown when or is .
- public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, object? chatClientServiceKey)
+ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
@@ -71,7 +74,7 @@ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, s
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService() : sp.GetRequiredKeyedService(chatClientServiceKey);
var tools = sp.GetKeyedServices(name).ToList();
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
- });
+ }, lifetime);
}
///
@@ -82,9 +85,10 @@ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, s
/// The instructions for the agent.
/// A description of the agent.
/// The key to use when resolving the chat client from the service provider. If , a non-keyed service will be resolved.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// The same instance so that additional calls can be chained.
/// Thrown when or is .
- public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, string? description, object? chatClientServiceKey)
+ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, string? instructions, string? description, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(services);
Throw.IfNullOrEmpty(name);
@@ -93,7 +97,7 @@ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, s
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService() : sp.GetRequiredKeyedService(chatClientServiceKey);
var tools = sp.GetKeyedServices(name).ToList();
return new ChatClientAgent(chatClient, instructions: instructions, name: key, description: description, tools: tools);
- });
+ }, lifetime);
}
///
@@ -102,15 +106,16 @@ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, s
/// The service collection to configure.
/// The name of the agent.
/// A factory delegate that creates the AI agent instance. The delegate receives the service provider and agent key as parameters.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// The same instance so that additional calls can be chained.
/// Thrown when , , or is .
/// Thrown when the agent factory delegate returns or an agent whose does not match .
- public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, Func createAgentDelegate)
+ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, string name, Func createAgentDelegate, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(services);
Throw.IfNull(name);
Throw.IfNull(createAgentDelegate);
- services.AddKeyedSingleton(name, (sp, key) =>
+ services.AddKeyedService(name, (sp, key) =>
{
Throw.IfNull(key);
var keyString = key as string;
@@ -122,8 +127,18 @@ public static IHostedAgentBuilder AddAIAgent(this IServiceCollection services, s
}
return agent;
- });
+ }, lifetime);
- return new HostedAgentBuilder(name, services);
+ return new HostedAgentBuilder(name, services, lifetime);
+ }
+
+ ///
+ /// Registers a keyed service with the specified lifetime.
+ ///
+ internal static void AddKeyedService(this IServiceCollection services, object? serviceKey, Func factory, ServiceLifetime lifetime)
+ where T : class
+ {
+ var descriptor = new ServiceDescriptor(typeof(T), serviceKey, (sp, key) => factory(sp, key), lifetime);
+ services.Add(descriptor);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderAgentExtensions.cs
index 434024866a..2d8620611a 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderAgentExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderAgentExtensions.cs
@@ -2,6 +2,7 @@
using System;
using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Shared.Diagnostics;
@@ -18,12 +19,13 @@ public static class HostApplicationBuilderAgentExtensions
/// The host application builder to configure.
/// The name of the agent.
/// The instructions for the agent.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// The configured host application builder.
/// Thrown when , , or is null.
- public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions)
+ public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
- return builder.Services.AddAIAgent(name, instructions);
+ return builder.Services.AddAIAgent(name, instructions, lifetime);
}
///
@@ -33,13 +35,14 @@ public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builde
/// The name of the agent.
/// The instructions for the agent.
/// The chat client which the agent will use for inference.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// The configured host application builder.
/// Thrown when , , or is null.
- public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, IChatClient chatClient)
+ public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, IChatClient chatClient, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
Throw.IfNullOrEmpty(name);
- return builder.Services.AddAIAgent(name, instructions, chatClient);
+ return builder.Services.AddAIAgent(name, instructions, chatClient, lifetime);
}
///
@@ -50,13 +53,14 @@ public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builde
/// The instructions for the agent.
/// A description of the agent.
/// The key to use when resolving the chat client from the service provider. If null, a non-keyed service will be resolved.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// The configured host application builder.
/// Thrown when , , or is null.
- public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, string? description, object? chatClientServiceKey)
+ public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, string? description, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
Throw.IfNullOrEmpty(name);
- return builder.Services.AddAIAgent(name, instructions, description, chatClientServiceKey);
+ return builder.Services.AddAIAgent(name, instructions, description, chatClientServiceKey, lifetime);
}
///
@@ -66,12 +70,13 @@ public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builde
/// The name of the agent.
/// The instructions for the agent.
/// The key to use when resolving the chat client from the service provider. If null, a non-keyed service will be resolved.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// The configured host application builder.
/// Thrown when , , or is null.
- public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, object? chatClientServiceKey)
+ public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, string? instructions, object? chatClientServiceKey, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
- return builder.Services.AddAIAgent(name, instructions, chatClientServiceKey);
+ return builder.Services.AddAIAgent(name, instructions, chatClientServiceKey, lifetime);
}
///
@@ -80,12 +85,13 @@ public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builde
/// The host application builder to configure.
/// The name of the agent.
/// A factory delegate that creates the AI agent instance. The delegate receives the service provider and agent key as parameters.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// The configured host application builder.
/// Thrown when , , or is null.
/// Thrown when the agent factory delegate returns null or an invalid AI agent instance.
- public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, Func createAgentDelegate)
+ public static IHostedAgentBuilder AddAIAgent(this IHostApplicationBuilder builder, string name, Func createAgentDelegate, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
- return builder.Services.AddAIAgent(name, createAgentDelegate);
+ return builder.Services.AddAIAgent(name, createAgentDelegate, lifetime);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs
index 8075caec59..cbefe94f1f 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs
@@ -19,19 +19,20 @@ public static class HostApplicationBuilderWorkflowExtensions
/// The to configure.
/// The unique name for the workflow.
/// A factory function that creates the instance. The function receives the service provider and workflow name as parameters.
+ /// The DI service lifetime for the workflow registration. Defaults to .
/// An that can be used to further configure the workflow.
/// Thrown when , , or is null.
/// Thrown when is empty.
///
/// Thrown when the factory delegate returns null or a workflow with a name that doesn't match the expected name.
///
- public static IHostedWorkflowBuilder AddWorkflow(this IHostApplicationBuilder builder, string name, Func createWorkflowDelegate)
+ public static IHostedWorkflowBuilder AddWorkflow(this IHostApplicationBuilder builder, string name, Func createWorkflowDelegate, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
Throw.IfNull(builder);
Throw.IfNull(name);
Throw.IfNull(createWorkflowDelegate);
- builder.Services.AddKeyedSingleton(name, (sp, key) =>
+ builder.Services.AddKeyedService(name, (sp, key) =>
{
Throw.IfNull(key);
var keyString = key as string;
@@ -43,7 +44,7 @@ public static IHostedWorkflowBuilder AddWorkflow(this IHostApplicationBuilder bu
}
return workflow;
- });
+ }, lifetime);
return new HostedWorkflowBuilder(name, builder);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilder.cs
index 89bf096b62..2d2d9bc5ed 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilder.cs
@@ -9,15 +9,17 @@ internal sealed class HostedAgentBuilder : IHostedAgentBuilder
{
public string Name { get; }
public IServiceCollection ServiceCollection { get; }
+ public ServiceLifetime Lifetime { get; }
- public HostedAgentBuilder(string name, IHostApplicationBuilder builder)
- : this(name, builder.Services)
+ public HostedAgentBuilder(string name, IHostApplicationBuilder builder, ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ : this(name, builder.Services, lifetime)
{
}
- public HostedAgentBuilder(string name, IServiceCollection serviceCollection)
+ public HostedAgentBuilder(string name, IServiceCollection serviceCollection, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
this.Name = name;
this.ServiceCollection = serviceCollection;
+ this.Lifetime = lifetime;
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs
index 12c1e08dfd..d1397fcda4 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs
@@ -42,17 +42,19 @@ public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder buil
/// The host agent builder to configure.
/// A factory function that creates an agent session store instance using the provided service provider and agent
/// name.
+ /// The DI service lifetime for the session store registration. Defaults to
+ /// because session stores persist conversation state across requests and are consumed independently of the agent's lifetime.
/// The same host agent builder instance, enabling further configuration.
- public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder builder, Func createAgentSessionStore)
+ public static IHostedAgentBuilder WithSessionStore(this IHostedAgentBuilder builder, Func createAgentSessionStore, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
- builder.ServiceCollection.AddKeyedSingleton(builder.Name, (sp, key) =>
+ builder.ServiceCollection.AddKeyedService(builder.Name, (sp, key) =>
{
Throw.IfNull(key);
var keyString = key as string;
Throw.IfNullOrEmpty(keyString);
return createAgentSessionStore(sp, keyString) ??
throw new InvalidOperationException($"The agent session store factory did not return a valid {nameof(AgentSessionStore)} instance for key '{keyString}'.");
- });
+ }, lifetime);
return builder;
}
@@ -98,13 +100,39 @@ public static IHostedAgentBuilder WithAITools(this IHostedAgentBuilder builder,
///
/// The hosted agent builder.
/// A factory function that creates a AI tool using the provided service provider.
- public static IHostedAgentBuilder WithAITool(this IHostedAgentBuilder builder, Func factory)
+ /// The DI service lifetime for the tool registration. If , the agent's lifetime is used.
+ /// The same instance so that additional calls can be chained.
+ /// Thrown when or is .
+ ///
+ /// Thrown when the effective tool lifetime is shorter than the agent's lifetime, which would cause a captive dependency.
+ /// For example, a singleton agent cannot use scoped or transient tools.
+ ///
+ public static IHostedAgentBuilder WithAITool(this IHostedAgentBuilder builder, Func factory, ServiceLifetime? lifetime = null)
{
Throw.IfNull(builder);
Throw.IfNull(factory);
- builder.ServiceCollection.AddKeyedSingleton(builder.Name, (sp, name) => factory(sp));
+ var effectiveLifetime = lifetime ?? builder.Lifetime;
+ ValidateToolLifetime(builder.Lifetime, effectiveLifetime);
+
+ builder.ServiceCollection.AddKeyedService(builder.Name, (sp, name) => factory(sp), effectiveLifetime);
return builder;
}
+
+ ///
+ /// Validates that the tool lifetime is compatible with the agent lifetime.
+ /// A tool's lifetime must be at least as long as the agent's lifetime to prevent captive dependency issues.
+ ///
+ internal static void ValidateToolLifetime(ServiceLifetime agentLifetime, ServiceLifetime toolLifetime)
+ {
+ // ServiceLifetime enum: Singleton=0, Scoped=1, Transient=2
+ // A higher value means a shorter lifetime.
+ if (toolLifetime > agentLifetime)
+ {
+ throw new InvalidOperationException(
+ $"A tool with lifetime '{toolLifetime}' cannot be registered for an agent with lifetime '{agentLifetime}'. " +
+ "The tool's lifetime must be at least as long as the agent's lifetime to avoid captive dependency issues.");
+ }
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowBuilderExtensions.cs
index f01a12c7ea..abee1cb566 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowBuilderExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowBuilderExtensions.cs
@@ -14,22 +14,24 @@ public static class HostedWorkflowBuilderExtensions
/// Registers the workflow as an AI agent in the dependency injection container.
///
/// The instance to extend.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// An that can be used to further configure the agent.
- public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder)
- => builder.AddAsAIAgent(name: null);
+ public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ => builder.AddAsAIAgent(name: null, lifetime: lifetime);
///
/// Registers the workflow as an AI agent in the dependency injection container.
///
/// The instance to extend.
/// The optional name for the AI agent. If not specified, the workflow name is used.
+ /// The DI service lifetime for the agent registration. Defaults to .
/// An that can be used to further configure the agent.
- public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, string? name)
+ public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, string? name, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
var workflowName = builder.Name;
var agentName = name ?? workflowName;
return builder.HostApplicationBuilder.AddAIAgent(agentName, (sp, key) =>
- sp.GetRequiredKeyedService(workflowName).AsAIAgent(name: key));
+ sp.GetRequiredKeyedService(workflowName).AsAIAgent(name: key), lifetime);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/IHostedAgentBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/IHostedAgentBuilder.cs
index f67f4eb7cd..0751ba630b 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting/IHostedAgentBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/IHostedAgentBuilder.cs
@@ -18,4 +18,9 @@ public interface IHostedAgentBuilder
/// Gets the service collection for configuration.
///
IServiceCollection ServiceCollection { get; }
+
+ ///
+ /// Gets the DI service lifetime used for the agent registration.
+ ///
+ ServiceLifetime Lifetime { get; }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs
index 03ab65c9f2..4d0a829933 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs
@@ -105,7 +105,7 @@ public void AddAIAgentWithFactory_ValidParameters_ReturnsBuilder()
}
///
- /// Verifies that AddAIAgent registers the agent as a keyed singleton service.
+ /// Verifies that AddAIAgent registers the agent as a keyed singleton service by default.
///
[Fact]
public void AddAIAgent_RegistersKeyedSingleton()
@@ -203,4 +203,94 @@ public void AddAIAgent_ValidSpecialCharactersInName_Succeeds(string name)
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
}
+
+ ///
+ /// Verifies that AddAIAgent registers with the specified scoped lifetime.
+ ///
+ [Fact]
+ public void AddAIAgent_WithScopedLifetime_RegistersKeyedScoped()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var mockAgent = new Mock();
+ const string AgentName = "scopedAgent";
+
+ // Act
+ var result = services.AddAIAgent(AgentName, (sp, key) => mockAgent.Object, ServiceLifetime.Scoped);
+
+ // Assert
+ var descriptor = services.FirstOrDefault(
+ d => (d.ServiceKey as string) == AgentName &&
+ d.ServiceType == typeof(AIAgent));
+
+ Assert.NotNull(descriptor);
+ Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
+ Assert.Equal(ServiceLifetime.Scoped, result.Lifetime);
+ }
+
+ ///
+ /// Verifies that AddAIAgent registers with the specified transient lifetime.
+ ///
+ [Fact]
+ public void AddAIAgent_WithTransientLifetime_RegistersKeyedTransient()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var mockAgent = new Mock();
+ const string AgentName = "transientAgent";
+
+ // Act
+ var result = services.AddAIAgent(AgentName, (sp, key) => mockAgent.Object, ServiceLifetime.Transient);
+
+ // Assert
+ var descriptor = services.FirstOrDefault(
+ d => (d.ServiceKey as string) == AgentName &&
+ d.ServiceType == typeof(AIAgent));
+
+ Assert.NotNull(descriptor);
+ Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime);
+ Assert.Equal(ServiceLifetime.Transient, result.Lifetime);
+ }
+
+ ///
+ /// Verifies that the builder exposes the correct lifetime for default registration.
+ ///
+ [Fact]
+ public void AddAIAgent_DefaultLifetime_BuilderExposesSingleton()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var mockAgent = new Mock();
+
+ // Act
+ var result = services.AddAIAgent("agentName", (sp, key) => mockAgent.Object);
+
+ // Assert
+ Assert.Equal(ServiceLifetime.Singleton, result.Lifetime);
+ }
+
+ ///
+ /// Verifies that AddAIAgent with instructions overload respects the lifetime parameter.
+ ///
+ [Theory]
+ [InlineData(ServiceLifetime.Singleton)]
+ [InlineData(ServiceLifetime.Scoped)]
+ [InlineData(ServiceLifetime.Transient)]
+ public void AddAIAgent_InstructionsOverload_RespectsLifetime(ServiceLifetime lifetime)
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ var result = services.AddAIAgent("agent", "instructions", lifetime);
+
+ // Assert
+ var descriptor = services.FirstOrDefault(
+ d => (d.ServiceKey as string) == "agent" &&
+ d.ServiceType == typeof(AIAgent));
+
+ Assert.NotNull(descriptor);
+ Assert.Equal(lifetime, descriptor.Lifetime);
+ Assert.Equal(lifetime, result.Lifetime);
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderAgentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderAgentExtensionsTests.cs
index 0036a60cc7..f80d2b7c32 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderAgentExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderAgentExtensionsTests.cs
@@ -127,7 +127,7 @@ public void AddAIAgentWithFactory_ValidParameters_ReturnsBuilder()
}
///
- /// Verifies that AddAIAgent registers the agent as a keyed singleton service.
+ /// Verifies that AddAIAgent registers the agent as a keyed singleton service by default.
///
[Fact]
public void AddAIAgent_RegistersKeyedSingleton()
@@ -235,4 +235,77 @@ public void AddAIAgent_ValidSpecialCharactersInName_Succeeds(string name)
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
}
+
+ ///
+ /// Verifies that AddAIAgent registers with the specified scoped lifetime via the host builder.
+ ///
+ [Fact]
+ public void AddAIAgent_WithScopedLifetime_RegistersKeyedScoped()
+ {
+ // Arrange
+ var builder = new HostApplicationBuilder();
+ var mockAgent = new Mock();
+ const string AgentName = "scopedAgent";
+
+ // Act
+ var result = builder.AddAIAgent(AgentName, (sp, key) => mockAgent.Object, ServiceLifetime.Scoped);
+
+ // Assert
+ var descriptor = builder.Services.FirstOrDefault(
+ d => (d.ServiceKey as string) == AgentName &&
+ d.ServiceType == typeof(AIAgent));
+
+ Assert.NotNull(descriptor);
+ Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
+ Assert.Equal(ServiceLifetime.Scoped, result.Lifetime);
+ }
+
+ ///
+ /// Verifies that AddAIAgent registers with the specified transient lifetime via the host builder.
+ ///
+ [Fact]
+ public void AddAIAgent_WithTransientLifetime_RegistersKeyedTransient()
+ {
+ // Arrange
+ var builder = new HostApplicationBuilder();
+ var mockAgent = new Mock();
+ const string AgentName = "transientAgent";
+
+ // Act
+ var result = builder.AddAIAgent(AgentName, (sp, key) => mockAgent.Object, ServiceLifetime.Transient);
+
+ // Assert
+ var descriptor = builder.Services.FirstOrDefault(
+ d => (d.ServiceKey as string) == AgentName &&
+ d.ServiceType == typeof(AIAgent));
+
+ Assert.NotNull(descriptor);
+ Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime);
+ Assert.Equal(ServiceLifetime.Transient, result.Lifetime);
+ }
+
+ ///
+ /// Verifies that AddAIAgent with instructions overload respects the lifetime parameter via the host builder.
+ ///
+ [Theory]
+ [InlineData(ServiceLifetime.Singleton)]
+ [InlineData(ServiceLifetime.Scoped)]
+ [InlineData(ServiceLifetime.Transient)]
+ public void AddAIAgent_InstructionsOverload_RespectsLifetime(ServiceLifetime lifetime)
+ {
+ // Arrange
+ var builder = new HostApplicationBuilder();
+
+ // Act
+ var result = builder.AddAIAgent("agent", "instructions", lifetime);
+
+ // Assert
+ var descriptor = builder.Services.FirstOrDefault(
+ d => (d.ServiceKey as string) == "agent" &&
+ d.ServiceType == typeof(AIAgent));
+
+ Assert.NotNull(descriptor);
+ Assert.Equal(lifetime, descriptor.Lifetime);
+ Assert.Equal(lifetime, result.Lifetime);
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs
index d27b9a17e3..1c5649d17c 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs
@@ -63,7 +63,7 @@ public void AddWorkflow_ValidParameters_ReturnsBuilder()
}
///
- /// Verifies that AddWorkflow registers the workflow as a keyed singleton service.
+ /// Verifies that AddWorkflow registers the workflow as a keyed singleton service by default.
///
[Fact]
public void AddWorkflow_RegistersKeyedSingleton()
@@ -328,6 +328,77 @@ public void AddAsAIAgent_WithEmptyName_UsesEmptyStringAsAgentName()
Assert.NotNull(agentDescriptor);
}
+ ///
+ /// Verifies that AddWorkflow registers with the specified scoped lifetime.
+ ///
+ [Fact]
+ public void AddWorkflow_WithScopedLifetime_RegistersKeyedScoped()
+ {
+ // Arrange
+ var builder = new HostApplicationBuilder();
+ const string WorkflowName = "scopedWorkflow";
+
+ // Act
+ builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key), ServiceLifetime.Scoped);
+
+ // Assert
+ var descriptor = builder.Services.FirstOrDefault(
+ d => (d.ServiceKey as string) == WorkflowName &&
+ d.ServiceType == typeof(Workflow));
+
+ Assert.NotNull(descriptor);
+ Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
+ }
+
+ ///
+ /// Verifies that AddWorkflow registers with the specified transient lifetime.
+ ///
+ [Fact]
+ public void AddWorkflow_WithTransientLifetime_RegistersKeyedTransient()
+ {
+ // Arrange
+ var builder = new HostApplicationBuilder();
+ const string WorkflowName = "transientWorkflow";
+
+ // Act
+ builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key), ServiceLifetime.Transient);
+
+ // Assert
+ var descriptor = builder.Services.FirstOrDefault(
+ d => (d.ServiceKey as string) == WorkflowName &&
+ d.ServiceType == typeof(Workflow));
+
+ Assert.NotNull(descriptor);
+ Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime);
+ }
+
+ ///
+ /// Verifies that AddAsAIAgent respects the lifetime parameter.
+ ///
+ [Theory]
+ [InlineData(ServiceLifetime.Singleton)]
+ [InlineData(ServiceLifetime.Scoped)]
+ [InlineData(ServiceLifetime.Transient)]
+ public void AddAsAIAgent_RespectsLifetime(ServiceLifetime lifetime)
+ {
+ // Arrange
+ var builder = new HostApplicationBuilder();
+ const string WorkflowName = "testWorkflow";
+ var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key));
+
+ // Act
+ var agentBuilder = workflowBuilder.AddAsAIAgent("agent", lifetime);
+
+ // Assert
+ var descriptor = builder.Services.FirstOrDefault(
+ d => (d.ServiceKey as string) == "agent" &&
+ d.ServiceType == typeof(AIAgent));
+
+ Assert.NotNull(descriptor);
+ Assert.Equal(lifetime, descriptor.Lifetime);
+ Assert.Equal(lifetime, agentBuilder.Lifetime);
+ }
+
///
/// Helper method to create a simple test workflow with a given name.
///
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostedAgentBuilderToolsExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostedAgentBuilderToolsExtensionsTests.cs
index 28b621714f..eb482964b0 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostedAgentBuilderToolsExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostedAgentBuilderToolsExtensionsTests.cs
@@ -7,6 +7,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
+using Moq;
namespace Microsoft.Agents.AI.Hosting.UnitTests;
@@ -250,6 +251,179 @@ public void WithAIToolFactory_ToolsAvailableOnAgent()
Assert.Contains(factoryTool, agentTools);
}
+ ///
+ /// Verifies that WithAITool factory method defaults to the agent's lifetime when no explicit lifetime is specified.
+ ///
+ [Theory]
+ [InlineData(ServiceLifetime.Singleton)]
+ [InlineData(ServiceLifetime.Scoped)]
+ [InlineData(ServiceLifetime.Transient)]
+ public void WithAIToolFactory_DefaultsToAgentLifetime(ServiceLifetime agentLifetime)
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, agentLifetime);
+
+ // Act
+ builder.WithAITool(_ => new DummyAITool());
+
+ // Assert
+ var toolDescriptor = services.FirstOrDefault(
+ d => (d.ServiceKey as string) == "test-agent" &&
+ d.ServiceType == typeof(AITool));
+
+ Assert.NotNull(toolDescriptor);
+ Assert.Equal(agentLifetime, toolDescriptor.Lifetime);
+ }
+
+ ///
+ /// Verifies that WithAITool factory method accepts an explicit lifetime override.
+ ///
+ [Fact]
+ public void WithAIToolFactory_ExplicitLifetimeOverridesDefault()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, ServiceLifetime.Transient);
+
+ // Act - Transient agent with Singleton tool is valid (longer-lived dependency)
+ builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Singleton);
+
+ // Assert
+ var toolDescriptor = services.FirstOrDefault(
+ d => (d.ServiceKey as string) == "test-agent" &&
+ d.ServiceType == typeof(AITool));
+
+ Assert.NotNull(toolDescriptor);
+ Assert.Equal(ServiceLifetime.Singleton, toolDescriptor.Lifetime);
+ }
+
+ ///
+ /// Verifies that WithAITool factory throws for singleton agent with scoped tool (captive dependency).
+ ///
+ [Fact]
+ public void WithAIToolFactory_SingletonAgentWithScopedTool_ThrowsInvalidOperationException()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, ServiceLifetime.Singleton);
+
+ // Act & Assert
+ Assert.Throws(() =>
+ builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Scoped));
+ }
+
+ ///
+ /// Verifies that WithAITool factory throws for singleton agent with transient tool (captive dependency).
+ ///
+ [Fact]
+ public void WithAIToolFactory_SingletonAgentWithTransientTool_ThrowsInvalidOperationException()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, ServiceLifetime.Singleton);
+
+ // Act & Assert
+ Assert.Throws(() =>
+ builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Transient));
+ }
+
+ ///
+ /// Verifies that WithAITool factory throws for scoped agent with transient tool (captive dependency).
+ ///
+ [Fact]
+ public void WithAIToolFactory_ScopedAgentWithTransientTool_ThrowsInvalidOperationException()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, ServiceLifetime.Scoped);
+
+ // Act & Assert
+ Assert.Throws(() =>
+ builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Transient));
+ }
+
+ ///
+ /// Verifies all valid tool lifetime combinations do not throw.
+ ///
+ [Theory]
+ [InlineData(ServiceLifetime.Singleton, ServiceLifetime.Singleton)]
+ [InlineData(ServiceLifetime.Scoped, ServiceLifetime.Singleton)]
+ [InlineData(ServiceLifetime.Scoped, ServiceLifetime.Scoped)]
+ [InlineData(ServiceLifetime.Transient, ServiceLifetime.Singleton)]
+ [InlineData(ServiceLifetime.Transient, ServiceLifetime.Scoped)]
+ [InlineData(ServiceLifetime.Transient, ServiceLifetime.Transient)]
+ public void WithAIToolFactory_ValidLifetimeCombinations_DoNotThrow(ServiceLifetime agentLifetime, ServiceLifetime toolLifetime)
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, agentLifetime);
+
+ // Act & Assert - should not throw
+ builder.WithAITool(_ => new DummyAITool(), toolLifetime);
+ }
+
+ ///
+ /// Verifies that ValidateToolLifetime correctly identifies all invalid combinations.
+ ///
+ [Theory]
+ [InlineData(ServiceLifetime.Singleton, ServiceLifetime.Scoped)]
+ [InlineData(ServiceLifetime.Singleton, ServiceLifetime.Transient)]
+ [InlineData(ServiceLifetime.Scoped, ServiceLifetime.Transient)]
+ public void ValidateToolLifetime_InvalidCombinations_Throw(ServiceLifetime agentLifetime, ServiceLifetime toolLifetime)
+ {
+ // Act & Assert
+ Assert.Throws(() =>
+ HostedAgentBuilderExtensions.ValidateToolLifetime(agentLifetime, toolLifetime));
+ }
+
+ ///
+ /// Verifies that the WithSessionStore factory method defaults to Singleton regardless of agent lifetime.
+ ///
+ [Theory]
+ [InlineData(ServiceLifetime.Singleton)]
+ [InlineData(ServiceLifetime.Scoped)]
+ [InlineData(ServiceLifetime.Transient)]
+ public void WithSessionStoreFactory_DefaultsToSingleton(ServiceLifetime agentLifetime)
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, agentLifetime);
+
+ // Act
+ builder.WithSessionStore((sp, name) => new InMemoryAgentSessionStore());
+
+ // Assert
+ var storeDescriptor = services.FirstOrDefault(
+ d => (d.ServiceKey as string) == "test-agent" &&
+ d.ServiceType == typeof(AgentSessionStore));
+
+ Assert.NotNull(storeDescriptor);
+ Assert.Equal(ServiceLifetime.Singleton, storeDescriptor.Lifetime);
+ }
+
+ ///
+ /// Verifies that the WithSessionStore factory method accepts an explicit lifetime override.
+ ///
+ [Fact]
+ public void WithSessionStoreFactory_ExplicitLifetimeOverridesDefault()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, ServiceLifetime.Transient);
+
+ // Act
+ builder.WithSessionStore((sp, name) => new InMemoryAgentSessionStore(), ServiceLifetime.Singleton);
+
+ // Assert
+ var storeDescriptor = services.FirstOrDefault(
+ d => (d.ServiceKey as string) == "test-agent" &&
+ d.ServiceType == typeof(AgentSessionStore));
+
+ Assert.NotNull(storeDescriptor);
+ Assert.Equal(ServiceLifetime.Singleton, storeDescriptor.Lifetime);
+ }
+
///
/// Dummy AITool implementation for testing.
///