diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 5866f1f895..2c57bd5071 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -12,6 +12,7 @@ When contributing to this repository, please follow these guidelines:
Here are some general guidelines that apply to all code.
+- All new files must be saved with UTF-8 encoding with BOM (Byte Order Mark). This is required for `dotnet format` to work correctly.
- The top of all *.cs files should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
- All public methods and classes should have XML documentation comments.
- After adding, modifying or deleting code, run `dotnet build`, and then fix any reported build errors.
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 630afbd6a5..26a4eba95b 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -137,6 +137,7 @@
+
@@ -411,6 +412,7 @@
+
@@ -432,6 +434,7 @@
+
@@ -454,6 +457,7 @@
+
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
new file mode 100644
index 0000000000..0b6c06a5a8
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
new file mode 100644
index 0000000000..98d8c95aeb
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to use the FoundryMemoryProvider to persist and recall memories for an agent.
+// The sample stores conversation messages in an Azure AI Foundry memory store and retrieves relevant
+// memories for subsequent invocations, even across new sessions.
+//
+// Note: Memory extraction in Azure AI Foundry is asynchronous and takes time. This sample demonstrates
+// a simple polling approach to wait for memory updates to complete before querying.
+
+using System.Text.Json;
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.FoundryMemory;
+
+string foundryEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string memoryStoreName = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_MEMORY_STORE_NAME") ?? "memory-store-sample";
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_MODEL") ?? "gpt-4.1-mini";
+string embeddingModelName = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_EMBEDDING_MODEL") ?? "text-embedding-ada-002";
+
+// Create an AIProjectClient for Foundry with Azure Identity authentication.
+AzureCliCredential credential = new();
+AIProjectClient projectClient = new(new Uri(foundryEndpoint), credential);
+
+// Get the ChatClient from the AIProjectClient's OpenAI property using the deployment name.
+AIAgent agent = await projectClient.CreateAIAgentAsync(deploymentName,
+ options: new ChatClientAgentOptions()
+ {
+ Name = "TravelAssistantWithFoundryMemory",
+ ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
+ AIContextProviderFactory = (ctx, ct) => new ValueTask(ctx.SerializedState.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined
+ // If each session should have its own scope, you can create a new id per session here:
+ // ? new FoundryMemoryProvider(projectClient, new FoundryMemoryProviderScope() { Scope = Guid.NewGuid().ToString() }, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName })
+ // In this case we are storing memories scoped by user so that memories are retained across sessions.
+ ? new FoundryMemoryProvider(projectClient, new FoundryMemoryProviderScope() { Scope = "sample-user-123" }, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName })
+ // For cases where we are restoring from serialized state:
+ : new FoundryMemoryProvider(projectClient, ctx.SerializedState, ctx.JsonSerializerOptions, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName }))
+ });
+
+AgentSession session = await agent.CreateSessionAsync();
+
+FoundryMemoryProvider memoryProvider = session.GetService()!;
+
+Console.WriteLine("\n>> Setting up Foundry Memory Store\n");
+
+// Ensure the memory store exists (creates it with the specified models if needed).
+await memoryProvider.EnsureMemoryStoreCreatedAsync(deploymentName, embeddingModelName, "Sample memory store for travel assistant");
+
+// Clear any existing memories for this scope to demonstrate fresh behavior.
+await memoryProvider.EnsureStoredMemoriesDeletedAsync();
+
+Console.WriteLine(await agent.RunAsync("Hi there! My name is Taylor and I'm planning a hiking trip to Patagonia in November.", session));
+Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", session));
+
+// Memory extraction in Azure AI Foundry is asynchronous and takes time to process.
+// WhenUpdatesCompletedAsync polls all pending updates and waits for them to complete.
+Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
+await memoryProvider.WhenUpdatesCompletedAsync();
+
+Console.WriteLine("Updates completed.\n");
+
+Console.WriteLine(await agent.RunAsync("What do you already know about my upcoming trip?", session));
+
+Console.WriteLine("\n>> Serialize and deserialize the session to demonstrate persisted state\n");
+JsonElement serializedSession = session.Serialize();
+AgentSession restoredSession = await agent.DeserializeSessionAsync(serializedSession);
+Console.WriteLine(await agent.RunAsync("Can you recap the personal details you remember?", restoredSession));
+
+Console.WriteLine("\n>> Start a new session that shares the same Foundry Memory scope\n");
+
+Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
+await memoryProvider.WhenUpdatesCompletedAsync();
+
+AgentSession newSession = await agent.CreateSessionAsync();
+Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newSession));
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
new file mode 100644
index 0000000000..dfea386d82
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
@@ -0,0 +1,57 @@
+# Agent with Memory Using Azure AI Foundry
+
+This sample demonstrates how to create and run an agent that uses Azure AI Foundry's managed memory service to extract and retrieve individual memories across sessions.
+
+## Features Demonstrated
+
+- Creating a `FoundryMemoryProvider` with Azure Identity authentication
+- Automatic memory store creation if it doesn't exist
+- Multi-turn conversations with automatic memory extraction
+- Memory retrieval to inform agent responses
+- Session serialization and deserialization
+- Memory persistence across completely new sessions
+
+## Prerequisites
+
+1. Azure subscription with Azure AI Foundry project
+2. Azure OpenAI resource with a chat model deployment (e.g., gpt-4o-mini) and an embedding model deployment (e.g., text-embedding-ada-002)
+3. .NET 10.0 SDK
+4. Azure CLI logged in (`az login`)
+
+## Environment Variables
+
+```bash
+# Azure AI Foundry project endpoint and memory store name
+export FOUNDRY_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
+export FOUNDRY_PROJECT_MEMORY_STORE_NAME="my_memory_store"
+
+# Model deployment names (models deployed in your Foundry project)
+export FOUNDRY_PROJECT_MODEL="gpt-4o-mini"
+export FOUNDRY_PROJECT_EMBEDDING_MODEL="text-embedding-ada-002"
+```
+
+## Run the Sample
+
+```bash
+dotnet run
+```
+
+## Expected Output
+
+The agent will:
+1. Create the memory store if it doesn't exist (using the specified chat and embedding models)
+2. Learn your name (Taylor), travel destination (Patagonia), timing (November), companions (sister), and interests (scenic viewpoints)
+3. Wait for Foundry Memory to index the memories
+4. Recall those details when asked about the trip
+5. Demonstrate memory persistence across session serialization/deserialization
+6. Show that a brand new session can still access the same memories
+
+## Key Differences from Mem0
+
+| Aspect | Mem0 | Azure AI Foundry Memory |
+|--------|------|------------------------|
+| Authentication | API Key | Azure Identity (DefaultAzureCredential) |
+| Scope | ApplicationId, UserId, AgentId, ThreadId | Single `Scope` string |
+| Memory Types | Single memory store | User Profile + Chat Summary |
+| Hosting | Mem0 cloud or self-hosted | Azure AI Foundry managed service |
+| Store Creation | N/A (automatic) | Explicit via `EnsureMemoryStoreCreatedAsync` |
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/README.md b/dotnet/samples/GettingStarted/AgentWithMemory/README.md
index 903fcf1b78..6e36ba0511 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/README.md
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/README.md
@@ -7,3 +7,4 @@ These samples show how to create an agent with the Agent Framework that uses Mem
|[Chat History memory](./AgentWithMemory_Step01_ChatHistoryMemory/)|This sample demonstrates how to enable an agent to remember messages from previous conversations.|
|[Memory with MemoryStore](./AgentWithMemory_Step02_MemoryUsingMem0/)|This sample demonstrates how to create and run an agent that uses the Mem0 service to extract and retrieve individual memories.|
|[Custom Memory Implementation](./AgentWithMemory_Step03_CustomMemory/)|This sample demonstrates how to create a custom memory component and attach it to an agent.|
+|[Memory with Azure AI Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Azure AI Foundry's managed memory service to extract and retrieve individual memories.|
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs
new file mode 100644
index 0000000000..9e24703d92
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ClientModel;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Internal extension methods for to provide MemoryStores helper operations.
+///
+internal static class AIProjectClientExtensions
+{
+ ///
+ /// Creates a memory store if it doesn't already exist.
+ ///
+ internal static async Task CreateMemoryStoreIfNotExistsAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string? description,
+ string chatModel,
+ string embeddingModel,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ await client.MemoryStores.GetMemoryStoreAsync(memoryStoreName, cancellationToken).ConfigureAwait(false);
+ return false; // Store already exists
+ }
+ catch (ClientResultException ex) when (ex.Status == 404)
+ {
+ // Store doesn't exist, create it
+ }
+
+ MemoryStoreDefaultDefinition definition = new(chatModel, embeddingModel);
+ await client.MemoryStores.CreateMemoryStoreAsync(memoryStoreName, definition, description, cancellationToken: cancellationToken).ConfigureAwait(false);
+ return true;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs
new file mode 100644
index 0000000000..2c9cfd740b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Provides JSON serialization utilities for the Foundry Memory provider.
+///
+internal static class FoundryMemoryJsonUtilities
+{
+ ///
+ /// Gets the default JSON serializer options for Foundry Memory operations.
+ ///
+ public static JsonSerializerOptions DefaultOptions { get; } = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = false,
+ TypeInfoResolver = FoundryMemoryJsonContext.Default
+ };
+}
+
+///
+/// Source-generated JSON serialization context for Foundry Memory types.
+///
+[JsonSourceGenerationOptions(
+ JsonSerializerDefaults.General,
+ UseStringEnumConverter = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ WriteIndented = false)]
+[JsonSerializable(typeof(FoundryMemoryProviderScope))]
+[JsonSerializable(typeof(FoundryMemoryProvider.FoundryMemoryState))]
+internal partial class FoundryMemoryJsonContext : JsonSerializerContext;
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
new file mode 100644
index 0000000000..fb263e3d56
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
@@ -0,0 +1,457 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Shared.Diagnostics;
+using OpenAI.Responses;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Provides an Azure AI Foundry Memory backed that persists conversation messages as memories
+/// and retrieves related memories to augment the agent invocation context.
+///
+///
+/// The provider stores user, assistant and system messages as Foundry memories and retrieves relevant memories
+/// for new invocations using the memory search endpoint. Retrieved memories are injected as user messages
+/// to the model, prefixed by a configurable context prompt.
+///
+public sealed class FoundryMemoryProvider : AIContextProvider
+{
+ private const string DefaultContextPrompt = "## Memories\nConsider the following memories when answering user questions:";
+
+ private readonly string _contextPrompt;
+ private readonly string _memoryStoreName;
+ private readonly int _maxMemories;
+ private readonly int _updateDelay;
+ private readonly bool _enableSensitiveTelemetryData;
+
+ private readonly AIProjectClient _client;
+ private readonly ILogger? _logger;
+
+ private readonly FoundryMemoryProviderScope _scope;
+ private readonly ConcurrentQueue _pendingUpdateIds = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Azure AI Project client configured for your Foundry project.
+ /// The scope configuration for memory storage and retrieval.
+ /// Provider options including memory store name.
+ /// Optional logger factory.
+ public FoundryMemoryProvider(
+ AIProjectClient client,
+ FoundryMemoryProviderScope scope,
+ FoundryMemoryProviderOptions? options = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ Throw.IfNull(client);
+ Throw.IfNull(scope);
+
+ if (string.IsNullOrWhiteSpace(scope.Scope))
+ {
+ throw new ArgumentException("The Scope property must be provided.", nameof(scope));
+ }
+
+ FoundryMemoryProviderOptions effectiveOptions = options ?? new FoundryMemoryProviderOptions();
+
+ if (string.IsNullOrWhiteSpace(effectiveOptions.MemoryStoreName))
+ {
+ throw new ArgumentException("The MemoryStoreName option must be provided.", nameof(options));
+ }
+
+ this._logger = loggerFactory?.CreateLogger();
+ this._client = client;
+
+ this._contextPrompt = effectiveOptions.ContextPrompt ?? DefaultContextPrompt;
+ this._memoryStoreName = effectiveOptions.MemoryStoreName;
+ this._maxMemories = effectiveOptions.MaxMemories;
+ this._updateDelay = effectiveOptions.UpdateDelay;
+ this._enableSensitiveTelemetryData = effectiveOptions.EnableSensitiveTelemetryData;
+ this._scope = new FoundryMemoryProviderScope(scope);
+ }
+
+ ///
+ /// Initializes a new instance of the class, with existing state from a serialized JSON element.
+ ///
+ /// The Azure AI Project client configured for your Foundry project.
+ /// A representing the serialized state of the provider.
+ /// Optional settings for customizing the JSON deserialization process.
+ /// Provider options including memory store name.
+ /// Optional logger factory.
+ public FoundryMemoryProvider(
+ AIProjectClient client,
+ JsonElement serializedState,
+ JsonSerializerOptions? jsonSerializerOptions = null,
+ FoundryMemoryProviderOptions? options = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ Throw.IfNull(client);
+
+ FoundryMemoryProviderOptions effectiveOptions = options ?? new FoundryMemoryProviderOptions();
+
+ if (string.IsNullOrWhiteSpace(effectiveOptions.MemoryStoreName))
+ {
+ throw new ArgumentException("The MemoryStoreName option must be provided.", nameof(options));
+ }
+
+ this._logger = loggerFactory?.CreateLogger();
+ this._client = client;
+
+ this._contextPrompt = effectiveOptions.ContextPrompt ?? DefaultContextPrompt;
+ this._memoryStoreName = effectiveOptions.MemoryStoreName;
+ this._maxMemories = effectiveOptions.MaxMemories;
+ this._updateDelay = effectiveOptions.UpdateDelay;
+ this._enableSensitiveTelemetryData = effectiveOptions.EnableSensitiveTelemetryData;
+
+ JsonSerializerOptions jso = jsonSerializerOptions ?? FoundryMemoryJsonUtilities.DefaultOptions;
+ FoundryMemoryState? state = serializedState.Deserialize(jso.GetTypeInfo(typeof(FoundryMemoryState))) as FoundryMemoryState;
+
+ if (state?.Scope == null || string.IsNullOrWhiteSpace(state.Scope.Scope))
+ {
+ throw new InvalidOperationException("The FoundryMemoryProvider state did not contain the required scope property.");
+ }
+
+ this._scope = state.Scope;
+ }
+
+ ///
+ public override async ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(context);
+
+ List messageItems = context.RequestMessages
+ .Where(m => !string.IsNullOrWhiteSpace(m.Text))
+ .Select(m => (ResponseItem)ToResponseItem(m.Role, m.Text!))
+ .ToList();
+
+ if (messageItems.Count == 0)
+ {
+ return new AIContext();
+ }
+
+ try
+ {
+ MemorySearchOptions searchOptions = new(this._scope.Scope!)
+ {
+ ResultOptions = new MemorySearchResultOptions { MaxMemories = this._maxMemories }
+ };
+
+ foreach (ResponseItem item in messageItems)
+ {
+ searchOptions.Items.Add(item);
+ }
+
+ ClientResult result = await this._client.MemoryStores.SearchMemoriesAsync(
+ this._memoryStoreName,
+ searchOptions,
+ cancellationToken).ConfigureAwait(false);
+
+ MemoryStoreSearchResponse response = result.Value;
+
+ List memories = response.Memories
+ .Select(m => m.MemoryItem?.Content ?? string.Empty)
+ .Where(c => !string.IsNullOrWhiteSpace(c))
+ .ToList();
+
+ string? outputMessageText = memories.Count == 0
+ ? null
+ : $"{this._contextPrompt}\n{string.Join(Environment.NewLine, memories)}";
+
+ if (this._logger?.IsEnabled(LogLevel.Information) is true)
+ {
+ this._logger.LogInformation(
+ "FoundryMemoryProvider: Retrieved {Count} memories. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ memories.Count,
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+
+ if (outputMessageText is not null && this._logger.IsEnabled(LogLevel.Trace))
+ {
+ this._logger.LogTrace(
+ "FoundryMemoryProvider: Search Results\nOutput:{MessageText}\nMemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this.SanitizeLogData(outputMessageText),
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ }
+
+ return new AIContext
+ {
+ Messages = [new ChatMessage(ChatRole.User, outputMessageText)]
+ };
+ }
+ catch (ArgumentException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ if (this._logger?.IsEnabled(LogLevel.Error) is true)
+ {
+ this._logger.LogError(
+ ex,
+ "FoundryMemoryProvider: Failed to search for memories due to error. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+
+ return new AIContext();
+ }
+ }
+
+ ///
+ public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
+ {
+ if (context.InvokeException is not null)
+ {
+ return; // Do not update memory on failed invocations.
+ }
+
+ try
+ {
+ List messageItems = context.RequestMessages
+ .Concat(context.ResponseMessages ?? [])
+ .Where(m => IsAllowedRole(m.Role) && !string.IsNullOrWhiteSpace(m.Text))
+ .Select(m => (ResponseItem)ToResponseItem(m.Role, m.Text!))
+ .ToList();
+
+ if (messageItems.Count == 0)
+ {
+ return;
+ }
+
+ MemoryUpdateOptions updateOptions = new(this._scope.Scope!)
+ {
+ UpdateDelay = this._updateDelay
+ };
+
+ foreach (ResponseItem item in messageItems)
+ {
+ updateOptions.Items.Add(item);
+ }
+
+ ClientResult result = await this._client.MemoryStores.UpdateMemoriesAsync(
+ this._memoryStoreName,
+ updateOptions,
+ cancellationToken).ConfigureAwait(false);
+
+ MemoryUpdateResult response = result.Value;
+
+ if (response.UpdateId is not null)
+ {
+ this._pendingUpdateIds.Enqueue(response.UpdateId);
+ }
+
+ if (this._logger?.IsEnabled(LogLevel.Information) is true)
+ {
+ this._logger.LogInformation(
+ "FoundryMemoryProvider: Sent {Count} messages to update memories. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}', UpdateId: '{UpdateId}'.",
+ messageItems.Count,
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope),
+ response.UpdateId);
+ }
+ }
+ catch (Exception ex)
+ {
+ if (this._logger?.IsEnabled(LogLevel.Error) is true)
+ {
+ this._logger.LogError(
+ ex,
+ "FoundryMemoryProvider: Failed to send messages to update memories due to error. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ }
+ }
+
+ ///
+ /// Ensures all stored memories for the configured scope are deleted.
+ /// This method handles cases where the scope doesn't exist (no memories stored yet).
+ ///
+ /// Cancellation token.
+ public async Task EnsureStoredMemoriesDeletedAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ await this._client.MemoryStores.DeleteScopeAsync(this._memoryStoreName, this._scope.Scope!, cancellationToken).ConfigureAwait(false);
+ }
+ catch (ClientResultException ex) when (ex.Status == 404)
+ {
+ // Scope doesn't exist (no memories stored yet), nothing to delete
+ if (this._logger?.IsEnabled(LogLevel.Debug) is true)
+ {
+ this._logger.LogDebug(
+ "FoundryMemoryProvider: No memories to delete for scope. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ }
+ }
+
+ ///
+ /// Ensures the memory store exists, creating it if necessary.
+ ///
+ /// The deployment name of the chat model for memory processing.
+ /// The deployment name of the embedding model for memory search.
+ /// Optional description for the memory store.
+ /// Cancellation token.
+ public async Task EnsureMemoryStoreCreatedAsync(
+ string chatModel,
+ string embeddingModel,
+ string? description = null,
+ CancellationToken cancellationToken = default)
+ {
+ bool created = await this._client.CreateMemoryStoreIfNotExistsAsync(
+ this._memoryStoreName,
+ description,
+ chatModel,
+ embeddingModel,
+ cancellationToken).ConfigureAwait(false);
+
+ if (created)
+ {
+ if (this._logger?.IsEnabled(LogLevel.Information) is true)
+ {
+ this._logger.LogInformation(
+ "FoundryMemoryProvider: Created memory store '{MemoryStoreName}'.",
+ this._memoryStoreName);
+ }
+ }
+ else
+ {
+ if (this._logger?.IsEnabled(LogLevel.Debug) is true)
+ {
+ this._logger.LogDebug(
+ "FoundryMemoryProvider: Memory store '{MemoryStoreName}' already exists.",
+ this._memoryStoreName);
+ }
+ }
+ }
+
+ ///
+ /// Waits for all pending memory update operations to complete.
+ ///
+ ///
+ /// Memory extraction in Azure AI Foundry is asynchronous. This method polls all pending updates
+ /// in parallel and returns when all have completed, failed, or been superseded.
+ ///
+ /// The interval between status checks. Defaults to 5 seconds.
+ /// Cancellation token.
+ /// Thrown if any update operation failed, containing all failures.
+ public async Task WhenUpdatesCompletedAsync(
+ TimeSpan? pollingInterval = null,
+ CancellationToken cancellationToken = default)
+ {
+ TimeSpan interval = pollingInterval ?? TimeSpan.FromSeconds(5);
+
+ // Collect all pending update IDs
+ List updateIds = [];
+ while (this._pendingUpdateIds.TryDequeue(out string? updateId))
+ {
+ updateIds.Add(updateId);
+ }
+
+ if (updateIds.Count == 0)
+ {
+ return;
+ }
+
+ // Poll all updates in parallel
+ await Task.WhenAll(updateIds.Select(updateId => this.WaitForUpdateAsync(updateId, interval, cancellationToken))).ConfigureAwait(false);
+ }
+
+ private async Task WaitForUpdateAsync(string updateId, TimeSpan interval, CancellationToken cancellationToken)
+ {
+ while (true)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ ClientResult result = await this._client.MemoryStores.GetUpdateResultAsync(
+ this._memoryStoreName,
+ updateId,
+ cancellationToken).ConfigureAwait(false);
+
+ MemoryUpdateResult response = result.Value;
+ MemoryStoreUpdateStatus status = response.Status;
+
+ if (this._logger?.IsEnabled(LogLevel.Debug) is true)
+ {
+ this._logger.LogDebug(
+ "FoundryMemoryProvider: Update status for '{UpdateId}': {Status}",
+ updateId,
+ status);
+ }
+
+ if (status == MemoryStoreUpdateStatus.Completed || status == MemoryStoreUpdateStatus.Superseded)
+ {
+ return;
+ }
+
+ if (status == MemoryStoreUpdateStatus.Failed)
+ {
+ throw new InvalidOperationException($"Memory update operation '{updateId}' failed: {response.ErrorDetails}");
+ }
+
+ if (status == MemoryStoreUpdateStatus.Queued || status == MemoryStoreUpdateStatus.InProgress)
+ {
+ await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ throw new InvalidOperationException($"Unknown update status '{status}' for update '{updateId}'.");
+ }
+ }
+ }
+
+ ///
+ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ FoundryMemoryState state = new(this._scope);
+
+ JsonSerializerOptions jso = jsonSerializerOptions ?? FoundryMemoryJsonUtilities.DefaultOptions;
+ return JsonSerializer.SerializeToElement(state, jso.GetTypeInfo(typeof(FoundryMemoryState)));
+ }
+
+ private static MessageResponseItem ToResponseItem(ChatRole role, string text)
+ {
+ if (role == ChatRole.Assistant)
+ {
+ return ResponseItem.CreateAssistantMessageItem(text);
+ }
+
+ if (role == ChatRole.System)
+ {
+ return ResponseItem.CreateSystemMessageItem(text);
+ }
+
+ return ResponseItem.CreateUserMessageItem(text);
+ }
+
+ private static bool IsAllowedRole(ChatRole role) =>
+ role == ChatRole.User || role == ChatRole.Assistant || role == ChatRole.System;
+
+ private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : "";
+
+ internal sealed class FoundryMemoryState
+ {
+ [JsonConstructor]
+ public FoundryMemoryState(FoundryMemoryProviderScope scope)
+ {
+ this.Scope = scope;
+ }
+
+ public FoundryMemoryProviderScope Scope { get; set; }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs
new file mode 100644
index 0000000000..b6b157431d
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Options for configuring the .
+///
+public sealed class FoundryMemoryProviderOptions
+{
+ ///
+ /// Gets or sets the name of the pre-existing memory store in Azure AI Foundry.
+ ///
+ ///
+ /// The memory store must be created in your Azure AI Foundry project before using this provider.
+ ///
+ public string? MemoryStoreName { get; set; }
+
+ ///
+ /// When providing memories to the model, this string is prefixed to the retrieved memories to supply context.
+ ///
+ /// Defaults to "## Memories\nConsider the following memories when answering user questions:".
+ public string? ContextPrompt { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of memories to retrieve during search.
+ ///
+ /// Defaults to 5.
+ public int MaxMemories { get; set; } = 5;
+
+ ///
+ /// Gets or sets the delay in seconds before memory updates are processed.
+ ///
+ ///
+ /// Setting to 0 triggers updates immediately without waiting for inactivity.
+ /// Higher values allow the service to batch multiple updates together.
+ ///
+ /// Defaults to 0 (immediate).
+ public int UpdateDelay { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether sensitive data such as user ids and user messages may appear in logs.
+ ///
+ /// Defaults to .
+ public bool EnableSensitiveTelemetryData { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs
new file mode 100644
index 0000000000..542e5bf995
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Allows scoping of memories for the .
+///
+///
+/// Azure AI Foundry memories are scoped by a single string identifier that you control.
+/// Common patterns include using a user ID, team ID, or other unique identifier
+/// to partition memories across different contexts.
+///
+public sealed class FoundryMemoryProviderScope
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FoundryMemoryProviderScope() { }
+
+ ///
+ /// Initializes a new instance of the class by cloning an existing scope.
+ ///
+ /// The scope to clone.
+ public FoundryMemoryProviderScope(FoundryMemoryProviderScope sourceScope)
+ {
+ Throw.IfNull(sourceScope);
+ this.Scope = sourceScope.Scope;
+ }
+
+ ///
+ /// Gets or sets the scope identifier used to partition memories.
+ ///
+ ///
+ /// This value controls how memory is partitioned in the memory store.
+ /// Each unique scope maintains its own isolated collection of memory items.
+ /// For example, use a user ID to ensure each user has their own individual memory.
+ ///
+ public string? Scope { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj
new file mode 100644
index 0000000000..419a5b22f0
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj
@@ -0,0 +1,39 @@
+
+
+
+ preview
+ $(NoWarn);OPENAI001
+
+
+
+ true
+ true
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Microsoft Agent Framework - Azure AI Foundry Memory integration
+ Provides Azure AI Foundry Memory integration for Microsoft Agent Framework.
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/Shared/IntegrationTests/FoundryMemoryConfiguration.cs b/dotnet/src/Shared/IntegrationTests/FoundryMemoryConfiguration.cs
new file mode 100644
index 0000000000..b752754e59
--- /dev/null
+++ b/dotnet/src/Shared/IntegrationTests/FoundryMemoryConfiguration.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Shared.IntegrationTests;
+
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+#pragma warning disable CA1812 // Internal class that is apparently never instantiated.
+
+internal sealed class FoundryMemoryConfiguration
+{
+ public string Endpoint { get; set; }
+ public string MemoryStoreName { get; set; }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs
new file mode 100644
index 0000000000..2a5e154cbc
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs
@@ -0,0 +1,190 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Shared.IntegrationTests;
+
+namespace Microsoft.Agents.AI.FoundryMemory.IntegrationTests;
+
+///
+/// Integration tests for against a configured Azure AI Foundry Memory service.
+///
+public sealed class FoundryMemoryProviderTests : IDisposable
+{
+ private const string SkipReason = "Requires an Azure AI Foundry Memory service configured"; // Set to null to enable.
+
+ private readonly AIProjectClient? _client;
+ private readonly string? _memoryStoreName;
+ private bool _disposed;
+
+ public FoundryMemoryProviderTests()
+ {
+ IConfigurationRoot configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets(optional: true)
+ .Build();
+
+ var foundrySettings = configuration.GetSection("FoundryMemory").Get();
+
+ if (foundrySettings is not null &&
+ !string.IsNullOrWhiteSpace(foundrySettings.Endpoint) &&
+ !string.IsNullOrWhiteSpace(foundrySettings.MemoryStoreName))
+ {
+ this._client = new AIProjectClient(new Uri(foundrySettings.Endpoint), new AzureCliCredential());
+ this._memoryStoreName = foundrySettings.MemoryStoreName;
+ }
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task CanAddAndRetrieveUserMemoriesAsync()
+ {
+ // Arrange
+ var question = new ChatMessage(ChatRole.User, "What is my name?");
+ var input = new ChatMessage(ChatRole.User, "Hello, my name is Caoimhe.");
+ var storageScope = new FoundryMemoryProviderScope { Scope = "it-user-1" };
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
+
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
+
+ // Act
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([input], aiContextProviderMessages: null));
+ var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+
+ // Assert
+ Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxAfterClearing.Messages?[0].Text ?? string.Empty);
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task CanAddAndRetrieveAssistantMemoriesAsync()
+ {
+ // Arrange
+ var question = new ChatMessage(ChatRole.User, "What is your name?");
+ var assistantIntro = new ChatMessage(ChatRole.Assistant, "Hello, I'm a friendly assistant and my name is Caoimhe.");
+ var storageScope = new FoundryMemoryProviderScope { Scope = "it-agent-1" };
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
+
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
+
+ // Act
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
+ var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+
+ // Assert
+ Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxAfterClearing.Messages?[0].Text ?? string.Empty);
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task DoesNotLeakMemoriesAcrossScopesAsync()
+ {
+ // Arrange
+ var question = new ChatMessage(ChatRole.User, "What is your name?");
+ var assistantIntro = new ChatMessage(ChatRole.Assistant, "I'm an AI tutor and my name is Caoimhe.");
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut1 = new FoundryMemoryProvider(this._client!, new FoundryMemoryProviderScope { Scope = "it-scope-a" }, options);
+ var sut2 = new FoundryMemoryProvider(this._client!, new FoundryMemoryProviderScope { Scope = "it-scope-b" }, options);
+
+ await sut1.EnsureStoredMemoriesDeletedAsync();
+ await sut2.EnsureStoredMemoriesDeletedAsync();
+
+ var ctxBefore1 = await sut1.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ var ctxBefore2 = await sut2.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ Assert.DoesNotContain("Caoimhe", ctxBefore1.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxBefore2.Messages?[0].Text ?? string.Empty);
+
+ // Act
+ await sut1.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
+ var ctxAfterAdding1 = await GetContextWithRetryAsync(sut1, question);
+ var ctxAfterAdding2 = await GetContextWithRetryAsync(sut2, question);
+
+ // Assert
+ Assert.Contains("Caoimhe", ctxAfterAdding1.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxAfterAdding2.Messages?[0].Text ?? string.Empty);
+
+ // Cleanup
+ await sut1.EnsureStoredMemoriesDeletedAsync();
+ await sut2.EnsureStoredMemoriesDeletedAsync();
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task ClearStoredMemoriesRemovesAllMemoriesAsync()
+ {
+ // Arrange
+ var input1 = new ChatMessage(ChatRole.User, "My favorite color is blue.");
+ var input2 = new ChatMessage(ChatRole.User, "My favorite food is pizza.");
+ var question = new ChatMessage(ChatRole.User, "What do you know about my preferences?");
+ var storageScope = new FoundryMemoryProviderScope { Scope = "it-clear-test" };
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
+
+ await sut.EnsureStoredMemoriesDeletedAsync();
+
+ // Act - Add multiple memories
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([input1], aiContextProviderMessages: null));
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([input2], aiContextProviderMessages: null));
+ var ctxBeforeClear = await GetContextWithRetryAsync(sut, question, searchTerms: ["blue", "pizza"]);
+
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ var ctxAfterClear = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+
+ // Assert
+ var textBefore = ctxBeforeClear.Messages?[0].Text ?? string.Empty;
+ var textAfter = ctxAfterClear.Messages?[0].Text ?? string.Empty;
+
+ Assert.True(textBefore.Contains("blue") || textBefore.Contains("pizza"), "Should contain at least one preference before clear");
+ Assert.DoesNotContain("blue", textAfter);
+ Assert.DoesNotContain("pizza", textAfter);
+ }
+
+ private static async Task GetContextWithRetryAsync(
+ FoundryMemoryProvider provider,
+ ChatMessage question,
+ string[]? searchTerms = null,
+ int attempts = 5,
+ int delayMs = 2000)
+ {
+ searchTerms ??= ["Caoimhe"];
+ AIContext? ctx = null;
+
+ for (int i = 0; i < attempts; i++)
+ {
+ ctx = await provider.InvokingAsync(new AIContextProvider.InvokingContext([question]), CancellationToken.None);
+ var text = ctx.Messages?[0].Text ?? string.Empty;
+
+ if (Array.Exists(searchTerms, term => text.Contains(term, StringComparison.OrdinalIgnoreCase)))
+ {
+ break;
+ }
+
+ await Task.Delay(delayMs);
+ }
+
+ return ctx!;
+ }
+
+ public void Dispose()
+ {
+ if (!this._disposed)
+ {
+ this._disposed = true;
+ }
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj
new file mode 100644
index 0000000000..652178aef8
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
new file mode 100644
index 0000000000..12307c24ca
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
@@ -0,0 +1,169 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+
+namespace Microsoft.Agents.AI.FoundryMemory.UnitTests;
+
+///
+/// Tests for constructor validation and serialization.
+///
+///
+/// Since directly uses ,
+/// integration tests are used to verify the memory operations. These unit tests focus on:
+/// - Constructor parameter validation
+/// - Serialization and deserialization of provider state
+///
+public sealed class FoundryMemoryProviderTests
+{
+ [Fact]
+ public void Constructor_Throws_WhenClientIsNull()
+ {
+ // Act & Assert
+ ArgumentNullException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ null!,
+ new FoundryMemoryProviderScope { Scope = "test" },
+ new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.Equal("client", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenScopeIsNull()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
+ // Act & Assert
+ ArgumentNullException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ null!,
+ new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.Equal("scope", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenScopeValueIsEmpty()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
+ // Act & Assert
+ ArgumentException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ new FoundryMemoryProviderScope(),
+ new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.StartsWith("The Scope property must be provided.", ex.Message);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenMemoryStoreNameIsMissing()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
+ // Act & Assert
+ ArgumentException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ new FoundryMemoryProviderScope { Scope = "test" },
+ new FoundryMemoryProviderOptions()));
+ Assert.StartsWith("The MemoryStoreName option must be provided.", ex.Message);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenMemoryStoreNameIsNull()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
+ // Act & Assert
+ ArgumentException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ new FoundryMemoryProviderScope { Scope = "test" },
+ null));
+ Assert.StartsWith("The MemoryStoreName option must be provided.", ex.Message);
+ }
+
+ [Fact]
+ public void DeserializingConstructor_Throws_WhenClientIsNull()
+ {
+ // Arrange - use source-generated JSON context
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(
+ new TestState { Scope = new TestScope { Scope = "test" } },
+ TestJsonContext.Default.TestState);
+
+ // Act & Assert
+ ArgumentNullException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ null!,
+ jsonElement,
+ options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.Equal("client", ex.ParamName);
+ }
+
+ [Fact]
+ public void DeserializingConstructor_Throws_WithEmptyJsonElement()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+ JsonElement jsonElement = JsonDocument.Parse("{}").RootElement;
+
+ // Act & Assert
+ InvalidOperationException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ jsonElement,
+ options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.StartsWith("The FoundryMemoryProvider state did not contain the required scope property.", ex.Message);
+ }
+
+ [Fact]
+ public void DeserializingConstructor_Throws_WithMissingScopeValue()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(
+ new TestState { Scope = new TestScope() },
+ TestJsonContext.Default.TestState);
+
+ // Act & Assert
+ InvalidOperationException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ jsonElement,
+ options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.StartsWith("The FoundryMemoryProvider state did not contain the required scope property.", ex.Message);
+ }
+
+ [Fact]
+ public void Serialize_RoundTripsScope()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+ FoundryMemoryProviderScope scope = new() { Scope = "user-456" };
+ FoundryMemoryProvider sut = new(testClient.Client, scope, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ // Act
+ JsonElement stateElement = sut.Serialize();
+ using JsonDocument doc = JsonDocument.Parse(stateElement.GetRawText());
+
+ // Assert (JSON uses camelCase naming policy)
+ Assert.True(doc.RootElement.TryGetProperty("scope", out JsonElement scopeElement));
+ Assert.Equal("user-456", scopeElement.GetProperty("scope").GetString());
+ }
+
+ [Fact]
+ public void DeserializingConstructor_RestoresScope()
+ {
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+ FoundryMemoryProviderScope originalScope = new() { Scope = "restored-user-789" };
+ FoundryMemoryProvider original = new(testClient.Client, originalScope, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ // Act
+ JsonElement serializedState = original.Serialize();
+ FoundryMemoryProvider restored = new(testClient.Client, serializedState, options: new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ // Assert - serialize again to verify scope was restored
+ JsonElement restoredState = restored.Serialize();
+ using JsonDocument doc = JsonDocument.Parse(restoredState.GetRawText());
+ Assert.True(doc.RootElement.TryGetProperty("scope", out JsonElement scopeElement));
+ Assert.Equal("restored-user-789", scopeElement.GetProperty("scope").GetString());
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj
new file mode 100644
index 0000000000..1fe8dc57bd
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj
@@ -0,0 +1,16 @@
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
new file mode 100644
index 0000000000..25c041f754
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel.Primitives;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Azure.Core;
+
+namespace Microsoft.Agents.AI.FoundryMemory.UnitTests;
+
+///
+/// Creates a testable AIProjectClient with a mock HTTP handler.
+///
+internal sealed class TestableAIProjectClient : IDisposable
+{
+ private readonly HttpClient _httpClient;
+
+ public TestableAIProjectClient(
+ string? searchMemoriesResponse = null,
+ string? updateMemoriesResponse = null,
+ HttpStatusCode? searchStatusCode = null,
+ HttpStatusCode? updateStatusCode = null,
+ HttpStatusCode? deleteStatusCode = null,
+ HttpStatusCode? createStoreStatusCode = null,
+ HttpStatusCode? getStoreStatusCode = null)
+ {
+ this.Handler = new MockHttpMessageHandler(
+ searchMemoriesResponse,
+ updateMemoriesResponse,
+ searchStatusCode,
+ updateStatusCode,
+ deleteStatusCode,
+ createStoreStatusCode,
+ getStoreStatusCode);
+
+ this._httpClient = new HttpClient(this.Handler);
+
+ AIProjectClientOptions options = new()
+ {
+ Transport = new HttpClientPipelineTransport(this._httpClient)
+ };
+
+ // Using a valid format endpoint
+ this.Client = new AIProjectClient(
+ new Uri("https://test.services.ai.azure.com/api/projects/test-project"),
+ new MockTokenCredential(),
+ options);
+ }
+
+ public AIProjectClient Client { get; }
+
+ public MockHttpMessageHandler Handler { get; }
+
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ this.Handler.Dispose();
+ }
+}
+
+///
+/// Mock HTTP message handler for testing.
+///
+internal sealed class MockHttpMessageHandler : HttpMessageHandler
+{
+ private readonly string? _searchMemoriesResponse;
+ private readonly string? _updateMemoriesResponse;
+ private readonly HttpStatusCode _searchStatusCode;
+ private readonly HttpStatusCode _updateStatusCode;
+ private readonly HttpStatusCode _deleteStatusCode;
+ private readonly HttpStatusCode _createStoreStatusCode;
+ private readonly HttpStatusCode _getStoreStatusCode;
+
+ public MockHttpMessageHandler(
+ string? searchMemoriesResponse = null,
+ string? updateMemoriesResponse = null,
+ HttpStatusCode? searchStatusCode = null,
+ HttpStatusCode? updateStatusCode = null,
+ HttpStatusCode? deleteStatusCode = null,
+ HttpStatusCode? createStoreStatusCode = null,
+ HttpStatusCode? getStoreStatusCode = null)
+ {
+ this._searchMemoriesResponse = searchMemoriesResponse ?? """{"memories":[]}""";
+ this._updateMemoriesResponse = updateMemoriesResponse ?? """{"update_id":"test-update-id","status":"queued"}""";
+ this._searchStatusCode = searchStatusCode ?? HttpStatusCode.OK;
+ this._updateStatusCode = updateStatusCode ?? HttpStatusCode.OK;
+ this._deleteStatusCode = deleteStatusCode ?? HttpStatusCode.NoContent;
+ this._createStoreStatusCode = createStoreStatusCode ?? HttpStatusCode.Created;
+ this._getStoreStatusCode = getStoreStatusCode ?? HttpStatusCode.NotFound;
+ }
+
+ public string? LastRequestUri { get; private set; }
+ public string? LastRequestBody { get; private set; }
+ public HttpMethod? LastRequestMethod { get; private set; }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ this.LastRequestUri = request.RequestUri?.ToString();
+ this.LastRequestMethod = request.Method;
+
+ if (request.Content != null)
+ {
+#if NET472
+ this.LastRequestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
+#else
+ this.LastRequestBody = await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+#endif
+ }
+
+ string path = request.RequestUri?.AbsolutePath ?? "";
+
+ // Route based on path and method
+ if (path.Contains("/memory-stores/") && path.Contains("/search") && request.Method == HttpMethod.Post)
+ {
+ return CreateResponse(this._searchStatusCode, this._searchMemoriesResponse);
+ }
+
+ if (path.Contains("/memory-stores/") && path.Contains("/memories") && request.Method == HttpMethod.Post)
+ {
+ return CreateResponse(this._updateStatusCode, this._updateMemoriesResponse);
+ }
+
+ if (path.Contains("/memory-stores/") && path.Contains("/scopes") && request.Method == HttpMethod.Delete)
+ {
+ return CreateResponse(this._deleteStatusCode, "");
+ }
+
+ if (path.Contains("/memory-stores") && request.Method == HttpMethod.Post)
+ {
+ return CreateResponse(this._createStoreStatusCode, """{"name":"test-store","status":"active"}""");
+ }
+
+ if (path.Contains("/memory-stores/") && request.Method == HttpMethod.Get)
+ {
+ return CreateResponse(this._getStoreStatusCode, """{"name":"test-store","status":"active"}""");
+ }
+
+ // Default response
+ return CreateResponse(HttpStatusCode.NotFound, "{}");
+ }
+
+ private static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string? content)
+ {
+ return new HttpResponseMessage(statusCode)
+ {
+ Content = new StringContent(content ?? "{}", Encoding.UTF8, "application/json")
+ };
+ }
+}
+
+///
+/// Mock token credential for testing.
+///
+internal sealed class MockTokenCredential : TokenCredential
+{
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new AccessToken("mock-token", DateTimeOffset.UtcNow.AddHours(1));
+ }
+
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new ValueTask(new AccessToken("mock-token", DateTimeOffset.UtcNow.AddHours(1)));
+ }
+}
+
+///
+/// Source-generated JSON serializer context for unit test types.
+///
+[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+[JsonSerializable(typeof(TestState))]
+[JsonSerializable(typeof(TestScope))]
+internal sealed partial class TestJsonContext : JsonSerializerContext
+{
+}
+
+///
+/// Test state class for deserialization tests.
+///
+internal sealed class TestState
+{
+ public TestScope? Scope { get; set; }
+}
+
+///
+/// Test scope class for deserialization tests.
+///
+internal sealed class TestScope
+{
+ public string? Scope { get; set; }
+}