<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[My Ramblings On Microsoft Data Stack]]></title><description><![CDATA[From Synapse Analytics, Power BI, Spark, Microsoft Fabric,ASP.NET Core and recently Agentic AI on .NET I try to explore, learn and share all aspects of Microsof]]></description><link>https://www.azureguru.net</link><generator>RSS for Node</generator><lastBuildDate>Tue, 08 Sep 2026 18:28:24 GMT</lastBuildDate><atom:link href="https://www.azureguru.net/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Integrating GitHub with Microsoft Agent Framework with OAuth Authentication and Octokit]]></title><description><![CDATA[My blogs in the past were focused on Microsoft Agent Framework(MAF) agent integration with Microsoft services like Microsoft Azure, Microsoft Fabric and other M365 service like Microsoft Purview. The ]]></description><link>https://www.azureguru.net/integrating-github-with-microsoft-agent-framework-with-oauth-authentication-and-octokit</link><guid isPermaLink="true">https://www.azureguru.net/integrating-github-with-microsoft-agent-framework-with-oauth-authentication-and-octokit</guid><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[llm]]></category><category><![CDATA[aiagents]]></category><category><![CDATA[Octokit]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Tue, 08 Sep 2026 00:29:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c74ee34a-e73c-4205-a53d-a7db4ba66667.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My blogs in the past were focused on Microsoft Agent Framework(MAF) agent integration with Microsoft services like Microsoft Azure, Microsoft Fabric and other M365 service like Microsoft Purview. The approach in them were pretty straightforward as all these services lie within the Microsoft ecosystem and the permissions can be easily managed through delegated permissions in Microsoft Entra.</p>
<p>But the challenge comes in when you have a service that sits outside of Microsoft ecosystem for example <strong>GitHub</strong>.</p>
<p>The authentication and authorization model is fundamentally different from Microsoft services and it becomes even more challenging when the MAF agent needs to access this service that has their own identity and authorization model.</p>
<p>For instance, you can generally establish a Microsoft Entra identity context and use Microsoft-native authorization mechanisms when accessing Microsoft services as the identity and permissions flow through the Microsoft security model. But for external service like GitHub the permission model does not flow through Microsoft authorization model and has its own security limitation.</p>
<p>In this article we will look into the approach on how to overcome this limitation. We wont use any third party authorization providers and will try to leverage native techniques that can be available at our disposal.</p>
<p>So, the intended flow is straightforward : a user sends a GitHub request through an MAF agent to create an issue in a repository. The agent should perform the operation only if the user has the required permissions on that repository. If the user does not have the necessary permissions the operation should fail and the issue should not be created.</p>
<p>We will use <strong>Azure Functions for MCP</strong> for MCP endpoint and <a href="https://github.com/octokit">Octokit</a> to perform GitHub operations.</p>
<blockquote>
<p>The biggest challenge is that, unlike MSAL which provides a redirect flow for authentication in windows, GitHub requires the application to manage the OAuth flow itself. But it does provide redirects to the callback URL with a temporary authorization code and through this code we can derive the GitHub access token .</p>
</blockquote>
<blockquote>
<p>We will use HttpListener to act as a temporary local callback server to call URL once GitHub authenticates the user credentials.</p>
</blockquote>
<p>Our MCP endpoint will be an Azure Function and the client will be a console application that calls the MCP endpoint .</p>
<p>We will implement an OAuth based authentication model for GitHub. Think of it as a delegated authority where the application acts on behalf of the user. The OAuth will return an access token for the logged in user.</p>
<p>A token for the logged in user and GitHub uses the delegated authorization in combination with the user's existing permissions.</p>
<h3>GitHub Settings</h3>
<p>Repo <strong>GithubActions</strong> has two users : <strong>Sachin- Nand</strong> and <strong>SachinNandanwar</strong></p>
<p><strong>Sachin-Nand</strong> has write access while <strong>SachinNandanwar</strong> only has read access.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a7ec0f20-fe4a-4292-ba4e-d9c1718cf989.png" alt="" style="display:block;margin:0 auto" />

<p>We first have to set OAuth Apps to enable GitHub OAuth flow.</p>
<p>Under <strong>Settings</strong> &gt;&gt; <strong>Profile</strong> &gt;&gt; <strong>Developer settings</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/91ef713c-5855-477b-a867-9976e104e287.png" alt="" style="display:block;margin:0 auto" />

<p>Click on <strong>OAuth Apps</strong> and <strong>New OAuth apps</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7d32502f-d416-46a2-9795-504eb69d51d3.png" alt="" style="display:block;margin:0 auto" />

<p>Enter App name and create a new client secret.</p>
<p>Note down the Client Id and Client Secret</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e87b4758-f0cf-4a78-926a-1a9dfecbf68b.png" alt="" style="display:block;margin:0 auto" />

<p>The app created above is named <strong>GHub API.</strong></p>
<p>Scroll down to the bottom of the page and for Redirect URI enter value</p>
<p><strong><a href="http://127.0.0.1:50505/callback">http://127.0.0.1:50505/callback</a>.</strong> This is our loopback url.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/05f18c1a-66c9-4ee6-be9f-d3c79e25d98d.png" alt="" style="display:block;margin:0 auto" />

<p>You can use any port of your choice.</p>
<h3>Azure Function</h3>
<p>Create a new <strong>Azure Function for MCP</strong> project and add the following packages</p>
<pre><code class="language-csharp">dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Mcp
dotnet add package Microsoft.Extensions.Logging
dotnet add package Octokit
</code></pre>
<p>Create a MCP function that accepts the repo name and the owner name.</p>
<pre><code class="language-csharp">[Function(nameof(GetGitHubRepository))]
public async Task&lt;string&gt; GetGitHubRepository(
  [McpToolTrigger("get_github_repository", "Gets information about a GitHub repository.")] ToolInvocationContext context,
  [McpToolProperty("owner", "GitHub repository owner.", isRequired: true)] string owner,
  [McpToolProperty("repository", "GitHub repository name.", isRequired: true)] string repository)
{
    var client = new GitHubClient(new ProductHeaderValue("MyGitHubApp"));
  
    if (context.TryGetHttpTransport(out var authHeaders))
    {
        var authenticatedClient = new GitHubClient(new ProductHeaderValue("MyGitHubApp"));

        authenticatedClient.Credentials = new Credentials(authHeaders.Headers["Authorization"]);
        var newIssue = new NewIssue("Bug: App crashes on launch")
        {
            Body = "The application immediately crashes when on start up."
        };
        var issue = await authenticatedClient.Issue.Create(owner, repository, newIssue);
        return "Issue created !!!";
    }
    else
    {
        return "Error !!!";
    }
}
</code></pre>
<p>Nothing fancy or complicated. Just a simple function that accepts the user inputs and authenticates the calls through <strong>ToolInvocationContext</strong> by checking the Authorization value in the request header.</p>
<p>Once authenticated and permissions are validated, an GitHub issue is logged through the <strong>GitHubClient</strong> object of <strong>Octokit</strong>.</p>
<blockquote>
<p>Ideally the issue title and body should also come through the user input but for brevity I have mentioned it in the MCP function.</p>
</blockquote>
<p><strong>MCP Client</strong></p>
<p>As mentioned earlier, the client will be a Console Application.</p>
<p>Add the following references</p>
<pre><code class="language-csharp">dotnet add package Azure;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Agents.AI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package ModelContextProtocol.Server;
dotnet add package System.Text.Json.Serialization;
dotnet add package Microsoft.Extensions.Configuration;
dotnet add package Microsoft.Extensions.DependencyInjection;
dotnet add package Microsoft.Extensions.Logging;
dotnet add package ModelContextProtocol.Client;
</code></pre>
<p>Add <strong>appsetting.json</strong> to the project</p>
<pre><code class="language-csharp">"AppSettings": { 
    "Chat_DeploymentName": "Deployment Name",
    "EndPoint": "Azure OpenAI endpoint",
    "ApiKey": "Azure OpenAI API key"
}
</code></pre>
<h3><strong>Code</strong></h3>
<p>Once all the artifacts in place, add the following code to <strong>Program.cs</strong></p>
<p><strong>Program.cs &gt;&gt;</strong></p>
<p><strong>Read from the Config file</strong></p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p><strong>Declare HttpClient</strong></p>
<pre><code class="language-csharp">private static readonly HttpClient client = new HttpClient();
public static HttpClient httpClient =&gt; client;
</code></pre>
<p><strong>Read credentials and register</strong> <code>Chatclient</code></p>
<pre><code class="language-csharp">var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

servicecollection.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
   .AsIChatClient()
    )
);
</code></pre>
<p><strong>Create a DI container</strong></p>
<pre><code class="language-csharp"> ServiceCollection servicecollection = new ServiceCollection();
</code></pre>
<p><strong>Register ChatClientAgent in the DI container and build the Service Provider</strong></p>
<pre><code class="language-csharp">servicecollection.AddSingleton&lt;ChatClientAgent&gt;();
ServiceProvider serviceprovider = servicecollection.BuildServiceProvider();
</code></pre>
<p><strong>Fetch keyed IChatclient from DI container</strong></p>
<pre><code class="language-csharp">var chatclient = new ChatClientAgent(serviceprovider.GetKeyedService&lt;IChatClient&gt;("ChatClient")).ChatClient;
</code></pre>
<p><strong>Create ChatClientAgentOptions</strong></p>
<pre><code class="language-csharp">  var options = new ChatClientAgentOptions()
  {
      ChatOptions = new ChatOptions()
      {
          Instructions = "You perform github operations",
          ToolMode = AutoChatToolMode.Auto,
          Tools = [.. await ConnectGitHubMCP()],
      },
      Name = "GitHub Agent"
  };
</code></pre>
<p><strong>ConnectGitHubMCP</strong></p>
<pre><code class="language-csharp"> public static async Task&lt;List&lt;McpClientTool&gt;&gt; ConnectGitHubMCP()
 {
     string clientId = "XXXXXXXXXXXXXX";
     string redirectUri = "http://127.0.0.1:50505/callback";
     using var listener = new HttpListener();
     listener.Prefixes.Add("http://127.0.0.1:50505/");
     listener.Start();

     var authUrl =
         "https://github.com/login/oauth/authorize" +
         $"?client_id={Uri.EscapeDataString(clientId)}" +
         $"&amp;redirect_uri={Uri.EscapeDataString(redirectUri)}";

     Process.Start(
     new ProcessStartInfo
     {
         FileName = authUrl,
         UseShellExecute = true
     });

     var context = await listener.GetContextAsync();

     var tokenRequest = new Dictionary&lt;string, string&gt;
     {
         ["client_id"] = clientId,
         ["client_secret"] = "XXXXXXXXXXXXXX",
         ["code"] = context.Request.QueryString["code"],
         ["redirect_uri"] = redirectUri
     };
    
     var content = new FormUrlEncodedContent(tokenRequest);
     var response = await httpClient.PostAsync(
         "https://github.com/login/oauth/access_token",
         content);
     var accesstoken = response.Content.ReadAsStringAsync().Result.Split("=")[1].Replace("&amp;expires_in", "");

     const string html = """
         &lt;!DOCTYPE html&gt;
         &lt;html&gt;
         &lt;head&gt;
             &lt;title&gt;Authentication Complete&lt;/title&gt;
         &lt;/head&gt;
         &lt;body&gt;
             &lt;p&gt;Authentication successful. You can close this window.&lt;/p&gt;
             &lt;script&gt;
                 window.close();
             &lt;/script&gt;
         &lt;/body&gt;
         &lt;/html&gt;
         """;

     var buffer = Encoding.UTF8.GetBytes(html);
     context.Response.ContentType = "text/html";
     context.Response.ContentLength64 = buffer.Length;
     await context.Response.OutputStream.WriteAsync(buffer);
     context.Response.OutputStream.Close();

     listener.Stop();
     httpClient.Timeout = Timeout.InfiniteTimeSpan;

     var transport = new ModelContextProtocol.Client.HttpClientTransport(
         new HttpClientTransportOptions
         {
             Endpoint = new Uri("http://localhost:7287/runtime/webhooks/mcp"),
             Name = "Custom GitHub MCP",
             TransportMode = HttpTransportMode.AutoDetect,
             EnableStandaloneGetStream = false,
             AdditionalHeaders = new Dictionary&lt;string, string&gt;
             {
                 { "Authorization" , accesstoken }
             }
         },
             httpClient
        );

     var mcpClient = await McpClient.CreateAsync(transport);
     var tools = await mcpClient.ListToolsAsync();
     return tools.ToList();
 }
</code></pre>
<p>Lets look at the important aspects of the above code.</p>
<p>We first create and start a <strong>HttpListener</strong> with the <strong>redirectUri</strong> pointed to the GitHub callback URI that we set on the GitHub settings page of the account.</p>
<pre><code class="language-csharp"> string clientId = "XXXXXXXXXXXXXX";
 string redirectUri = "http://127.0.0.1:50505/callback";
 using var listener = new HttpListener();
 listener.Prefixes.Add("http://127.0.0.1:50505/");
 listener.Start();
</code></pre>
<p>We then build a GitHub OAuth authorization URL with two parameters ,<strong>client_id</strong> and <strong>redirect_uri.</strong></p>
<p>Client id is the GitHub Client Id of the OAuth App called <strong>GHub API</strong> that we had set earlier and the redirect uri is the callback url.</p>
<pre><code class="language-csharp">var authUrl =
    "https://github.com/login/oauth/authorize" +
    $"?client_id={Uri.EscapeDataString(clientId)}" +
    $"&amp;redirect_uri={Uri.EscapeDataString(redirectUri)}";
</code></pre>
<p>Start a process to trigger the opening of the GitHub authorization URL in the browser.</p>
<pre><code class="language-csharp">Process.Start(
new ProcessStartInfo
{
    FileName = authUrl,
    UseShellExecute = true
});
</code></pre>
<p>Create a token request with the following default parameters, the most important one being <strong>context.Request.QueryString["code"]</strong> that extracts the authorization code that GitHub sends back in the callback URL. Its a temporary authorization code required only for token generation.</p>
<pre><code class="language-csharp">var context = await listener.GetContextAsync();

var tokenRequest = new Dictionary&lt;string, string&gt;
{
    ["client_id"] = clientId,
    ["client_secret"] = clientSecret,
    ["code"] = context.Request.QueryString["code"],
    ["redirect_uri"] = redirectUri
};
</code></pre>
<p>Exchanging the code for our access token and then extract the <strong>accesstoken</strong> from the response.</p>
<pre><code class="language-csharp">var content = new FormUrlEncodedContent(tokenRequest);

var response = await httpClient.PostAsync(
            "https://github.com/login/oauth/access_token",
            content);

var accesstoken = response.Content.ReadAsStringAsync().Result.Split("=")[1].Replace("&amp;expires_in", "");
</code></pre>
<p>The part below is optional. The reason for this code is to notify the user that the authentication is successful and the user can then close the browser window.</p>
<p>Since GitHub does not support a native browser client redirects, I have injected my own custom HTML to the client</p>
<pre><code class="language-csharp"> const string html = """
     &lt;!DOCTYPE html&gt;
     &lt;html&gt;
     &lt;head&gt;
         &lt;title&gt;Authentication Complete&lt;/title&gt;
     &lt;/head&gt;
     &lt;body&gt;
         &lt;p&gt;Authentication successful. You can close this window.&lt;/p&gt;
         &lt;script&gt;
             window.close();
         &lt;/script&gt;
     &lt;/body&gt;
     &lt;/html&gt;
     """;

 var buffer = Encoding.UTF8.GetBytes(html);
 context.Response.ContentType = "text/html";
 context.Response.ContentLength64 = buffer.Length;
 await context.Response.OutputStream.WriteAsync(buffer);
 context.Response.OutputStream.Close();
 listener.Stop();
</code></pre>
<p>Create a client function that connects to the MCP endpoints and returns the MCP Server tool list.</p>
<p>MCP endpoint used here is : <a href="http://localhost:7287/runtime/webhooks/mcp"><strong>http://localhost:7287/runtime/webhooks/mcp</strong></a></p>
<pre><code class="language-csharp">var transport = new ModelContextProtocol.Client.HttpClientTransport(
    new HttpClientTransportOptions
    {
        Endpoint = new Uri("http://localhost:7287/runtime/webhooks/mcp"),
        Name = "Custom GitHub MCP",
        TransportMode = HttpTransportMode.AutoDetect,
        EnableStandaloneGetStream = false,
        AdditionalHeaders = new Dictionary&lt;string, string&gt;
        {
            { "Authorization" , accesstoken }
        }
    }
  );

var mcpClient = await McpClient.CreateAsync(transport);
var tools = await mcpClient.ListToolsAsync();
return tools.ToList();
</code></pre>
<p>Send the following prompt to the agent</p>
<pre><code class="language-yaml">Create an issue on the repo GitHubActions owned by Sachin-Nand
</code></pre>
<p>as user <strong>Sachin-Nand</strong> and the GitHub issue is created.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/5cb93826-c04d-4ec2-bab9-1983349d2a12.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/56fa54e1-7299-4bf8-978d-f3c19392e43b.png" alt="" style="display:block;margin:0 auto" />

<p>while doing so as user <strong>SachinNandanwar</strong> the creation fails.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/33f3c374-792a-4142-ba9c-a2cc99d23f3a.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Execution</strong></h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d2849dd0-28b4-4133-ad82-361c13f0ed61.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>With this approach the possibilities of leveraging MAF agents to external services that exist out of Microsoft ecosystem becomes significantly broader with out the need for any third party OAuth providers.</p>
<p>More importantly, this approach allows MAF agents to combine capabilities across multiple ecosystems without being restricted to Microsoft native services.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[RAG for Microsoft AI Agent through TextSearchProvider and Redis]]></title><description><![CDATA[Earlier this year I had published an article on implemention of InMemory Vector Embeddings in Semantic kernel.
At that time my focus was to understand how vector embeddings can be used in Semantic Ker]]></description><link>https://www.azureguru.net/rag-for-microsoft-ai-agent-through-textsearchprovider-and-redis</link><guid isPermaLink="true">https://www.azureguru.net/rag-for-microsoft-ai-agent-through-textsearchprovider-and-redis</guid><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[llm]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[aiagent]]></category><category><![CDATA[vector embeddings]]></category><category><![CDATA[VectorSearch]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Wed, 26 Aug 2026 04:21:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d46496ef-dd01-45f8-a412-32c2021bfb12.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Earlier this year I had published an <a href="https://www.azureguru.net/inmemory-vector-embeddings-in-semantic-kernel">article</a> on implemention of <strong>InMemory Vector Embeddings</strong> in <strong>Semantic kerne</strong>l.</p>
<p>At that time my focus was to understand how vector embeddings can be used in Semantic Kernel (SK) along with search embeddings without an external vector database.</p>
<p>But there are some inherent drawbacks with that approach, the major one is that all the embeddings are kept in memory. This works well when dealing with small amount of data but as the data increases coupled with user concurrency, your application will hit a major bottleneck. Hence it becomes prudent to store the embeddings and the underlying data in a persistent storage and query only the necessary data based on the user prompt.</p>
<p>In this article, I will reference the InMemory Vector Embeddings approach and explore how it can be integrated with Microsoft Agent Framework and Redis for persistent storage. The idea is not only to store embeddings in Redis but also to make them useful for an agent so that it can retrieve the relevant information based on the user prompt.</p>
<h3>Custom Embedding vs Azure AI Search</h3>
<p>Custom Embedding approach is more viable if you require complete control over storage, embedding generation, retrieval process and more importantly if most of the underlying data is centralized. But for a more enterprise applications, Azure AI Search is more relevant when the data is spread across different data sources for instance if your data is spread across say Fabric and Cosmos DB.</p>
<p>A custom embedding approach is preferred when the need is for a simple retrieval requirement. Azure AI Search is preferable when you need a no code/low code enterprise-grade search solution with advanced retrieval capabilities.</p>
<p>This article will look into the custom embedding process. For Azure AI search approach please refer to my article on the topic <a href="https://www.azureguru.net/azure-ai-search-for-fabric-one-lake-unstructured-data">here</a> and <a href="https://www.azureguru.net/azure-ai-search-rest-api-s-for-fabric-one-lake-unstructured-data">here</a>.</p>
<h3>Redis</h3>
<p>To get started we will spin up an Redis image on Docker.</p>
<pre><code class="language-yaml">docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
</code></pre>
<blockquote>
<p>You will have to spin up a <strong>redis-stack</strong> and <strong>not local-redis</strong> else you will get the following error : <strong>RedisVectorStore Fails to Create Index and Upsert Records with "no such index"</strong></p>
</blockquote>
<p>More details : <a href="https://github.com/microsoft/semantic-kernel/issues/12734">https://github.com/microsoft/semantic-kernel/issues/12734</a> and the solution is in the same thread <a href="https://github.com/microsoft/semantic-kernel/issues/12734#issuecomment-3099281983">here</a> .</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e999b27e-0df4-4262-add2-a4fa22236aeb.png" alt="" style="display:block;margin:0 auto" />

<p>Though the bug report says that its fixed for SK(Semantic Kernel) this issue crops up with MAF(Microsoft Agent Framework) but honestly I don't believe that issue is related to specific agent framework (SK or MAF) but more with SK embedding library.</p>
<p>The bug report highlighted that Redis integration does work with the <strong>Microsoft.SemanticKernel.Connectors.AzureAISearch</strong> but this library is deprecated so I couldn't use it.</p>
<p>So I used the <strong>Microsoft.SemanticKernel.Connectors.AzureOpenAI</strong> library but still ended up with the same error.</p>
<p>Thankfully the <a href="https://github.com/microsoft/semantic-kernel/issues/12734#issuecomment-3099281983">solution</a> of using <strong>redis-stack</strong> worked well and the AzureOpenAI library also provides the necessary DI extension method for registering the embedding services.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a42a68b6-f58d-45c7-aaee-34f4cba6c053.png" alt="" style="display:block;margin:0 auto" />

<p>Redis on docker is running on port number : <strong>6379</strong></p>
<h3>TextSearchProvider</h3>
<p>This class was introduced in the <strong>Microsoft.Agents.AI</strong> namespace. It was not available with Semantic Kernel. With <strong>TextSearchProvider</strong> we can directly inject the results to the AI agents AI invocation context to enable RAG in the agent.</p>
<p>Now you might ask what advantages does it bring ?</p>
<p>With TextSearchProvider, you don't have to manually implement RAG for the agent.</p>
<p>It also maintains the conversation context through <strong>TextSearchProviderState</strong> class which exposes a property called <strong>RecentMessagesText</strong> which is handy if you wish to store the conversational messages to an external storage skipping the complexity of implementing your own custom <strong>ChatChistoryProvider</strong>.</p>
<p>I have an article on ChatChistoryProvider for MAF that you can refer <a href="https://www.azureguru.net/summarizingchatreducer-in-microsoft-agent-framework">here</a>.</p>
<p>TextSearchProvider also exposes another class called <strong>TextSearchResult</strong> that can be used to retrieve the raw representation of the search result through the <strong>RawRepresentation</strong> property along with the <strong>Text</strong> property which returns the textual context.</p>
<h3><strong>Project SetUp</strong></h3>
<p>Create a new console application and add the following packages</p>
<pre><code class="language-csharp">dotnet add package Azure;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Agents.AI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package StackExchange.Redis;
dotnet add package System.Text.Json.Serialization;
dotnet add package Microsoft.Extensions.Configuration;
dotnet add package Microsoft.Extensions.DependencyInjection;
dotnet add package Microsoft.Extensions.Logging;
dotnet add package CommunityToolkit.VectorData.Redis;
</code></pre>
<p>Of the above <strong>CommunityToolkit.VectorData.Redis</strong> is a .NET package that uses Redis as a vector store for semantic search and <strong>StackExchange.Redis</strong> is the Redis client that provides API for Redis commands.</p>
<p><code>Microsoft.Extensions.AI</code> exposes an interface <strong>IEmbeddingGenerator</strong> to create the necessary embeddings .</p>
<p>Add <strong>appsetting.json</strong> to the project</p>
<pre><code class="language-csharp">"AppSettings": { 
    "Chat_DeploymentName": "Deployment Name",,
    "Embed_DeploymentName": "Embedding model",
    "EndPoint": "Azure OpenAI endpoint",
    "ApiKey": "Azure OpenAI API key"
}
</code></pre>
<p><strong>Note :</strong> Apart from the chat model, reference to the embedding model is required.</p>
<h3>Code</h3>
<p><strong>Create a DI container</strong></p>
<pre><code class="language-csharp"> ServiceCollection servicecollection = new ServiceCollection();
</code></pre>
<p><strong>Read credentials and register</strong> <code>Chatclient</code></p>
<pre><code class="language-csharp">var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

servicecollection.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
   .AsIChatClient()
    )
);
</code></pre>
<p><strong>Read the embedding generator and add it to the DI container</strong></p>
<pre><code class="language-csharp">servicecollection.AddAzureOpenAIEmbeddingGenerator(
deploymentName: configuration["AppSettings:Embed_DeploymentName"],
endpoint: configuration["AppSettings:EndPoint"],
apiKey: configuration["AppSettings:ApiKey"]);
</code></pre>
<p><strong>Register ChatClientAgent in the DI container and build the Service Provider</strong></p>
<pre><code class="language-csharp">servicecollection.AddSingleton&lt;ChatClientAgent&gt;();
ServiceProvider serviceprovider = servicecollection.BuildServiceProvider();
</code></pre>
<p>Let's create some sample data. But before that, we have to define the object structure.</p>
<pre><code class="language-csharp">using Microsoft.Extensions.VectorData;

public class Hotel
{
      
[VectorStoreKey]
public string HotelId { get; set; }

[VectorStoreData(IsIndexed = true)]
public string HotelName { get; set; }

[VectorStoreData]
public string Description { get; set; }

[VectorStoreData]
public string Source { get; set; }

[VectorStoreVector(1536)]
public ReadOnlyMemory&lt;float&gt;? DescriptionEmbedding { get; set; }  

[VectorStoreData]
public string[] Tags { get; set; }

[VectorStoreVector(1536)]
public ReadOnlyMemory&lt;float&gt;? TagListEmbedding { get; set; }

}
</code></pre>
<p>What we have above is</p>
<ul>
<li><p><strong>VectorStoreKey</strong> → This acts as a unique record identifier (primary key)</p>
</li>
<li><p><strong>VectorStoreData</strong> → This is used to store metadata field and can be optionally indexed</p>
</li>
<li><p><strong>VectorStoreVector</strong>(1536) → This is an embedding vector used for similarity search with specified dimension size. In our case we have the dimension size of 1536 for <strong>TagListEmbedding</strong> and <strong>DescriptionEmbedding</strong></p>
</li>
</ul>
<h3><strong>Data</strong></h3>
<p>Create some sample data that matches the object structure defined above.</p>
<pre><code class="language-csharp">  private static List&lt;Hotel&gt; CreateHotelRecords()
  {
      var hotel = new List&lt;Hotel&gt;
  {
      new Hotel {
          HotelId = "1",
          HotelName = "Sea Breeze Resort",
          Description = "Beachfront resort with stunning ocean-view rooms and a seafood restaurant serving fresh, locally sourced cuisine. Guests can enjoy direct access to the beach, relaxing sunsets, spacious accommodations, and a range of recreational activities. The resort also features a swimming pool, spa services, water sports, and comfortable lounge areas, making it an ideal destination for a relaxing seaside vacation.",
          Source = "www.beachfronthotel.com",
          Tags = new[] { "beach", "resort", "seafood", "luxury" }
      },

      new Hotel {
          HotelId = "2",
          HotelName = "City Central Hotel",
          Description = "Modern hotel located in the heart of the downtown area, offering stylish rooms, contemporary amenities, and convenient access to shopping malls, restaurants, cafes, and vibrant nightlife. Guests can enjoy comfortable accommodations, high-speed Wi-Fi, a fitness center, an on-site restaurant, and easy access to public transportation. The hotel is ideal for both business and leisure travelers looking to explore the city's attractions, entertainment venues, and cultural landmarks.",
            Source = "www.partytimehotel.com",
          Tags = new[] { "city", "business", "shopping", "nightlife" }
      },

      new Hotel {
          HotelId = "3",
          HotelName = "Lakeview Retreat",
          Description = "Peaceful retreat near the lake with spa and yoga facilities. Enjoy breathtaking lake views, serene surroundings, and comfortable accommodations designed for relaxation. The property offers rejuvenating spa treatments, daily yoga sessions, wellness activities, and tranquil spaces to unwind. Guests can also explore scenic walking trails, enjoy healthy meals, and experience a peaceful escape from the hustle and bustle of everyday life.",
          Source = "www.lakeviewhotel.com",
          Tags = new[] { "lake", "spa", "relaxation", "yoga" }
      },

      new Hotel {
          HotelId = "4",
          HotelName = "Desert Mirage Inn",
          Description = "Boutique desert hotel offering a unique blend of traditional charm and modern comfort in a peaceful desert setting. Guests can enjoy guided camel tours across the dunes, breathtaking desert sunsets, and an unforgettable outdoor dining experience featuring local cuisine under the open sky. The property also offers comfortable rooms, relaxing lounge areas, cultural activities, and opportunities to experience the natural beauty and tranquility of the desert.",
           Source = "www.desertadventurehotek.com",
          Tags = new[] { "desert", "boutique", "sunset", "adventure" }
      }
  };
      return hotel;
  }
</code></pre>
<p>There are two distinct pipeline processes involved: <strong>Insertion</strong> and <strong>Retrieval</strong>.</p>
<h3><strong>Insertion</strong></h3>
<p><strong>Define an Embedding generator</strong></p>
<pre><code class="language-csharp"> var embeddingGenerator = serviceprovider.GetRequiredService&lt;IEmbeddingGenerator&lt;string, Embedding&lt;float&gt;&gt;&gt;();
</code></pre>
<p>Earlier, we registered an embedding model inside the Dependency Injection (DI) container. In the above code, we are requesting the same service to be returned back through the <strong>IEmbeddingGenerator</strong> interface so that the data embeddings could be generated.</p>
<p><strong>JSON Collection</strong></p>
<pre><code class="language-csharp">RedisJsonCollection&lt;string, Hotel&gt; redisCollection = new RedisJsonCollection&lt;string, Hotel&gt;(
ConnectionMultiplexer.Connect("localhost:6379").GetDatabase(), "hotel_index", new() { EmbeddingGenerator = embeddingGenerator });
</code></pre>
<p>Embeddings will be stored as type <strong>RedisJsonCollection</strong>.There is a second option called <strong>RedisHashSetCollection.</strong></p>
<blockquote>
<p>The vabriable redisCollection declared above is essentially a client-side object used to interact with Redis. The actual records and their embeddings are stored in Redis, not inside the redisCollection variable.</p>
</blockquote>
<p>With RedisJsonCollection type, the objects are stored in the JSON format while fore RedisHashSetCollection, they are stored in the hash key format.</p>
<p>Our collection is named as <strong>hotel_index</strong> .The value of the EmbeddingGenerator property is assigned through the variable <strong>embeddingGenerator</strong> that was declared earlier.</p>
<blockquote>
<p><strong>Note :</strong> With RedisHashSetCollection, datatype string[] is not supported. Our structure has string[] data for Tags so we cant use the RedisHashSetCollection type.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/b32d5d56-a40e-41d5-8bfc-67787aada075.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Data Creation</strong></p>
<pre><code class="language-csharp"> await redisCollection.EnsureCollectionExistsAsync();
 var hotelRecords = CreateHotelRecords().ToList();
</code></pre>
<p>We first ensure that the Redis collection exists if not then create it and then store the hotel data to the variable <strong>hotelRecords.</strong></p>
<p><strong>Note</strong> : At this stage we haven't updated the collection with the vector embedding values.</p>
<h3><strong>Create Embeddings</strong></h3>
<p>To create the embeddings we will have to traverse the collection and create the embeddings for each record in the collection.</p>
<pre><code class="language-csharp">void InsertCollection(RedisJsonCollection &lt;string, Hotel&gt; redisCollection) 
{
  redisCollection.EnsureCollectionExistsAsync();

  var hotelRecords = CreateHotelRecords().ToList();

  foreach(var hotel in hotelRecords) {
    var descriptionEmbeddingTask = embeddingGenerator.GenerateAsync(hotel.Description);
    var featureListEmbeddingTask = embeddingGenerator.GenerateAsync(string.Join("\n", hotel.Tags));
    hotel.DescriptionEmbedding =(descriptionEmbeddingTask).Result.Vector;
    hotel.TagListEmbedding = (featureListEmbeddingTask).Result.Vector;
  }
  redisCollection.UpsertAsync(hotelRecords);
}
</code></pre>
<p>and finally update the <strong>redisCollection</strong>(declared earlier)with the <strong>UpsertAsync</strong> method. This concludes our ingestion pipeline.</p>
<h3>Verify Redis Collection</h3>
<p>Post execution of the ingestion pipeline, we can verify the collection in Redis. For that we will use have to enable <strong>redis-cli .</strong></p>
<p>Execute the following command in the Docker terminal to enable it.</p>
<pre><code class="language-yaml">docker exec -it redis-stack redis-cli
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7edf4aec-3dca-450e-8bb5-4f48212cff3f.png" alt="" style="display:block;margin:0 auto" />

<p>Get a list of collection</p>
<pre><code class="language-yaml">FT._LIST
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/963b7175-1c9a-4039-a9a8-5e7772f0a995.png" alt="" style="display:block;margin:0 auto" />

<p>Return the details for the collection</p>
<pre><code class="language-yaml">FT.INFO hotel_index
</code></pre>
<p>It returns a list of different attributes of the collection that we just created.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/b3606f6d-f780-455e-8442-682c1dcb7eac.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Retrieval</strong></h3>
<p>Now that we have verified the collection in Redis, the next step is the retrieval process.</p>
<p>Retrieval will be through the AI Agent <strong>AIContextProvider</strong> property.</p>
<p>Before that, we have to set the SearchAdapater which acts as an input to AIContextProvider.</p>
<pre><code class="language-csharp">
async Task &lt;IEnumerable&lt;TextSearchProvider.TextSearchResult&gt;&gt; SearchAdapter(string query, CancellationToken cancellationToken) {

  List &lt;TextSearchProvider.TextSearchResult&gt; results = new();

  var searchVector = await embeddingGenerator.GenerateAsync(query);

  var resultRecords = await redisCollection.SearchAsync(
    searchVector, top: 1, new() {
      VectorProperty = r =&gt; r.DescriptionEmbedding
    }).ToListAsync();

  var finalresults = resultRecords.Select(result =&gt;
    new TextSearchProvider.TextSearchResult {

      Text = result.Record.Description,
        SourceName = result.Record.Source,
        RawRepresentation = result.Record.HotelName,

    });

  return await Task.FromResult &lt;IEnumerable&lt; TextSearchProvider.TextSearchResult&gt;&gt; (finalresults);
}
</code></pre>
<p>What we are doing above is that, we are embedding the user input with the embeddinggenerator which acts as an input to the redisCollection that was declared earlier and through <strong>SearchAsync</strong> we the return the TOP 1 matching rows by comparing the <strong>VectorProperty</strong> of the user input with the <strong>DescriptionEmbedding</strong> property of the Hotel object.</p>
<pre><code class="language-csharp">var searchVector = await embeddingGenerator.GenerateAsync(query);

var resultRecords = await redisCollection.SearchAsync(
  searchVector, top: 1, new() {
    VectorProperty = r =&gt; r.DescriptionEmbedding
  }).ToListAsync();
</code></pre>
<p>And finally we return the results in form of <strong>TextSearchResult</strong> property of the <strong>TextSearchProvider</strong> object.</p>
<pre><code class="language-csharp"> var finalresults = resultRecords.Select(result =&gt;
   new TextSearchProvider.TextSearchResult {
     Text = result.Record.Description,
       SourceName = result.Record.Source,
       RawRepresentation = result.Record.HotelName
   });

return await Task.FromResult &lt;IEnumerable&lt; TextSearchProvider.TextSearchResult&gt;&gt; (finalresults);
</code></pre>
<p>This adapter now acts as an input of the AIContextProvider of the agent.</p>
<p>We will set it in the <strong>ChatClientAgentOptions</strong> of the Agent through <strong>TextSearchProvider</strong> class.</p>
<pre><code class="language-csharp">var options = new ChatClientAgentOptions
        {
            ChatOptions = new ChatOptions
            {
                Instructions = "You are a helpful chat assistant .Answer briefly and succinctly. Be personable.Recommend the best matching hotel.Explain briefly why it matches the request.Use only provided hotels.If the user query matches any of the hotel's tags, recommend it.If no hotel context is provided, say\"I'm sorry, but there is no matching hotel available for your request. If you have any specific preferences or criteria, please let me know, and I'll do my best to assist you!\".Otherwise assume the provided hotel is the correct match.Use the provided hotel data to answer\r\n- Answer based ONLY on given hotel.Use hotels only from the list provided to you.Add more details about the location and activities that the user can enjoy as bulleted point."
            },           
            AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)]

        };
</code></pre>
<blockquote>
<p>Note the Instructions set for the agent. The instructions are very extensive that provides the agent information on how to handle the user request and how to interpret prompts and what do do incase the underlying data does not have the necessary information for user requests.</p>
</blockquote>
<p><strong>Fetch keyed IChatclient from the DI container</strong></p>
<pre><code class="language-csharp">var _chatclient = new ChatClientAgent(serviceprovider.GetKeyedService&lt;IChatClient&gt;("ChatClient")).ChatClient;
</code></pre>
<p><strong>Set the ChatClientAgentOptions to the above ChatClientAgent</strong></p>
<pre><code class="language-csharp">var agent = _chatclient.AsAIAgent(options);
</code></pre>
<p><strong>Pass the prompt to the Agent</strong></p>
<pre><code class="language-csharp">Console.WriteLine(await agent.RunAsync(new ChatMessage(ChatRole.User, "I am looking for a hotel close to the beach.") 
{
   CreatedAt = DateTimeOffset.UtcNow, AuthorName = "Sachin"
 }, session: agentsession, new AgentRunOptions 
{
   AllowBackgroundResponses = true
 }) + "\n");
</code></pre>
<p><strong>Output</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f198a894-4d7f-40ec-84bb-2da22d6fc10f.png" alt="" style="display:block;margin:0 auto" />

<h3>Execution</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/cf6278be-5b43-475c-ab96-efab3ac382ab.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>This was an introductory article on how vector embeddings can be created and stored in an external storage that overcomes the shortcomings of InMemoryVector embedding . This approach is more prudent when you need more control over storage ,embeddings and the retrieval process.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[I created a Fabric MCP endpoint with Fabric REST API, Graph API, Azure Functions and Microsoft Agent Framework]]></title><description><![CDATA[Disclaimer : If you think that I used a coding assistant to develop this MCP endpoint, then sorry I have to disappoint you :) because the core of this approach is ingrained into the application logic ]]></description><link>https://www.azureguru.net/i-created-a-fabric-mcp-endpoint-with-fabric-rest-api-graph-api-azure-functions-and-microsoft-agent-framework</link><guid isPermaLink="true">https://www.azureguru.net/i-created-a-fabric-mcp-endpoint-with-fabric-rest-api-graph-api-azure-functions-and-microsoft-agent-framework</guid><category><![CDATA[Microsoft.Extensions.AI]]></category><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[#AzureFunctions ]]></category><category><![CDATA[GraphAPI]]></category><category><![CDATA[Microsoft Graph API]]></category><category><![CDATA[llm]]></category><category><![CDATA[aiagents]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Thu, 20 Aug 2026 20:50:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c6f34900-c282-4b29-bfe9-44020f4e913d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Disclaimer</strong> : If you think that I used a coding assistant to develop this MCP endpoint, then sorry I have to disappoint you :) because the core of this approach is ingrained into the application logic that I developed in Jan-2025. Check out the date of that <a href="https://www.azureguru.net/retrieve-and-export-user-access-details-for-various-objects-in-microsoft-fabric">article</a> .</p>
<p>That implementation was limited in a sense that the entire operation was done through a console application. In this article I tried to take that logic a step ahead by integrating it with MAF and Azure Functions and expose it through MCP.</p>
<h3>Introduction</h3>
<p>Recently, <a href="https://learn.microsoft.com/en-us/rest/api/fabric/articles/mcp-servers/core-remote/overview-core-mcp-server"><strong>Fabric Core MCP Server</strong></a> was released and is currently under preview. Its integration with other applications is quite seamless and it exposes a lot of useful tools that can significantly simplify and enhance external tool interactions with Microsoft Fabric.</p>
<p>The screenshot below lists the complete set of tools available through the Fabric MCP server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/307cb3b9-932c-45c8-87e4-2aa026d6180b.png" alt="" style="display:block;margin:0 auto" />

<p>I published a detailed article a couple of days ago explaining on how to integrate your external client application with Fabric Core MCP Server. You can go through that article <a href="https://www.azureguru.net/integrate-microsoft-agent-framework-with-microsoft-fabric-core-mcp-server">here</a> .</p>
<p>Before we get started, following is a list of key tools and technologies used in the implementation of this MCP tool.</p>
<ul>
<li><p><a href="https://learn.microsoft.com/en-us/rest/api/fabric/articles/"><strong>Fabric REST API's</strong></a></p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/graph/use-the-api"><strong>Microsoft Graph API</strong></a></p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/entra/msidweb/call-downstream-apis/graph-service-client"><strong>Microsoft Graph Service Client</strong></a></p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-mcp?pivots=programming-language-csharp"><strong>MCP for Azure Functions</strong></a></p>
</li>
<li><p><a href="https://csharp.sdk.modelcontextprotocol.io/v2/"><strong>MCP C# SDK</strong></a></p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/agent-framework/overview/"><strong>Microsoft Agent Framework</strong></a></p>
</li>
</ul>
<p>It would be helpful if you have some understanding of the above tools and technologies. Of the above list, Microsoft Agent Framework (MAF) is the least used as its implementation is limited for setting up the instructions, tool bindings and passing the user prompts to the MCP Server (Azure Function).</p>
<h3>What's the need for this MCP tool ?</h3>
<p>Imagine an employee leaving an organization and you wanna know which Fabric items does the user has access to. Wouldn't it be great if you could just send a prompt like</p>
<pre><code class="language-csharp">Give me list of items user ABC has access to.
Or
Give me list of items user ABC has access to, which are of type Reports
</code></pre>
<p>I couldn't find a tool function in the <a href="https://learn.microsoft.com/en-us/rest/api/fabric/articles/mcp-servers/core-remote/overview-core-mcp-server"><strong>Fabric Core MCP Server</strong></a> that would return item access of an user.</p>
<p>This is exactly why this MCP is designed for. As mentioned at the very start, I created a similar utility about an year and half ago and I blogged it <a href="https://www.azureguru.net/retrieve-and-export-user-access-details-for-various-objects-in-microsoft-fabric">here</a>.</p>
<p>In that implementation I extensively used the <a href="https://learn.microsoft.com/en-us/rest/api/fabric/admin/users/list-access-entities?tabs=HTTP"><strong>Users - List Access Entities</strong></a> Fabric REST API's in a combination with <a href="https://learn.microsoft.com/en-us/graph/use-the-api"><strong>Microsoft Graph API's</strong></a><strong>.</strong></p>
<p>If you want to know more about Microsoft Graph API's, I have a detailed <a href="https://www.azureguru.net/microsoft-graph-api">article</a> on that topic as well.</p>
<h3>Delegated Permissions and Scopes</h3>
<p>We need to assign the following Delegated permissions to the service principal we will be using.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9c018fb3-9ba1-4237-84fc-799cd258e1f0.png" alt="" style="display:block;margin:0 auto" />

<p>We will require two scopes. One for Graph API and second for Fabric REST API's</p>
<ul>
<li><p><strong><a href="https://graph.microsoft.com/.default">https://graph.microsoft.com/.default</a></strong></p>
</li>
<li><p><strong><a href="https://api.fabric.microsoft.com/.default">https://api.fabric.microsoft.com/.default</a></strong></p>
</li>
</ul>
<p>So the authentication header will required two access tokens. One for Microsoft Graph and second for Microsoft Fabric.</p>
<p>Thank fully MCP SDK supports passing multiple tokens through a type Dictionary.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/11698060-7419-404f-8f2a-2c2593b7ca50.png" alt="" style="display:block;margin:0 auto" />

<h3>TokenCredentials</h3>
<p><strong>GraphServiceClient</strong> authentication requires <a href="https://learn.microsoft.com/en-us/dotnet/api/azure.core.tokencredential?view=azure-dotnet">TokenCredentials</a> for authentication, so just using the access token will not be useful. For that we will have to convert bearer token to TokenCredentials.</p>
<p>I have an detailed <a href="https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric">article</a> on how that can be implemented.</p>
<p>The example in the above article is specific for <strong>OneLake authentication</strong> but can be used for other implementations as well. OneLake authentication also requires the bearer access token to be converted into a TokenCredential before it can be used with the Azure SDK.</p>
<p>Along with that, we will use <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.identity.client.publicclientapplicationbuilder?view=msal-dotnet-latest">PublicApplicationBuilder</a> from the <a href="https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview">MSAL</a>(Microsoft Authentication Library) to create the bearer access token so that we don't have maintain client secret of the underlying service principal.</p>
<h3>Set Up</h3>
<p>We will create a Azure Function for MCP endpoints and a console application as the client. The client will send prompts to the MCP Azure functions through an <strong>AIAgent</strong> . The agent tools are exposed through the MCP C# SDK.</p>
<p><strong>Lets first set up the Azure Function</strong></p>
<p>The Azure function type is MCP Tool trigger as our Azure Function execution should be tool driven instead of prompt driven.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/03cc1016-ab7c-45e7-a2fb-e5cba20d893d.png" alt="" style="display:block;margin:0 auto" />

<p>Add the following references to the Azure Function project.</p>
<pre><code class="language-csharp">dotnet add package Microsoft.Azure.Functions.Worker;
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Mcp
dotnet add package Microsoft.Extensions.Logging;
dotnet add package Microsoft.Graph;
dotnet add package Newtonsoft.Json.Linq;
</code></pre>
<p>The Fabric REST API output is in JSON format and is very dynamic.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8a3eb4bd-75e9-45a8-8104-7649719b5442.png" alt="" style="display:block;margin:0 auto" />

<p>So we will use <a href="https://www.newtonsoft.com/json">Json.NET</a> to traverse the response object.</p>
<p>To get started, declare a bunch of variables and define the constructor of class in the Azure function that is assigned to the <strong>_logger</strong> variable.</p>
<p>The fabric endpoint used is <strong><a href="https://api.fabric.microsoft.com/v1">https://api.fabric.microsoft.com/v1</a></strong></p>
<pre><code class="language-csharp"> private static Dictionary&lt;string, string&gt; itemlist = new();
 private static string endpoint = "https://api.fabric.microsoft.com/v1";
 private static readonly HttpClient client = new HttpClient();
 private static GraphServiceClient graph_Service_Client;
 private ILogger&lt;Function1&gt; _logger;

 public Function1(ILogger&lt;Function1&gt; logger)
 {
     _logger = logger;
 }
</code></pre>
<p><strong>GetAsync &gt;&gt;</strong></p>
<p>This function returns HTTP response received from the endpoint using an <strong>HttpClient</strong>.</p>
<pre><code class="language-csharp">  public async static Task&lt;string&gt; GetAsync(string url, string token)
  {
      client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
      HttpResponseMessage response = await client.GetAsync(url);
      try
      {
          response.EnsureSuccessStatusCode();
          return await response.Content.ReadAsStringAsync();
      }
      catch (HttpRequestException httpRequestException)
      {
          return null;
      }

  }
</code></pre>
<p><strong>GetUserDetails &gt;&gt;</strong></p>
<p>This function is used to parse the JSON response and extract the relevant data</p>
<pre><code class="language-csharp"> [Function(nameof(GetUserDetails))]
 public async Task &lt;IDictionary&lt;string,string&gt;&gt; GetUserDetails
(
   [McpToolTrigger(nameof(GetUserDetails), "Gets the user access details")] ToolInvocationContext context,
   [McpToolProperty(nameof(username), "The name of the user for whom access details are sought")] string ? username) {
   List &lt;string&gt; tokens = new();

   if (context.TryGetHttpTransport(out var authHeaders)) 
   {     tokens.Add(authHeaders.Headers["Authorization_graph"].Replace("Bearer ", ""));
     tokens.Add(authHeaders.Headers["Authorization_fabric"].Replace("Bearer ", ""));
   }

   AccessTokenCredential tokenCredential = new AccessTokenCredential(tokens[0]);
   graph_Service_Client = new GraphServiceClient(tokenCredential);

   Microsoft.Graph.Models.UserCollectionResponse result = await graph_Service_Client.Users.GetAsync((requestConfiguration) =&gt; requestConfiguration.QueryParameters.Top = 999);

   IDictionary &lt; string, object &gt; userdetails = new Dictionary &lt; string, object &gt; ();

   int i = 0;

   if (username == "All") 
{
     foreach(var str in result.Value) 
{
       userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
       i++;
     }
   } 
else 
{
     foreach(var str in result.Value.Select(i =&gt; i.DisplayName == username)) 
{
       if (str == false) { i++; continue; }
       userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
       i++;
     }
   }

   i = 0;
   itemlist.Clear();

   foreach(var user in userdetails) 
 {
     string userId = ((Microsoft.Graph.Models.Entity) userdetails.Where(a =&gt; a.Key == user.Key).ToList()[0].Value).Id.ToString();
     string response = await GetAsync(endpoint + "/admin/Users/" + userId + "/access", tokens[1]);

     if (response == null) 
     {
       continue;
     }
     JObject j_response = JObject.Parse(response);
     JArray j_array = (JArray) j_response["accessEntities"];
     foreach(JObject path in j_array) {

       JToken itemid = path["id"];
       JToken item = path["itemAccessDetails"]["type"];
       JToken name = path["displayName"];

       JToken permissions = path["itemAccessDetails"]["permissions"];
       JArray permissions_arr = (JArray) permissions;
       string permission = "";
       foreach(JToken p in permissions_arr) 
       {
         permission = permission + ("/" + p.ToString());
         p.ToString();
       }

       JToken additionalPermissions = path["itemAccessDetails"]["additionalPermissions"];
       if (additionalPermissions != null) 
       {
         JArray permissions_ad_arr = (JArray) additionalPermissions;
         string permission_a = "";
         foreach(JToken p in permissions_ad_arr) {
           permission_a = permission_a + ("/" + p.ToString());
           p.ToString();
         }
         itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + permission_a);
       } 
     else 
       {
         itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + "N/A");

       }
       i++;
     }
     return itemlist;
   }
   return null;
 }
</code></pre>
<p>Lets look at the important aspects of the above function.</p>
<p>We have <strong>McpToolTrigger</strong> of type <strong>GetUserDetails</strong> that accepts username as <strong>McpToolProperty.</strong> It is required for MCP tool to process a prompt, something like the below</p>
<pre><code class="language-plaintext">Give me list of items user ABC has access to.
</code></pre>
<p>Then we have the <a href="https://github.com/Azure/azure-functions-mcp-extension/blob/main/src/Microsoft.Azure.Functions.Worker.Extensions.Mcp/Abstractions/ToolInvocationContext.cs">ToolInvocationContext</a> that exposes the underlying tools, HTTP headers and arguments of the tool call.</p>
<blockquote>
<p>With ToolInvocationContext, it becomes quite easy to read the HTTP headers invoked by the client. Recall that earlier I mentioned that we will require two bearer tokens, one for Fabric REST API and second for Graph API.</p>
</blockquote>
<pre><code class="language-csharp">List &lt;string&gt; tokens = new();

if (context.TryGetHttpTransport(out var authHeaders)) 
{
tokens.Add(authHeaders.Headers["Authorization_graph"].Replace("Bearer ", ""));
  tokens.Add(authHeaders.Headers["Authorization_fabric"].Replace("Bearer ", ""));
}
</code></pre>
<p>Also earlier I mentioned that Graph API requires bearer token to be converted to <strong>TokenCredentials</strong>. The following line of code does that.</p>
<pre><code class="language-csharp">AccessTokenCredential tokenCredential = new AccessTokenCredential(tokens[0]);
graph_Service_Client = new GraphServiceClient(tokenCredential);
</code></pre>
<p><strong>tokens[0]</strong> above is the token for Graph API and <strong>AccessTokenCredential</strong> is the custom class that does the conversion. I will post the code for that class later in the article.</p>
<p><strong>GraphServiceClient</strong> gets a list of users and add it to <strong>UserCollectionResponse</strong> object of the Graph Model object.</p>
<pre><code class="language-csharp">Microsoft.Graph.Models.UserCollectionResponse result = await graph_Service_Client.Users.GetAsync((requestConfiguration) =&gt; requestConfiguration.QueryParameters.Top = 999);
</code></pre>
<p>We then add the usernames to a dictionary object called <strong>userdetails</strong> <strong>.</strong></p>
<pre><code class="language-csharp">IDictionary&lt;string,object&gt; userdetails = new Dictionary&lt;string, object&gt;();
int i = 0;

if (username == "All")
{
    foreach (var str in result.Value)
    {
        userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
        i++;
    }
}
else
{
    foreach (var str in result.Value.Select(i =&gt; i.DisplayName == username))
    {
        userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
        i++;
    }
}
</code></pre>
<p>and then filter the dictionary based on filters passed through the prompt.</p>
<p>If the prompt contains the keyword <strong>"All"</strong> we save all the usernames to userdetails dictionary and if not, then just store the filtered username to the userdetails dictionary.</p>
<p>The code below calls the <a href="https://learn.microsoft.com/en-us/rest/api/fabric/admin/users/list-access-entities?tabs=HTTP"><strong>Users - List Access Entities</strong></a> API for each user object in the userdetails dictionary and the response is traversed with <a href="http://Json.Net">JSON.Net</a> library.</p>
<pre><code class="language-csharp">foreach(var user in userdetails) 
{
    string userId = ((Microsoft.Graph.Models.Entity) userdetails.Where(a =&gt; a.Key == user.Key).ToList()[0].Value).Id.ToString();
    string response = await GetAsync(endpoint + "/admin/Users/" + userId + "/access", tokens[1]);
    if (response == null) 
    {
      continue;
    }
    JObject j_response = JObject.Parse(response);
    JArray j_array = (JArray) j_response["accessEntities"];
    foreach(JObject path in j_array) 
   {

      JToken itemid = path["id"];
      JToken item = path["itemAccessDetails"]["type"];
      JToken name = path["displayName"];

      JToken permissions = path["itemAccessDetails"]["permissions"];
      JArray permissions_arr = (JArray) permissions;
      string permission = "";
      foreach(JToken p in permissions_arr) 
      {
        permission = permission + ("/" + p.ToString());
        p.ToString();
      }

      JToken additionalPermissions = path["itemAccessDetails"]["additionalPermissions"];
      if (additionalPermissions != null) {
        JArray permissions_ad_arr = (JArray) additionalPermissions;
        string permission_a = "";
        foreach(JToken p in permissions_ad_arr) 
        {
          permission_a = permission_a + ("/" + p.ToString());
          p.ToString();
        }
        itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + permission_a);
      } 
     else 
     {
        itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + "N/A");
      }
      i++;
    }
return itemlist;
</code></pre>
<p>API call is : <strong><a href="https://api.fabric.microsoft.com/v1/admin/users/%7BuserId%7D/access">https://api.fabric.microsoft.com/v1/admin/users/{userId}/access</a></strong> that fetches the userId from the Graph API.</p>
<p><strong>API Response &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/af0b64b3-e2bb-4156-bbf8-e82f4f83a745.png" alt="" style="display:block;margin:0 auto" />

<p>The access details are finally stored in a dictionary called <strong>itemlist</strong> with "~" used as the separator.</p>
<p><strong>itemlist Dictionary &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f8a18bd4-77f3-4408-8911-81f20bd4a6bd.png" alt="" style="display:block;margin:0 auto" />

<p>the user access details are stored in the dictionary object in the following format which is sent back the agent for the display.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/0b36283f-758a-49d2-a748-53a96a6a98f5.png" alt="" style="display:block;margin:0 auto" />

<p><strong>GetUserDetailsForItemType</strong> <strong>&gt;&gt;</strong></p>
<p>This function is similar to the <strong>GetUserDetails</strong> function with the only difference being that it is used to get access details for a specific item type.</p>
<pre><code class="language-csharp">[Function(nameof(GetUserDetailsForItemType))]
public async Task &lt;IDictionary &lt;string,string&gt;&gt; GetUserDetailsForItemType
(
  [McpToolTrigger(nameof(GetUserDetailsForItemType), "Gets the user access detail for a given item type")] ToolInvocationContext context,
  [McpToolProperty(nameof(username), "The name of the user for whom access details are sought")] string ? username,
  [McpToolProperty(nameof(itemtype), "The name of the item type for whom access details are sought")] string ? itemtype) {

  List &lt;string&gt; tokens = new();

  if (context.TryGetHttpTransport(out var authHeaders)) 
{
    tokens.Add(authHeaders.Headers["Authorization_graph"].Replace("Bearer ", ""));
    tokens.Add(authHeaders.Headers["Authorization_fabric"].Replace("Bearer ", ""));

  }
  AccessTokenCredential tokenCredential = new AccessTokenCredential(tokens[0]);
  graph_Service_Client = new GraphServiceClient(tokenCredential);

  Microsoft.Graph.Models.UserCollectionResponse result =
  await graph_Service_Client.Users.GetAsync((requestConfiguration) =&gt; requestConfiguration.QueryParameters.Top = 999);

  IDictionary &lt;string,object&gt; userdetails = new Dictionary&lt; string,object&gt; ();
  int i = 0;
  if (username == "All") 
   {
    foreach(var str in result.Value) 
   {
      userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
      i++;
    }
  } 
  else 
   {
    foreach(var str in result.Value.Select(i =&gt; i.DisplayName == username)) 
    {
      userdetails.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
      i++;
    }
  }
  itemlist.Clear();

  foreach(var user in userdetails) 
  {
    string userId = ((Microsoft.Graph.Models.Entity) userdetails.Where(a =&gt; a.Key == user.Key).ToList()[0].Value).Id.ToString();
    string response = await GetAsync(endpoint + "/admin/Users/" + userId + "/access?type=" + itemtype, tokens[1]);

    if (response == null) 
    {
      continue;
    }

    JObject j_response = JObject.Parse(response);

    JArray j_array = (JArray) j_response["accessEntities"];
    foreach(JObject path in j_array) {

      JToken itemid = path["id"];
      JToken item = path["itemAccessDetails"]["type"];
      JToken name = path["displayName"];

      JToken permissions = path["itemAccessDetails"]["permissions"];
      JArray permissions_arr = (JArray) permissions;
      string permission = "";
      foreach(JToken p in permissions_arr) 
      {
        permission = permission + ("/" + p.ToString());
        p.ToString();
      }

      JToken additionalPermissions = path["itemAccessDetails"]["additionalPermissions"];

      if (additionalPermissions.HasValues == true) 
      {
        JArray permissions_ad_arr = (JArray) additionalPermissions;
        string permission_a = "";
        foreach(JToken p in permissions_ad_arr) 
        {
          permission_a = permission_a + ("/" + p.ToString());
          p.ToString();
        }
        itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + permission_a);
      } 
     else 
      {
        itemlist.Add(i + "`" + item, "Item Type" + "~" + item + "~" + "Item Name" + "~" + name + "Permissions" + "~" + permissions + "~" + "Additional Permissions" + "~" + "N/A");
      }

      i++;

    }
    return itemlist;

  }
  return null;
}
</code></pre>
<p>Unlike <strong>GetUserDetails</strong> that accepts only username as <strong>McpToolProperty,</strong> the function <strong>GetUserDetailsForItemType</strong> has two <strong>McpToolProperty:</strong> username and itemtype.</p>
<p>This is required for MCP tool to process a prompt something like the below</p>
<pre><code class="language-plaintext">Get access details for user ABC for item type Notebooks
</code></pre>
<p>The API call is now made to the following endpoint :</p>
<p><strong><a href="https://api.fabric.microsoft.com/v1/admin/users/%7BuserId%7D/access/access?type=%7Bitemtype%7D">https://api.fabric.microsoft.com/v1/admin/users/{userId}/access/access?type={itemtype}</a></strong></p>
<p><strong>Client Console Application &gt;&gt;</strong></p>
<p>This console application is used to call the MCP endpoint. You can use any other client tool like VSCode Copilot or through a Minimal API incase you want to expose the output through API's</p>
<pre><code class="language-csharp">using Azure;
using Azure.AI.OpenAI;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Client;
using ModelContextProtocol.Client;

public static class Program 
{
  private static string RedirectURI = "http://localhost";
  private static string clientId = "Service Principal Client Id";
  private static string tenantId = "Tenant Id";
  private static readonly HttpClient client = new HttpClient();
  private static string[] scopes_g = new string[] {
    "https://graph.microsoft.com/.default"};
  private static string[] scopes_f = new string[] {
    "https://api.fabric.microsoft.com/.default"};
  private static string Authority = $"https://login.microsoftonline.com/{tenantId}";
  public static HttpClient Client =&gt; client;

  private static async Task Main() 
{

    ServiceCollection servicecollection = new();

    var configuration = new ConfigurationBuilder()
      .SetBasePath(Directory.GetCurrentDirectory())
      .AddJsonFile("appsettings.json", optional: false)
      .Build();

    var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

    servicecollection.AddKeyedChatClient("ChatClient", 
   (sp =&gt; new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)      .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
      .AsIChatClient()));

    servicecollection.AddSingleton &lt;ChatClientAgent&gt; (sp =&gt; 
   {
      return new ChatClientAgent(sp.GetKeyedService &lt;IChatClient &gt; ("ChatClient"));
    });

    ServiceProvider serviceprovider = servicecollection.BuildServiceProvider();

    var chatlient = serviceprovider.GetServices &lt; ChatClientAgent &gt; ();
    List &lt; ChatClientAgent &gt; lstchatclient = new(chatlient);

    var options = new ChatClientAgentOptions() {

      ChatOptions = new ChatOptions() {
          Instructions = "You are a Fabric Agent and you execute the appropriate tools. " +
            "You will display the results in table format with columns Item Type, Item Name, Access Details. Please singularize the itemtype if use prompt contains them." +
            "For example, if the prompt contains reports then singularize to report and if Notebooks then to Notebook and so and so forth",
            ToolMode = AutoChatToolMode.Auto,
            Tools = [..await ConnectMCP()]
        },
        Name = "Fabric Agent"
    };

    var agent = lstchatclient[0].ChatClient.AsAIAgent(options);

    AgentResponse agentresponse = await agent.RunAsync("Get access details for user ABC that he has access to");

    Console.Write(agentresponse.Text);

  }

  public async static Task &lt;AuthenticationResult&gt; ReturnAuthenticationResult(string[] scopes) {
    string AccessToken;
    PublicClientApplicationBuilder PublicClientAppBuilder =
      PublicClientApplicationBuilder.Create(clientId)
      .WithAuthority(Authority)
      .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
      .WithRedirectUri(RedirectURI);

    IPublicClientApplication PublicClientApplication = PublicClientAppBuilder.Build();
    var accounts = await PublicClientApplication.GetAccountsAsync();
    AuthenticationResult result;
    try 
    {
      result = await PublicClientApplication.AcquireTokenSilent(scopes, accounts.First())
        .ExecuteAsync()
        .ConfigureAwait(false);
    } 
  catch 
   {
      result = await PublicClientApplication.AcquireTokenInteractive(scopes)
        .ExecuteAsync()
        .ConfigureAwait(false);
    }
    return result;
  }

  public static async Task &lt;List&lt;McpClientTool&gt;&gt; ConnectMCP() 
{
    AuthenticationResult result_graph = await ReturnAuthenticationResult(scopes_g);
    AuthenticationResult result_fabric = await ReturnAuthenticationResult(scopes_f);

    var httpClient = new HttpClient 
    {
      Timeout = Timeout.InfiniteTimeSpan
    };
    var transport = new ModelContextProtocol.Client.HttpClientTransport(
      new HttpClientTransportOptions {
        Endpoint = new Uri("http://{Azure Function URL }/runtime/webhooks/mcp"),
          Name = "Custom Fabric MCP",
          TransportMode = HttpTransportMode.AutoDetect,
          EnableStandaloneGetStream = false,
          AdditionalHeaders = new Dictionary &lt; string, string &gt; {
            {
              "Authorization_graph",
              $ "Bearer {result_graph.AccessToken}"
            },
            {
              "Authorization_fabric",
              $ "Bearer {result_fabric.AccessToken}"
            }
          }
      },
      httpClient
    );

    var mcpClient = await McpClient.CreateAsync(transport);
    var tools = await mcpClient.ListToolsAsync();
    return tools.ToList();
  }
}
</code></pre>
<p>The above code is similar to the one used in my earlier <a href="https://www.azureguru.net/integrate-microsoft-agent-framework-with-microsoft-fabric-core-mcp-server#code">article</a> on Fabric MCP. Only difference being that I am passing two tokens through the header</p>
<pre><code class="language-csharp">AdditionalHeaders = new Dictionary &lt;string,string&gt; 
         {
            {
              "Authorization_graph",
              $ "Bearer {result_graph.AccessToken}"
            },
            {
              "Authorization_fabric",
              $ "Bearer {result_fabric.AccessToken}"
            }
          }
</code></pre>
<p>which is used to call <a href="https://learn.microsoft.com/en-us/entra/msidweb/call-downstream-apis/graph-service-client"><strong>GraphServiceClient</strong></a> &amp; <a href="https://learn.microsoft.com/en-us/rest/api/fabric/admin/users/list-access-entities?tabs=HTTP"><strong>Users - List Access Entities</strong></a> API .</p>
<p>Also notice the agent instructions</p>
<pre><code class="language-csharp">You are a Fabric Agent and you execute the appropriate tools.
You will display the results in table format with columns Item Type, Item Name, Access Details. Please singularize the itemtype if use prompt contains them. For example, if the prompt contains reports then singularize to report and if Notebooks then to Notebook and so and so forth.
</code></pre>
<p><strong>AccessTokenCredential.cs &gt;&gt;</strong></p>
<p>This class converts bearer token generated by <strong>ReturnAuthenticationResult</strong> in the client code above to TokenCredentials for reasons explained earlier</p>
<pre><code class="language-csharp">using Azure.Core;
using System.IdentityModel.Tokens.Jwt;

namespace AccesTokenCredentials
{
    public class AccessTokenCredential : Azure.Identity.ClientSecretCredential
    {
        public AccessTokenCredential(string accessToken)
        {
            AccessToken = accessToken;
        }

        private string AccessToken;

        public AccessToken FetchAccessToken()
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

        public override ValueTask&lt;AccessToken&gt; GetTokenAsync(
            TokenRequestContext requestContext,
            CancellationToken cancellationToken
        )
        {
            return new ValueTask&lt;AccessToken&gt;(FetchAccessToken());
        }

        public override AccessToken GetToken(
            TokenRequestContext requestContext,
            CancellationToken cancellationToken
        )
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }
    }
}
</code></pre>
<p><strong>Limitations &gt;&gt;</strong></p>
<p>The output does not lists the workspace under which a particular item exists and also it does not lists access details of a service principal. This can be a major shortcoming but I have a solution for that as well :) which I will post in my upcoming blog.</p>
<h3>Execution</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/46a15883-2758-4689-b851-8345ee229238.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>The intention of this article is not to make a production level agent but to provide a starting point incase you do decide to create a custom agent that interacts with the Fabric services.</p>
<p>The possibilities and quite endless and when coupled with Microsoft Agent Framework(MAF) you can leverage the fantastic features and properties of MAF that could interact with Fabric services and ecosystem.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Integrate Microsoft Agent Framework with Microsoft Fabric Core MCP Server]]></title><description><![CDATA[Microsoft Fabric is revolutionizing the data analytics space and is bringing together data integration, data engineering and data analytics under a unified platform. This reduces overall complexities ]]></description><link>https://www.azureguru.net/integrate-microsoft-agent-framework-with-microsoft-fabric-core-mcp-server</link><guid isPermaLink="true">https://www.azureguru.net/integrate-microsoft-agent-framework-with-microsoft-fabric-core-mcp-server</guid><category><![CDATA[microsoftfabric]]></category><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[llm]]></category><category><![CDATA[mcp]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Tue, 18 Aug 2026 01:13:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/fb974ef1-783b-43bc-8017-005b40a4659f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Microsoft Fabric is revolutionizing the data analytics space and is bringing together data integration, data engineering and data analytics under a unified platform. This reduces overall complexities involved maintaining multiple, disconnected tools and services.</p>
<p>Recently, the <a href="https://learn.microsoft.com/en-us/rest/api/fabric/articles/mcp-servers/core-remote/overview-core-mcp-server">Fabric Core MCP Server</a> was released. At the time of this writing this feature is in preview. So please test it thoroughly before you use it in your production environment.</p>
<p>Fabric Core MCP Server is a remote endpoint that enables AI agents to interact with the Microsoft Fabric ecosystem. This opens a window of opportunity where MAF Agents can seamlessly communicate with artifacts within the Microsoft Fabric Ecosystem which eventually can reduce the complexities involved in overall code and structural customization.</p>
<p>Couple of weeks ago I had published an <a href="https://www.azureguru.net/build-ai-agents-with-microsoft-agent-framework-to-access-azure-services-using-entra-oauth">article</a> that demonstrated the integration MAF AIAgent with Azure Services. To be honest, there were a lot of nuts and bolts involved in getting this set up running. But leveraging Fabric Core MCP Server you skip all such complexities when it comes to integrating it with Microsoft Fabric.</p>
<p>In this article we will deep dive in how to integrate MAF <strong>AIAgent</strong> with <strong>Fabric MCP Server</strong>.</p>
<p>To get started, we have to setup a few delegated permissions for the service principal that we will use.</p>
<p>The service principal will impersonate the logged in user and based on the user permissions assigned to the user for a specific Fabric item, the AIAgent will interact with the underlying Fabric services.</p>
<p>I granted the following delegated permissions to the service principal that I will use.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/b4b317b4-f392-485d-a004-d7d0ac065aa6.png" alt="" style="display:block;margin:0 auto" />

<p>We will use the usual Fabric scope : <strong><a href="https://api.fabric.microsoft.com/.default">https://api.fabric.microsoft.com/.default</a></strong> and <a href="https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview"><strong>MSAL</strong></a> (Microsoft Authentication Library) for token generation and the Fabric MCP Server endpoint <strong><a href="https://api.fabric.microsoft.com/v1/mcp/core">https://api.fabric.microsoft.com/v1/mcp/core</a></strong></p>
<h3><strong>SetUp</strong></h3>
<p>There are two Entra users</p>
<p><strong>sachin.nandanwar @</strong> <a href="http://azureguru.net"><strong>azureguru.net</strong></a> and <strong>sachin_nandanwar @</strong> <a href="http://azureguru.net"><strong>azureguru.net</strong></a></p>
<p>User <strong>sachin.nandanwar @</strong> <a href="http://azureguru.net"><strong>azureguru.net</strong></a> is an admin on the Fabric tenant while the user <strong>sachin_nandanwar @</strong> <a href="http://azureguru.net"><strong>azureguru.net</strong></a> only has Viewer access on one of the Fabric workspaces called "My Test workspace"</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8f380e0d-593c-4add-b82d-f70a3b80c4eb.png" alt="" style="display:block;margin:0 auto" />

<p>We will test a few prompts that would allow us to interact with the workspace "My Test workspace" on the Fabric tenant.</p>
<p>Following is the list of all the tools available through the Fabric MCP server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/febe522e-ad73-4903-b21b-33a18926af59.png" alt="" style="display:block;margin:0 auto" />

<p>By a simple prompt, you could list workspaces, create workspaces, create/update/delete items and so and so forth.</p>
<p>Of course the outcome of these actions will be dependent on the level of access the logged in user has for the underlying objects.</p>
<p>As mentioned earlier, there are Entra two users</p>
<ul>
<li><p><strong>(Sachin.Nand)</strong> <strong>sachin.nandanwar @</strong> <a href="http://azureguru.net"><strong>azureguru.net</strong></a></p>
</li>
<li><p><strong>(Sachin Nandanwar)</strong> <strong>sachin_nandanwar @</strong> <a href="http://azureguru.net"><strong>azureguru.net</strong></a></p>
</li>
</ul>
<p>I will test the prompts by logging in with both users.</p>
<h3><strong>Code</strong></h3>
<p>Add the following references to a new C# console project</p>
<pre><code class="language-csharp">dotnet add package Azure;
dotnet add package Azure.Core;
dotnet add package Microsoft.Agents.AI;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package ModelContextProtocol.Client;
dotnet add package Microsoft.Identity.Client;
dotnet add package Microsoft.Extensions.DependencyInjection;
</code></pre>
<p>Declare a bunch of variables in <strong>Program.cs</strong></p>
<pre><code class="language-csharp">private static string RedirectURI = "http://localhost";
private static string clientId = "Service Principal Client Id";
private static string tenantId = "Fabric tenant Id";
private static readonly HttpClient client = new HttpClient();
private static string[] scopes = new string[] { "https://api.fabric.microsoft.com/.default" };
private static string Authority = $"https://login.microsoftonline.com/{tenantId}";
public static HttpClient Client =&gt; client;
</code></pre>
<p>Next , define an <strong>Authentication</strong> method that validates the user sign-in and delegated scopes using Microsoft Entra ID and returns <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.identity.client.authenticationresult?view=msal-dotnet-latest">AuthenticationResult</a>.</p>
<p><strong>ReturnAuthenticationResult &gt;&gt;</strong></p>
<pre><code class="language-csharp">public async static Task &lt;AuthenticationResult&gt; ReturnAuthenticationResult() {
  string AccessToken;
  PublicClientApplicationBuilder PublicClientAppBuilder =
    PublicClientApplicationBuilder.Create(clientId)
    .WithAuthority(Authority)
    .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
    .WithRedirectUri(RedirectURI);

  IPublicClientApplication PublicClientApplication = PublicClientAppBuilder.Build();
  var accounts = await PublicClientApplication.GetAccountsAsync();
  AuthenticationResult result;
  try {

    result = await PublicClientApplication.AcquireTokenSilent(scopes, accounts.First())
      .ExecuteAsync()
      .ConfigureAwait(false);
  } catch {
    result = await PublicClientApplication.AcquireTokenInteractive(scopes)
      .ExecuteAsync()
      .ConfigureAwait(false);

  }
  return result;
}
</code></pre>
<p>Add the following code in <strong>Program.cs</strong> to read settings from the config file <strong>appsettings.json</strong></p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p><strong>Read credentials and register</strong> <strong>Chatclient and return a ChatClientAgent</strong></p>
<pre><code class="language-csharp">WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

servicecollection.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
   .AsIChatClient()
    )
);

builder.Services.AddSingleton&lt;ChatClientAgent&gt;(sp =&gt;
{
    return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"));
});
</code></pre>
<p>Now create a DI container to the <strong>ChatClientAgent</strong></p>
<pre><code class="language-csharp">ServiceProvider serviceprovider = servicecollection.BuildServiceProvider();

var chatlient = serviceprovider.GetServices&lt;ChatClientAgent&gt;();

List&lt;ChatClientAgent&gt; lstchatclient = new(chatlient);
</code></pre>
<p>Create a client function that connects to the MCP endpoints and returns the MCP Server tool list.</p>
<pre><code class="language-csharp">public static async Task &lt;List&lt;McpClientTool&gt;&gt; ConnectMCP() 
{
 
AuthenticationResult result = await ReturnAuthenticationResult();

      var httpClient = new HttpClient {
        Timeout = Timeout.InfiniteTimeSpan
      };
      var transport = new ModelContextProtocol.Client.HttpClientTransport(
        new HttpClientTransportOptions {
          Endpoint = new Uri("https://api.fabric.microsoft.com/v1/mcp/core"),
            Name = "MCP Client",
            TransportMode = HttpTransportMode.AutoDetect,
            EnableStandaloneGetStream = false,
            AdditionalHeaders = new Dictionary &lt;string,string&gt; {
              {
                "Authorization",$ "Bearer {result.AccessToken}"
              }
            }
        },
        httpClient
      );

      var mcpClient = await McpClient.CreateAsync(transport);
      var tools = await mcpClient.ListToolsAsync();
      return tools.ToList();
    }
</code></pre>
<p>In the above code, we connect to MCP server endpoint through the MCP <strong>HttpClientTransport</strong> class and pass the bearer token through the method <strong>ReturnAuthenticationResult</strong> which returns an <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.identity.client.authenticationresult?view=msal-dotnet-latest"><strong>AuthenticationResult</strong></a> object.</p>
<p>Now, configure the <strong>ChatClientAgentOptions</strong> and set its <strong>Tools</strong> property to the method <strong>ConnectMCP()</strong> that was defined above.</p>
<pre><code class="language-csharp">  var options = new ChatClientAgentOptions()
  {
      ChatOptions = new ChatOptions()
      {
          Instructions = "You are a Fabric Agent and you execute the appropriate tools",
          ToolMode = AutoChatToolMode.Auto,
          Tools = [.. await ConnectMCP()]
      },
      Name = "Fabric Agent"
  };
</code></pre>
<p>Next, assign the options configured above to the <strong>ChatClient</strong>, create an <strong>AIAgent</strong> from it and then send a prompt to the agent to create a lakehouse.</p>
<pre><code class="language-csharp">var agent = lstchatclient [0].ChatClient.AsAIAgent(options);

AgentResponse  agentresponse = await agent.RunAsync("Create a lakehouse with name Fabric_MCP_LakeHouse in workspace My Test Workspace.");

Console.Write(agentresponse.Text);
</code></pre>
<p>Logging in as <strong>sachin.nandanwar @</strong> <a href="http://azureguru.net"><strong>azureguru.net</strong></a> and the lakehouse creation succeeds</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8c6c5a92-c97e-4506-abd7-17564d478c2e.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f0eb3835-1203-42cc-8554-49451c3ceead.png" alt="" />

<p>Trying to do the same logging in as <strong>sachin_nandanwar @</strong> <a href="http://azureguru.net"><strong>azureguru.net</strong></a></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/694a0f5a-4050-464d-81a1-5d7143ced7d1.png" alt="" style="display:block;margin:0 auto" />

<p>This is because <strong>sachin_nandanwar @</strong> <a href="http://azureguru.net"><strong>azureguru.net</strong></a> only has Viewer access for the workspace "My Test Workspace".</p>
<p>Now let me try to list items(semantic models) in the same workspace. The prompt should return list of the semantic models as the user (<strong>sachin_nandanwar @</strong> <a href="http://azureguru.net"><strong>azureguru.net</strong></a> ) has viewer access for the workspace.</p>
<p>The updated prompt is</p>
<pre><code class="language-csharp">AgentResponse  agentresponse = await agent.RunAsync("Provide a list of semantic models in My Test Workspace.");

Console.Write(agentresponse.Text);
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/88a424f5-dc6e-4672-b3a9-841d532dbdf9.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3af5993f-919b-4de0-9dd9-5b1fbd8d674a.png" alt="" />

<p>There are many amazing things that you could do using the Fabric MCP Server tools, of course being limited to the underlying permissions the logged in user has.</p>
<p>But you might argue that all these features are already available through <strong>VS Code GitHub Copilot</strong>.</p>
<p>Yes it is, but what if you want to extend this functionality to other external applications or AI agents ?</p>
<p>This is where exposing your agent through <strong>MCP</strong> becomes particularly useful. Instead of limiting the functionality to a specific environment you can now make your agent available as an MCP tool for Fabric ecosystem that can be consumed by any MCP-compatible client.</p>
<h3>Execution</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7740ce2d-a78e-48b5-8478-584d86f2d1c9.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>Integrating the <strong>Microsoft Agent Framework (MAF)</strong> with the <strong>Fabric Core MCP Server</strong> provides a powerful way to extend AI agents with Fabric capabilities.</p>
<p>Through this article I tried to demonstrates how <strong>MAF and MCP</strong> can work together to connect AI agents with external data and services while maintaining authentication and integrating the Fabric security model.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[MCP in Microsoft Agent Framework: Exposing AI Agent as an MCP Tool-with a twist]]></title><description><![CDATA[The title of the article is not clickbait. It is intentional. There is a very specific reason behind it.
I will explain the reason, the problems and why I think that the title reflects what I am about]]></description><link>https://www.azureguru.net/mcp-in-microsoft-agent-framework-exposing-ai-agent-as-an-mcp-tool</link><guid isPermaLink="true">https://www.azureguru.net/mcp-in-microsoft-agent-framework-exposing-ai-agent-as-an-mcp-tool</guid><category><![CDATA[mcp]]></category><category><![CDATA[mcp server]]></category><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[aiagents]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Fri, 14 Aug 2026 01:34:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/83da0f50-0381-44b8-b9be-bf4feab81c82.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The title of the article is not clickbait. It is intentional. There is a very specific reason behind it.</p>
<p>I will explain the reason, the problems and why I think that the title reflects what I am about to discuss in this article.</p>
<h3>Model Context Protocol (MCP)</h3>
<p>To get started with MCP, there are many online resources available that cover everything from MCP fundamental concepts and architecture to tools, resources, prompts and so on and so forth.</p>
<p>You can refer to the following links to understand what MCP is all about</p>
<ul>
<li><p><strong>Introduction to MCP</strong> &gt;&gt; <a href="https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro">https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro</a></p>
</li>
<li><p><strong>C# MCP SDK &gt;&gt;</strong> <a href="https://csharp.sdk.modelcontextprotocol.io/v1/api/ModelContextProtocol.Server.McpServerTool.html">https://csharp.sdk.modelcontextprotocol.io/v1/api/ModelContextProtocol.Server.McpServerTool.html</a></p>
</li>
<li><p><strong>Build MCP Server in C# &gt;&gt;</strong> <a href="https://devblogs.microsoft.com/dotnet/build-a-model-context-protocol-mcp-server-in-csharp/">https://devblogs.microsoft.com/dotnet/build-a-model-context-protocol-mcp-server-in-csharp/</a></p>
</li>
</ul>
<p>I don't want to repeat the same MCP concepts in this article that are already covered in abundance in the above resources and are more technically superior and in-depth compared to my abilities to explain them :)</p>
<p>The intention of this article is to focus on a specific issue that I faced.</p>
<h3>Problem Statement</h3>
<p>So, I had a scenario where I wanted to expose an <strong>AIAgent</strong> as an MCP tool . The conventional approach is to expose it as the <strong>.AsAIFunction()</strong> method to the <a href="https://csharp.sdk.modelcontextprotocol.io/v1/api/ModelContextProtocol.Server.McpServerTool.html"><strong>McpServerTool</strong></a> class.</p>
<pre><code class="language-csharp">McpServerTool tool = McpServerTool.Create(agent.AsAIFunction());
</code></pre>
<p>and then add McpServerTool <code>(tool)</code> to the builder services collection</p>
<pre><code class="language-csharp">builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithTools([tool]);
</code></pre>
<p>This registers the McpServerTool with the MCP server, adding it to the server's tool collection so that the tool can be exposed to MCP clients.</p>
<p>Assume that there is an agent called <strong>SomeAIAgent</strong> with <strong>SomeAIFunction</strong> registered as one of its tools.</p>
<pre><code class="language-csharp">AIAgent SomeAIAgent = new OpenAIClient(apiKey)
   .......,
   .......,
   .AsAIAgent(.....),
   tools: [AIFunctionFactory.Create(SomeAIFunction)]
   );
</code></pre>
<p>But here comes the twist.</p>
<p>What if your AIFunctions are registered with DI (Dependency Injection) and you have to resolve and invoke these AIFunctions as MCP endpoint ?</p>
<p>Look at the following example where an AIFunction is registered with the DI container.</p>
<pre><code class="language-csharp">builder.Services.AddSingleton&lt;AIFunction&gt;(sp =&gt;
    {
 return AIFunctionFactory.Create(SomeAIFunction, new AIFunctionFactoryOptions { Name = "SomeAIFunctionName", Description = "Some Description", SerializerOptions = SomeeSerializerContext.Default.Options });
    });
</code></pre>
<p>This is where things starts to get interesting.</p>
<p>Normally, to use the above function I will have to rewrite them in a class marked <strong>[McpServerToolType]</strong> and expose methods with the [<strong>McpServerTool]</strong> annotation.</p>
<p>For example the following screenshot taken from the example <a href="https://devblogs.microsoft.com/dotnet/build-a-model-context-protocol-mcp-server-in-csharp/#defining-our-first-tool">here</a> shows how MCP Server tool is defined so that they can be exposed through a MCP endpoint.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/076e11ad-c4bd-4e83-9ee5-a6517b74dce9.png" alt="MCP and Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Of course I didn't wanted to redesign all my AIFunctions to be inline and compatible with the above design. This would had required a lot of code refactoring.</p>
<p>So I had to somehow figure out a way where in I can use the DI registered AIFunctions without having to rewrite them all over again.</p>
<h3>MCP Inspector</h3>
<p>But before we get into more details of the issue , lets first walkthrough the basic steps of setting up and installing MCP inspector to test our MCP endpoints.</p>
<p>To run MCP inspector you need <strong>Node.js</strong> in your system. To verify if Node.js is installed , run the following command in PowerShell</p>
<pre><code class="language-csharp">node -v
</code></pre>
<p>If it does not return a version number then it indicates that Node.js is not installed.</p>
<p>Execute the following command in <strong>PowerShell</strong> to install Node.js</p>
<pre><code class="language-csharp">winget install OpenJS.NodeJS.LTS
</code></pre>
<p>Accept the license agreement and verify the installation by re executing the following command.</p>
<pre><code class="language-csharp">node -v
</code></pre>
<p>It should now return the node version number.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3c06aa96-300b-4fe2-b13e-ef0b86d9b9cc.png" alt="MCP , Install MCP inspector" />

<p>Once its verified that node.js is installed , execute the following command to install MCP inspector</p>
<pre><code class="language-csharp">npm install -g @modelcontextprotocol/inspector
</code></pre>
<p>Now execute the following command in PowerShell to run MCP inspector.</p>
<pre><code class="language-csharp"> npx -y @modelcontextprotocol/inspector
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/96755ade-247f-4f48-b262-0d9cc4845077.png" alt="MCP , Install MCP inspector" style="display:block;margin:0 auto" />

<p>This will open the MCP inspector UI in a new browser page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/b3e9c330-630e-4e22-8e0c-1adc8c51e171.png" alt="MCP , Install MCP inspector" style="display:block;margin:0 auto" />

<p>We will have to add MCP server details which basically is going to be the AIAgent that we are going to expose as a MCP tool. We will come back to this later in the article.</p>
<h3><strong>Project Setup</strong></h3>
<p>Create a new <strong>ASP.Net</strong> core project and add the following packages</p>
<pre><code class="language-csharp">dotnet add package Azure;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Agents.AI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package ModelContextProtocol.Server;
dotnet add package System.Text.Json.Serialization;
dotnet add package Microsoft.Extensions.Configuration;
dotnet add package Microsoft.Extensions.DependencyInjection;
dotnet add package Microsoft.Extensions.Logging;
</code></pre>
<p>Add <strong>appsetting.json</strong> to the project</p>
<pre><code class="language-csharp">"AppSettings": { 
    "Chat_DeploymentName": "Deployment Name",
    "EndPoint": "Azure OpenAI endpoint",
    "ApiKey": "Azure OpenAI API key"
}
</code></pre>
<h3><strong>Code</strong></h3>
<p>Once all the artifacts in place, add the following code to <strong>Program.cs</strong></p>
<p><strong>Program.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p><strong>Create a web application instance</strong></p>
<pre><code class="language-csharp">WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
</code></pre>
<p><strong>Read credentials and register</strong> <code>Chatclient</code></p>
<pre><code class="language-csharp">var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

servicecollection.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
   .AsIChatClient()
    )
);
</code></pre>
<p><strong>Inject and register a</strong> <code>ChatClientAgent</code></p>
<pre><code class="language-csharp"> builder.Services.AddSingleton&lt;ChatClientAgent&gt;(sp =&gt;
 {
     return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"));
 });
</code></pre>
<p>Lets have a function called <strong>ReturnCityTemperature</strong> that returns the temperature of a given city. This function is to be registered with the DI container as an AIFunction.</p>
<p>We define the required request and response objects for <strong>CityTemperature</strong></p>
<p><strong>CityTemperatureSearchRequest &amp; CityTemperatureSearchResponse</strong> <strong>&gt;&gt;</strong></p>
<pre><code class="language-csharp"> public class CityTemperatureSearchRequest
 {
     public string City { get; set; }
 }

 public class CityTemperatureSearchResponse
 {
     public string City { get; set; }
     public string Temperature { get; set; }
 }
</code></pre>
<p>Next, create serialization metadata for both request and response types.</p>
<pre><code class="language-csharp">[JsonSerializable(typeof(CityTemperatureSearchRequest))]
[JsonSerializable(typeof(CityTemperatureSearchResponse))]
internal sealed partial class CityTemperatureSerializerContext : JsonSerializerContext;
</code></pre>
<p>The following delegate returns the underlying data</p>
<p><strong>ReturnCityTemperature &gt;&gt;</strong></p>
<pre><code class="language-csharp">public static Func&lt;CityTemperatureSearchRequest, CityTemperatureSearchResponse&gt; ReturnCityTemperature = (CityTemperatureSearchRequest) =&gt;

{
    switch (CityTemperatureSearchRequest.City)
    {

        case "Mumbai":

            return new CityTemperatureSearchResponse
            {

                City = "Mumbai",
                Temperature = "40"
            };

            break;

        case "Pune":

            return new CityTemperatureSearchResponse
            {

                City = "Pune",
                Temperature = "41"
            };

            break;

        case "Delhi":

            return new CityTemperatureSearchResponse
            {

                City = "Delhi",
                Temperature = "42"
            };

            break;

        case "Chennai":

            return new CityTemperatureSearchResponse
            {

                City = "Chennai",
                Temperature = "43"
            };
            break;
    }

    return new CityTemperatureSearchResponse
    {

        City = CityTemperatureSearchRequest.City,
        Temperature = "Unknown"
    };

};
</code></pre>
<p>Register the above delegate function with the DI container as an <strong>AIFunction</strong> along with the corresponding <strong>SerializerOptions</strong> that were defined earlier.</p>
<pre><code class="language-csharp">  builder.Services.AddSingleton &lt;AIFunction&gt; (sp =&gt; {
   return AIFunctionFactory.Create(ReturnCityTemperature, new AIFunctionFactoryOptions {
     Name = "ReturnCityTemperature", Description = "Gets the temperature of a city", SerializerOptions = CityTemperatureSerializerContext.Default.Options
   });
 });
</code></pre>
<p><strong>Register the MCP Server &gt;&gt;</strong></p>
<p>MCP server is added to the service collection and with streamable HTTP configured through <strong>WithHttpTransport</strong> extension and mapped to <strong>"api/mcp".</strong></p>
<pre><code class="language-csharp">builder.Services.AddMcpServer()
.WithHttpTransport(options =&gt;
{      
    options.Stateless = true;
}
);
    WebApplication app = builder.Build();        
    app.MapMcp(pattern: "api/mcp");
    app.Run();
</code></pre>
<p>There is a very important extension called <strong>.WithToolsFromAssembly()</strong> that auto discovers all classes and tools annotated with <strong>[McpServerToolType]</strong> and [<strong>McpServerTool]</strong> respectively.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/40524c41-905e-46cf-a2b6-6f55e35f38f8.png" alt="MCP and Microsoft Agent Framework" />

<p>which unfortunately will be of no use for me, as my tools are DI registered.</p>
<p>Now, here comes the interesting and the important aspect.</p>
<p>How to expose all the DI registered tools through the MCP Server ?</p>
<p>This is how to do it</p>
<pre><code class="language-csharp">builder.Services.AddSingleton &lt;McpServerTool&gt; (sp =&gt; 
{
  var chatClientList = sp.GetRequiredKeyedService &lt;IChatClient&gt; ("ChatClient");
  var aifunctions = sp.GetServices &lt;AIFunction&gt; ();
  List &lt;AITool&gt; functions = new(aifunctions);

  var options = new ChatClientAgentOptions 
{
    ChatOptions = new ChatOptions 
    {
      Tools = functions
    }
};
  var agent = chatClientList.AsAIAgent(options);
  return McpServerTool.Create(agent.AsAIFunction
(
 new AIFunctionFactoryOptions 
{    Name = "WeatherAgent"
  }));
});

builder.Services.AddMcpServer()
  .WithHttpTransport(options =&gt; 
  {
    options.Stateless = true;
  });

WebApplication app = builder.Build();
app.MapMcp(pattern: "api/mcp");
app.Run();
</code></pre>
<p><strong>Lets dig into the above code step by step &gt;&gt;</strong></p>
<p>Register <code>McpServerTool</code> as a singleton in the DI container.</p>
<pre><code class="language-csharp">builder.Services.AddSingleton&lt;McpServerTool&gt;
</code></pre>
<p>Fetch keyed <strong>IChatClient</strong> and <strong>AIFunction</strong> from the DI container.</p>
<p>Add them to the collection variable <strong>functions</strong> of type <strong>List</strong></p>
<pre><code class="language-csharp">var chatClientList = sp.GetRequiredKeyedService&lt;IChatClient&gt;("ChatClient");

  var aifunctions = sp.GetServices&lt;AIFunction&gt;();

  List&lt;AITool&gt; functions = new(aifunctions);

  var options = new ChatClientAgentOptions
  {
      ChatOptions = new ChatOptions
      {
          Tools = functions
      }
  };

  var agent = chatClientList.AsAIAgent(options);
</code></pre>
<p>Set <strong>ChatClientAgentOptions</strong> and assign functions variable to <strong>Tools</strong> property in <strong>ChatOptions</strong> and then assign it to chatClientList with an <strong>.AsAIAgent()</strong> extension method chain to create an AIAgent.</p>
<pre><code class="language-csharp">var agent = chatClientList.AsAIAgent(options);
</code></pre>
<p>Now return the agent as an AIFunction with the required options</p>
<pre><code class="language-csharp">return McpServerTool.Create(agent.AsAIFunction(new AIFunctionFactoryOptions
   {
       Name = "WeatherAgent"
   }));
</code></pre>
<p>Now things even get more interesting.</p>
<p>Let me send a prompt that queries the underlying data.</p>
<pre><code class="language-csharp">What is the temperature in Pune?
</code></pre>
<p>The result is just the response text with no additional details that you would normally expect from an agent response.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/737eb586-ddc3-410e-9505-fd80c1ce3f22.png" alt="MCP and Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Now, let me make a few changes in how I expose the agent to the MCP Server.</p>
<pre><code class="language-csharp">    builder.Services.AddSingleton&lt;McpServerTool&gt;(sp =&gt;
   {
       var chatClientList = sp.GetRequiredKeyedService&lt;IChatClient&gt;("ChatClient");

       var aifunctions = sp.GetServices&lt;AIFunction&gt;();

       List&lt;AITool&gt; functions = new(aifunctions);

       var options = new ChatClientAgentOptions
       {
           ChatOptions = new ChatOptions
           {

               Tools = functions
           }
       };
       var agent = chatClientList.AsAIAgent(options);

      var agentFunction = AIFunctionFactory.Create(

     async (string message) =&gt;
     {
         var response = await agent.RunAsync(message);

         return response;
     },
     new AIFunctionFactoryOptions
     {
         Name = "WeatherAgent"
     });

       return McpServerTool.Create(agentFunction);

   });

    builder.Services.AddMcpServer()
   .WithHttpTransport(options =&gt;
   {
       options.Stateless = true;

   }
   );

    WebApplication app = builder.Build();
    app.MapMcp(pattern: "api/mcp");
    app.Run();
}
</code></pre>
<p>So the changes that I have done is that I am executing the agent by capturing the user i/p and agent response in the AIFunction itself and then exposing the AIFunction(agentFunction) to the McpServerTool.</p>
<p>I created an on the fly AIFunction that takes user input (message argument) and executed the agent within the AIFunction and it then returns the agent response.</p>
<p>Now instead of passing the agent as AIFunction (agent.AsAIFunction) to the MCPServerTool, I am passing an agentFunction to it</p>
<pre><code class="language-csharp"> var agentFunction = AIFunctionFactory.Create(

  async (string message) =&gt;
  {
      var response = await agent.RunAsync(message);

      return response;
  },
  new AIFunctionFactoryOptions
  {
      Name = "WeatherAgent"
  });

    return McpServerTool.Create(agentFunction);

});
</code></pre>
<p>and now try the execution with the same prompt that I tried earlier</p>
<pre><code class="language-csharp">What is the temperature in Pune?
</code></pre>
<p>and this time the agent output format changes, which is lot more detailed</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f2424a70-db9b-4c01-ad70-7e2baf5f47c8.png" alt="MCP and Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>The reason for this behavior ? Honestly I don't know :).</p>
<p>I only found out this behavior by accident. I will have to put in some time to understand this behavior.</p>
<p>Lets now look into how to configure MCP to test the agent.</p>
<p>Once MCP inspector is installed and running, click the <strong>Add Server</strong> option on top right and select <strong>Add manually</strong> option</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/652cfb16-d530-452c-a024-3072b49cba3d.png" alt="MCP and Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>In the pop up that follows, select <strong>streamable-http</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9a377730-0d0f-449a-aa56-e13270088113.png" alt="MCP and Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Select <strong>WithHttpTransport</strong> because we have used this option to set our MCP Server</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e9421cc7-69ad-4a24-9b0a-6d9fa749b287.png" alt="" />

<p>In the next pop up, add the project endpoint</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/cce4e003-57c0-423c-ac24-1a2d149207cf.png" alt="MCP and Microsoft Agent Framework" />

<p>In this example its running on : <strong><a href="http://localhost:5176">http://localhost:5176</a></strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/ba1f2644-cc4b-4e74-a534-d4219a1807ae.png" alt="MCP and Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Under Servers option the registered MCP server should be visible.</p>
<p>If everything has been configured properly the server should display the Connected status which the indicates the client has successfully connected to the server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/b6ab2640-240d-4d36-ab89-b04b2a1c56a8.png" alt="MCP and Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Select the Tools option and it will list the Name of the AIFunction that was used to set the MCPServerTool.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/355c5e91-4031-4a11-a99f-1ef799b69731.png" alt="MCP and Microsoft Agent Framework" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9bf4016b-3aaf-480d-a665-d35157f17b4a.png" alt="" style="display:block;margin:0 auto" />

<p>Run the prompt and now you will have a more detailed output.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/0ffa491f-dcc6-447c-8127-26de416e9fea.png" alt="MCP and Microsoft Agent Framework" style="display:block;margin:0 auto" />

<h3>Execution</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9cafc526-642f-4cd7-a1d8-8d76cd8e2e7d.gif" alt="" style="display:block;margin:0 auto" />

<h3>Final Take</h3>
<p>Though this is an edge case there might be possibility that you might face such a situation where your agentic architecture revolves more around DI pattern.</p>
<p>Rather than introducing a completely separate approach for exposing the agent through MCP, you can leverage the existing DI-based approach that i have suggested in this article to integrate MCP with it. This allows the agent to remain consistent with the application's DI pattern while making it compatible with the MCP requirements.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Chat History with Redis, ChatReducer and SummarizingChatReducer in Microsoft Agent Framework]]></title><description><![CDATA[My previous article on Chat History in Microsoft Agent Framework focused on storing and retrieving chat history using a file-based approach.The intention of that article was to provide a simple introd]]></description><link>https://www.azureguru.net/summarizingchatreducer-in-microsoft-agent-framework</link><guid isPermaLink="true">https://www.azureguru.net/summarizingchatreducer-in-microsoft-agent-framework</guid><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[Redis]]></category><category><![CDATA[llm]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Tue, 11 Aug 2026 03:21:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/fda1a998-b1a7-4f21-9586-454c1854e374.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My previous article on <a href="https://www.azureguru.net/chat-history-in-microsoft-agent-framework">Chat History in Microsoft Agent Framework</a> focused on storing and retrieving chat history using a file-based approach.The intention of that article was to provide a simple introduction about implementing a basic chat history mechanism in Microsoft Agent Framework (MAF).</p>
<p>While a file-based approach is useful for understanding the underlying concepts it is definitely not an ideal choice for most real-world production set ups. As the number of users and conversations grows, storing chat history in file systems can be very challenging with added complexities.</p>
<p>In a production environment, chat history needs to be stored on a persistent data store so that it can be accessed across multiple instances of an application. This is where a distributed data stores such as <strong>Redis</strong> becomes useful. It provides a fast and scalable mechanism for storing messages and the maintain chat history.</p>
<p>But just simply storing the entire conversation indefinitely introduces another important challenge: <strong>the size of the conversation context</strong>. As a conversation becomes longer, sending the complete chat history to the underlying agents on every request increases token consumption, latency and cost. It can also eventually exceed the model's context window.</p>
<p>Though MAF provides <strong>ChatReducer</strong> to address this problem, one particularly useful implementation is <strong>SummarizingChatReducer</strong> that reduces growing conversation by summarizing older messages unlike ChatReducer that simply discards them. This helps to preserve important conversation context in place.</p>
<p>This article will demonstrate storing conversation to Redis and how to leverage SummarizingChatReducer to summarize and reduce long conversations while at the same time maintain the conversation context.</p>
<h3><strong>Custom Chat History Provider</strong></h3>
<p>In MAF, the most crucial component is the <strong>ProviderSessionState</strong> object wrt custom chat history implementation. It acts as the abstraction layer through which MAF manages and retrieves chat history through session <strong>StateBag.</strong></p>
<p>In my previous <a href="https://www.azureguru.net/chat-history-in-microsoft-agent-framework">article</a> , I used <strong>IChatReducer</strong> to reduce chat size stored in <strong>ProviderSessionState .</strong> But drawback with this approach is that you lose the conversation context as the reduction is based on a fixed number (<code>N</code>) of messages. Because of this, important aspects of the conversation is discarded. The agent therefore has access only to the most recent <code>N</code> messages.</p>
<p>Given the drawback of the above approach a more feasible and practical approach is to leverage <strong>SummarizingChatReducer</strong> where values for <strong>targetCount</strong> and <strong>threshold</strong> properties define the number of messages that are summarized and stored so that correct conversational context is maintained.</p>
<blockquote>
<p>Property targetCount is the used to specify the number of recent messages that should be retained post reduction while threshold is the number of messages allowed beyond targetCount before summarization.</p>
</blockquote>
<p>When the conversation grows beyond <strong>targetCount + threshold</strong> values , older messages are summarized into a single message that persists the entire conversational context. This message replaces the old messages which are discarded and the most recent messages defined by targetCount stays.</p>
<p>For example : Lets assume that SummarizingChatReducer has targetCount of 1 and threshold of 5 and the no of prompts sent are 6.</p>
<p>Once the total number of chat messages recorded in <strong>ProviderSessionState</strong> is equal or greater than 6 (1+5), summarization kicks in. 1 recent message is retained post summarization in its original form and all messages are summarized.</p>
<p>There are two major approaches for storing and retrieving summarized conversational context.</p>
<ul>
<li><p>In the first approach , you store the entire conversation along with summarized messages in a persistent storage like Redis and retrieve only the summarized messages in the <strong>ProvideChatHistoryAsync</strong> method. The major advantage with this approach is that you have the true conversation history at your disposal but disadvantage being that the size of the persistent storage would grow and managing it can be challenging.</p>
</li>
<li><p>In the second approach ,only the summarized messages are stored in a persistent storage like Redis along with all the messages above the limit set by the targetCount value. Major advantage with this approach is that you have less data to manage but disadvantage is that you lose all historical trails of the conversation as only the summarized conversational context is available and stored.</p>
</li>
</ul>
<h3>Redis</h3>
<p>You can either use Redis on Azure or use a Redis image on Docker. I am using Docker. Spin up Redis on Docker through following command in the Docker terminal.</p>
<pre><code class="language-yaml">docker run -d --name local-redis -p 6379:6379 redis:latest
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3b7307e7-b44d-44be-a7a9-18fb3a666677.png" alt="" style="display:block;margin:0 auto" />

<p>Redis is running on port number : <strong>6379</strong></p>
<h3><strong>Project SetUp</strong></h3>
<p>Create a new console application and add the following packages</p>
<pre><code class="language-csharp">dotnet add package Azure;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Agents.AI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package StackExchange.Redis;
dotnet add package System.Text.Json.Serialization;
dotnet add package Microsoft.Extensions.Configuration;
dotnet add package Microsoft.Extensions.DependencyInjection;
dotnet add package Microsoft.Extensions.Logging;
</code></pre>
<p>Add <strong>appsetting.json</strong> to the project</p>
<pre><code class="language-csharp">"AppSettings": { 
    "Chat_DeploymentName": "Deployment Name",
    "EndPoint": "Azure OpenAI endpoint",
    "ApiKey": "Azure OpenAI API key"
}
</code></pre>
<h3><strong>Code</strong></h3>
<p>Once all the artifacts in place, add the following code to <strong>Program.cs</strong></p>
<p><strong>Program.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p><strong>Create a DI container</strong></p>
<pre><code class="language-csharp"> ServiceCollection servicecollection = new ServiceCollection();
</code></pre>
<p><strong>Read credentials and register</strong> <code>Chatclient</code></p>
<pre><code class="language-csharp">var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

servicecollection.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
   .AsIChatClient()
    )
);
</code></pre>
<p><strong>Register ChatClientAgent in the DI container and build the Service Provider</strong></p>
<pre><code class="language-csharp">servicecollection.AddSingleton&lt;ChatClientAgent&gt;();
ServiceProvider serviceprovider = servicecollection.BuildServiceProvider();
</code></pre>
<p><strong>Fetch keyed IChatclient from the DI container</strong></p>
<pre><code class="language-csharp">var _chatclient = new ChatClientAgent(serviceprovider.GetKeyedService&lt;IChatClient&gt;("ChatClient")).ChatClient;
</code></pre>
<p><strong>Set SummarizingChatReducer properties</strong></p>
<pre><code class="language-csharp">SummarizingChatReducer summaryReducer = new SummarizingChatReducer(
chatClient: _chatclient,
targetCount: 2,
threshold: 3);
</code></pre>
<p>Here, the <strong>targetCount</strong> is set to 2 and the <strong>threshold</strong> is set to 3. This means that the conversation can grow to 5 (2+3) messages before summarization is triggered. Once threshold reaches 5 (2+3), keep 2 (targetCount) most recent messages in its raw form and summarize all the messages.</p>
<p><strong>ChatClientAgentOptions and ChatHistoryProvider</strong></p>
<pre><code class="language-csharp"> var options = new ChatClientAgentOptions
 {
     ChatOptions = new ChatOptions
     {
         Instructions = "You are a helpful chat assistant."
     },
     ChatHistoryProvider = new RedisChatHistoryProvider(summarizingChatReducer: summaryReducer, targetCount: 2, threshold: 3)

 };
</code></pre>
<blockquote>
<p>Why am I passing targetCount and threshold values to the constructor of CustomChatReducer(RedisChatHistoryProvider) when these values have been already configured for SummarizingChatReducer ?</p>
<p>This is because, SummarizingChatReducer does not expose targetCount and threshold as publicly accessible properties. Also I am passing summarizingChatReducer instance as constructor argument.</p>
</blockquote>
<p><strong>Agent and Session</strong></p>
<pre><code class="language-csharp"> var agent = _chatclient.AsAIAgent(options);
 var agentsession = await agent!.CreateSessionAsync();
</code></pre>
<p>Define CustomChatHistoryProvider (<strong>RedisChatHistoryProvider)</strong></p>
<p><strong>RedisChatHistoryProvider.cs &gt;&gt;</strong></p>
<p>Define properties of session state in a class called <strong>SessionState.</strong></p>
<p>We have defined _targetCount , _threshold and _summarizingChatReducer because we want these properties to be part of <strong>InvokingContext</strong> through <strong>ProviderSessionState</strong> (explained later).These properties can be accessed through the <strong>ProvideChatHistoryAsync</strong> and <strong>StoreChatHistoryAsync</strong> methods.</p>
<pre><code class="language-csharp">   internal class SessionState
 {
     public int _targetCount { get; set; } = 0;

     public int _threshold { get; set; } = 0;

     public SummarizingChatReducer _summarizingChatReducer { get; set; } = null;

     [JsonPropertyName("Messages")]
     public List&lt;ChatMessage&gt; lstChatMessages { get; set; } = [];

     [JsonPropertyName("UserName")]
     public string UserName { get; set; } = "";
   
 }
</code></pre>
<p><strong>Redis Connection &gt;&gt;</strong></p>
<pre><code class="language-csharp">ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379");
private IDatabase db;
</code></pre>
<p><strong>RedisChatHistoryProvider Constructor &gt;&gt;</strong></p>
<p>We create an instance of ProviderSessionState for storing and handling the SessionState.</p>
<pre><code class="language-csharp">private readonly Microsoft.Agents.AI.ProviderSessionState&lt;SessionState&gt; _sessionstate;

 public RedisChatHistoryProvider(Func&lt;AgentSession, SessionState&gt; initializer = null, string statekey = "", SummarizingChatReducer summarizingChatReducer = null, int targetCount = 0, int threshold = 0)
 {
     _sessionstate = new(initializer =&gt;
     {
         db = redis.GetDatabase();        
         string messages = db.StringGet(statekey);

         return new SessionState
         {
             lstChatMessages = String.IsNullOrEmpty(messages) ? Enumerable.Empty&lt;ChatMessage&gt;().ToList() : JsonSerializer.Deserialize&lt;List&lt;ChatMessage&gt;&gt;(messages),
             _targetCount = targetCount,
             _threshold = threshold,
             _summarizingChatReducer = summarizingChatReducer,
             UserName = statekey

         };
     }
      , statekey = "Sachin"

    );
 }
</code></pre>
<blockquote>
<p>I have hardcoded statekey value to "Sachin".You can use any other uniquely identifiable value for statekey.</p>
</blockquote>
<p>In the above code, the constructor parameters targetCount, threshold ,summarizingChatReducer are respectively assigned to the Session state properties of _targetCount, _threshold and _summarizingChatReducer. This way these properties are now part of the <strong>InvokingContext</strong> of the overridden methods ProvideChatHistoryAsync and StoreChatHistoryAsync.</p>
<p><strong>ProviderSessionState</strong> is used for initializing and persisting provider state in the session's StateBag. State(in our case it is SessionState object) is a inbuild session state management that maintains conversation history and is stored in the StateBag using the StateKey property as the key.</p>
<p>As mentioned earlier, we can use either of two approaches: store the entire conversation in Redis and retrieve only the relevant summarized/reduced messages when required or store only the summarized messages in Redis.</p>
<p>We will use the second approach, where we store only the summarized messages along with the number of messages specified by targetCount in Redis.</p>
<p><strong>StoreChatHistoryAsync &gt;&gt;</strong></p>
<pre><code class="language-csharp">  protected async override ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
  {
      var cntx = _sessionstate.GetOrInitializeState(context.Session);

      IEnumerable&lt;ChatMessage&gt; reducedMessages = new List&lt;ChatMessage&gt;();

      var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
      cntx.lstChatMessages.AddRange(allNewMessages);
      
      var jsonSerializerOptions = new JsonSerializerOptions
      {
          WriteIndented = true,
          Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
      };

      if (cntx.lstChatMessages.Count &gt;= cntx._targetCount + cntx._threshold)
      {
          
          reducedMessages = await cntx._summarizingChatReducer.ReduceAsync(cntx.lstChatMessages, cancellationToken);
          await db.StringSetAsync(cntx.UserName, System.Text.Json.JsonSerializer.Serialize(reducedMessages, jsonSerializerOptions));
      }
      else
      {
          await db.StringSetAsync(cntx.UserName, System.Text.Json.JsonSerializer.Serialize(cntx.lstChatMessages, jsonSerializerOptions));

      }

  }
</code></pre>
<p>In above code there are two lists : <strong>allNewMessages</strong> and <strong>reducedMessages</strong></p>
<p>allNewMessages holds response and request messages which is then added to lstChatMessages of <strong>InvokedContext</strong> context declared as <strong>cntx</strong>.</p>
<pre><code class="language-csharp">var cntx = _sessionstate.GetOrInitializeState(context.Session);

IEnumerable&lt;ChatMessage&gt; reducedMessages = new List&lt;ChatMessage&gt;();

var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);

cntx.lstChatMessages.AddRange(allNewMessages); 
</code></pre>
<blockquote>
<p>Recall that lstChatMessages is a property in the SessionState class</p>
</blockquote>
<p><strong>SerializationOption</strong></p>
<pre><code class="language-csharp">   var jsonSerializerOptions = new JsonSerializerOptions
   {
       WriteIndented = true,
       Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
   };
</code></pre>
<p>Check if the conversation count is greatest than targetCount and threshold combined together, trigger the <strong>ReduceAsync</strong> method and save the reduced messages in Redis if not then save all the conversation messages to Redis</p>
<pre><code class="language-csharp">  if (cntx.lstChatMessages.Count &gt;= cntx._targetCount + cntx._threshold)
  {
      reducedMessages = await cntx._summarizingChatReducer.ReduceAsync(cntx.lstChatMessages, cancellationToken);

      await db.StringSetAsync(cntx.UserName, System.Text.Json.JsonSerializer.Serialize(reducedMessages, jsonSerializerOptions));
  }
  else
  {
      await db.StringSetAsync(cntx.UserName, System.Text.Json.JsonSerializer.Serialize(cntx.lstChatMessages, jsonSerializerOptions));

  }
</code></pre>
<p><strong>Prompts</strong></p>
<p>I will first send the following five prompts together in a single session</p>
<pre><code class="language-csharp">Console.WriteLine(await agent.RunAsync(new ChatMessage(ChatRole.User, "My name is Sachin") { CreatedAt = DateTimeOffset.UtcNow, AuthorName = "Sachin" }, session: agentsession, new AgentRunOptions { AllowBackgroundResponses = true }) + "\n");//Prompt 1 

Console.WriteLine(await agent.RunAsync(new ChatMessage(ChatRole.User, "I live in India") { CreatedAt = DateTimeOffset.UtcNow, AuthorName = "Sachin" }, session: agentsession, new AgentRunOptions { AllowBackgroundResponses = true }) + "\n");//Prompt 2

Console.WriteLine(await agent.RunAsync(new ChatMessage(ChatRole.User, "I work in the IT industry") { CreatedAt = DateTimeOffset.UtcNow, AuthorName = "Sachin" }, session: agentsession, new AgentRunOptions { AllowBackgroundResponses = true }) + "\n"); //Prompt 3

Console.WriteLine(await agent.RunAsync(new ChatMessage(ChatRole.User, "I am a software developer") { CreatedAt = DateTimeOffset.UtcNow, AuthorName = "Sachin" }, session: agentsession, new AgentRunOptions { AllowBackgroundResponses = true }) + "\n");//Prompt 4 

Console.WriteLine(await agent.RunAsync(new ChatMessage(ChatRole.User, "I work mainly with C# and .NET") { CreatedAt = DateTimeOffset.UtcNow, AuthorName = "Sachin" }, session: agentsession, new AgentRunOptions { AllowBackgroundResponses = true }) + "\n"); //Prompt 5
</code></pre>
<blockquote>
<p>Recall that we have set the values of targetCount and threshold to 2 and 3 respectively.</p>
</blockquote>
<p>In the above case <strong>Prompt 5</strong> and the agent response will be stored to Redis in its raw form while the rest of prompts are summarized.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c95bce73-fa08-4275-9e67-6bba6e074aff.png" alt="" style="display:block;margin:0 auto" />

<p>In the image above <strong>1</strong> in red is the summarized text and <strong>2</strong> and <strong>3</strong> in red are the stored messages that are based on the targetCount value.</p>
<h3>Execution</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a5a781a4-2849-4c99-b76e-92e5e3f488da.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>ChatSummarizer provides an effective way to manage conversation history by reducing a large number of messages into a concise summary while preserving the important and required context. This helps prevent conversation history from growing indefinitely and reduces the amount of data that needs to be stored and processed.This gives us a balance between retaining recent conversation details and maintaining a conversational context of older messages.</p>
<p>This helps improve memory efficiency, reduce token usage and there by maintain efficient and relevant context throughout longer conversations.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Secure AI Functions in Microsoft Agent Framework with Microsoft Entra OAuth Role-Based Access Control ]]></title><description><![CDATA[Lets take a scenario where you have an MAF (Microsoft Agent Framework) AIAgent that has tool calls to multiple AIFunctions.
You definitely wouldn't want an AIAgent to blindly invoke every available to]]></description><link>https://www.azureguru.net/secure-ai-functions-in-microsoft-agent-framework-with-microsoft-entra-oauth-role-based-access-control</link><guid isPermaLink="true">https://www.azureguru.net/secure-ai-functions-in-microsoft-agent-framework-with-microsoft-entra-oauth-role-based-access-control</guid><category><![CDATA[ai functions]]></category><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[llm]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[Entra ID]]></category><category><![CDATA[AI agent security]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Wed, 05 Aug 2026 23:42:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d43df34d-4f8b-4549-8d0f-f9a972a38f6d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Lets take a scenario where you have an MAF (Microsoft Agent Framework) <strong>AIAgent</strong> that has tool calls to multiple <strong>AIFunctions.</strong></p>
<p>You definitely wouldn't want an AIAgent to blindly invoke every available tool or function solely based on a user's prompt or simply because a user requested it. Instead there should be some solid guardrails in place to validate user permissions for invoking a given AIFunction. Before invoking a function, the agent should verify that the user has the required role or scope to perform the requested operation.</p>
<p>The conventional approach is to store user permissions in a database and retrieve them whenever a user logs in. While this works it would require maintaining a separate authorization store, keeping it synchronized with user identities on Entra and performing database lookups to determine user permissions.</p>
<p>But with Microsoft Entra ID , all the requisite roles and permissions are embedded directly into the access token as claims. Once the token has been validated the application can authorize tool calls based on these claims without querying an additional database.</p>
<p>This is more robust approach as the user details and permissions are already centralized in Entra and there is no need maintain custom permission tables or synchronization logic in a database.</p>
<h3>UseCase</h3>
<p>Lets use a hypothetical scenario. An AIAgent invokes two AIFunctions</p>
<ul>
<li><p><strong>ReturnCountryCapital</strong></p>
</li>
<li><p><strong>ReturnCityTemperature</strong></p>
</li>
</ul>
<p>There are two users</p>
<p><strong>sachin.nandanwar @ azureguru.net</strong> and <strong>sachin_nandanwar @ azureguru.net</strong></p>
<blockquote>
<p>Though I am using Entra users in this example, the same approach can be extended to Entra groups. So instead of assigning roles to individual users you can assign them to groups allowing all group members to inherit the same permissions assigned to the Entra group that they are a member of.</p>
</blockquote>
<p><strong>sachin.nandanwar @ azureguru.net</strong> should have permissions to invoke both the AIFunctions (<strong>ReturnCountryCapital and ReturnCityTemperature)</strong> while <strong>sachin_nandanwar @ azureguru.net</strong> permissions should be limited to invoke only <strong>ReturnCountryCapital</strong></p>
<h3><strong>SetUp</strong></h3>
<blockquote>
<p>This article is heavily influenced by my article on Delegated Tokens and Application tokens wrt setting up role based access in Entra. You can refer to that article <a href="https://www.azureguru.net/understanding-delegated-tokens-vs-application-tokens-through-claims-based-authorization-in-microsoft-entra-id">here</a>.</p>
</blockquote>
<p>But I would still cover the same steps from the above article in this article as well.</p>
<p>As mentioned earlier, there are two users</p>
<ul>
<li><p><strong>(Sachin.Nand)</strong> <strong>sachin.nandanwar @ azureguru.net</strong></p>
</li>
<li><p><strong>(Sachin Nandanwar)</strong> <strong>sachin_nandanwar @ azureguru.net</strong></p>
</li>
</ul>
<p>sachin.nandanwar @ azureguru.net will have full permissions to invoke both the AIFunctions (<strong>ReturnCountryCapital and ReturnCityTemperature)</strong> while sachin_nandanwar @ azureguru.net will have partial permissions limited to invoke only <strong>ReturnCountryCapital</strong> AIFunction</p>
<blockquote>
<p><em>Ensure that Scalar set up is configured in your</em> <em>ASP.NET</em> <em>core project. For more details refer to my following article</em></p>
</blockquote>
<p><a href="https://www.azureguru.net/customize-scalar-UI-for-net-api"><strong>https://www.azureguru.net/customize-scalar-UI-for-net-api</strong></a></p>
<p>Also ensure that you have a thorough understanding of Minimal API's and implementation of <strong>ClaimPrincipal, Authentication</strong> and <strong>Authorization</strong> for Minimal API's. Please refer to my article below to have a better understanding of the topic.</p>
<p><a href="https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core"><strong>https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core</strong></a></p>
<p>You can ignore the custom JWToken aspect from the above article as in that article the focus was on creating custom JWTokens.</p>
<p>The first step is to create a Service Principal. I created one called <strong>App Service Principal.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7ecfb62e-9c1c-41dd-b202-e1f9c0bf82e1.png" alt="AI Function access in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>In the next step , click the <strong>Expose an API</strong> option. The format of the URI is <strong>api://{ApplicationId}</strong>.</p>
<blockquote>
<p><strong>ApplicationId is the Service Principal ClientId</strong></p>
</blockquote>
<p>Define a scope by clicking the <strong>Add a scope</strong> option.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8f602d97-99f5-4620-801c-19bbee5f3daa.png" alt="AI Function access in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>I defined a scope with name <strong>temperature_capital.read</strong> and used that name across all other properties for the scope.</p>
<blockquote>
<p>Scope temperature_capital.read will be referred in our code to generate access token for the service principal on behalf of the signed in user.</p>
</blockquote>
<p>In the next step, click <strong>App roles</strong> &gt;&gt; <strong>Create app role</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e3d65fb6-3679-4a13-bf97-c93f354b4104.png" alt="AI Function access in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>and define two app roles</p>
<ul>
<li><p><strong>Temperature and Capital Read Role</strong></p>
</li>
<li><p><strong>Capital Read Role</strong></p>
</li>
</ul>
<blockquote>
<p>The <strong>Temperature and Capital Read</strong> role is assigned to users who require access to invoke both the R<strong>eturnCountryCapital</strong> and <strong>ReturnCityTemperature</strong> functions while the <strong>Capital Read</strong> role is assigned to users who only need access to invoke the <strong>ReturnCountryCapital</strong> function. Users with this role are denied permission to invoke the <strong>ReturnCityTemperature</strong> function.</p>
</blockquote>
<p>Now that we have app roles and API's defined, in the next step , under <strong>API permissions</strong> , click <strong>Add a permission</strong> and select and add the API created in the previous step.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9e8c26c8-6368-42d4-bb8b-598e43f23089.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/142af34b-b53e-458d-829f-4a756bea17d9.png" alt="AI Function access in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<blockquote>
<p><em>This might sound counterintuitive to see that we have to assign permissions for an API to the service principal that created that API in the first place. But that’s the way it is. Not doing so results in an Unauthorized access error.</em></p>
</blockquote>
<p>The next step is to enable <strong>Assignment required</strong> property.</p>
<p>Browse to <strong>Entra ID</strong> &gt;&gt; <strong>Enterprise apps</strong> &gt;&gt; <strong>All applications</strong></p>
<p>Select your application. In our case it is <strong>App Service Principal</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3cdca9a7-93de-419b-9fb2-2d88457f2144.png" alt="AI Function access in Microsoft Agent Framework" />

<p>Ensure that <strong>Assignment required?</strong> is set to <strong>Yes</strong> . By default it is <strong>No</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3a286463-9ec4-483a-a17f-2be992abbea8.png" alt="AI Function access in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Navigate to <strong>Users and groups</strong> in the same page add click <strong>Add user/group</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c1d52bde-eb0a-419d-8e0f-071d0a4e0bd8.png" alt="AI Function access in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Click <strong>None Selected</strong> and search for the user/group to whom you want to grant Assignments.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/99217e5d-3497-472b-ad95-8add93740b33.png" alt="AI Function access in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>I granted <strong>Temperature and Capital Read Role</strong> to the user <strong>Sachin Nand</strong> i.e. <strong>sachin.nandanwar @ azureguru.net</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a9d3a4be-0929-4d0a-9ebc-e074c24caf49.png" alt="AI Function access in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>I then granted <strong>Capital Read Role</strong> to user <strong>sachin.nandanwar @ azureguru.net</strong> i.e. <strong>Sachin Nandanwar</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/4c8bf0e7-cb8c-4a72-965e-6e6e1c427995.png" alt="AI Function access in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Users assigned to a role is listed under <strong>Users and groups</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7b85fd80-3051-43d4-a2cb-3c6609bfd15e.png" alt="AI Function access in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Now that the set up and the required prerequisite is in place , lets move on to the code.</p>
<h3><strong>Code</strong></h3>
<p>Add the following references to your ASP.NET core project</p>
<pre><code class="language-csharp">dotnet add package Azure;
dotnet add package Azure.Core;
dotnet add package Microsoft.IdentityModel.Tokens;
dotnet add package Scalar.AspNetCore;
dotnet add package System.Security.Claims;
dotnet add package Microsoft.Agents.AI;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package System.Text.Json.Serialization;
</code></pre>
<p>Lets first set up the Scalar API interface.</p>
<p><strong>Scalar.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

internal sealed class BearerSecuritySchemeTransformer : IOpenApiDocumentTransformer
{
    private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

    public BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider)
    {
        _authenticationSchemeProvider = authenticationSchemeProvider;
    }

    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await _authenticationSchemeProvider.GetAllSchemesAsync();

        if (authenticationSchemes.Any(authScheme =&gt; authScheme.Name == "Bearer"))
        {
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes ??= new Dictionary&lt;string, IOpenApiSecurityScheme&gt;();
            document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
            {
                Type = SecuritySchemeType.Http,
                Scheme = "bearer",
                In = ParameterLocation.Header,
                BearerFormat = "JWT"
            };

            foreach (var operation in document.Paths.Values.SelectMany(path =&gt; path.Operations))
            {
                if (operation.Value.Security == null)
                {
                    operation.Value.Security = new List&lt;OpenApiSecurityRequirement&gt;();
                }
                var securityRequirement = new OpenApiSecurityRequirement
                {
                    [new OpenApiSecuritySchemeReference("Bearer", document)] = []
                };

                operation.Value.Security ??= new List&lt;OpenApiSecurityRequirement&gt;();
                operation.Value.Security.Add(securityRequirement);
            }
        }
    }
}
</code></pre>
<p><strong>Scalar UI &gt;&gt;</strong></p>
<pre><code class="language-csharp"> builder.Services.AddOpenApi(
    options =&gt;
    {
        options.AddDocumentTransformer&lt;BearerSecuritySchemeTransformer&gt;();
    }
);

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapScalarApiReference(
    options =&gt;
    {
        options.Title = "Scalar API";
        options.DarkMode = true;
        options.Favicon = "path";
        options.DefaultHttpClient = new KeyValuePair&lt;ScalarTarget, ScalarClient&gt;(
            ScalarTarget.CSharp,
            ScalarClient.RestSharp
        );
        options.HideModels = false;
        options.Layout = ScalarLayout.Modern;
        options.ShowSidebar = true;
        options.Authentication = new ScalarAuthenticationOptions
        {
            PreferredSecuritySchemes = new List&lt;string&gt; { "Bearer" }
        };
    }
);
</code></pre>
<p>The above code customizes the Scalar UI to include the Bearer token section.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/51e76cd2-4e73-496a-8c3b-1a0896265105.png" alt="" style="display:block;margin:0 auto" />

<p>For more details refer to my article on the topic <a href="https://www.azureguru.net/customize-scalar-UI-for-net-api"><strong>here</strong></a>.</p>
<p>Next , define an <strong>Authentication</strong> class that validates the user's sign-in and requested scopes with Microsoft Entra ID and then returns a JWT token.</p>
<blockquote>
<p><em>Unlike implementing custom JWT tokens which I demonstrated in my earlier article</em> <a href="https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core"><em>here</em></a><em>, with Microsoft Entra ID-issued tokens there is no need for generating, signing, rotating or validating JWTs ourselves. Microsoft Entra ID auto handles all these aspects at its end.</em></p>
</blockquote>
<p><strong>Authentication.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">    using Microsoft.Identity.Client;
    using System.IdentityModel.Tokens.Jwt;

    namespace Security
    {
        internal class Authentication
        {
            public static string clientId = "Service Principal Client Id";
            private static string[] scopes = { $"api://{clientId}/.default" };
            private static string Authority = "https://login.microsoftonline.com/organizations";
            private static string RedirectURI = "http://localhost";

            public async static Task&lt;JwtSecurityToken&gt; ReturnAuthenticationResult()
            {
                string AccessToken;
                IPublicClientApplication PublicClientApplication = PublicClientApplicationBuilder
                    .Create(clientId)
                    .WithAuthority(Authority)
                    .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
                    .WithRedirectUri(RedirectURI)
                    .Build();

                var accounts = await PublicClientApplication.GetAccountsAsync();
                AuthenticationResult result;

                try
                {
                    result = await PublicClientApplication
                        .AcquireTokenSilent(scopes, accounts.First())
                        .ExecuteAsync()
                        .ConfigureAwait(false);
                }
                catch
                {
                    result = await PublicClientApplication
                        .AcquireTokenInteractive(scopes)
                        .ExecuteAsync()
                        .ConfigureAwait(false);
                }

                JwtSecurityToken token = new JwtSecurityToken(result.AccessToken);
                return token;
            }
        }
    }
</code></pre>
<blockquote>
<p>For brevity, I have defined ClientId as a variable. Ideally it should be placed in a config file or as an Environment variable</p>
</blockquote>
<p>Scope used is : <strong>api://{Service Principal ClientId}/.default</strong> for the claims token.</p>
<p>Next, add the following code in <strong>Program.cs</strong> to read settings from the config file <strong>appsettings.json</strong></p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p><strong>Read credentials and register</strong> <strong>Chatclient and return a ChatClientAgent</strong></p>
<pre><code class="language-csharp">WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

servicecollection.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
   .AsIChatClient()
    )
);

builder.Services.AddSingleton&lt;ChatClientAgent&gt;(sp =&gt;
{
    return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"));
});
</code></pre>
<p><em><strong>Authentication</strong></em> <em>&gt;&gt;</em></p>
<pre><code class="language-csharp">
builder.Services.AddAuthentication().AddJwtBearer(options =&gt;
       {
           options.Authority = $"https://login.microsoftonline.com/{TenantId}";
           options.Validate();
           options.MapInboundClaims = false;
           options.TokenValidationParameters = new TokenValidationParameters
           {
               ValidIssuer = $"https://sts.windows.net/{TenantId}/",
               ValidAudience = $"api://{ClientId}",
               ValidateIssuer = true,
               ValidateAudience = true
           };
       });
</code></pre>
<p>Authentication mechanism is used to validate the incoming tokens. It checks for two parameters, <strong>ValidIssuer</strong> and <strong>ValidAudience</strong> and ensures that the values for these parameters in the bearer token matches with</p>
<ul>
<li><p><strong><a href="https://sts.windows.net/%7BTenantId%7D/">https://sts.windows.net/{TenantId}/</a></strong> and <strong>api://{ClientId}</strong></p>
</li>
<li><p><strong>options.MapInboundClaims = false</strong> . This setting is very important wrt validating the claims . Check the following screenshot</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c076ae1b-33a2-417b-9e5d-a4559b767622.png" alt="" style="display:block;margin:0 auto" /></li>
</ul>
<p>The claims and the scope format above is a standard JWT format. If <strong>options.MapInboundClaims = true</strong> (which the default) , the claim check through will fail . For example something like</p>
<pre><code class="language-csharp">var roles = claims.FindFirst("roles")?.Value
</code></pre>
<p>roles will always be null as it checks for the keyword <strong>"roles"</strong></p>
<p>But setting <strong>options.MapInboundClaims = false</strong> the format changes</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8c6cc89c-e97a-4e18-9eda-602544a385ed.png" alt="" style="display:block;margin:0 auto" />

<p><em><strong>Authorization</strong></em> <em>&gt;&gt;</em></p>
<pre><code class="language-csharp">builder.Services.AddAuthorization(options =&gt;
  {
      options.AddPolicy("CapitalTemperatureReadAccess", policy =&gt;
      {
          policy.RequireAuthenticatedUser();
          policy.RequireClaim("aud", $"api://{ClientId}");
          policy.RequireClaim("iss", $"https://sts.windows.net/{TenantId}/");
          policy.RequireClaim("scp", "temperature_capital.read");             

      });
  });
</code></pre>
<p>Create an authorization policy but before that ensure that request contains authenticated users and then check if the claims contain "<strong>aud</strong>", "<strong>scp</strong>", "<strong>iss</strong>" and validate its values.</p>
<p>Next, define Request and Response objects for Temperature and Capital searches</p>
<p><strong>CityTemperatureSearchRequest &amp; CityTemperatureSearchResponse</strong> <strong>&gt;&gt;</strong></p>
<pre><code class="language-csharp"> public class CityTemperatureSearchRequest
 {
     public string City { get; set; }
 }

 public class CityTemperatureSearchResponse
 {
     public string City { get; set; }
     public string Temperature { get; set; }
 }
</code></pre>
<p><strong>CountryCapitalSearchRequest &amp; CountryCapitalSearchResponse &gt;&gt;</strong></p>
<pre><code class="language-csharp"> public class CountryCapitalSearchRequest
 {
     public string Country { get; set; }
 }

 public class CountryCapitalSearchResponse
 {
     public string Country { get; set; }
     public string Capital { get; set; }
 }
</code></pre>
<p>Next, create serialization metadata for both request and response types.</p>
<pre><code class="language-csharp">[JsonSerializable(typeof(CountryCapitalSearchRequest))]
[JsonSerializable(typeof(CountryCapitalSearchResponse))]
internal sealed partial class CountryCapitalSerializerContext : JsonSerializerContext;


[JsonSerializable(typeof(CityTemperatureSearchRequest))]
[JsonSerializable(typeof(CityTemperatureSearchResponse))]
internal sealed partial class CityTemperatureSerializerContext : JsonSerializerContext;
</code></pre>
<p>Define a <strong>delegate</strong> named <strong>ReturnCityTemperature</strong> that takes a search request <strong>CityTemperatureSearchRequest</strong> as input and returns a search response <strong>CityTemperatureSearchResponse</strong>.</p>
<pre><code class="language-csharp">   public static Func&lt;CityTemperatureSearchRequest, CityTemperatureSearchResponse&gt; ReturnCityTemperature = (CityTemperatureSearchRequest) =&gt;

    {
        switch (CityTemperatureSearchRequest.City)
        {

            case "Mumbai":

                return new CityTemperatureSearchResponse
                {

                    City = "Mumbai",
                    Temperature = "40"
                };

                break;

            case "Pune":

                return new CityTemperatureSearchResponse
                {

                    City = "Pune",
                    Temperature = "41"
                };

                break;

            case "Delhi":

                return new CityTemperatureSearchResponse
                {

                    City = "Delhi",
                    Temperature = "42"
                };

                break;

            case "Chennai":

                return new CityTemperatureSearchResponse
                {

                    City = "Chennai",
                    Temperature = "43"
                };
                break;

        }

        return new CityTemperatureSearchResponse
        {

            City = CityTemperatureSearchRequest.City,
            Temperature = "Unknown"
        };

    };
</code></pre>
<p>Similarly, define a <strong>delegate</strong> named <strong>ReturnCountryCapital</strong> that takes a search request <strong>CountryCapitalSearchRequest</strong> as input and returns a search response <strong>CountryCapitalSearchResponse</strong>.</p>
<pre><code class="language-csharp">    public static Func&lt;CountryCapitalSearchRequest, CountryCapitalSearchResponse&gt; ReturnCountryCapital = (CountryCapitalSearchRequest) =&gt;

       {
           switch (CountryCapitalSearchRequest.Country)
           {

               case "India":

                   return new CountryCapitalSearchResponse
                   {

                       Country = "India",
                       Capital = "New Delhi"
                   };
                   break;

               case "USA":

                   return new CountryCapitalSearchResponse
                   {

                       Country = "USA",
                       Capital = "Washington"
                   };
                   break;


               case "Germany":

                   return new CountryCapitalSearchResponse
                   {

                       Country = "Germany",
                       Capital = "Berlin"
                   };
                   break;

               case "Russia":

                   return new CountryCapitalSearchResponse
                   {

                       Country = "Russia",
                       Capital = "Moscow"
                   };
                   break;
           }

           return new CountryCapitalSearchResponse
           {

               Country = CountryCapitalSearchRequest.Country,
               Capital = "Unknown"
           };

       };
}
</code></pre>
<p>Register the above two delegates in the DI container as <strong>AIFunction</strong> with the corresponding <strong>SerializerOptions</strong> defined earlier.</p>
<pre><code class="language-csharp">  builder.Services.AddSingleton&lt;AIFunction&gt;(sp =&gt;
     {
         return AIFunctionFactory.Create(ReturnCountryCapital, new AIFunctionFactoryOptions { Name = "ReturnCountryCapital", Description = "Gets the capital city for a specific country.", SerializerOptions = CountryCapitalSerializerContext.Default.Options });
     });


  builder.Services.AddSingleton&lt;AIFunction&gt;(sp =&gt;
    {
        return AIFunctionFactory.Create(ReturnCityTemperature, new AIFunctionFactoryOptions { Name = "ReturnCityTemperature", Description = "Gets the temperature of a city", SerializerOptions = CityTemperatureSerializerContext.Default.Options });
    });
</code></pre>
<p><em><strong>Login Endpoint &gt;&gt;</strong></em></p>
<p>Here we fetch the access token issued by Entra ID which is part of the OIDC flow.</p>
<pre><code class="language-csharp">app.MapPost("/login", async() =&gt;
{
tokens = await Security.Authentication.ReturnAuthenticationResult();
return Results.Ok(tokens);
}).WithOpenApi();
</code></pre>
<p><em><strong>Chat Endpoint &gt;&gt;</strong></em></p>
<pre><code class="language-csharp">app.MapGet("/chat", async (string request, ClaimsPrincipal claims) =&gt;
 {
     var aud = claims.FindFirst("aud").Value;
     var issuer = claims.FindFirst("iss").Value;
     var roles = claims.FindFirst("roles").Value;
     var scp = claims.FindFirst("scp").Value;

     var chatclientlist = app.Services.GetRequiredKeyedService&lt;IChatClient&gt;("ChatClient");

     var aifunctions = app.Services.GetServices&lt;AIFunction&gt;();

     List&lt;AITool&gt; functions = new(aifunctions);

     if (aud == $"api://{ClientId}" &amp;&amp; issuer == $"https://sts.windows.net/{TenantId}/")
     {
         var options = new ChatClientAgentOptions
         {
                 ChatOptions = new ChatOptions
                 {

                     Tools = functions
                 }
             };
         

         if (roles == "TemperatureCapital.Read" &amp;&amp; scp == "temperature_capital.read")
         {
             options.ChatOptions.Instructions = "You return capital city of a country and you also provide temperature of a city.You will use only the data provided to you and not use any external data";

         }

         if (roles == "Capital.Read" &amp;&amp; scp == "temperature_capital.read")
         {

             options.ChatOptions.Instructions = "You return ONLY the capital city of a country and if user asks for temperature you respond 'You are not are authorized to access temperature data'. You will use only the data provided to you and not use any external data";
         }

         var agent = chatclientlist.AsAIAgent(options);

         AgentResponse response = await agent.RunAsync(request);

         return Results.Ok(response.Text);
     }

     return Results.Unauthorized();

 }).RequireAuthorization("CapitalTemperatureReadAccess");
</code></pre>
<p>Let's break down the above code step by step.</p>
<p>Fetch <strong>audience</strong>, <strong>iss(issuer)</strong>, <strong>scp(scopes)</strong> and <strong>roles</strong> values from claims.</p>
<pre><code class="language-csharp">var aud = claims.FindFirst("aud").Value;
var issuer = claims.FindFirst("iss").Value;
var roles = claims.FindFirst("roles").Value;
var scp = claims.FindFirst("scp").Value;
</code></pre>
<p>Retrieve <strong>IChatClient</strong> keyed service and <strong>AIFunction</strong> from the service collection and set it to the functions list variable of type <strong>List</strong></p>
<pre><code class="language-csharp">
var chatclientlist = app.Services.GetRequiredKeyedService&lt;IChatClient&gt;("ChatClient");

var aifunctions = app.Services.GetServices&lt;AIFunction&gt;();

List&lt;AITool&gt; functions = new(aifunctions);
</code></pre>
<p>Validate <strong>issuer</strong> and <strong>audience</strong> and if validated create <strong>ChatClientAgentOptions and</strong> set the Tools to the above functions list.</p>
<pre><code class="language-csharp"> if (aud == $"api://{ClientId}" &amp;&amp; issuer == $"https://sts.windows.net/{TenantId}/")
 {
     var options = new ChatClientAgentOptions
     {
             ChatOptions = new ChatOptions
             {
                 Tools = functions
             }
         };     

     if (roles == "TemperatureCapital.Read" &amp;&amp; scp == "temperature_capital.read")
     {
         options.ChatOptions.Instructions = "You return capital city of a country and you also provide temperature of a city.You will use only the data provided to you and not use any external data";

     }

     if (roles == "Capital.Read" &amp;&amp; scp == "temperature_capital.read")
     {

         options.ChatOptions.Instructions = "You return ONLY the capital city of a country and if user asks for temperature you respond 'You are not are authorized to access temperature data'. You will use only the data provided to you and not use any external data";
     }

     var agent = chatclientlist.AsAIAgent(options);
     AgentResponse response = await agent.RunAsync(request);
     return Results.Ok(response.Text);
 }

 return Results.Unauthorized();
</code></pre>
<p>Then validate <strong>roles</strong> and <strong>scopes</strong> values . If roles is <strong>TemperatureCapital.Read</strong> then set <strong>ChatOptions</strong> instructions to return both capital of the country and temperature of the city and if the roles is <strong>Capital.Read</strong> then set <strong>ChatOptions</strong> instructions to return only the capital and deny any other requests with a custom message <strong>'You are not are authorized to access temperature data'</strong>.</p>
<p>Assign <strong>ChatOptions</strong> to agent derived from <strong>IChatClient</strong> and send the prompt to the agent and return the response.</p>
<p>And if the validations fail, return an <strong>Unauthorized</strong> error.</p>
<p>Login as : <strong>sachin.nandanwar @ azureguru.net</strong> and send the following prompt</p>
<pre><code class="language-plaintext">What is capital of India and whats the temperature in Mumbai ?
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/b9de56b7-f4b5-479c-a438-2fd6b0063be0.png" alt="" style="display:block;margin:0 auto" />

<p>Login as : <strong>sachin.nandanwar @ azureguru.net</strong> and send the the same prompt</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/b60b7f79-6696-43d1-a8da-bb5c8d43c85b.png" alt="" style="display:block;margin:0 auto" />

<p>and the access to the temperature data is denied.</p>
<h3>Execution</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c62dd7ef-3408-48f3-a161-271956c82517.gif" alt="" style="display:block;margin:0 auto" />

<h3>Closing Notes</h3>
<p>Instead of building a separate security layer for your AIAgents to handle permissions for AIFunction invocation , leveraging user claims through Entra issued OAuth tokens can be more robust ,clean and maintainable architecture.</p>
<p>With this architecture you get a centralized control over permissions required for user driven agent function invocation which ensures that users can invoke only the functions they are permitted to access.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Build AI Agents with Microsoft Agent Framework to Access Azure Services Using Entra OAuth]]></title><description><![CDATA[Imagine a scenario where you have an AI agent that needs to access Azure services, such as Azure Storage. Simply granting the agent open and unrestricted access to Azure resources is not an option. Do]]></description><link>https://www.azureguru.net/build-ai-agents-with-microsoft-agent-framework-to-access-azure-services-using-entra-oauth</link><guid isPermaLink="true">https://www.azureguru.net/build-ai-agents-with-microsoft-agent-framework-to-access-azure-services-using-entra-oauth</guid><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[OIDC]]></category><category><![CDATA[oauth]]></category><category><![CDATA[OAuth2]]></category><category><![CDATA[Azure]]></category><category><![CDATA[azure-storage]]></category><category><![CDATA[Entra ID]]></category><category><![CDATA[access-token]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Mon, 03 Aug 2026 19:54:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8903656a-62e4-4be9-b15f-14db91e8d43f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine a scenario where you have an AI agent that needs to access Azure services, such as Azure Storage. Simply granting the agent open and unrestricted access to Azure resources is not an option. Doing so is a significant security risk, allowing the agent to perform operations beyond what the end user is authorized to do.</p>
<p>In this example I am going to demonstrate how to leverage OpenID Connect (OIDC) to enable an AI agent to securely access Azure Storage through an ASP.NET Core Minimal API by validating the underlying OIDC claims and also enforcing permissions.</p>
<p><strong>But why minimal API's ?</strong></p>
<p>Honestly, I couldn't figure out a straightforward approach to leverage <strong>AG-UI</strong> protocol to pass OAuth 2.0 access token back to the server.</p>
<p>If you would like to know more about AG-UI protocol in Microsoft Agent Framework , then you can refer to my article on the topic <a href="https://www.azureguru.net/ag-ui-protocol-in-microsoft-agent-framework">here</a>.</p>
<p>In this article, I will use Scalar UI as the API interface which I have done in most of my previous articles on Minimal API's. The article <a href="https://www.azureguru.net/securing-azure-storage-in-asp-net-core-minimal-api-s-with-microsoft-entra-id-and-openid-connect-oidc">here</a> is a deep dive on how to leverage it.</p>
<p>So the aim is to access list of directories in an Azure storage container based on the user identity and prompted through Microsoft Agent Framework (MAF) AI Agent provided the signed-in user user has the required RBAC role access to the Azure storage. The agent uses OAuth 2.0 access tokens issued on behalf of the signed-in user. The Azure storage is an ADLS GEN2 storage.</p>
<h3>Flow</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d18a00fd-4cf8-490b-9903-37853dbe4c26.png" alt="Microsoft Agent Framework and Azure services" style="display:block;margin:0 auto" />

<h3>SetUp</h3>
<p>To add more context to the above flow, the scope used is</p>
<pre><code class="language-html">https://storage.azure.com/
</code></pre>
<p>The ADLS GEN2 storage has the following structure</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/5fcd2c53-8803-4fa4-8c5c-c7aed9d179d3.png" alt="Microsoft Agent Framework and Azure services" style="display:block;margin:0 auto" />

<p>The expectation from the code is that it should be capable of recursively traversing all directories within a container prompted through the AI Agent. As shown in the screenshot above, the <strong>customers</strong> container contains directories that are nested up to three levels deep and this structure can be dynamic.</p>
<p>So, there are two users :</p>
<ul>
<li><p><strong>sachin.nandanwar @ azureguru.net</strong></p>
</li>
<li><p><strong>sachin_nandanwar @ azureguru.net</strong></p>
</li>
</ul>
<p><a href="mailto:sachin.nandanwar@azureguru.net"><strong>sachin.nandanwar@azureguru.net</strong></a> is assigned the necessary RBAC role accesses to the storage. Typically it has to be <strong>Storage Blob Data Owner</strong> role.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/fedd98ba-a400-48c9-9bb5-0d4c96d0f135.png" alt="Microsoft Agent Framework and Azure services" style="display:block;margin:0 auto" />

<p>You could also grant <strong>Storage Blob Data Contributor role</strong> but the <strong>Storage Blob Data Owner</strong> role has POSIX access control (ACL access) that auto grants all the (r-w-x) privileges to the owner for all the underlying objects under the container.</p>
<p>For instance in the following screenshot we can see that the owner i.e. the <strong>Storage Blob Data Owner</strong> was auto assigned the (r-w-x) access .</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/770ff9a7-8d13-42ef-a576-cdff6643a639.png" alt="Microsoft Agent Framework and Azure services" style="display:block;margin:0 auto" />

<p><strong>sachin_nandanwar @ azureguru.net</strong> is not assigned any RBAC role.</p>
<p>Create a Service Principal and grant <strong>Azure Storage</strong> delegated permissions.</p>
<p>I created one named <strong>ADLS GEN2 Service Principal.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/03ba6eeb-e87f-4d86-8bff-ccec087d2ad5.png" alt="Microsoft Agent Framework and Azure services" style="display:block;margin:0 auto" />

<blockquote>
<p><em><strong>Ensure that you the Scalar set up is configured in your</strong></em> <em><strong>ASP.NET</strong></em> <em><strong>core project. For more details refer to my following article</strong></em></p>
</blockquote>
<p><a href="https://www.azureguru.net/customize-scalar-UI-for-net-api"><strong>https://www.azureguru.net/customize-scalar-UI-for-net-api</strong></a></p>
<p>Also ensure that you have a thorough understanding of Minimal API's and implementation of <strong>ClaimPrincipal, Authentication</strong> and <strong>Authorization</strong> for Minimal API's. Please refer to my article to have a better understanding of the topic.</p>
<p><a href="https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core"><strong>https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core</strong></a></p>
<p>You can ignore the custom JWToken aspect from the above article as in that article the focus was on creating custom JWTokens.</p>
<h3><strong>Code</strong></h3>
<p>Add the following references to your ASP.NET core project</p>
<pre><code class="language-csharp">dotnet add package Azure.Core;
dotnet add Azure.AI.OpenAI;
dotnet add package Azure.Storage.Files.DataLake;
dotnet add package Microsoft.IdentityModel.Tokens;
dotnet add package Scalar.AspNetCore;
dotnet add package System.Security.Claims;
dotnet add Microsoft.Extensions.AI;
dotnet add Microsoft.Extensions.Configuration;
dotnet add Microsoft.Extensions.DependencyInjection;
dotnet add Microsoft.Extensions.Hosting;
</code></pre>
<p>First, we define an <strong>Authentication</strong> class that validates the user's sign-in and requested scopes with Microsoft Entra ID and then returns a Entra JWT token .</p>
<blockquote>
<p><em><strong>Unlike implementing custom JWT tokens which I demonstrated in my earlier article</strong></em> <a href="https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core"><em><strong>here</strong></em></a><em><strong>, with Microsoft Entra ID-issued tokens there is no need for generating, signing, rotating or validating JWTs ourselves. Microsoft Entra ID auto handles all these aspects at its end.</strong></em></p>
</blockquote>
<p><strong>Authentication.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Microsoft.Identity.Client;
using System.IdentityModel.Tokens.Jwt;

namespace Security
{
    internal class Authentication
    {
        public static string clientId = "Service Principal Client Id";
        private static string[] scopes = { "https://storage.azure.com/.default" };
        private static string Authority = "https://login.microsoftonline.com/organizations";
        private static string RedirectURI = "http://localhost";

        public async static Task&lt;JwtSecurityToken&gt; ReturnAuthenticationResult()
        {
            string AccessToken;
            IPublicClientApplication PublicClientApplication = PublicClientApplicationBuilder
                .Create(clientId)
                .WithAuthority(Authority)
                .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
                .WithRedirectUri(RedirectURI)
                .Build();

            var accounts = await PublicClientApplication.GetAccountsAsync();
            AuthenticationResult result;

            try
            {
                result = await PublicClientApplication
                    .AcquireTokenSilent(scopes, accounts.First())
                    .ExecuteAsync()
                    .ConfigureAwait(false);
            }
            catch
            {
                result = await PublicClientApplication
                    .AcquireTokenInteractive(scopes)
                    .ExecuteAsync()
                    .ConfigureAwait(false);
            }

            JwtSecurityToken token = new JwtSecurityToken(result.AccessToken);
            return token;
        }
    }
}
</code></pre>
<blockquote>
<p><em><strong>For brevity I have defined ClientId and other details in variables. Ideally they should be placed in a config file and their values fetched from there.</strong></em></p>
</blockquote>
<p><strong>AccessTokenCredential.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Azure.Core;
using System.IdentityModel.Tokens.Jwt;

namespace AccesTokenCredentials
{
    public class AccessTokenCredential : Azure.Identity.ClientSecretCredential
    {
        public AccessTokenCredential(string accessToken)
        {
            AccessToken = accessToken;
        }

        private string AccessToken;

        public AccessToken FetchAccessToken()
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

        public override ValueTask&lt;AccessToken&gt; GetTokenAsync(
            TokenRequestContext requestContext,
            CancellationToken cancellationToken
        )
        {
            return new ValueTask&lt;AccessToken&gt;(FetchAccessToken());
        }

        public override AccessToken GetToken(
            TokenRequestContext requestContext,
            CancellationToken cancellationToken
        )
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }
    }
}
</code></pre>
<blockquote>
<p><em><strong>The above class converts bearer tokens to TokenCredentials. We will require TokenCredentials for authenticating DataLakeServiceclient and return the directory structure for the ADLS Gen2 storage.</strong></em></p>
</blockquote>
<p>For more information as to why this is required, please refer to the following article <a href="https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric"><strong>https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric</strong></a></p>
<p>Add <strong>appsetting.json</strong> to the project</p>
<pre><code class="language-yaml">"AppSettings": { 
    "Chat_DeploymentName": "Deployment Name",
    "EndPoint": "Azure OpenAI endpoint",
    "ApiKey": "Azure OpenAI API key"
}
</code></pre>
<p>In <strong>launchSettings.json</strong>, configure the ports on which the server should listen.</p>
<pre><code class="language-yaml">{
    "$schema": "https://json.schemastore.org/launchsettings.json",
    "profiles": {       
        "https": {
            "commandName": "Project",
            "dotnetRunMessages": true,
            "launchBrowser": false,
            "applicationUrl": "https://localhost:7129;http://localhost:5176",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        }
    }
}
</code></pre>
<p>In the above settings , the application is configured to listen on ports <strong>7129</strong> (HTTPS) and <strong>5176</strong> (HTTP). For this article, we will use <strong>7129</strong> on https.</p>
<h3><strong>Code</strong></h3>
<p>Now that we have all the underlying artifacts in place, add the following code to read the settings from <strong>appsettings.json</strong> in <strong>Program.cs</strong></p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p><strong>Create a application builder</strong></p>
<pre><code class="language-csharp">WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
</code></pre>
<p><strong>Create a IChatClient DI container</strong></p>
<pre><code class="language-csharp">builder.Services.AddHttpClient().AddLogging();

var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

builder.Services.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
                .AsIChatClient()
    )
);

builder.Services.AddSingleton&lt;ChatClientAgent&gt;(
    sp =&gt;
    {
        return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"));
    }
);
</code></pre>
<p><strong>AIFunction</strong></p>
<p>Register an <strong>AIFunction</strong> with name <strong>ReturnDirectories</strong> in the DI container. I am using <strong>FunctionInvokingChatClient</strong> to pass the access token as the parameter to the AIFunction.</p>
<pre><code class="language-csharp">builder.Services.AddSingleton&lt;AIFunction&gt;(
    sp =&gt;
    {
        return AIFunctionFactory.Create(
            async (string ContainerName) =&gt;
                await ReturnContainerDirectories(
                    ContainerName,
                    FunctionInvokingChatClient.CurrentContext.Options.AdditionalProperties[
                        "AccessToken"
                    ].ToString()
                ),
            new AIFunctionFactoryOptions
            {
                Name = "ReturnDirectories",
                Description = "Returns a list of directories"
            }
        );
    }
);
</code></pre>
<p>For more details on FunctionInvokingChatClient you can refer to my article <a href="https://www.azureguru.net/functioninvokingchatclient-for-tool-calling-in-microsoft-agent-framework">here</a>.</p>
<p><strong>Authentication</strong></p>
<p>Authentication mechanism validates the incoming tokens. In this example it checks for two parameters, <strong>ValidIssuer</strong> and <strong>ValidAudience</strong> and ensures that the values for these parameters in the bearer token matches with</p>
<ul>
<li><a href="https://sts.windows.net/%7BTenantId%7D/"><strong>https://sts.windows.net/{TenantId}/</strong></a> and <a href="https://storage.azure.com"><strong>https://storage.azure.com</strong></a></li>
</ul>
<p>and if the claim does not match it rejects the token.</p>
<pre><code class="language-csharp">builder.Services.AddAuthentication().AddJwtBearer(options =&gt;
{
    options.Authority = $"https://login.microsoftonline.com/{TenantId}";
    options.Validate();

    options.TokenValidationParameters = new TokenValidationParameters
    {
         ValidIssuer = "https://sts.windows.net/{TenantId}/",
         ValidAudience = "https://storage.azure.com",
         ValidateIssuer = true,
         ValidateAudience = true
    };
});
</code></pre>
<p><strong>Authorization</strong></p>
<p>Create an authorization policy but before that ensure that request contains authenticated users and then check if the claims contain <strong>"aud</strong>" and <strong>"iss"</strong> and then validate its values.</p>
<pre><code class="language-csharp"> builder.Services.AddAuthorization(options =&gt;
 {
     options.AddPolicy("AzureStorageAccess", policy =&gt;
     {
         policy.RequireAuthenticatedUser();
         policy.RequireClaim("aud", "https://storage.azure.com");
         policy.RequireClaim("iss", $"https://sts.windows.net/{TenantId}/");
     });
 });
</code></pre>
<p><strong>Scalar.cs</strong></p>
<pre><code class="language-csharp">using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

internal sealed class BearerSecuritySchemeTransformer : IOpenApiDocumentTransformer
{
    private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

    public BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider)
    {
        _authenticationSchemeProvider = authenticationSchemeProvider;
    }

    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await _authenticationSchemeProvider.GetAllSchemesAsync();

        if (authenticationSchemes.Any(authScheme =&gt; authScheme.Name == "Bearer"))
        {
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes ??= new Dictionary&lt;string, IOpenApiSecurityScheme&gt;();
            document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
            {
                Type = SecuritySchemeType.Http,
                Scheme = "bearer",
                In = ParameterLocation.Header,
                BearerFormat = "JWT"
            };

            foreach (var operation in document.Paths.Values.SelectMany(path =&gt; path.Operations))
            {
                if (operation.Value.Security == null)
                {
                    operation.Value.Security = new List&lt;OpenApiSecurityRequirement&gt;();
                }
                var securityRequirement = new OpenApiSecurityRequirement
                {
                    [new OpenApiSecuritySchemeReference("Bearer", document)] = []
                };

                operation.Value.Security ??= new List&lt;OpenApiSecurityRequirement&gt;();
                operation.Value.Security.Add(securityRequirement);
            }
        }
    }
}
</code></pre>
<p><strong>Scalar UI</strong></p>
<pre><code class="language-csharp"> builder.Services.AddOpenApi(
    options =&gt;
    {
        options.AddDocumentTransformer&lt;BearerSecuritySchemeTransformer&gt;();
    }
);

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapScalarApiReference(
    options =&gt;
    {
        options.Title = "Scalar API";
        options.DarkMode = true;
        options.Favicon = "path";
        options.DefaultHttpClient = new KeyValuePair&lt;ScalarTarget, ScalarClient&gt;(
            ScalarTarget.CSharp,
            ScalarClient.RestSharp
        );
        options.HideModels = false;
        options.Layout = ScalarLayout.Modern;
        options.ShowSidebar = true;
        options.Authentication = new ScalarAuthenticationOptions
        {
            PreferredSecuritySchemes = new List&lt;string&gt; { "Bearer" }
        };
    }
);
</code></pre>
<p>The above code customizes the Scalar UI to include the Bearer token section.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/db438985-06ab-49f4-a580-d841f41e0ef3.png" alt="Scalar UI" style="display:block;margin:0 auto" />

<p>Register the <strong>OpenAPI</strong> service in the DI container add a Scalar document transformer.</p>
<pre><code class="language-csharp">builder.Services.AddOpenApi(options =&gt;
 {     options.AddDocumentTransformer&lt;BearerSecuritySchemeTransformer&gt;();
 });
</code></pre>
<p>For more details refer to my article on the topic <a href="https://www.azureguru.net/customize-scalar-UI-for-net-api"><strong>here</strong></a>.</p>
<p><strong>ReturnContainerDirectories Function</strong></p>
<p>This function contains arguments container name and token.The token value is passed to the <strong>TokenCredential</strong> object of the <strong>DatalakeServiceClient</strong> which then eventually returns the directory structure for the Azure container through the <strong>TraverseDirectory</strong> function.</p>
<pre><code class="language-csharp">public static Func&lt;string, string, Task&lt;List&lt;string&gt;&gt;&gt; ReturnContainerDirectories = async (
    ContainerName,
    token
) =&gt;
{
    DataLakeServiceClient datalake_Service_Client;
    DataLakeFileSystemClient dataLake_FileSystem_Client;
    string dfsUri = $"https://adlsfilestore.dfs.core.windows.net";
    TokenCredential tokenCredential = new AccessTokenCredential(token.ToString());
    datalake_Service_Client = new DataLakeServiceClient(new Uri(dfsUri), tokenCredential);
    dataLake_FileSystem_Client = datalake_Service_Client.GetFileSystemClient(ContainerName);
    DataLakeDirectoryClient rootDirectory_ = dataLake_FileSystem_Client.GetDirectoryClient("");
    listoutput.Clear();

    return await TraverseDirectory(rootDirectory_);
};
</code></pre>
<p><strong>TraverseDirectory</strong></p>
<p>The function recursively traverses the directory structure of a given Azure container . For more in-depth details on the approach you can refer to my article</p>
<p><a href="https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage"><strong>https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage</strong></a></p>
<pre><code class="language-csharp">public static async Task&lt;List&lt;string&gt;&gt; TraverseDirectory(DataLakeDirectoryClient directoryClient)
{

    await foreach (var item in directoryClient.GetPathsAsync())
    {
        if (item.IsDirectory == true)
        {
            listoutput.Add(item.Name);
            string[] split = item.Name.Split("/");
            var subDir = directoryClient.GetSubDirectoryClient(split.Length == 1 ? split[0] : split[split.Length - 1]);
            await TraverseDirectory(subDir);
        }
    }

    return listoutput;
}
</code></pre>
<p><strong>Login Endpoint</strong></p>
<p>The access token issued by Entra ID which is part of the OIDC flow and is passed to this endpoint.</p>
<pre><code class="language-csharp">app.MapPost("/login", () =&gt;
{
return Results.Ok(new { token = Security.Authentication.ReturnAuthenticationResult() });
}).WithOpenApi();
</code></pre>
<p><strong>Chat Endpoint</strong></p>
<p>The <strong>chat</strong> endpoint is the most crucial piece of code.</p>
<pre><code class="language-csharp">app.MapGet("/chat", async (string request, HttpContext httpcontext, ClaimsPrincipal claims) =&gt;
 {
     var aud = claims.FindFirst("aud").Value;
     var issuer = claims.FindFirst("iss").Value;
     var chatclientlist = app.Services.GetRequiredKeyedService&lt;IChatClient&gt;("ChatClient");

     var aifunctions = app.Services.GetServices&lt;AIFunction&gt;();

     List&lt;AITool&gt; functions = new(aifunctions);

     if (aud == "https://storage.azure.com" &amp;&amp; issuer == $"https://sts.windows.net/{TenantId}/")
     {
         var agent = chatclientlist.AsAIAgent(new ChatClientAgentOptions
         {
             ChatOptions = new ChatOptions
             {
                 Tools = functions,
                 Instructions = "You return list of Azure directories for the given container",
                 AdditionalProperties = new AdditionalPropertiesDictionary {["AccessToken"] = httpcontext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "") }
             }
         }
            );
         AgentResponse response = await agent.RunAsync(request);              

         return Results.Ok(response.Text);
     }

     return Results.Unauthorized();

 }).RequireAuthorization("AzureStorageAccess");

app.Run();
</code></pre>
<p>Let's break down the code step by step.</p>
<p>First, retrieve the audience and issuer values from JWT claims.</p>
<pre><code class="language-csharp">var aud = claims.FindFirst("aud").Value;
var issuer = claims.FindFirst("iss").Value;
</code></pre>
<p>Then, retrieve a service of type <strong>IChatClient</strong> from the DI container that was registered earlier.</p>
<pre><code class="language-csharp">var chatclientlist = app.Services.GetRequiredKeyedService&lt;IChatClient&gt;("ChatClient");
</code></pre>
<p>Get a list of all <strong>AIFunction</strong> from the DI container. In our case we only have one AIFunction i.e <strong>ReturnDirectories</strong>.</p>
<pre><code class="language-csharp">var aifunctions = app.Services.GetServices&lt;AIFunction&gt;();
List&lt;AITool&gt; functions = new(aifunctions);
</code></pre>
<p>Validate the <strong>aud</strong> and <strong>issuer(iss)</strong> values. Then create an <strong>AIAgent</strong> from the <strong>IChatClient</strong> instance <strong>chatclientlist</strong> declared earlier.</p>
<p>Next, configure <strong>ChatOptions</strong> with <strong>Tools</strong> and set <strong>AdditionalProperties</strong> AccessToken that is derived from <strong>HttpContext</strong>. AdditionalProperties values are accessed through <strong>FunctionInvokingChatClient</strong> in the <strong>AIFunction</strong>.</p>
<p>Pass the request to the agent and return the response.</p>
<pre><code class="language-csharp">if (aud == "https://storage.azure.com" &amp;&amp; issuer == $"https://sts.windows.net/{TenantId}/")
{
    var agent = chatclientlist.AsAIAgent(
        new ChatClientAgentOptions
        {
            ChatOptions = new ChatOptions
            {
                Tools = functions,
                Instructions = "You return list of Azure directories for the given container",
                AdditionalProperties = new AdditionalPropertiesDictionary
                {
                    ["AccessToken"] = httpcontext.Request.Headers["Authorization"]
                        .ToString()
                        .Replace("Bearer ", "")
                }
            }
        }
    );
    AgentResponse response = await agent.RunAsync(request);

    return Results.Ok(response.Text);
}
</code></pre>
<p>I first logged in as <strong>sachin.nandanwar @ azureguru.net</strong> and passed the following prompt to the agent.</p>
<pre><code class="language-plaintext">Give me list of directories from the container customers.
</code></pre>
<p>the agent returns the list of all the directories in the <strong>customers</strong> container.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/87668d2e-8d3f-4ba1-93a6-36ab876c8431.png" alt="Microsoft Agent Framework and Azure services" />

<p>But when I logged in as <strong>sachin_nandanwar @ azureguru.net</strong> as expected, the access to the directories was restricted.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7742afad-1919-480b-b27f-aaa114fca80d.png" alt="Microsoft Agent Framework and Azure services" style="display:block;margin:0 auto" />

<h3>Execution</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d62e74f6-0f46-459a-96d3-b5f34cbacb29.gif" alt="Microsoft Agent Framework and Azure services" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>By combining the Microsoft Agent Framework with Microsoft Entra ID, you can build AI agents that securely access Azure services.</p>
<p>The agent uses OAuth 2.0 access tokens issued on behalf of the signed-in user that ensures that every operation is performed within the user's identity and the user RBAC permissions.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Federated Credentials with User-Managed Identity to Access Azure Services through GitHub actions]]></title><description><![CDATA[The tittle sounds confusing right ?
So how to access Azure services with federated credentials that are tied with User Managed Identities ? Even I was skeptical if it is even possible because its more]]></description><link>https://www.azureguru.net/federated-credentials-with-user-managed-identity-to-access-azure-services-through-github-actions</link><guid isPermaLink="true">https://www.azureguru.net/federated-credentials-with-user-managed-identity-to-access-azure-services-through-github-actions</guid><category><![CDATA[Federated Identity]]></category><category><![CDATA[azure-security]]></category><category><![CDATA[Azure Managed Identities]]></category><category><![CDATA[ManagedIdentity ]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[GitHub Actions]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Tue, 28 Jul 2026 16:57:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/0c712a3c-2cbb-41ee-a3fd-b98d27f45e6e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The tittle sounds confusing right ?</p>
<p>So how to access Azure services with federated credentials that are tied with User Managed Identities ? Even I was skeptical if it is even possible because its more common and widely known that federated identity credentials (OIDC federation) are tied with service principal and not managed identities.</p>
<p>In this article I am going to delve into details on how to achieve this. But before that I strongly recommend reading the following article</p>
<p><a href="https://www.azureguru.net/configuring-federated-credentials-in-microsoft-azure-using-github-actions">https://www.azureguru.net/configuring-federated-credentials-in-microsoft-azure-using-github-actions</a></p>
<p>that I published last year, which provides a detailed walkthrough of configuring federated credentials using GitHub actions as IdP (Identity Provider) through service principal.</p>
<p>Before we set up and test GitHub actions lets first ensure that a defined <strong>User Managed Identity</strong> can access the <strong>Azure ADLS GEN2</strong> storage. For that lets create an <strong>Azure Function</strong> and use an user managed identity in the Azure Function to access Azure storage.</p>
<h3>User Managed Identity</h3>
<p>Create a new user managed identity or use an existing one. I created one with name <strong>Federated_Credentials_Function_Identity</strong> .</p>
<p>In the next step grant role access <strong>Storage Blob Data Owner</strong> to the created Managed Identity that needs access to the Azure storage.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/af087e1f-5953-48a6-8205-61d5edb22727.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>You could also grant <strong>Storage Blob Data Contributor</strong> role but the <strong>Storage Blob Data Owner</strong> role has POSIX access control (ACL access) that auto grants all the (r-w-x) privileges to the owner for all the underlying objects under the container.</p>
<p>For instance in the following screenshot we can see that the owner i.e. the <strong>Storage Blob Data Owner</strong> was auto assigned the (r-w-x) access .</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/029f995d-6a2d-4686-a067-881e34696c04.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>Note down the <strong>ClientID</strong> of the created User Managed Identity. We will need it to reference in our Azure Function.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c5e69c12-0e3a-41af-a138-c52131779381.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<h3>Azure Function Code</h3>
<p>Create a new Azure function and add the following code.</p>
<blockquote>
<p>Before that ensure that you have added all the required project references.</p>
</blockquote>
<pre><code class="language-csharp">using Azure.Identity;
using Azure.Storage.Files.DataLake;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

namespace AzureFunction;

public class Function1
{
    private readonly ILogger&lt;Function1&gt; _logger;
    public static List&lt;string&gt; listoutput = new();

    public Function1(ILogger&lt;Function1&gt; logger)
    {
        _logger = logger;
    }

    [Function("Function1")]
    public async Task&lt;OkObjectResult&gt; Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
    {
        {
            string storageAccount = "Storage Account";
            string fileSystemName = "Storage Container";           
            string userAssignedClientId = "Managed Identity ClientID";
            DataLakeServiceClient datalake_Service_Client;
            DataLakeFileSystemClient dataLake_FileSystem_Client;
            try
            {
                string dfsUri = $"https://{storageAccount}.dfs.core.windows.net";

                var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
                {
                    ManagedIdentityClientId = userAssignedClientId,
                    
                });

                datalake_Service_Client = new DataLakeServiceClient(new Uri(dfsUri), credential);
                dataLake_FileSystem_Client = datalake_Service_Client.GetFileSystemClient(fileSystemName);
                DataLakeDirectoryClient rootDirectory_ = dataLake_FileSystem_Client.GetDirectoryClient("");
                listoutput.Clear();

                return new OkObjectResult(new
                {
                    Output = TraverseDirectory(rootDirectory_)
                });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error accessing ADLS Gen2");

            }

            return new OkObjectResult("");

        }

    }

    public static async Task&lt;List&lt;string&gt;&gt; TraverseDirectory(DataLakeDirectoryClient directoryClient)
    {

        await foreach (var item in directoryClient.GetPathsAsync())
        {
            if (item.IsDirectory == true)
            {
                listoutput.Add(item.Name);
                string[] split = item.Name.Split("/");
                var subDir = directoryClient.GetSubDirectoryClient(split.Length == 1 ? split[0] : split[split.Length - 1]);
                await TraverseDirectory(subDir);
            }
        }

        return listoutput;
    }
}
</code></pre>
<blockquote>
<p>The above code returns only the top level directories of the given container. It is a simplified version of the code from the article <a href="https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage">here</a> that recursively returns hierarchical directory structure for a given Azure storage container.</p>
</blockquote>
<p>Deploy the above code to Azure, execute it and verify that it returns all the subdirectories within the specified container.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d44941f3-5a56-4906-8ea2-ac7e861ae68a.gif" alt="" style="display:block;margin:0 auto" />

<p>Following is the output from the execution of the Azure function.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/af4c59db-d15a-48af-aadc-6fec72781157.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>Now that <strong>Federated_Credentials_Function_Identity</strong> which is an User Managed Identity can access the Azure storage , in the next step we can go ahead and create federated credentials tied to this specific User Managed Identity.</p>
<p>Go back to the created User Managed Identity on the Azure portal and click <strong>Federated Credentials option</strong> followed by <strong>Add Credentials</strong> .</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/4e4e8a4e-3d49-4162-b1d1-94973b2c7b83.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>In the next window, select <strong>GitHub Actions deploying Azure resources</strong> option under <strong>Federated credential scenarios</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/cb27e8c9-2420-4e79-89a5-3f0d327ce743.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>In the next step, enter the following details on <strong>Edit Federated Credential</strong> page</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7f0cf3a6-6853-4cfb-9f01-e521c83f0737.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<ul>
<li><p><strong>Organization</strong> is the username or organization name of the repo</p>
</li>
<li><p><strong>Repository</strong> is the name of the repository that you want to use. By default it also will be the name of the Federated Credentials.</p>
</li>
<li><p><strong>Entity</strong> is the type of entity. Following are the available options .</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f3c84986-516c-48cd-8909-4a6b3eb3cc10.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<ul>
<li><p><strong>Branch</strong> specifies the repo branch through which the request would be trusted by federated credentials.</p>
</li>
<li><p><strong>Audience</strong> keep it to default</p>
</li>
</ul>
<p>Click Update and double check if the Federated Credential is created.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/6361aaa0-95c7-48cc-82d8-82512e96a83b.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>In this example the Federated Credentials is named <strong>GitHubActions</strong></p>
<p>We now need to set the environment variables for the GitHub repository.</p>
<p>Go to the GitHub repository and under <strong>Secrets and variables</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/43d1364b-bbf5-40aa-8238-35c944cdae39.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>and under <strong>Actions</strong> declare the following Repository secrets that you see in the screenshot below and enter their corresponding values</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/af0faa5a-0754-41d4-bfa4-383ab9e4c612.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>Fetch the values for <strong>Azure_Client_ID</strong> and <strong>Azure_Subscription_ID</strong> from the User Managed Identity that was set for GitHub actions. The value for secret <strong>Azure_Tenant_Id</strong> will be the Azure Tenant Id.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/ae66db8f-f7ea-49f6-9c5c-4e4b7a5c90f9.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>The <strong>Azure_Storage_Account</strong> value should be the ADLS Gen2 storage account from which you want to retrieve the directory structure.</p>
<p>In next step , Go to the GitHub repo and under <strong>Actions</strong> select <strong>New WorkFlow</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/6e6234ae-19f5-4358-9bf3-1a841b833736.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>and then under <strong>Simple Workflow</strong> click <strong>Configure</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f0987a51-bbec-45b5-9f6c-829375444c7b.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>and add the following code to the <strong>*. yml</strong> file. In this example the file is named <strong>Workflow.yml</strong></p>
<pre><code class="language-yaml">name: GitHubActions Azure Federated Credentials Demo

on:
  push:
    branches:
      - main

jobs:
  auth-azure:
    runs-on: ubuntu-latest

    permissions:
      id-token: write    
      contents: read

    steps:
      - name: 'Login to Azure with OIDC federated credentials'
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Show success message
        run: echo "Successfully authenticated for Azure using federated credentials!"

      - name: Get List of Folders
        uses: azure/cli@v2
        with:
          azcliversion: latest
          inlineScript: |
            echo "Fetching all directories..."
            
              FOLDERS=$(az storage fs file list \
            --account-name "${{ secrets.AZURE_STORAGE_ACCOUNT }}" \
            --file-system customers \
            --auth-mode login \
            --query "[?isDirectory].name" \
            --output table)
              
            echo "Available folders:"
            echo "$FOLDERS"
</code></pre>
<p>The code above logins into the Azure tenant based on the Environment variables set for the repository and lists recursively outputs all the directories for the given container (<strong>customers</strong> container in this example).</p>
<p>Commit the changes</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d78146c0-6c2d-4dd7-8629-0b4cedf47396.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<p>As expected, the output contains all the underlying directories of container <strong>customers.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/4e6197dd-dea4-4a28-8984-d96a63037dfe.png" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<h3>Execution&gt;&gt;</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/38c3ff5b-b266-4c42-b9ea-001e5f6b65fa.gif" alt="Microsoft Entra Federated Credentials" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>In this article, we explored how to configure Federated Credentials for a User Managed Identity, enabling password less authentication to Azure services for external Identity providers. By leveraging workload identity federation, you can grant external workloads (like GitHub Actions, Kubernetes workloads) access to Azure resources using short-lived tokens through federated credentials.</p>
<p>I hope this guide helps you to get started on implementation of federated credentials with User Managed Identities in your Azure environments.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Understanding Delegated Tokens vs Application Tokens through Claims-Based Authorization in Microsoft Entra ID ]]></title><description><![CDATA[My earlier article on securing Azure Storage in ASP.NET core Minimal API's extensively leveraged RBAC access control and through Microsoft Entra ID for user sign-in through OpenID Connect (OIDC) to au]]></description><link>https://www.azureguru.net/understanding-delegated-tokens-vs-application-tokens-through-claims-based-authorization-in-microsoft-entra-id</link><guid isPermaLink="true">https://www.azureguru.net/understanding-delegated-tokens-vs-application-tokens-through-claims-based-authorization-in-microsoft-entra-id</guid><category><![CDATA[minimal-apis]]></category><category><![CDATA[OIDC]]></category><category><![CDATA[azure rbac]]></category><category><![CDATA[Azure]]></category><category><![CDATA[azure-security]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Thu, 23 Jul 2026 22:33:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/067da2e0-35a9-4164-84e8-6f7b2a367b21.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My earlier <a href="https://www.azureguru.net/securing-azure-storage-in-asp-net-core-minimal-api-s-with-microsoft-entra-id-and-openid-connect-oidc">article</a> on securing Azure Storage in ASP.NET core Minimal API's extensively leveraged RBAC access control and through <strong>Microsoft Entra ID</strong> for user sign-in through <strong>OpenID Connect (OIDC)</strong> to authenticate and authorized users.</p>
<p>But sometimes there can be scenarios where maintaining RBAC access cannot be feasible . Imagine managing an Azure tenant in an organization that has 1,000+ employees and hundreds of storage accounts and Azure services and where every user requires different access to these services. Though manageable through RBAC but managing and maintaining such a large number of roles can be challenging and cause operational overhead.</p>
<p>An alternative solution can be a claim based authorization through Microsoft Entra ID issued access tokens along with authorization policies.</p>
<p>Instead of assigning Azure RBAC roles to every user for every resource, Microsoft Entra ID can issue access tokens containing claims that describe the user's permissions. The application can then evaluate these claims through authorization policies and determine if the user is allowed to perform a particular operation.</p>
<p>In this article, we will build an <strong>ASP.NET Core Minimal API</strong> that authenticates users with <strong>Microsoft Entra ID.</strong> Once the claims are validated from the access token, the API will securely accesses <strong>Azure ADLS Gen2</strong> Storage through <strong>DataLakeServiceClient</strong> (i.e. return a list of all the directories in a given Azure ADLS Gen2 storage container) on behalf of the user, based on access granted to him through <strong>App roles</strong> in Microsoft Entra.</p>
<p>A sample claim based access token might contain application-specific claims similar to the following:</p>
<pre><code class="language-plaintext">{
  "sub": "1234567890",
  "name": "ABC",
   "roles": [
        "Storage.Access"
      ],
  "permissions": [
    "storage.read",
    "storage.write"
  ]
}
</code></pre>
<p>In the above example, the permissions claim is interpreted by the application to determine whether the user is authorized to perform read or write operations on the storage based on the roles .</p>
<blockquote>
<p>Instead of granting RBAC role access to individual user, only the service principal is granted RBAC role on the Azure service, in our case the access is granted on Azure ADLS Gen2 storage to the service principal.</p>
</blockquote>
<p>The application authenticates on the Azure Storage using a service principal (or managed identity) that already has the necessary Azure RBAC permissions.</p>
<blockquote>
<p>In such a scenario we require two access tokens. The first token will contain the user claims and the second token will have the necessary scope permissions to perform the underlying actions on the Azure service (in this case Azure ADLS Gen2 storage) on behalf of the user.</p>
</blockquote>
<p>The application first validates the claims in the user access token and once its validated it uses the service principal token to authenticate the Azure storage based on the audience value of the delegated scopes assigned to it in Microsoft Entra ID.</p>
<p>The initial steps to be performed are similar to the article below i.e. exposing a service principal API ,defining custom scopes and finally granting permissions to the exposed API.</p>
<p><a href="https://www.azureguru.net/azure-services-authentication-through-microsoft-managed-identity">https://www.azureguru.net/azure-services-authentication-through-microsoft-managed-identity</a></p>
<h3>Flow</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f4181c61-286c-4d5f-84f3-6ba74f242339.png" alt="" style="display:block;margin:0 auto" />

<p>To add more context to the above flow, the scopes used will be</p>
<pre><code class="language-html">api://{ApplicationId} and https://storage.azure.com
</code></pre>
<p>created through the <strong>Expose API</strong> option and <strong>Delegated Permissions</strong> for the service principal.</p>
<h3>SetUp</h3>
<p>There are two users :</p>
<ul>
<li><p><strong>(Sachin.Nand)</strong> <a href="mailto:sachin.nandanwar@azureguru.net">sachin.nandanwar@azureguru.net</a></p>
</li>
<li><p><strong>(Sachin Nandanwar)</strong> sachin_<a href="mailto:nandanwar@azureguru.net">nandanwar@azureguru.net</a></p>
</li>
</ul>
<p>Only <strong>(Sachin.Nand)</strong> <a href="mailto:sachin.nandanwar@azureguru.net">sachin.nandanwar@azureguru.net</a> will be assigned the necessary <strong>App Role</strong> accesses for the service principal.</p>
<blockquote>
<p><em><strong>Ensure that you the Scalar set up is configured in your ASP.NET core project. For more details refer to my following article</strong></em></p>
</blockquote>
<p><a href="https://www.azureguru.net/customize-scalar-UI-for-net-api"><strong>https://www.azureguru.net/customize-scalar-UI-for-net-api</strong></a></p>
<p>Also ensure that you have a thorough understanding of Minimal API's and implementation of <strong>ClaimPrincipal, Authentication</strong> and <strong>Authorization</strong> for Minimal API's. Please refer to my article below to have a better understanding of the topic.</p>
<p><a href="https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core"><strong>https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core</strong></a></p>
<p>You can ignore the custom JWToken aspect from the above article as in that article the focus was on creating custom JWTokens.</p>
<p>Create a Service Principal and grant the <strong>Azure Storage</strong> delegated permissions. I created one called <strong>ADLS GEN2 Service Principal.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/769d615c-a697-4527-8aa9-85681f3d8e7c.png" alt="" style="display:block;margin:0 auto" />

<p>Grant <strong>Storage Blob Data Owner</strong> role access to the service principal for the storage service.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/074a71dc-0b44-44a0-aacb-f2a816c1d266.png" alt="" style="display:block;margin:0 auto" />

<p>In the next step ,under the <strong>Expose an API</strong> option, click the Add button. By default the Application ID URI is the ClientID of the Service Principal.</p>
<p>The format is api://<strong>{ApplicationId}</strong>.</p>
<blockquote>
<p><strong>ApplicationID is the Service Principal ClientId</strong></p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/eb968e99-761b-4552-9b39-4b3d61c027f3.png" alt="" style="display:block;margin:0 auto" />

<p>Cross check the Application ID URI with the Client ID of the service principal.</p>
<p>In the next step define a new scope.</p>
<p>Click <strong>“Add a scope”</strong> option and enter <strong>storage_read</strong> as the scope name.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/fcee0ab0-268d-4572-8527-3bac7e717cb3.png" alt="" style="display:block;margin:0 auto" />

<p>Once added, the scope should be visible on the <strong>Expose an API</strong> page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/548cc029-3d12-4409-9e1c-4d7222c2917c.png" alt="" style="display:block;margin:0 auto" />

<p>In the next step , under <strong>API permissions</strong> , click <strong>Add a permission</strong> and select the API that was created in the previous step.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/2b39db6f-bd50-45af-996c-60af6b378061.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>This might sound counterintuitive to see that we have to assign permissions for an API to the service principal that created that API in the first place. But that’s the way it is. Not doing so results in an Unauthorized access error.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/47cb090f-3ae3-4fc4-9d23-1d490dbb2aa8.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>Please ensure that Azure Storage Delegated permission is also assigned to service principal.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/024db801-9161-4a8c-af24-cd9f0ff48f65.png" alt="" style="display:block;margin:0 auto" />

<p>In the next step, create an App Role.</p>
<blockquote>
<p>App roles (Application Roles) is a feature of Role-Based Access Control (RBAC) that defines what actions a user, group or service is allowed to perform within a specific application at the apps identity level.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8bd5f304-7bf0-4198-99ed-13d20b6e6cd3.png" alt="" style="display:block;margin:0 auto" />

<p>App role name is <strong>AzureStorage</strong> and value is <strong>AzureStorage.Read</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/eefa4723-6d5a-4e7e-ba54-e380c3f06bd3.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c0b741b4-5855-4273-b998-3f226e1ee4b9.png" alt="" style="display:block;margin:0 auto" />

<p>The role <strong>AzureStorage</strong> created will be part of the access token. This is very crucial aspect to validate user permissions.</p>
<p>The next step is to enable <strong>Assignment required</strong> property.</p>
<p>Browse to <strong>Entra ID</strong> &gt; <strong>Enterprise apps</strong> &gt;&gt; <strong>All applications</strong></p>
<p>Select your application. In our case it is <strong>ADLS Gen2 Service Principal</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e22925f4-0ec9-453a-a7b5-84dd2e667585.png" alt="" style="display:block;margin:0 auto" />

<p>Ensure that <strong>Assignment required?</strong> is set to <strong>Yes</strong> . By default it is <strong>No</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/38aaa21a-abb7-4762-8d35-41c361b1cf32.png" alt="" style="display:block;margin:0 auto" />

<p>Navigate to <strong>Users and groups</strong> in the same page add click <strong>Add user/group</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/4d7bf74e-8e1d-4640-8353-148da7fed0bd.png" alt="" style="display:block;margin:0 auto" />

<p>Click <strong>None Selected</strong> and Search for user to whom you want to grant the Assignements.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/65f94c4b-72fa-4e64-9998-eb554a247c6a.png" alt="" style="display:block;margin:0 auto" />

<p>Grant assignment to the selected user . In this case I will assign it only to the user <strong>Sachin Nand</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/863fd1e3-8e1a-440d-bf6c-496932bf7c64.png" alt="" style="display:block;margin:0 auto" />

<p>Now that the set up and the required prerequisite is in place , lets move on to the code.</p>
<h3><strong>Code</strong></h3>
<p>Add the following references to your ASP.NET core project</p>
<pre><code class="language-csharp">dotnet add package Azure.Core;
dotnet add package Azure.Storage.Files.DataLake;
dotnet add package Microsoft.IdentityModel.Tokens;
dotnet add package Scalar.AspNetCore;
dotnet add package System.Security.Claims;
</code></pre>
<p>We first define an <strong>Authentication</strong> class that validates the user's sign-in and requested scopes with Microsoft Entra ID and then returns a JWT token.</p>
<blockquote>
<p><em><strong>Unlike implementing custom JWT tokens which I demonstrated in my earlier article</strong></em> <a href="https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core"><em><strong>here</strong></em></a><em><strong>, with Microsoft Entra ID-issued tokens there is no need for generating, signing, rotating or validating JWTs ourselves. Microsoft Entra ID auto handles all these aspects at its end.</strong></em></p>
</blockquote>
<p><strong>Authentication.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Microsoft.Identity.Client;
using System.IdentityModel.Tokens.Jwt;

namespace Security {
  internal class Authentication {  
  
    public static string TenantId = "Tenant Id";
    public static string ClientId = "Client Id of the Service Principal";
    public static string ClientSecret = "Client secret of the Service Principal"     

    private static IEnumerable &lt; string &gt; scopes = new List &lt; string &gt; {
      "https://storage.azure.com/.default",
      "api://{ApplicationID}/.default"
    };

    private static string Authority = $"https://login.microsoftonline.com/{TenantId}";
    private static string RedirectURI = "http://localhost";

    public async static Task &lt; JwtSecurityToken[] &gt; ReturnAuthenticationResult() {
      string AccessToken;
      IPublicClientApplication PublicClientApplication_claims =
        PublicClientApplicationBuilder.Create(clientId)
        .WithAuthority(Authority)
        .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
        .WithRedirectUri(RedirectURI).Build();

      var accounts_claims = await PublicClientApplication_claims.GetAccountsAsync();
      AuthenticationResult claims_result, storage_result;
      var scopeList = scopes.ToList();
      try {

        claims_result = await PublicClientApplication_claims.AcquireTokenSilent(new [] {
            scopeList.ElementAt(0)
          }, accounts_claims.First())
          .ExecuteAsync()
          .ConfigureAwait(false);

      } catch {
        claims_result = await PublicClientApplication_claims.AcquireTokenInteractive(new [] {
            scopeList.ElementAt(0)
          })
          .ExecuteAsync()
          .ConfigureAwait(false);

      }

      JwtSecurityToken claims_token = new JwtSecurityToken(claims_result.AccessToken);

      var app = ConfidentialClientApplicationBuilder
        .Create(ClientId)
        .WithClientSecret(ClientSecret)
        .WithAuthority(Authority)
        .Build();

      storage_result = await app
        .AcquireTokenForClient(new [] {
          scopeList.ElementAt(1)
        })
        .ExecuteAsync();

      JwtSecurityToken storage_token = new JwtSecurityToken(storage_result.AccessToken);

      return [claims_token, storage_token];

    }
  }
}
</code></pre>
<blockquote>
<p><em><strong>For brevity I have defined ClientId, ClientSecret and TenantId in variables. Ideally they should be placed in a config file and ClientSecret stored to a secured location like Environment variables or Key vault and their values fetched from there.</strong></em></p>
</blockquote>
<p>Few important aspects of the above code.</p>
<p><strong>ReturnAuthenticationResult</strong> function returns two tokens (<strong>storage token</strong> and <strong>claim token</strong>) through array <strong>JwtSecurityToken[]</strong> .</p>
<pre><code class="language-csharp"> return [claims_token, storage_token];
</code></pre>
<p>The scopes used are : <strong><a href="https://storage.azure.com/.default">https://storage.azure.com/.default</a></strong> for the storage token and <strong>api://{ApplicationID}/.default</strong> for the claims token.</p>
<blockquote>
<p>claims_token is a delegated user token (idtyp = user) and storage_token is a service principal token (idtyp = app)</p>
</blockquote>
<p>Why two different token types (<strong>idtyp = user</strong> and <strong>idtyp = app</strong>) is required ?</p>
<p>Its because the user no longer has any RBAC role access to the storage but the service principal has (we had granted that earlier).So only service principal token will be authorized for any access to the storage.</p>
<p><strong>AccessTokenCredential.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Azure.Core;
using System.IdentityModel.Tokens.Jwt;

namespace AccesTokenCredentials
{
    public class AccessTokenCredential : Azure.Identity.ClientSecretCredential
    {
        public AccessTokenCredential(string accessToken)
        {
            AccessToken = accessToken;
        }

        private string AccessToken;

        public AccessToken FetchAccessToken()
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

        public override ValueTask&lt;AccessToken&gt; GetTokenAsync(
            TokenRequestContext requestContext,
            CancellationToken cancellationToken
        )
        {
            return new ValueTask&lt;AccessToken&gt;(FetchAccessToken());
        }

        public override AccessToken GetToken(
            TokenRequestContext requestContext,
            CancellationToken cancellationToken
        )
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }
    }
}
</code></pre>
<blockquote>
<p><em><strong>The above class converts bearer tokens to TokenCredentials. We will require TokenCredentials for authenticating DataLakeServiceclient and return the directory structure for the ADLS Gen2 storage.</strong></em></p>
</blockquote>
<p>For more information as to why this is required, please refer to the following article <a href="https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric">https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric</a></p>
<p><strong>Program.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">
using AccesTokenCredentials;
using Azure.Core;
using Azure.Storage.Files.DataLake;
using Microsoft.IdentityModel.Tokens;
using Scalar.AspNetCore;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

public static class Program
{
     public static string TenantId = "Tenant Id";
     public static string ClientId = "Client Id of the Service Principal";
     public static string StorageAccount = "ADLS GEN2 Storage Account"; 
    public static List&lt;string&gt; listoutput = new();
    public static JwtSecurityToken[] tokens;
    private static async Task Main(string[] args)
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
        builder.Services.AddAuthentication().AddJwtBearer(options =&gt;
        {
            options.Authority = $"https://login.microsoftonline.com/{TenantId}";
            options.MapInboundClaims = false;
            options.Validate();
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = $"https://sts.windows.net/{TenantId}/",
                ValidAudience = $"api://{ClientId}",
                ValidateIssuer = true,
                ValidateAudience = true
            };
        });

        builder.Services.AddAuthorization(options =&gt;
        {
            options.AddPolicy("AzureStorageAccess", policy =&gt;
            {
                policy.RequireAuthenticatedUser();
                policy.RequireClaim("aud", $"api://{ClientId}");
                policy.RequireClaim("iss", $"https://sts.windows.net/{TenantId}/");
                policy.RequireClaim("scp", "storage.read");
                policy.RequireClaim("roles", "AzureStorage.Read");

            });

        });

        builder.Services.AddOpenApi(options =&gt;
         {
             options.AddDocumentTransformer&lt;BearerSecuritySchemeTransformer&gt;();
         });

        WebApplication app = builder.Build();

        if (app.Environment.IsDevelopment())
        {
            app.MapOpenApi();
        }

        app.MapScalarApiReference(options =&gt;
        {
            options.Title = "Scalar API";
            options.DarkMode = true;
            options.Favicon = "path";
            options.DefaultHttpClient = new KeyValuePair&lt;ScalarTarget, ScalarClient&gt;(ScalarTarget.CSharp, ScalarClient.RestSharp);
            options.HideModels = false;
            options.Layout = ScalarLayout.Modern;
            options.ShowSidebar = true;
            options.Authentication = new ScalarAuthenticationOptions
            {
                PreferredSecuritySchemes = new List&lt;string&gt; { "Bearer" }
            };
        });

        app.UseHttpsRedirection();


        app.MapPost("/login", async () =&gt;
         {     
             tokens = await Security.Authentication.ReturnAuthenticationResult();
             return Results.Ok(tokens);
         });


        app.MapGet("/accessazurestorage", async (ClaimsPrincipal claims, HttpContext context) =&gt;
        {
            var audience = claims.FindFirst("aud")?.Value;
            var issuer = claims.FindFirst("iss")?.Value;
            var scp = claims.FindFirst("scp")?.Value;
            var roles = claims.FindFirst("roles")?.Value;

            if (audience != $"api://{ClientId}")
            {
                throw new UnauthorizedAccessException("Unauthorized access !!!");
            }

            if (issuer != $"https://sts.windows.net/{TenantId}/")
            {
                throw new UnauthorizedAccessException("Unauthorized access !!!");
            }


            if (scp != "storage.read")
            {
                throw new UnauthorizedAccessException("Unauthorized access !!!");
            }

            var token = context.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");

            DataLakeServiceClient datalake_Service_Client;
            DataLakeFileSystemClient dataLake_FileSystem_Client;
            string dfsUri = $"https://{StorageAccount}.dfs.core.windows.net";
            TokenCredential tokenCredential = new AccessTokenCredential(tokens[0].RawData.ToString());
            datalake_Service_Client = new DataLakeServiceClient(new Uri(dfsUri), tokenCredential);
            dataLake_FileSystem_Client = datalake_Service_Client.GetFileSystemClient("customers");
            DataLakeDirectoryClient rootDirectory_ = dataLake_FileSystem_Client.GetDirectoryClient("");
            listoutput.Clear();
            return Results.Ok(new
            {
                Output = TraverseDirectory(rootDirectory_) 

            });
        })
        .RequireAuthorization("AzureStorageAccess");

        app.MapGet("/fakeaccessazurestorage", () =&gt;
        {
        }).RequireAuthorization("AzureStorageAccess");

        app.Run();
    }

    public static async Task&lt;List&lt;string&gt;&gt; TraverseDirectory(DataLakeDirectoryClient directoryClient)
    {

        await foreach (var item in directoryClient.GetPathsAsync())
        {
            if (item.IsDirectory == true)
            {
                listoutput.Add(item.Name);
                string[] split = item.Name.Split("/");
                var subDir = directoryClient.GetSubDirectoryClient(split.Length == 1 ? split[0] : split[split.Length - 1]);
                await TraverseDirectory(subDir);
            }
        }

        return listoutput;
    }
}
</code></pre>
<p>Lets dissect the major aspects of the above code</p>
<p>We declare an array of type <strong>JwtSecurityToken[]</strong> which is used to store the values for <strong>claims_token</strong> and <strong>storage_token</strong> returned by the <strong>Authentication</strong> class defined earlier.</p>
<pre><code class="language-csharp"> public static JwtSecurityToken[] tokens;
</code></pre>
<p><em><strong>Authentication</strong></em> <em>&gt;&gt;</em></p>
<pre><code class="language-csharp">builder.Services.AddAuthentication().AddJwtBearer(options =&gt;
{
    options.Authority = $"https://login.microsoftonline.com/{TenantId}";
    options.MapInboundClaims = false;
    options.Validate();
    options.TokenValidationParameters = new TokenValidationParameters
    {
         ValidIssuer = "https://sts.windows.net/{TenantId}/",
         ValidAudience = "https://storage.azure.com",
         ValidateIssuer = true,
         ValidateAudience = true
    };
});
</code></pre>
<p>Authentication mechanism is used to validate the incoming tokens. It checks for two parameters, <strong>ValidIssuer</strong> and <strong>ValidAudience</strong> and ensures that the values for these parameters in the bearer token matches with</p>
<ul>
<li><p><a href="https://sts.windows.net/%7BTenantId%7D/"><strong>https://sts.windows.net/{TenantId}/</strong></a> and <a href="https://storage.azure.com"><strong>https://storage.azure.com</strong></a></p>
</li>
<li><p><strong>options.MapInboundClaims = false</strong> . This setting is very important wrt validating the claims .Check the following screenshot</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/617f530e-9ecd-4e72-9b0c-0c1496401bdc.png" alt="" style="display:block;margin:0 auto" />

<p>The claims and the scope format above is a standard JWT format . If <strong>options.MapInboundClaims = true</strong> (which the default) , the claim check through the <strong>HttpContext</strong> will fail . For example something like</p>
<pre><code class="language-csharp">var roles = claims.FindFirst("roles")?.Value
</code></pre>
<p>roles will always be null as it checks for the keyword <strong>"roles"</strong></p>
<p>But setting <strong>options.MapInboundClaims = false</strong> the format changes</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/66869bf9-8fd4-47ee-a8a3-9a6b20d0b2b4.png" alt="" style="display:block;margin:0 auto" />

<p><em><strong>Authorization</strong></em> <em>&gt;&gt;</em></p>
<pre><code class="language-csharp">   builder.Services.AddAuthorization(options =&gt;
  {
      options.AddPolicy("AzureStorageAccess", policy =&gt;
      {
          policy.RequireAuthenticatedUser();
          policy.RequireClaim("aud", $"api://{ClientId}");
          policy.RequireClaim("iss", $"https://sts.windows.net/{TenantId}/");
          policy.RequireClaim("scp", "storage.read");
          policy.RequireClaim("roles", "AzureStorage.Read");
      });
  });
</code></pre>
<p>Create an authorization policy but before that ensure that request contains authenticated users and then check if the claims contain "aud", "scp", "roles", "iss" and then validate its values.</p>
<p><strong>Scalar.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

internal sealed class BearerSecuritySchemeTransformer : IOpenApiDocumentTransformer
{
    private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

    public BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider)
    {
        _authenticationSchemeProvider = authenticationSchemeProvider;
    }

    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await _authenticationSchemeProvider.GetAllSchemesAsync();

        if (authenticationSchemes.Any(authScheme =&gt; authScheme.Name == "Bearer"))
        {
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes ??= new Dictionary&lt;string, IOpenApiSecurityScheme&gt;();
            document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
            {
                Type = SecuritySchemeType.Http,
                Scheme = "bearer",
                In = ParameterLocation.Header,
                BearerFormat = "JWT"
            };

            foreach (var operation in document.Paths.Values.SelectMany(path =&gt; path.Operations))
            {
                if (operation.Value.Security == null)
                {
                    operation.Value.Security = new List&lt;OpenApiSecurityRequirement&gt;();
                }
                var securityRequirement = new OpenApiSecurityRequirement
                {
                    [new OpenApiSecuritySchemeReference("Bearer", document)] = []
                };

                operation.Value.Security ??= new List&lt;OpenApiSecurityRequirement&gt;();
                operation.Value.Security.Add(securityRequirement);
            }
        }
    }
}
</code></pre>
<p><strong>Scalar UI &gt;&gt;</strong></p>
<pre><code class="language-csharp"> builder.Services.AddOpenApi(
    options =&gt;
    {
        options.AddDocumentTransformer&lt;BearerSecuritySchemeTransformer&gt;();
    }
);

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapScalarApiReference(
    options =&gt;
    {
        options.Title = "Scalar API";
        options.DarkMode = true;
        options.Favicon = "path";
        options.DefaultHttpClient = new KeyValuePair&lt;ScalarTarget, ScalarClient&gt;(
            ScalarTarget.CSharp,
            ScalarClient.RestSharp
        );
        options.HideModels = false;
        options.Layout = ScalarLayout.Modern;
        options.ShowSidebar = true;
        options.Authentication = new ScalarAuthenticationOptions
        {
            PreferredSecuritySchemes = new List&lt;string&gt; { "Bearer" }
        };
    }
);
</code></pre>
<p>The above code customizes the Scalar UI to include the Bearer token section.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/51e76cd2-4e73-496a-8c3b-1a0896265105.png" alt="" style="display:block;margin:0 auto" />

<p>For more details refer to my article on the topic <a href="https://www.azureguru.net/customize-scalar-UI-for-net-api"><strong>here</strong></a>.</p>
<p><em><strong>Login Endpoint &gt;&gt;</strong></em></p>
<pre><code class="language-csharp">app.MapPost("/login", async() =&gt;
{
 tokens = await Security.Authentication.ReturnAuthenticationResult();
return Results.Ok(tokens);
}).WithOpenApi();
</code></pre>
<p>In Login endpoint, we fetch the access token issued by Entra ID that is part of the OIDC flow.</p>
<p><em><strong>TraverseDirectory &gt;&gt;</strong></em></p>
<pre><code class="language-csharp">public static async Task&lt;List&lt;string&gt;&gt; TraverseDirectory(DataLakeDirectoryClient directoryClient)
{

    await foreach (var item in directoryClient.GetPathsAsync())
    {
        if (item.IsDirectory == true)
        {
            listoutput.Add(item.Name);
            string[] split = item.Name.Split("/");
            var subDir = directoryClient.GetSubDirectoryClient(split.Length == 1 ? split[0] : split[split.Length - 1]);
            await TraverseDirectory(subDir);
        }
    }

    return listoutput;
}
</code></pre>
<p>The above code recursively traverses the directory structure of a given Azure container . For more in-depth details on the approach you can refer to my article</p>
<p><a href="https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage">https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage</a></p>
<p><em><strong>Accessazurestorage Endpoint &gt;&gt;</strong></em></p>
<pre><code class="language-csharp">        app.MapGet("/accessazurestorage", async (ClaimsPrincipal claims, HttpContext context) =&gt; {
        var audience = claims.FindFirst("aud")?.Value;
        var issuer = claims.FindFirst("iss")?.Value;
        var scp = claims.FindFirst("scp")?.Value;
        var roles = claims.FindFirst("roles")?.Value;

        if (audience != $ "api://{ClientId}") {
          throw new UnauthorizedAccessException("Unauthorized access !!!");
        }

        if (issuer != $ "https://sts.windows.net/{TenantId}/") {
          throw new UnauthorizedAccessException("Unauthorized access !!!");
        }

        if (scp != "storage.read") {
          throw new UnauthorizedAccessException("Unauthorized access !!!");
        }

        if (roles != "AzureStorage.Read") {
          throw new UnauthorizedAccessException("Unauthorized access !!!");
        }

        DataLakeServiceClient datalake_Service_Client;
        DataLakeFileSystemClient dataLake_FileSystem_Client;
        string dfsUri = $ "https://adlsfilestore.dfs.core.windows.net";

        TokenCredential tokenCredential = new AccessTokenCredential(tokens[0].RawData.ToString());

        datalake_Service_Client = new DataLakeServiceClient(new Uri(dfsUri), tokenCredential);

        dataLake_FileSystem_Client = datalake_Service_Client.GetFileSystemClient("customers");

        DataLakeDirectoryClient rootDirectory_ = dataLake_FileSystem_Client.GetDirectoryClient("");

        listoutput.Clear();

        return Results.Ok(new {
          Output = TraverseDirectory(rootDirectory_)

        });
      })
      .RequireAuthorization("AzureStorageAccess");

    app.Run();
    }
</code></pre>
<p>The code is pretty straightforward. Validate <strong>audience</strong>, <strong>issuer</strong>, <strong>scp</strong> and <strong>roles</strong> values from claims.</p>
<p>If all the above criteria are met , then fetch the storage_token from the tokens array of type <strong>JwtSecurityToken[]</strong> (declare earlier) and pass the value to <strong>TokenCredential</strong> for <strong>DatalakeServiceClient</strong> which then eventually returns the directory structure for the Azure container <strong>customers</strong>.</p>
<p>Login in with <strong>sachin.nandanwar @ azureguru.net</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/0a7f3036-3840-43e9-9b78-1e3f0e2a898f.png" alt="" style="display:block;margin:0 auto" />

<p>Login in with <strong>sachin_nandanwar @ azureguru.net</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7b25d234-39dc-4341-93a7-6ef668b8feb1.png" alt="" style="display:block;margin:0 auto" />

<h3>Execution &gt;&gt;</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/22b310a6-ea3a-42f5-bcf4-803b545c1c3f.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion &gt;&gt;</h3>
<p>The two-token pattern keeps user authorization and resource authorization separate. Leveraging Claims-based authorization and Azure RBAC together results in a much cleaner security model. User are authorized to access the application through their claims while the service principal accesses Azure resources using its own identity. This removes the need to grant Azure RBAC permissions to every user across every Azure resource to which user needs access.</p>
<p>I hope this article helps you get started with claims-based authorization and the two-token pattern in Microsoft Entra ID.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[ A simple guide to securing Azure Storage in ASP.NET Core Minimal API's with Microsoft Entra ID and OpenID Connect (OIDC)]]></title><description><![CDATA[In this article, we'll take a deep dive into securing an ASP.NET Core Minimal API with Microsoft Entra ID and using it to access Azure ADLS Gen2 storage implementing Microsoft Entra ID for user sign-i]]></description><link>https://www.azureguru.net/securing-azure-storage-in-asp-net-core-minimal-api-s-with-microsoft-entra-id-and-openid-connect-oidc</link><guid isPermaLink="true">https://www.azureguru.net/securing-azure-storage-in-asp-net-core-minimal-api-s-with-microsoft-entra-id-and-openid-connect-oidc</guid><category><![CDATA[openid]]></category><category><![CDATA[OAuth2]]></category><category><![CDATA[Entra ID]]></category><category><![CDATA[Aspnetcore]]></category><category><![CDATA[minimal-apis]]></category><category><![CDATA[OIDC]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Mon, 20 Jul 2026 12:48:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a42d2325-6183-4c06-9098-091b4ae8d14a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, we'll take a deep dive into securing an ASP.NET Core <strong>Minimal API</strong> with <strong>Microsoft Entra ID</strong> and using it to access <strong>Azure ADLS Gen2</strong> storage implementing Microsoft Entra ID for user sign-in through <strong>OpenID Connect (OIDC)</strong>.</p>
<p>We'll explore how Microsoft Entra ID integrates with ASP.NET Core Minimal APIs, how access tokens are leveraged to authenticate requests and how Azure Storage authorizes those requests using Azure RBAC and Access Control Lists (ACLs).</p>
<p>As compared to other articles on the internet that only provide high-level overview on the topic, my article will focus on the actual implementation of authentication and authorization mechanisms involved for securing Azure services through Minimal API's by leveraging Microsoft Entra ID.</p>
<p>Unfortunately I wont go in-depth explaining all the underlying concepts involved. So before you begin please ensure that you should be familiar with the following concepts:</p>
<ul>
<li><p>ASP.NET Core Minimal APIs</p>
</li>
<li><p>Microsoft Entra ID and OpenID Connect (OIDC)</p>
</li>
<li><p>Azure Storage (Blob Storage or Data Lake Storage Gen2)</p>
</li>
<li><p>Azure Role-Based Access Control (Azure RBAC)</p>
</li>
<li><p>Azure Storage Access Control Lists (ACLs)</p>
</li>
</ul>
<p>A basic understanding of these technologies will help you get the most out of this article and follow the implementation steps details more easily.</p>
<p>Basic OAUTH 2.0 and OIDC flow for Microsoft Entra ID tokens.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/50632edc-6013-4f1f-9e39-70acb48b173d.png" alt="" style="display:block;margin:0 auto" />

<p>The implementation in this article is pretty straightforward.</p>
<p>We'll build an <strong>ASP.NET Core Minimal API</strong> that authenticates users with <strong>Microsoft Entra ID</strong> and once authenticated it will securely accesses <strong>Azure ADLS Gen2</strong> Storage through <strong>DataLakeServiceClient</strong> (i.e. return a list of all the directories in a given Azure ADLS Gen2 storage container) on behalf of the user.</p>
<p>I will use the following references from my other articles</p>
<ul>
<li><p><em>Leverage MSAL for Microsoft Fabric</em> &gt;&gt; <a href="https://www.azureguru.net/msal-for-microsoft-fabric">https://www.azureguru.net/msal-for-microsoft-fabric</a></p>
</li>
<li><p><em>Retrieve ADLS Gen 2 directory structure by DataLakeServiceClient &gt;&gt;</em> <a href="https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage">https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage</a></p>
</li>
<li><p><em>Leverage Service Principal to fetch Microsoft Fabric Directory structure</em> &gt;&gt; <a href="https://www.azureguru.net/service-principal-in-microsoft-fabric">https://www.azureguru.net/service-principal-in-microsoft-fabric</a></p>
</li>
<li><p><em>Creating a TokenCredential from a Bearer Token &gt;&gt;</em> <a href="https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric">https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric</a></p>
</li>
<li><p><em>Customizing Scalar UI for .NET API's &gt;&gt;</em> <a href="https://www.azureguru.net/customize-scalar-UI-for-net-api">https://www.azureguru.net/customize-scalar-UI-for-net-api</a></p>
</li>
<li><p><em>JWT tokens in Minimal API's &gt;&gt;</em> <a href="https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core">https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core</a></p>
</li>
</ul>
<p>The article <em><strong>JWT tokens in Minimal API's</strong></em> quoted above demonstrates how custom JWT tokens can be used to authorize access where requests were approved or denied based on defined authorization policies.</p>
<p>However, in this article we will leverage the access tokens generated by Microsoft Entra Id for a service principal and the delegated permissions granted to it . We then validate the token audience. Once the token is validated, the signed in user access is then checked for RBAC permissions granted for the given Azure ADLS Gen2 storage.</p>
<h3>Flow</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3e908e16-6154-4dda-9f3d-d4412f4d5b08.png" alt="" style="display:block;margin:0 auto" />

<p>To add more context to the above flow, the scope used is</p>
<pre><code class="language-html">https://storage.azure.com/
</code></pre>
<h3><strong>SetUp</strong></h3>
<p>The ADLS storage has the following structure</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/5fcd2c53-8803-4fa4-8c5c-c7aed9d179d3.png" alt="" style="display:block;margin:0 auto" />

<p>The expectation from the code is that it should be capable of recursively traversing all directories within a given container. As shown in the screenshot above, the <strong>customers</strong> container contains directories that are nested up to three levels deep and this structure can be dynamic.</p>
<p>So , there are two users :</p>
<ul>
<li><p><strong><a href="mailto:sachin.nandanwar@azureguru.net">sachin.nandanwar@azureguru.net</a></strong></p>
</li>
<li><p><strong>sachin_<a href="mailto:nandanwar@azureguru.net">nandanwar@azureguru.net</a></strong></p>
</li>
</ul>
<p><strong><a href="mailto:sachin.nandanwar@azureguru.net">sachin.nandanwar@azureguru.net</a></strong> is assigned the necessary RBAC role accesses to the storage. Typically it has to be <strong>Storage Blob Data Owner</strong> role.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/fedd98ba-a400-48c9-9bb5-0d4c96d0f135.png" alt="" style="display:block;margin:0 auto" />

<p>You could also grant <strong>Storage Blob Data Contributor role</strong> but the <strong>Storage Blob Data Owner</strong> role has POSIX access control (ACL access) that auto grants all the (r-w-x) privileges to the owner for all the underlying objects under the container.</p>
<p>For instance in the following screenshot we can see that the owner i.e. the <strong>Storage Blob Data Owner</strong> was auto assigned the (r-w-x) access .</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/770ff9a7-8d13-42ef-a576-cdff6643a639.png" alt="" style="display:block;margin:0 auto" />

<p><strong>sachin_<a href="mailto:nandanwar@azureguru.net">nandanwar@azureguru.net</a></strong> is not assigned any RBAC role.</p>
<p>Create a Service Principal and grant the <strong>Azure Storage</strong> delegated permissions. I created one called <strong>ADLS GEN2 Service Principal.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/03ba6eeb-e87f-4d86-8bff-ccec087d2ad5.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>Ensure that you the Scalar set up is configured in your ASP.NET core project. For more details refer to my following article</p>
</blockquote>
<p><a href="https://www.azureguru.net/customize-scalar-UI-for-net-api">https://www.azureguru.net/customize-scalar-UI-for-net-api</a></p>
<p>Also ensure that you have a thorough understanding of Minimal API's and implementation of <strong>ClaimPrincipal, Authentication</strong> and <strong>Authorization</strong> for Minimal API's. Please refer to my article to have a better understanding of the topic.</p>
<p><a href="https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core">https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core</a></p>
<p>You can ignore the custom JWToken aspect from the above article as in that article the focus was on creating custom JWTokens.</p>
<h3>Code</h3>
<p>Add the following references to your ASP.NET core project</p>
<pre><code class="language-csharp">dotnet add package Azure.Core;
dotnet add package Azure.Storage.Files.DataLake;
dotnet add package Microsoft.IdentityModel.Tokens;
dotnet add package Scalar.AspNetCore;
dotnet add package System.Security.Claims;
</code></pre>
<p>First, we define an <strong>Authentication</strong> class that validates the user's sign-in and requested scopes with Microsoft Entra ID and then returns a JWT token.</p>
<blockquote>
<p>Unlike implementing custom JWT tokens which I demonstrated in my earlier article <a href="https://www.azureguru.net/implementing-jwt-tokens-in-minimal-api-net-core">here</a>, with Microsoft Entra ID-issued tokens there is no need for generating, signing, rotating or validating JWTs ourselves. Microsoft Entra ID auto handles all these aspects at its end.</p>
</blockquote>
<p><strong>Authentication.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Microsoft.Identity.Client;
using System.IdentityModel.Tokens.Jwt;

namespace Security
{
    internal class Authentication
    {
        public static string clientId = "Service Principal Client Id";
        private static string[] scopes = { "https://storage.azure.com/.default" };
        private static string Authority = "https://login.microsoftonline.com/organizations";
        private static string RedirectURI = "http://localhost";

        public async static Task&lt;JwtSecurityToken&gt; ReturnAuthenticationResult()
        {
            string AccessToken;
            IPublicClientApplication PublicClientApplication = PublicClientApplicationBuilder
                .Create(clientId)
                .WithAuthority(Authority)
                .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
                .WithRedirectUri(RedirectURI)
                .Build();

            var accounts = await PublicClientApplication.GetAccountsAsync();
            AuthenticationResult result;

            try
            {
                result = await PublicClientApplication
                    .AcquireTokenSilent(scopes, accounts.First())
                    .ExecuteAsync()
                    .ConfigureAwait(false);
            }
            catch
            {
                result = await PublicClientApplication
                    .AcquireTokenInteractive(scopes)
                    .ExecuteAsync()
                    .ConfigureAwait(false);
            }

            JwtSecurityToken token = new JwtSecurityToken(result.AccessToken);
            return token;
        }
    }
}
</code></pre>
<blockquote>
<p>For brevity I have defined ClientId and other details in variables. Ideally they should be placed in a config file and their values fetched from there.</p>
</blockquote>
<p><strong>AccessTokenCredential.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Azure.Core;
using System.IdentityModel.Tokens.Jwt;

namespace AccesTokenCredentials
{
    public class AccessTokenCredential : Azure.Identity.ClientSecretCredential
    {
        public AccessTokenCredential(string accessToken)
        {
            AccessToken = accessToken;
        }

        private string AccessToken;

        public AccessToken FetchAccessToken()
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

        public override ValueTask&lt;AccessToken&gt; GetTokenAsync(
            TokenRequestContext requestContext,
            CancellationToken cancellationToken
        )
        {
            return new ValueTask&lt;AccessToken&gt;(FetchAccessToken());
        }

        public override AccessToken GetToken(
            TokenRequestContext requestContext,
            CancellationToken cancellationToken
        )
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }
    }
}
</code></pre>
<blockquote>
<p>The above class converts bearer tokens to TokenCredentials. We will require TokenCredentials for authenticating DataLakeServiceclient and return the directory structure for the ADLS Gen2 storage.</p>
</blockquote>
<p>For more information as to why this is required, please refer to the following article <a href="https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric">https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric</a></p>
<p><strong>Program.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">public static class Program {
  public static string TenantId = "Tenant Id";
  public static List &lt; string &gt; listoutput = new();

  private static async Task Main(string[] args) {
    WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

    builder.Services
      .AddAuthentication()
      .AddJwtBearer(
        options =&gt; {
          options.Authority = $ "https://login.microsoftonline.com/{TenantId}";
          options.Validate();
          options.TokenValidationParameters = new TokenValidationParameters {
            ValidIssuer = $ "https://sts.windows.net/{TenantId}/",
              ValidAudience = "https://storage.azure.com",
              ValidateIssuer = true,
              ValidateAudience = true
          };
        }
      );

    builder.Services.AddAuthorization(
      options =&gt; {
        options.AddPolicy(
          "AzureStorageAccess",
          policy =&gt; {
            policy.RequireAuthenticatedUser();
            policy.RequireClaim("aud", "https://storage.azure.com");
            policy.RequireClaim("iss", $ "https://sts.windows.net/{TenantId}/");
          }
        );
      }
    );

    builder.Services.AddOpenApi(
      options =&gt; {
        options.AddDocumentTransformer &lt; BearerSecuritySchemeTransformer &gt; ();
      }
    );

    WebApplication app = builder.Build();

    if (app.Environment.IsDevelopment()) {
      app.MapOpenApi();
    }

    app.MapScalarApiReference(
      options =&gt; {
        options.Title = "Scalar API";
        options.DarkMode = true;
        options.Favicon = "path";
        options.DefaultHttpClient = new KeyValuePair &lt; ScalarTarget, ScalarClient &gt; (
          ScalarTarget.CSharp,
          ScalarClient.RestSharp
        );
        options.HideModels = false;
        options.Layout = ScalarLayout.Modern;
        options.ShowSidebar = true;
        options.Authentication = new ScalarAuthenticationOptions {
          PreferredSecuritySchemes = new List &lt; string &gt; {
            "Bearer"
          }
        };
      }
    );

    app.UseHttpsRedirection();

    app.MapPost(
        "/login",
        () =&gt; {
          return Results.Ok(
            new {
              token = Security.Authentication.ReturnAuthenticationResult()
            }
          );
        }
      )
      .WithOpenApi();

    app.MapGet(
        "/accessazurestorage",
        async (ClaimsPrincipal claims, HttpContext context) =&gt; {
          var audience = claims.FindFirst("aud")?.Value;
          var issuer = claims.FindFirst("iss")?.Value;

          if (audience != "https://storage.azure.com") {
            throw new UnauthorizedAccessException("Unauthorized access !!!");
          }

          if (issuer != "$https://sts.windows.net/{TenantId}/") {
            throw new UnauthorizedAccessException("Unauthorized access !!!");
          }

          var token = context.Request.Headers["Authorization"]
            .ToString()
            .Replace("Bearer ", "");

          DataLakeServiceClient datalake_Service_Client;

          DataLakeFileSystemClient dataLake_FileSystem_Client;

          string dfsUri = $ "https://adlsfilestore.dfs.core.windows.net";

          TokenCredential tokenCredential = new AccessTokenCredential(token.ToString());

          datalake_Service_Client = new DataLakeServiceClient(
            new Uri(dfsUri),
            tokenCredential
          );

          dataLake_FileSystem_Client = datalake_Service_Client.GetFileSystemClient(
            "customers"
          );

          DataLakeDirectoryClient rootDirectory_ =
            dataLake_FileSystem_Client.GetDirectoryClient("");

          return Results.Ok(new {
            Output = TraverseDirectory(rootDirectory_)
          });
        }
      )
      .RequireAuthorization("AzureStorageAccess");

    app.MapGet("/fakeaccessazurestorage", () =&gt; {}).RequireAuthorization("AzureStorageAccess");

    app.Run();
  }

  public static async Task &lt; List &lt; string &gt;&gt; TraverseDirectory(
    DataLakeDirectoryClient directoryClient
  ) {
    await foreach(var item in directoryClient.GetPathsAsync()) {
      if (item.IsDirectory == true) {
        listoutput.Add(item.Name);
        string[] split = item.Name.Split("/");
        var subDir = directoryClient.GetSubDirectoryClient(
          split.Length == 1 ? split[0] : split[split.Length - 1]
        );
        await TraverseDirectory(subDir);
      }
    }

    return listoutput;
  }
}
</code></pre>
<p>Lets dissect the major aspects of the above code</p>
<p><em><strong>Authentication</strong></em> <em>&gt;&gt;</em></p>
<pre><code class="language-csharp">builder.Services.AddAuthentication().AddJwtBearer(options =&gt;
{
    options.Authority = $"https://login.microsoftonline.com/{TenantId}";
    options.Validate();

    options.TokenValidationParameters = new TokenValidationParameters
    {
         ValidIssuer = "https://sts.windows.net/{TenantId}/",
         ValidAudience = "https://storage.azure.com",
         ValidateIssuer = true,
         ValidateAudience = true
    };
});
</code></pre>
<p>Authentication mechanism is used to validate the incoming tokens. It checks for two parameters, <strong>ValidIssuer</strong> and <strong>ValidAudience</strong> and ensures that the values for these parameters in the bearer token matches with</p>
<ul>
<li><a href="https://sts.windows.net/%7BTenantId%7D/">https://sts.windows.net/{TenantId}/</a> and <a href="https://storage.azure.com">https://storage.azure.com</a></li>
</ul>
<p><em><strong>Authorization</strong></em> <em>&gt;&gt;</em></p>
<pre><code class="language-csharp"> builder.Services.AddAuthorization(options =&gt;
 {
     options.AddPolicy("AzureStorageAccess", policy =&gt;
     {
         policy.RequireAuthenticatedUser();
         policy.RequireClaim("aud", "https://storage.azure.com");
         policy.RequireClaim("iss", $"https://sts.windows.net/{TenantId}/");
     });
 });
</code></pre>
<p>Create an authorization policy but before that ensure that request contains authenticated users and then check if the claims contain <strong>"aud</strong>" and <strong>"iss"</strong> and then validate its values.</p>
<p><em><strong>Scalar.cs &gt;&gt;</strong></em></p>
<pre><code class="language-csharp">using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

internal sealed class BearerSecuritySchemeTransformer : IOpenApiDocumentTransformer
{
    private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;

    public BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider)
    {
        _authenticationSchemeProvider = authenticationSchemeProvider;
    }

    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await _authenticationSchemeProvider.GetAllSchemesAsync();

        if (authenticationSchemes.Any(authScheme =&gt; authScheme.Name == "Bearer"))
        {
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes ??= new Dictionary&lt;string, IOpenApiSecurityScheme&gt;();
            document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
            {
                Type = SecuritySchemeType.Http,
                Scheme = "bearer",
                In = ParameterLocation.Header,
                BearerFormat = "JWT"
            };

            foreach (var operation in document.Paths.Values.SelectMany(path =&gt; path.Operations))
            {
                if (operation.Value.Security == null)
                {
                    operation.Value.Security = new List&lt;OpenApiSecurityRequirement&gt;();
                }
                var securityRequirement = new OpenApiSecurityRequirement
                {
                    [new OpenApiSecuritySchemeReference("Bearer", document)] = []
                };

                operation.Value.Security ??= new List&lt;OpenApiSecurityRequirement&gt;();
                operation.Value.Security.Add(securityRequirement);
            }
        }
    }
}
</code></pre>
<p><em><strong>Scalar UI &gt;&gt;</strong></em></p>
<pre><code class="language-csharp"> builder.Services.AddOpenApi(
    options =&gt;
    {
        options.AddDocumentTransformer&lt;BearerSecuritySchemeTransformer&gt;();
    }
);

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapScalarApiReference(
    options =&gt;
    {
        options.Title = "Scalar API";
        options.DarkMode = true;
        options.Favicon = "path";
        options.DefaultHttpClient = new KeyValuePair&lt;ScalarTarget, ScalarClient&gt;(
            ScalarTarget.CSharp,
            ScalarClient.RestSharp
        );
        options.HideModels = false;
        options.Layout = ScalarLayout.Modern;
        options.ShowSidebar = true;
        options.Authentication = new ScalarAuthenticationOptions
        {
            PreferredSecuritySchemes = new List&lt;string&gt; { "Bearer" }
        };
    }
);
</code></pre>
<p>The above code customizes the Scalar UI to include the Bearer token section.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/db438985-06ab-49f4-a580-d841f41e0ef3.png" alt="" style="display:block;margin:0 auto" />

<p>For more details refer to my article on the topic <a href="https://www.azureguru.net/customize-scalar-UI-for-net-api">here</a>.</p>
<p><em><strong>Login &gt;&gt;</strong></em></p>
<pre><code class="language-csharp">app.MapPost("/login", () =&gt;
{
    return Results.Ok(new { token = Security.Authentication.ReturnAuthenticationResult() });
}).WithOpenApi();
</code></pre>
<p>Here we fetch the access token issued by Entra ID which is part of the OIDC flow.</p>
<p><em><strong>TraverseDirectory &gt;&gt;</strong></em></p>
<pre><code class="language-csharp">public static async Task&lt;List&lt;string&gt;&gt; TraverseDirectory(DataLakeDirectoryClient directoryClient)
{

    await foreach (var item in directoryClient.GetPathsAsync())
    {
        if (item.IsDirectory == true)
        {
            listoutput.Add(item.Name);
            string[] split = item.Name.Split("/");
            var subDir = directoryClient.GetSubDirectoryClient(split.Length == 1 ? split[0] : split[split.Length - 1]);
            await TraverseDirectory(subDir);
        }
    }

    return listoutput;
}
</code></pre>
<p>The above code recursively traverses the directory structure of a given Azure container . For more in-depth details on the approach you can refer to my article</p>
<p><a href="https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage">https://www.azureguru.net/retrieve-the-hierarchical-directory-structure-from-azure-adls-gen2-storage</a></p>
<p><em><strong>Accessazurestorage &gt;&gt;</strong></em></p>
<pre><code class="language-csharp">app.MapGet(
    "/accessazurestorage",
    async (ClaimsPrincipal claims, HttpContext context) =&gt; {
      var audience = claims.FindFirst("aud")?.Value;
      var issuer = claims.FindFirst("iss")?.Value;

      if (audience != "https://storage.azure.com") {
        throw new UnauthorizedAccessException("Unauthorized access !!!");
      }

      if (issuer != $ "https://sts.windows.net/{TenantId}/") {
        throw new UnauthorizedAccessException("Unauthorized access !!!");
      }

      var token = context.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");

      DataLakeServiceClient datalake_Service_Client;

      DataLakeFileSystemClient dataLake_FileSystem_Client;

      string dfsUri = $ "https://adlsfilestore.dfs.core.windows.net";

      TokenCredential tokenCredential = new AccessTokenCredential(token.ToString());

      datalake_Service_Client = new DataLakeServiceClient(new Uri(dfsUri), tokenCredential);

      dataLake_FileSystem_Client = datalake_Service_Client.GetFileSystemClient("customers");

      DataLakeDirectoryClient rootDirectory_ = dataLake_FileSystem_Client.GetDirectoryClient(
        ""
      );
      return Results.Ok(new {
        Output = TraverseDirectory(rootDirectory_)
      });
    }
  )
  .RequireAuthorization("AzureStorageAccess");
</code></pre>
<p>The code is pretty straightforward. Check the audience and issuer values from claims and and fetch the access token from <strong>HttpContext .</strong></p>
<p>Then convert the access token to <strong>TokenCredentials</strong> and use it to authenticate <strong>DataLakeServiceClient</strong> and then recursively traverse across the directory structure for the container <strong>customers</strong>. Note that it uses <strong>AzureStorageAccess</strong> policy (defined earlier in the login code) for Authorizing the request.</p>
<p>Executing the code the directory structure for the Azure container <strong>customers</strong> is displayed in the output</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/1aec9a73-9d48-4ba7-bf3e-264f1e6eb52f.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Fakeaccessazurestorage &gt;&gt;</strong></p>
<pre><code class="language-csharp">  app.MapGet("/fakeaccessazurestorage", () =&gt;
  {
  }).RequireAuthorization("AzureStorageAccess");
</code></pre>
<p>Accessing <strong>fakeaccessazurestorage</strong> endpoint without required <strong>audience</strong> and <strong>issuer</strong> details in the access token results in 401 Unauthorized exception.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/da2ccd8d-b6ad-4b53-9141-c6d01fff4133.png" alt="" style="display:block;margin:0 auto" />

<h3>Execution &gt;&gt;</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/968382d3-d2de-4b20-874d-8733ed5ded02.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>In this article, we explored how to secure an ASP.NET Core Minimal API using Microsoft Entra ID and leverage delegated user authentication to access Azure Storage (ADLS GEN2) securely. By combining OpenID Connect, JWT Bearer authentication and Azure SDK with TokenCredential we were able to established a secure end-to-end request flow from user sign-in to resource access.</p>
<p>I hope this article helps you get started with implementing delegated authentication and authorization using Microsoft Entra ID in ASP.NET Core applications. While we used Azure Storage as the primary example in the article the concepts covered here extend to many Azure services that support Microsoft Entra ID authentication.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Integrating Microsoft Agent Framework with Microsoft Purview Services for Data Governance for AI Agents]]></title><description><![CDATA[As more organizations adopt AI-driven setups and applications the need for governance and operational security across these services has been more than important than ever before. You definitely would]]></description><link>https://www.azureguru.net/integrating-microsoft-agent-framework-with-microsoft-purview-services-for-data-governance-for-ai-agents</link><guid isPermaLink="true">https://www.azureguru.net/integrating-microsoft-agent-framework-with-microsoft-purview-services-for-data-governance-for-ai-agents</guid><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[Microsoft365]]></category><category><![CDATA[Microsoft Purview]]></category><category><![CDATA[AI Governance]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[purview]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Mon, 13 Jul 2026 06:05:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3eec8673-257b-49a0-b0ec-cd87f748221b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As more organizations adopt AI-driven setups and applications the need for governance and operational security across these services has been more than important than ever before. You definitely wouldn't want your AI agents to have open access across your systems without any form of governance or compliances regulating them.</p>
<p>This is where <strong>Microsoft Purview</strong> provides a powerful platform for seamlessly securing and governing your data. Microsoft Purview helps reduce complexity and improve governance related to mitigating risks in the era of AI.</p>
<p><strong>Microsoft Agent Framework</strong> supports seamless integration with Microsoft Purview to help implement governance and compliance for your AI agents.</p>
<p>If you aren't much aware of Microsoft Purview and its capabilities, I would highly recommend to get familiar with it . A good starting point can be Microsoft official documentation on Microsoft Purview <a href="https://www.microsoft.com/en-in/security/business/microsoft-purview">here</a>.</p>
<h3>Prerequisites</h3>
<ul>
<li><p>You will require Azure subscription with a <strong>M365 E5</strong> license. This is a must. Any other license for example E3 or Business premium or Standard license unfortunately will not work.</p>
</li>
<li><p>Also required is <strong>Microsoft 365 pay-as-you-go</strong> option enabled.</p>
</li>
<li><p>A thorough understanding of Microsoft Entra and OIDC authentication through <strong>MSAL.NET</strong> and how they integrate in the overall Microsoft ecosystem. I have extensively used them across all of my blogs on Microsoft fabric and Microsoft Azure. I would like to highlight a couple of them to make understand on how to integrate them. You can find them <a href="https://www.azureguru.net/msal-for-microsoft-fabric">here</a> and <a href="https://www.azureguru.net/service-principal-for-azure">here</a> .Though the articles are specific for with Microsoft fabric and Azure , they will help you to get a general understanding of the concepts.</p>
</li>
<li><p>Some conceptual understanding of Microsoft Purview like policy framework and how these policies are used to govern organizational data.</p>
</li>
</ul>
<p>Below is a conceptual flow of <strong>MSAL.NET</strong> which is generic OAuth 2.0 and OIDC client.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/721485e8-c293-4b65-bfe2-8b2bc99f904c.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>SetUp</strong></h3>
<p>The very first step is to setup a Service Principal in Microsoft Entra that has the following Microsoft Graph permissions :</p>
<ul>
<li><p><strong>ProtectionScopes.Compute.All</strong></p>
</li>
<li><p><strong>ContentActivity.Write</strong></p>
</li>
<li><p><strong>Content.Process.All</strong></p>
</li>
<li><p><strong>Purview.DelegatedAccess</strong></p>
</li>
<li><p><strong>ProtectionScopes.Compute.All</strong></p>
</li>
</ul>
<p>I created a one named <strong>Purview_Monitoring</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/aae103ba-c722-4872-ae89-405f41a9e470.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>Next , the following prerequisites has to be configured in Purview.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e3fc7583-3e5e-495a-9716-30d772721c8d.png" alt="Microsoft Agent Framework Governance and Purview services" />

<blockquote>
<p>The "Configuration" names are little different in the new Purview UI</p>
</blockquote>
<p><strong>Microsoft Purview Audit &gt;&gt;</strong></p>
<p>Its possible to turn it ON in two ways</p>
<p>Through the Purview portal or PowerShell</p>
<p><em><strong>Purview Portal &gt;&gt;</strong></em></p>
<p>Click <strong>"Start recording user and admin activity"</strong> to enable the Audit option.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/31a9237f-11c7-4555-b442-6bf288fd2a45.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<p><em><strong>PowerShell &gt;&gt;</strong></em></p>
<p>To use <strong>PowerShell</strong> for Purview, you first have to use the following command</p>
<pre><code class="language-yaml">Connect-IPPSSession
</code></pre>
<p>to connect to Security &amp; Compliance</p>
<blockquote>
<p>Use PowerShell command line and not PowerShell ISE</p>
</blockquote>
<p><strong>Connect-IPPSSession</strong> will prompt you to authenticate your credentials</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/6e24ce17-caf3-47be-8ba4-e040fd8bf409.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>Once authenticated, load to <strong>ExchangeOnlineManagement</strong> cmdlet. If its not already installed, the cmdlet will auto install it for you.</p>
<pre><code class="language-yaml">Import-Module ExchangeOnlineManagement
</code></pre>
<p>Next, connect to Exchange Online PowerShell through your credentials. In my case it is <strong><a href="mailto:sachin.nandanwar@azureguru.net">sachin.nandanwar@azureguru.net</a></strong></p>
<pre><code class="language-yaml">Connect-ExchangeOnline -UserPrincipalName sachin.nandanwar@azureguru.net
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d700d157-f720-40e6-bd6d-02f3a83bba61.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<p>and then run the following command to enable Audit logs option.</p>
<pre><code class="language-yaml">Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true
</code></pre>
<p>It might take about 60 minutes for the Audit logs to be enabled</p>
<p>To check the status, under Solutions click the <strong>DSPM for AI (Classic)</strong> option</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/794953cc-d439-4d25-8017-b62d78194474.png" alt="Microsoft Agent Framework Governance and Purview services" />

<blockquote>
<p>The above option is not available under new DSPM page</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/cc358fb2-eaea-4137-8171-7f3b5f1af1fa.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p><strong>DSPM for AI &gt;&gt;</strong></p>
<p>In the next step , enable the <strong>KYD Policy.</strong></p>
<p>There is no option to set this up through the UI. You will have to set it up through <strong>PowerShell.</strong></p>
<pre><code class="language-dockerfile">$locations = "[{`"Workload`":`"Applications`",`"Location`":`"$myEntraAppId`",`"LocationDisplayName`":`"$myEntraAppName`",`"LocationSource`":`"Entra`",`"LocationType`":`"Individual`",`"Inclusions`":[{`"Type`":`"Tenant`",`"Identity`":`"All`"}]}]"

New-FeatureConfiguration `
    -FeatureScenario KnowYourData `
    -Name "Secure interactions from enterprise apps (preview)" `
    -Mode Enable `
    -ScenarioConfig '{"Activities":["UploadText","DownloadText"],"EnforcementPlanes":["Application"],"SensitiveTypeIds":["All"],"IsIngestionEnabled":true}' `
    -Locations $locations
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/615273ee-b8d1-4a40-ade2-45380687d04a.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<blockquote>
<p>I have blurred the the $myEntraAppId value. This is the ClientID of the service principal that was set earlier. Note that "Secure interactions from enterprise apps (preview)" is a policy.</p>
</blockquote>
<p>The PowerShell script above , ties up the service principal with the policy.</p>
<p>Once executed ,under <strong>DSPM for AI &gt;&gt;Policies</strong> option you should be able to see the created policy.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/adfeb261-f5a2-4d1d-8367-0d621e204500.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<p><strong>Communication Compliance &gt;&gt;</strong></p>
<p>Under Solutions, click <strong>Communication Compliance</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/0b62a332-8cdb-46ad-bb9d-131304a4caaf.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>Select the <strong>"Detect unethical interactions for agents"</strong> option</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8f5d785b-7358-4cd5-9ba5-b32f7eac9e21.png" alt="Microsoft Agent Framework Governance and Purview services" />

<blockquote>
<p>There is an issue while setting up this policy. Its not possible to set it up for a specific Service Principal.</p>
</blockquote>
<p>Check the screenshot below , where I tried to search for the service principal <strong>Purview_Monitoring</strong> that was configured earlier.</p>
<p>Its un available in the <strong>Select users</strong> option.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9dccd13e-2ac1-4428-88a6-fe63885db301.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>while the Entra users are visible</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/12a97ce4-7f5d-403d-a351-f73042b38f51.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>Anyways, setting it up for <strong>All users</strong> doesn't impact the flow.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/0e60ad73-2fa9-4e0a-bb2a-28bd4bf95123.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<p>The status is <strong>"Activating"</strong>. It will take sometime for the status to change to <strong>"Ready"</strong>.</p>
<p><strong>Insider Risk Management &gt;&gt;</strong></p>
<p>Under <strong>Insider Risk Management</strong> option select <strong>Quick Policy</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/78ba0867-d547-451e-b7a1-04d66e7341e8.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>and select <strong>"Risky AI Usage"</strong> policy</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/2de834ff-1a81-4fc7-9d8d-c5fec8e6a5b3.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>This is a standard general policy and is not tied to a specific user/s .</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f9b9ccc8-7450-494f-867b-8e9f607a6a59.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>This is the last step and this basically completes all the underlying prerequisites.</p>
<h3>Use Case</h3>
<p>Now that all the prerequisites are in place, let's look through a use case in which we create a Microsoft Purview policy to block a specific prompt.</p>
<p>Prompts such as <em><strong>"Remember the credit card number..."</strong></em> are typically blocked by the AI model itself and there is no operational need for Microsoft Purview to validate and block the prompt. So the challenge is to create a policy that blocks a specific prompt <strong>only</strong> when it originates from our set up i.e. through an agent that runs the service principal.</p>
<p>As we saw earlier, the Microsoft Purview UI does not currently provide a way to scope policies to an individual Microsoft Entra service principal under which an Agent executes. This is where <strong>PowerShell</strong> comes to the rescue.</p>
<p>Lets create a policy that blocks a text say <strong>"oranges"</strong>.</p>
<p>To get started, Run the following commands through PowerShell command line and <strong>NOT</strong> PowerShell ISE.</p>
<p>Also ensure that you are already connected through <strong>Connect-IPPSSession</strong></p>
<pre><code class="language-dockerfile">Connect-IPPSSession

Import-Module ExchangeOnlineManagement

$myEntraAppId = "Service Principal ClientId"

$myEntraAppName = "Service Principal Name"

$locations = "[{`"Workload`":`"Applications`",`"Location`":`"$myEntraAppId`",`"LocationDisplayName`":`"$myEntraAppName`",`"LocationSource`":`"Entra`",`"LocationType`":`"Individual`",`"Inclusions`":[{`"Type`":`"Tenant`",`"Identity`":`"All`"}]}]"

New-DlpCompliancePolicy -Name "Test Oranges DLP" -Mode Enable -Locations $locations -EnforcementPlanes @("Application")
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/36f27c38-564c-40bb-96db-42d66677c811.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<p>It takes some time for policy status to change from <strong>"Sync In Progress"</strong> to <strong>"Ready"</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/331fe8d9-77d7-49b8-8a98-b655aa006c77.png" alt="" style="display:block;margin:0 auto" />

<p>In the next step create a <strong>"Sensitive info types"</strong>.</p>
<p>There are a lot of predefined <strong>"Sensitive info types"</strong> but we will create a custom one for our use case.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/38fe7dac-0cc1-45d2-b95c-e730da976ad3.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<p>Name the sensitivity info type as <strong>"My Oranges Test SIT"</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/ae0899dd-6803-496f-afc2-b11b4e76574f.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<p>Next, create a pattern</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/28d1fa30-e4c5-4bcf-9c6d-35958bd91650.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<p>and select <strong>"Add primary element"</strong> and under it select <strong>"Regular expression"</strong> option.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c7fc0591-a899-4dc7-a416-b5de3672f379.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>Add ID and keyword <strong>"oranges".</strong> The text <strong>"oranges"</strong> will be checked</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d475f731-3744-467c-b25c-9aeabc2be371.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>If required, there is an option to use inbuilt regular expression by clicking the <strong>"Choose from existing regular expressions"</strong> in the screen above.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d18dd9e0-c6aa-4aff-a842-63ed9ee2cfbd.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>In the next screen, keep the Confidence level to <strong>High</strong> for highest accuracy.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/1bac1063-1d00-49c5-a501-c67522e62ae3.png" alt="Microsoft Agent Framework Governance and Purview services" />

<p>Click <strong>Next</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e45f47d5-a95b-4414-95db-d9495808c477.png" alt="Microsoft Agent Framework Governance and Purview services" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/2d109f80-01ba-4f1c-b495-b28b35931215.png" alt="Microsoft Agent Framework Governance and Purview services" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/5c07cc30-7e52-42d1-a907-55523e168754.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<p>You can double check the sensitivity type through the following <strong>PowerShell</strong> command.</p>
<pre><code class="language-dockerfile">Get-DlpSensitiveInformationType 'My Oranges Test SIT'
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9a747ffc-bd1e-485c-ac1f-f44cd72a0548.png" alt="" style="display:block;margin:0 auto" />

<p>In the next step, we will create a rule that is tied with the policy and leverages the sensitivity type that we created.</p>
<pre><code class="language-dockerfile">New-DlpComplianceRule -Name "Test Oranges Rule" -Policy "Test Oranges DLP" -ContentContainsSensitiveInformation @{Name = "My Oranges Test SIT"}  -GenerateAlert $true -GenerateIncidentReport @("siteadmin") -NotifyUser @("user@email.com") -RestrictAccess @(@{setting="UploadText";value="Block"})
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d16397a0-b08d-4a62-ac45-5c143cbb9dff.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<p>That's all the settings required in Purview.</p>
<p>At this point, all the prerequisites, policies and sensitivity types in Purview are configured.</p>
<h3>Code</h3>
<p>In a console application install the following packages</p>
<pre><code class="language-csharp">dotnet add package Azure;
dotnet add package  Azure.AI.OpenAI;
dotnet add package  Microsoft.Agents.AI;
dotnet add package  Microsoft.Agents.AI.Purview;
dotnet add package  Microsoft.Extensions.AI;
dotnet add package  Microsoft.Extensions.Configuration;
dotnet add package  Microsoft.Extensions.DependencyInjection;
</code></pre>
<p>After the above artifacts are in place, add the following code to read the settings from <code>appsettings.json</code> in <strong>Program.cs</strong> of the project.</p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p>Read the credentials and register a Chatclient</p>
<pre><code class="language-csharp">var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

ServiceCollection servicecollection = new();

builder.Services.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
                .AsIChatClient()
    )
);
</code></pre>
<p>In the next step, register the following AIAgent in the hosted DI container.</p>
<p><strong>PurviewAgent &gt;&gt;</strong></p>
<pre><code class="language-csharp">  servicecollection.AddSingleton&lt;ChatClientAgent&gt;(sp =&gt;

  {
      Func&lt;ChatClientAgentOptions&gt; func = () =&gt;
     {
         return new ChatClientAgentOptions
         {
             ChatOptions = new ChatOptions
             {
                 Instructions ="You are a secure assistant.",

             },
             Name = "PurviewAgent",
             Id = "1"
         };
     };

      return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"), options: func());

  });
</code></pre>
<p>Build service and fetch agent through <strong>ServiceProvider</strong> as <strong>ChatClientAgent.</strong></p>
<pre><code class="language-csharp">ServiceProvider serviceProvider = servicecollection.BuildServiceProvider();

var agent = serviceProvider.GetServices&lt;ChatClientAgent&gt;();

List&lt;ChatClientAgents&gt; chatclientagent = new(agent);
</code></pre>
<p>This is the most crucial part of the code. Here is where we leverage the Purview library</p>
<pre><code class="language-csharp">AIAgent agent_ = chatClientAgents[0].AsBuilder().WithPurview(await Security.Authentication.ReturnAuthenticationResult(), new PurviewSettings("Purview_Monitoring")
  {
      BlockedPromptMessage = "&lt;&lt;&lt; BLOCKED BY PURVIEW PROMPT &gt;&gt;&gt;",
      BlockedResponseMessage = "&lt;&lt;&lt; BLOCKED BY PURVIEW RESPONSE &gt;&gt;&gt;"
  }).Build();
</code></pre>
<p>We will have to pass <strong>TokenCredentials</strong> for the service principal authentication</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/698cd83c-426e-46b6-b3f0-18e806e95332.png" alt="" />

<p>To implement it, I have used the authentication method where the bearer token generated by <strong>MSAL</strong> is converted to <strong>TokenCredentials</strong>.</p>
<p>I have a blog on this topic where I used it for Fabric <strong>OneLake</strong> authentication for <strong>DataLakeServiceClient</strong> .</p>
<p><a href="https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric">https://www.azureguru.net/customize-clientsecretcredential-class-for-onelake-authentication-in-microsoft-fabric</a></p>
<p>First, we'll need to create a <strong>Credentials</strong> class.</p>
<p><strong>AccessTokenCredential.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Azure.Core;
using Azure.Identity;
using System.IdentityModel.Tokens.Jwt;

public class AccessTokenCredential : ClientSecretCredential
    {

        public AccessTokenCredential(string accessToken)
        {
            AccessToken = accessToken;
        }

        private string AccessToken;

        public AccessToken FetchAccessToken()
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

        public override ValueTask&lt;AccessToken&gt; GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
        {
            return new ValueTask&lt;AccessToken&gt;(FetchAccessToken());
        }

         public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
         {
             JwtSecurityToken token = new JwtSecurityToken(AccessToken);
             return new AccessToken(AccessToken, token.ValidTo);
         }    

      }
</code></pre>
<p>and then create authentication class that returns the <strong>TokenCredentials</strong></p>
<p><strong>Authentication.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Azure.Core;
using Microsoft.Identity.Client;
using System.IdentityModel.Tokens.Jwt;

namespace Security
{
    internal class Authentication
    {       
        public static string clientId = "Service Principal Client Id";
        private static string[] scopes = ["https://graph.microsoft.com/.default"];
        private static string Authority = "https://login.microsoftonline.com/organizations";
        private static string RedirectURI = "http://localhost";

        public async static Task&lt;TokenCredential&gt; ReturnAuthenticationResult()
        {
            string AccessToken;
            PublicClientApplicationBuilder PublicClientAppBuilder =
                PublicClientApplicationBuilder.Create(clientId)
                .WithAuthority(Authority)
                .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
                .WithRedirectUri(RedirectURI);

            IPublicClientApplication PublicClientApplication = PublicClientAppBuilder.Build();
            var accounts = await PublicClientApplication.GetAccountsAsync();
            AuthenticationResult result;

            try
            {

                result = await PublicClientApplication.AcquireTokenSilent(scopes, accounts.First())
                                 .ExecuteAsync()
                                 .ConfigureAwait(false);

            }
            catch
            {
                result = await PublicClientApplication.AcquireTokenInteractive(scopes)
                                 .ExecuteAsync()
                                 .ConfigureAwait(false);

            }
            
             JwtSecurityToken token = new JwtSecurityToken(result.AccessToken);
            AccessTokenCredential tokenCredential = new AccessTokenCredential(result.AccessToken);

            return tokenCredential;

        }

    }
}
</code></pre>
<p>Next, we send a prompt to the model asking about <strong>oranges</strong></p>
<pre><code class="language-csharp">AgentResponse agentResponse = await agent_.RunAsync("What are oranges ?");
Console.WriteLine(agentResponse);
</code></pre>
<p>And the purview would block the response</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/07a96f60-cd52-470b-acbf-6cf79cca72f0.png" alt="" style="display:block;margin:0 auto" />

<p>If the prompt is about <strong>grapes</strong> the agent will return a relevant response.</p>
<pre><code class="language-csharp">AgentResponse agentResponse = await agent_.RunAsync("What are grapes ?");
Console.WriteLine(agentResponse);
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/abf0eda3-4b35-4d1b-921a-8d8858b882bd.png" alt="Microsoft Agent Framework Governance and Purview services" style="display:block;margin:0 auto" />

<h3>Execution &gt;&gt;</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/200715ad-5472-4cf4-a05a-2a09b8774cec.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion &gt;&gt;</h3>
<p>To wrap it up , a good AI ecosystem success shouldn't be based only by what an AI agent <em><strong>can</strong></em> do, but also by what it <em><strong>should</strong></em> be allowed to do. Microsoft Purview helps bridge that gap by bringing governance directly into the AI application lifecycle.</p>
<p>Microsoft Purview and its seamless integration with the Microsoft Agent Framework, provides a powerful foundation for building AI agents that are not only intelligent but also secure, compliant, and enterprise-ready. Governance shouldn't be an afterthought it should be part of the architecture from day one.</p>
<p>Thanks for reading !!</p>
]]></content:encoded></item><item><title><![CDATA[HITL (Human In The Loop) For Durable Workflows In Microsoft Agent Framework]]></title><description><![CDATA[My previous article focused on implementing Durable workflows in MAF while an another article explored the implementation of HITL for MAF agents.
I would strongly recommend to have a thorough understa]]></description><link>https://www.azureguru.net/hitl-human-in-the-loop-for-durable-workflows-in-microsoft-agent-framework</link><guid isPermaLink="true">https://www.azureguru.net/hitl-human-in-the-loop-for-durable-workflows-in-microsoft-agent-framework</guid><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[agent workflows]]></category><category><![CDATA[AI Agents & Agentic Workflows]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Tue, 30 Jun 2026 00:57:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/802075cb-1a2e-4e8c-8620-8bb4066cb966.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My previous <a href="https://www.azureguru.net/durable-workflows-in-microsoft-agent-framework">article</a> focused on implementing Durable workflows in MAF while an another <a href="https://www.azureguru.net/human-in-the-loop-hitl-in-azure-durable-functions-for-microsoft-agent-framework">article</a> explored the implementation of HITL for MAF agents.</p>
<p>I would strongly recommend to have a thorough understanding of the concepts and the examples covered in both these articles.</p>
<p>To be fair, implementation of Durable workflows is far straightforward and simple compared to implementation of Durable agents. With durable workflows each registered workflow automatically gets an HTTP trigger without the need for implementing any custom routing logic from your end.</p>
<p>For example, with Durable agents you have to wire your own custom routing logic which starts with the <strong>StartOrchestration</strong> method.</p>
<pre><code class="language-csharp">[Function(nameof(StartOrchestrationAsync))]
public static async Task&lt;HttpResponseData&gt; StartOrchestrationAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "youragent/run")] HttpRequestData req, [DurableClient] DurableTaskClient client)
</code></pre>
<p>To trigger the workflow execution you then require a <strong>RunOrchestrationAsync</strong> custom method.</p>
<pre><code class="language-csharp">[Function(nameof(RunOrchestrationAsync))]
 public static async Task&lt;string&gt; RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
</code></pre>
<p>Then invoke the workflow Orchestration</p>
<pre><code class="language-python">Invoke-RestMethod -Method Post -Uri http://localhost:7001/api/youragent/run
</code></pre>
<p>Refer to this <a href="https://www.azureguru.net/multi-agent-orchestration-through-azure-durable-functions-in-microsoft-agent-framework">article</a> for step by step process that demonstrates Durable multi agent orchestration in MAF.</p>
<p>Implementation of HITL for Durable agents is even more complex.</p>
<p>You first have to define <strong>StartOrchestrationAsync</strong> and <strong>RunOrchestrationAsync</strong> methods and then under the <strong>RunOrchestrationAsync</strong> method, create an external event that waits for the user response.</p>
<pre><code class="language-csharp">[Function(nameof(RunOrchestrationAsync))]
public static async Task RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{

HumanApproval humanResponse;

humanResponse = await context.WaitForExternalEvent&lt;HumanApproval&gt;(
eventName: "HumanApproval", timeout: TimeSpan.FromHours(1));      

}
</code></pre>
<p>Then implement a custom routing trigger to handle the user input.</p>
<pre><code class="language-csharp">[Function(nameof(HumanApprovalAsync))]
 public static async Task&lt;HttpResponseData&gt; HumanApprovalAsync(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/notification/{instanceId}")] HttpRequestData req, string instanceId,[DurableClient] DurableTaskClient client)
 {
     var humanapproval = await req.ReadFromJsonAsync&lt;HumanApproval&gt;();    
     await client.RaiseEventAsync(instanceId, "HumanApproval", humanapproval);
     HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
     return response;
 }
</code></pre>
<p>To trigger the workflow execution</p>
<pre><code class="language-graphql">$body = @{
    input = "Some Text"    
} | ConvertTo-Json

Invoke-RestMethod -Method Post `
    -Uri http://localhost:{Port no set in your launchsettings.json file}/api/hitl/run `
    -ContentType application/json `
    -Body $body
</code></pre>
<p>and to invoke the approval process</p>
<pre><code class="language-csharp">$json = '{"IsApproved":"Yes"}'

Invoke-RestMethod `
-Uri "http://localhost:{Port no set in your launchsettings.json file}/api/hitl/notification/{InstanceId from StartOrchestrationAsync method}" `
-Method Post `
-ContentType "application/json" `
-Body $json
</code></pre>
<p>Refer to this <a href="https://www.azureguru.net/human-in-the-loop-hitl-in-azure-durable-functions-for-microsoft-agent-framework">article</a> for detailed steps involved for HITL in MAF durable agents.</p>
<p>But with HITL (Human In The Loop) for Durable workflows , there is no need to maintain all these complexities. Durable workflows natively handles all of it.</p>
<h3>Implementation</h3>
<p>We will use the use case from my previous <a href="https://www.azureguru.net/durable-workflows-in-microsoft-agent-framework">article</a> on MAF durable workflows.</p>
<p>Only addition in this case would be the introduction of property <strong>IsApproved</strong> to the <strong>_Response</strong> object and introducing <strong>RequestPort</strong> to the workflow.</p>
<p><em><strong>_Response* *Object Earlier Version</strong></em> <em><strong>&gt;&gt;</strong></em></p>
<pre><code class="language-csharp">public sealed class _Response
{
    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;

    [JsonPropertyName("inputnumber")]
    public string InputNumber { get; set; } = string.Empty;

    [JsonPropertyName("squareroot")]
    public string SquareRoot { get; set; } = string.Empty;
}
</code></pre>
<p><em><strong>_Response Object New Version &gt;&gt;</strong></em></p>
<pre><code class="language-csharp">public sealed class _Response
{
    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;

    [JsonPropertyName("inputnumber")]
    public string InputNumber { get; set; } = string.Empty;

    [JsonPropertyName("squareroot")]
    public string SquareRoot { get; set; } = string.Empty;

    [JsonPropertyName("isapproved")]
    public Boolean IsApproved { get; set; }
}
</code></pre>
<p><em><strong>RequestPort</strong></em></p>
<pre><code class="language-csharp">RequestPort requestPort = RequestPort.Create&lt;_Response, _Response&gt;("NotificationApproval");
</code></pre>
<p>add the <strong>RequestPort</strong> as an Edge to the workflow.</p>
<pre><code class="language-csharp">RequestPort requestPort = RequestPort.Create&lt;_Response, _Response&gt;("NotificationApproval");

WorkflowBuilder builder = new(typeDetectionExecutor);

Workflow workflow = builder.AddEdge(typeDetectionExecutor, squareRootcalculatorExecutor)
.AddEdge(squareRootcalculatorExecutor, requestPort)
.AddEdge(requestPort, sendNumberNotificationExecutor)
.WithOutputFrom(sendNumberNotificationExecutor).WithName("NumberDetector").Build();
</code></pre>
<p>That's all.. No need for any complex custom routing logic and handling external events through the code.</p>
<p>Execute the Durable Function and it auto creates the underlying endpoints</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/6724c581-cbc0-4fe8-93cb-74a55c798369.png" alt="Human In the Loop for Durable Functions In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>To trigger the workflow just invoke the endpoints through <strong>PowerShell</strong></p>
<pre><code class="language-csharp">$json = '{"inputnumber":"12"}'
Invoke-RestMethod -Uri "http://localhost:7001/api/workflows/NumberDetector/run" -Method POST -ContentType "application/json" -Body $json
</code></pre>
<p>It creates an Orchestration Id in the DTS</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d3c556f1-a2ff-4074-badc-5d55bf0d19e9.png" alt="Human In the Loop for Durable Functions In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/1554c354-8966-4309-9efd-41bc3b1f18f5.png" alt="Human In the Loop for Durable Functions In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Above, an OrchestrationId <strong>782ad04696204eaa8161c66c27b11a20</strong> is created.</p>
<p>Now use the OrchestrationId to invoke HITL for the worklfow.</p>
<pre><code class="language-csharp">$json = '{
  "eventName": "NotificationApproval",
  "response": { "isapproved": true}
}'

Invoke-RestMethod `
-Uri "http://localhost:7001/api/workflows/NumberDetector/respond/782ad04696204eaa8161c66c27b11a20" `
-Method Post `
-ContentType "text/json" `
-Body $json
</code></pre>
<p>where <strong>isapproved</strong> is the new added property in the <strong>_Response</strong> object, <strong>NumberDetector</strong> is the name of the workflow and the <strong>eventName NotificationApproval</strong> used in the invocation above is the name of the <strong>RequestPort</strong> that we created earlier.</p>
<pre><code class="language-csharp">RequestPort requestPort = RequestPort.Create&lt;_Response, _Response&gt;("NotificationApproval");
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a7f86cf5-eef3-4edd-9d3a-0259d42926ee.png" alt="Human In the Loop for Durable Functions In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>In the DTS (Durable Task Scheduler) dashboard the orchestration status is Completed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c2d94941-0028-4e89-bc4e-9588484092be.png" alt="Human In the Loop for Durable Functions In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>The workflow setup</p>
<pre><code class="language-csharp">Workflow workflow = builder.AddEdge(typeDetectionExecutor, squareRootcalculatorExecutor)
.AddEdge(squareRootcalculatorExecutor, requestPort)
.AddEdge(requestPort, sendNumberNotificationExecutor)
.WithOutputFrom(sendNumberNotificationExecutor).WithName("NumberDetector").Build();
</code></pre>
<p>The <strong>SendNumberNotificationExecutor</strong> Executor code</p>
<pre><code class="language-csharp">internal sealed class SendNumberNotificationExecutor() : Executor&lt;_Response&gt;("SendNumberNotificationExecutor")
{
    [YieldsOutput(typeof(string))]
    public override async ValueTask HandleAsync(_Response response, IWorkflowContext context, CancellationToken cancellationToken = default)
    {        
        if (response.IsApproved == true)
        {
            await context.YieldOutputAsync($"The approval is approved");
        }
        else
        {
            await context.YieldOutputAsync($"The approval is rejected");

        }
    }
}
</code></pre>
<p>One interesting aspect with Durable workflows is that, its possible to invoke the orchestration through your custom **runId .**In the below example , I used a custom <strong>runId = 123</strong></p>
<pre><code class="language-csharp">$json = '{"inputnumber":"12"}'

Invoke-RestMethod `
-Uri "http://localhost:7001/api/workflows/NumberDetector/run?runid=123" `
-Method Post `
-ContentType "text/json" `
-Body $json
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3a5614f8-63c2-44d3-9c77-0663662e052b.png" alt="Human In the Loop for Durable Functions In Microsoft Agent Framework" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d0230d0a-0cb6-4761-835b-afaaa35b3e6a.png" alt="Human In the Loop for Durable Functions In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Invoking the approval process with <strong>runid =123</strong></p>
<pre><code class="language-csharp">
$json = '{
  "eventName": "NotificationApproval",
  "response": { "isapproved": true}
}'

Invoke-RestMethod `
-Uri "http://localhost:7001/api/workflows/NumberDetector/respond/123" `
-Method Post `
-ContentType "text/json" `
-Body $json
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/21039e3b-2d7c-4617-b6c3-a230229e5c0b.png" alt="Human In the Loop for Durable Functions In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9e1f42de-8173-4972-8225-ce04b4c73f7b.png" alt="Human In the Loop for Durable Functions In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<h3><strong>Execution &gt;&gt;</strong></h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/0682ece2-8de2-4557-90d6-13bc639abeba.gif" alt="Human In the Loop for Durable Functions In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>As mentioned earlier in the article, implementation of Human In The Loop (HITL) for Durable workflows is far less simpler and pretty straightforward compared to the Human In The Loop (HITL) implementation for Durable agents. This eases out lot of behind the scene complexities that is required to maintain the durable workflows in MAF.</p>
<p>I hope this article and the examples in it were detailed enough to get you started on Human In The Loop (HITL) process for Durable workflows.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Durable Workflows In Microsoft Agent Framework]]></title><description><![CDATA[My earlier article on workflows in MAF was focused on implementing them within a Console application. Those types of workflows run entirely in memory through an in-process runner.
Another article focu]]></description><link>https://www.azureguru.net/durable-workflows-in-microsoft-agent-framework</link><guid isPermaLink="true">https://www.azureguru.net/durable-workflows-in-microsoft-agent-framework</guid><category><![CDATA[workflow-orchestration]]></category><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[llm]]></category><category><![CDATA[Workflow Automation]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[C#]]></category><category><![CDATA[agentic ai development]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Fri, 26 Jun 2026 01:14:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f4f65721-3b0b-494f-a0b3-8b468be60513.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My earlier <a href="https://www.azureguru.net/deep-dive-into-workflow-execution-in-microsoft-agent-framework">article</a> on workflows in MAF was focused on implementing them within a Console application. Those types of workflows run entirely in memory through an in-process runner.</p>
<p>Another <a href="https://www.azureguru.net/multi-agent-orchestration-through-azure-durable-functions-in-microsoft-agent-framework">article</a> focused on setting up Azure Durable agents with Docker Durable Task Scheduler (DTS) Emulator. In that article, we had to expose and manage custom API endpoints to trigger and control the agent orchestration process which requires additional implementation.</p>
<p>But with workflows running as durable workflows there is no need to define custom endpoints for workflow invocation. The durable workflow runtime automatically handles workflow execution , persistence, checkpointing and orchestration behind the scenes.</p>
<p>In this article we will see how Durable workflows can be implemented in MAF.</p>
<h3>Use Case</h3>
<p>The use case in this article is a more simplified version of the use case that I used in my <a href="https://www.azureguru.net/deep-dive-into-workflow-execution-in-microsoft-agent-framework">article</a> that was an introduction to the workflow execution. The reason I simplified the use case is for two reasons</p>
<ul>
<li><p>The use case was pretty complex and I received feedback from some readers suggesting that it made them quite confused.</p>
</li>
<li><p>The use case extensively used switch/conditions to set up conditional executors which unfortunately does not work in Durable workflows at the time of this writing. I have reported the issue on MAF GitHub.</p>
</li>
</ul>
<p>Link : <a href="https://github.com/microsoft/agent-framework/issues/6722">https://github.com/microsoft/agent-framework/issues/6722</a></p>
<p>A simplified use case in this article</p>
<p><strong>PrimeNumberDetector</strong> &gt;&gt; <strong>SquareRootCalculator</strong> &gt;&gt; <strong>NotificationSender</strong></p>
<p><strong>PrimeNumberDetector</strong> : Detects if a given number is a prime number.</p>
<p><strong>SquareRootCalculator :</strong> Calculates the square root of the given number.</p>
<p><strong>NotificationSender :</strong> Sends the findings of the above two executors.</p>
<p>Think of it as a serial workflow execution.</p>
<h3>SetUp</h3>
<p>Ensure that you have DTS running. You can refer to this <a href="https://www.azureguru.net/azure-durable-agents-in-microsoft-agent-framework-with-docker-durable-task-scheduler-dts-emulator">article</a> on how to setup <strong>Durable Task Scheduler (DTS)</strong> on Docker .</p>
<p>Create a new Azure Function project and add the following references.</p>
<pre><code class="language-csharp">dotnet add package Azure;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Agents.AI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package Microsoft.Extensions.Configuration;
dotnet add package Microsoft.Extensions.DependencyInjection;
dotnet add package Microsoft.Extensions.Hosting;
dotnet add package Microsoft.Agents.AI.Hosting.AzureFunctions--prerelase;
dotnet add package Microsoft.Azure.Functions.Worker.Builder;
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore;
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
dotnet add package  Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged;
</code></pre>
<p>Of the above ,ensure that you don't miss to reference the following two libraries in the project.</p>
<pre><code class="language-csharp">Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged;
</code></pre>
<p>Otherwise you will face issue that is highlighted in the following GitHub post.</p>
<p><a href="https://github.com/microsoft/agent-framework/issues/5927"><strong>https://github.com/microsoft/agent-framework/issues/5927</strong></a></p>
<p>Not referencing the above two libraries, will result in you having to declare a dummy orchestrator.</p>
<pre><code class="language-csharp">public static class MyDummyOrchestrator
{
    [Function(nameof(MyDummyOrchestrator))]
    public static Task RunOrchestrator(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        return Task.CompletedTask;
    }
}
</code></pre>
<p>This is because the function worker fails to find an entry point of execution.</p>
<p>These are the major packages and version numbers that I have referenced in the project.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/aaed9784-708e-44d5-8183-658d625816c1.png" alt="Microsoft Agent Framework and Agent Workflows" />

<h3><strong>Code</strong></h3>
<p>Implementation is pretty straightforward.</p>
<p>After the above artifacts in place, add the following code to read the settings from <code>appsettings.json</code> in <strong>Program.cs</strong> of the project.</p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p>Read the credentials and register a <code>Chatclient</code> as a keyedservice.</p>
<pre><code class="language-csharp">var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

ServiceCollection servicecollection = new();

builder.Services.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
                .AsIChatClient()
    )
);
</code></pre>
<p>In the next step, register the <strong>ChatClientAgent</strong> in the hosted DI container to identify if the given number is a Prime number or a Non Prime number.</p>
<pre><code class="language-csharp">servicecollection.AddSingleton&lt;ChatClientAgent&gt;(sp =&gt;
{
    Func&lt;ChatClientAgentOptions&gt; func = () =&gt;
    {
        return new ChatClientAgentOptions
        {
            ChatOptions = new ChatOptions
            {
                Instructions = "You are a helpful agent. You check if a given number is a prime number or a non prime number",
                ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema&lt;DetectionResult&gt;()
            },
            Name = "Number Detector",
            Id = "1"
        };
    };

    return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"), options: func());

});
</code></pre>
<p>Register another <strong>ChatClientAgent</strong> in the hosted DI container that calculates the square root of the provided number.</p>
<pre><code class="language-csharp">servicecollection.AddSingleton&lt;ChatClientAgent&gt;(sp =&gt;
{
    Func&lt;ChatClientAgentOptions&gt; func = () =&gt;
    {

        return new ChatClientAgentOptions
        {

            ChatOptions = new ChatOptions
            {
                Instructions = "You are a helpful assistant.You calculate the square root of the provided number",
                ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema&lt;_Response&gt;()
            },
            Name = "SquareRootCalculator",
            Id = "2"

        };

    };

return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"), options: func());
});
</code></pre>
<p>For the <strong>ResponseFormat</strong> for the above two agents create the underlying class structure.</p>
<pre><code class="language-csharp">public sealed class DetectionResult
{
    [JsonPropertyName("numberType")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public NumberType numberType { get; set; }

    [JsonPropertyName("reason")]
    public string Reason { get; set; } = string.Empty;

    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;

    [JsonPropertyName("inputnumber")]
    public string InputNumber { get; set; } = string.Empty;

}

public sealed class _Response
{
    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;

    [JsonPropertyName("inputnumber")]
    public string InputNumber { get; set; } = string.Empty;

    [JsonPropertyName("squareroot")]
    public string SquareRoot { get; set; } = string.Empty;
}
</code></pre>
<p>The <strong>NumberType</strong> property in the <strong>DetectionResult</strong> class is an Enum type.</p>
<pre><code class="language-csharp">public enum NumberType
{
    PrimeNumber,
    NotPrimeNumber,
    UnSure
}
</code></pre>
<p>The <strong>Input</strong> object maps to the user input.</p>
<pre><code class="language-csharp">internal sealed class Input
{
    [JsonPropertyName("id")]
    public string Id { get; set; } = String.Empty;

    [JsonPropertyName("inputnumber")]
    public string InputNumber { get; set; } = String.Empty;
}
</code></pre>
<p><strong>Scopes</strong> to be used for shared states</p>
<pre><code class="language-csharp">internal static class NumbervalueConstants
{
    public const string NumbervalueScope = "Numbervalue";
    public const string NumberTypeScope = "Numbertype";
}
</code></pre>
<p><strong>TypeDetectionExecutor &gt;&gt;</strong></p>
<p>The <strong>TypeDetectionExecutor</strong> identifies if given number is a Prime number or a Non Prime number through the first agent <strong>Number Detector</strong> declared earlier.</p>
<p>Input to the executor is the <strong>Input</strong> object and output is the <strong>_Response</strong> object.</p>
<pre><code class="language-csharp">internal sealed class TypeDetectionExecutor : Executor&lt;Input, DetectionResult&gt;
{
    private readonly AIAgent _typeDetectionAgent;

    public TypeDetectionExecutor(AIAgent typeDetectionAgent) : base("TypeDetectionExecutor")
    {
        this._typeDetectionAgent = typeDetectionAgent;
    }

    [MessageHandler]
    public override async ValueTask&lt;DetectionResult&gt; HandleAsync(Input input, IWorkflowContext context, CancellationToken cancellationToken = default)
    {
        var Input = new Input { Id = Guid.NewGuid().ToString(), InputNumber = (input.InputNumber) };
        await context.QueueStateUpdateAsync(Input.Id, Input, scopeName: NumbervalueConstants.NumbervalueScope);
        var output = await _typeDetectionAgent.RunAsync(input.InputNumber);
        var detectionResult = JsonSerializer.Deserialize&lt;DetectionResult&gt;(output.Text);
        detectionResult.Id = Input.Id;
        return detectionResult;
    }
}
</code></pre>
<p>We store the <strong>Input</strong> object as value with <strong>Input.Id</strong> being the key to the shared state through the <strong>NumbervalueScope.</strong></p>
<pre><code class="language-csharp">await context.QueueStateUpdateAsync(Input.Id,Input, scopeName: NumbervalueConstants.NumbervalueScope);
</code></pre>
<p><strong>SquareRootCalculatorExecutor &gt;&gt;</strong></p>
<p>This executor calculates the square root of the given value through the second agent <strong>SquareRootCalculator</strong> declared earlier.</p>
<p>Input to executor is <strong>DetectionResult</strong> object and output is <strong>_Response</strong> object.</p>
<pre><code class="language-csharp">internal sealed class SquareRootCalculatorExecutor : Executor&lt;DetectionResult, _Response&gt;
{
    public readonly AIAgent _squarerootagent;

    public SquareRootCalculatorExecutor(AIAgent squarerootagent) : base("SquareRootCalculatorExecutor")
    {
        this._squarerootagent = squarerootagent;
    }

    [MessageHandler]
    public override async ValueTask&lt;_Response&gt; HandleAsync(DetectionResult detectionResult, IWorkflowContext context, CancellationToken cancellationToken = default)
    {

        var input = await context.ReadStateAsync&lt;Input&gt;(detectionResult.Id, scopeName: NumbervalueConstants.NumbervalueScope);
        var output = await this._squarerootagent.RunAsync(input.InputNumber);
        var response = JsonSerializer.Deserialize&lt;_Response&gt;(output.Text);
        response.InputNumber = input.InputNumber;
        response.Id = input.Id;
        await context.QueueStateUpdateAsync(input.Id, detectionResult, scopeName: NumbervalueConstants.NumberTypeScope);
        return response;
    }
}
</code></pre>
<p>In the above code, we read input through the shared state <strong>NumbervalueScope</strong></p>
<pre><code class="language-csharp">var input = await context.ReadStateAsync&lt;Input&gt;(detectionResult.Id, scopeName: NumbervalueConstants.NumbervalueScope);
</code></pre>
<p>which then acts as the input to the agent as follows</p>
<pre><code class="language-csharp">var output = await this._squarerootagent.RunAsync(input.InputNumber);
</code></pre>
<p>Store <strong>detectionResult</strong> to the shared state through scope <strong>NumberTypeScope</strong></p>
<pre><code class="language-csharp">await context.QueueStateUpdateAsync(input.Id, detectionResult, scopeName: NumbervalueConstants.NumberTypeScope);
</code></pre>
<p><strong>SendNumberNotificationExecutor &gt;&gt;</strong></p>
<p>The Input to the executor is <strong>_Response</strong> object. The executor <strong>YieldsOutput</strong> of type string.</p>
<pre><code class="language-csharp">internal sealed class SendNumberNotificationExecutor() : Executor&lt;_Response&gt;("SendNumberNotificationExecutor")
{
    [YieldsOutput(typeof(string))]
    public override async ValueTask HandleAsync(_Response response, IWorkflowContext context, CancellationToken cancellationToken = default)
    {
        var numbertype = await context.ReadStateAsync&lt;DetectionResult&gt;(response.Id, scopeName: NumbervalueConstants.NumberTypeScope);
        await context.YieldOutputAsync($"The value {response.InputNumber} is a {numbertype.numberType} and its square root is {response.SquareRoot}");
    }
}
</code></pre>
<p>Now that we have all the underlying structure and objects ready, time to piece them together.</p>
<p>Build service and fetch the agent from <strong>ServiceProvider</strong> as a <strong>ChatClientAgent.</strong></p>
<pre><code class="language-csharp">ServiceProvider serviceProvider = servicecollection.BuildServiceProvider();
var agent = serviceProvider.GetServices&lt;ChatClientAgent&gt;();
List&lt;ChatClientAgent&gt; chatclientagent = new(agent);
</code></pre>
<p>Assign the chatclientagents to the executors.</p>
<pre><code class="language-csharp">var typeDetectionExecutor = new TypeDetectionExecutor(chatclientagent[0]);
var squareRootcalculatorExecutor = new SquareRootCalculatorExecutor(chatclientagent[1]);
</code></pre>
<p>Output of the workflow will be through the <strong>SendNumberNotificationExecutor</strong></p>
<pre><code class="language-csharp">SendNumberNotificationExecutor sendNumberNotificationExecutor = new();
</code></pre>
<p>Build the workflow and assign the <strong>Edges</strong></p>
<pre><code class="language-csharp">WorkflowBuilder builder = new(typeDetectionExecutor);

Workflow workflow = builder.AddEdge(typeDetectionExecutor, squareRootcalculatorExecutor)
.AddEdge(squareRootcalculatorExecutor, sendNumberNotificationExecutor)
.WithOutputFrom(sendNumberNotificationExecutor).WithName("NumberDetector").Build();
</code></pre>
<p><strong>NumberDetector</strong> is the workflow name and acts as the endpoint to be invoked through http invoke method.</p>
<p>Set the host and assign the workflow to it.</p>
<pre><code class="language-csharp">sing IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableWorkflows(options =&gt; options.AddWorkflow(workflow))
Build();
app.Run();
</code></pre>
<p>That's all.. Go ahead and test it through <strong>PowerShell</strong> or <strong>Curl</strong>.</p>
<p>I tested it through <strong>PowerShell</strong> using the following script.</p>
<pre><code class="language-python">$json = '{"inputnumber":"13"}'

Invoke-RestMethod `
-Uri "http://localhost:7001/api/workflows/NumberDetector/run" `
-Method Post `
-ContentType "text/json" `
-Body $json
</code></pre>
<p><strong>Timeline &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/ffaa1811-35e6-4b70-b096-b03167954bc3.png" alt="Durable Workflows In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p><strong>History &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/74d0ed4a-37f7-4344-bce9-5247cf5f8798.png" alt="Durable Workflows In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p><strong>Flow &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/949a6224-e74e-4a12-8e3e-a8dd8113b10a.png" alt="Durable Workflows In Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>You might wonder how does workflow know about property <strong>inputnumber</strong> sent through the request .</p>
<p>Basically the workflow auto identifies the property of the input object to the first executor in the workflow.</p>
<p>In this case the first executor to the workflow is <strong>TypeDetectionExecutor</strong> that has <strong>Input</strong> object which is the input to the executor which in turn has a property named <strong>InputNumber(JsonPropertyName("inputnumber"))</strong>.</p>
<p>This is how the workflow identifies the property. Following is the code snippet for the clarification.</p>
<p><strong>TypeDetectionExecutor :</strong></p>
<pre><code class="language-csharp">internal sealed class TypeDetectionExecutor : Executor&lt;Input, DetectionResult&gt;
</code></pre>
<p><strong>Input :</strong></p>
<pre><code class="language-csharp">internal sealed class Input
{
    [JsonPropertyName("id")]
    public string Id { get; set; } = String.Empty;

    [JsonPropertyName("inputnumber")]
    public string InputNumber { get; set; } = String.Empty;
}
</code></pre>
<h3><strong>Execution &gt;&gt;</strong></h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/ee643f38-be84-4056-8543-811680c91fb9.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>Durable Functions are a great way to build reliable, long-running and stateful workflows without having to manage the complexity of checkpoints or recovery mechanisms manually.</p>
<p>Through this article I tried to showcase how to design and develop Durable workflows for seamless process execution. I hope this article was helpful enough to get you started on Durable Workflows.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Observability in Microsoft Agent Framework through Open Telemetry in Azure Application Insights, KQL and Aspire on Docker]]></title><description><![CDATA[In a typical Agent AI and Workflow setups when something goes wrong major questions start piling up:

Which agent handled the request?

Which tool was invoked?

How long did each step take?

Where did]]></description><link>https://www.azureguru.net/observability-in-microsoft-agent-framework-through-open-telemetry-in-azure-application-insights-kql-and-aspire-on-docker</link><guid isPermaLink="true">https://www.azureguru.net/observability-in-microsoft-agent-framework-through-open-telemetry-in-azure-application-insights-kql-and-aspire-on-docker</guid><category><![CDATA[AI]]></category><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[observability]]></category><category><![CDATA[OpenTelemetry]]></category><category><![CDATA[opentelemetry collector]]></category><category><![CDATA[ai-agent]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Mon, 22 Jun 2026 22:42:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d33d4f6f-f7a5-42a3-b72d-3d6e15468064.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In a typical Agent AI and Workflow setups when something goes wrong major questions start piling up:</p>
<ul>
<li><p>Which agent handled the request?</p>
</li>
<li><p>Which tool was invoked?</p>
</li>
<li><p>How long did each step take?</p>
</li>
<li><p>Where did the failure occur?</p>
</li>
<li><p>Why is the workflow slower than expected?</p>
</li>
</ul>
<p>Without proper observability, answering these questions is very difficult.</p>
<p>This is where Open Telemetry, Azure Application Insights and .NET Aspire come together. They provide a powerful way to gain visibility into what's happening inside your Microsoft Agent Framework applications. Instead of relying on console logs and guesswork, you can trace requests end-to-end, monitor agent interactions, measure performance and quickly identify bottlenecks.</p>
<p>In this article, we'll build a Microsoft Agent Framework application running in Docker, integrate it with Azure Application Insights and use Aspires dashboard to visualize telemetry in real time.</p>
<h3>UseCase</h3>
<p>We will use an use case from this <a href="https://www.azureguru.net/microsoft-agent-framework-with-background-service-and-azure-service-bus">post</a> where an agent runs as a background service and the input to the agent is pushed from Azure Service Bus.</p>
<h3><strong>SetUp</strong></h3>
<p>Before we move to the <strong>OpenTelemetry</strong> implementation, we will have to set up Aspire service in Docker and the Log Analytics workspace on Azure.</p>
<p>Pull up the Docker desktop and run the following command in the Docker terminal.</p>
<pre><code class="language-yaml">docker run --rm -it  -p 18888:18888  -p 4317:18889  -p 4318:18890  -d --name aspire-dashboard  mcr.microsoft.com/dotnet/aspire-dashboard:latest
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/6b10579e-2cc8-40f1-b8b7-665186f1a22e.png" alt="" style="display:block;margin:0 auto" />

<p>Once installed , navigate to <a href="http://localhost:18888/login">http://localhost:18888/login</a> . The login page would prompt for a token value</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/4aaabe0b-5dc6-4cc7-a381-23cd78a1341f.png" alt="Microsoft Agent Framework and Agent Workflows" style="display:block;margin:0 auto" />

<p>Navigate to the Containers tab and under Logs you will find Aspire Dashboard details. We get Dashboard URL, Login URL with the embedded token and the corresponding gRPC and HTTP URLs.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/40461489-e493-46fc-aca0-74bdff88ee0e.png" alt="Microsoft Agent Framework and Agent Workflows" style="display:block;margin:0 auto" />

<p>or run the following command in the Docker terminal.</p>
<pre><code class="language-yaml">docker ps
</code></pre>
<p>Enter the token value and you will land up on the Aspire Dashboard</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8e57f52e-d374-4d4f-8411-1f6c405c34d9.png" alt="Microsoft Agent Framework and Agent Workflows" style="display:block;margin:0 auto" />

<p>Set up <strong>Log Analytics workspace</strong> and <strong>Application Insights</strong> resource, search for Log Analytics workspace and Application Insights in Azure Marketplace.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/ae46c208-3721-4860-8f83-f404d0aff49e.png" alt="Microsoft Agent Framework and Agent Workflows" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/90780ab7-3555-4b3c-b776-2d8797830ec0.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<p>While setting up Application Insights you will have to select an option to select the Log Analytics workspace.</p>
<p>The overall setup for these two resources is pretty straightforward.</p>
<p>If required, you can also change the Log Analytics Workspace for the Application Insights.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/4d9d5d1e-953d-4518-8ece-3896c9c73a01.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<p>In the next step, add the following resources to the project</p>
<pre><code class="language-csharp">dotnet add package Azure.Monitor.OpenTelemetry.Exporter
dotnet add package OpenTelemetry.Logs
dotnet add package OpenTelemetry.Metrics
dotnet add package OpenTelemetry.Resources
dotnet add package OpenTelemetry.Trace
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package System.Diagnostics
</code></pre>
<p>Then add the following telemetry services</p>
<pre><code class="language-csharp"> builder.Services.AddOpenTelemetry()
.WithTracing(tracing =&gt;
{
    tracing
        .SetSampler(new AlwaysOnSampler())
        .SetResourceBuilder(
        ResourceBuilder.CreateDefault()
        .AddService("MyApp"))   
        .AddSource("MyApp.Source")                      
        .AddOtlpExporter(options =&gt; options.Endpoint = new Uri("http://localhost:4317"))
        .AddHttpClientInstrumentation()
        .AddConsoleExporter()
        .AddAzureMonitorTraceExporter(options =&gt;
        {
         options.ConnectionString = "AppLicationInsights Connection String"
                ;
        });
});

 builder.Logging.ClearProviders();
 builder.Logging.AddConsole();
 builder.Logging.SetMinimumLevel(LogLevel.Trace);
</code></pre>
<p>Lets break down the major aspects of the above code :</p>
<p>The following code sets the <strong>service name</strong> that appears in the telemetry. Without a service name the app traces appears under a generic or auto-generated name.</p>
<pre><code class="language-csharp">.AddService("MyApp"))
</code></pre>
<p>Define a source <strong>MyApp.Source</strong> for the service <strong>MyApp</strong> that listens to the activities created in it.</p>
<pre><code class="language-csharp">.AddSource("MyApp.Source")
</code></pre>
<p>Now configure OpenTelemetry to to export traces, metrics, and logs to an OTLP collector. In our case the OTLP collector is an Aspire service running on Docker.</p>
<pre><code class="language-csharp">.AddOtlpExporter(options =&gt; options.Endpoint = new Uri("http://localhost:4317"))
</code></pre>
<p>You might wonder where did port 4317 come from.</p>
<p>Recall that we had configured 4317 while setting up the Aspire Docker container. So 4317 is the host port and 18889/18890 is the container port.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/1144404a-7991-492f-a1e0-efaa6b04952c.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<p>The next part of the code adds enrichment to logs when one service is calling another service using HTTP. This basically means that OpenTelemetry automatically adds extra context (metadata) to the logs, traces and spans about the outgoing HTTP request.</p>
<pre><code class="language-csharp">.AddHttpClientInstrumentation()
</code></pre>
<p>The next piece of code displays the trace to the console window during execution. This part can be optional as I don't feel it does provides much of an value addition. It simply makes the traces visible during execution.</p>
<pre><code class="language-plaintext"> .AddConsoleExporter()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/109e8930-9bca-4a04-a43b-9f3147e0379b.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<p>The following piece of code exports the logs to the Azure Applications Insight resource that we created earlier.</p>
<pre><code class="language-csharp">.AddAzureMonitorTraceExporter(options =&gt;
{
options.ConnectionString = "AppLicationInsights Connection String";
}     
</code></pre>
<p>The connection string is available through the Applications Insight dashboard.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/ada90491-1d0b-4187-9fc6-d8e6c34f4541.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<p>In the following code , we first clear all the default loggers that ASP.NET adds. If required we add the logging info to the console and then set the minimum logging level to <strong>Trace</strong> which implies that all the details <strong>Debug</strong>, <strong>Info</strong>, <strong>Warning</strong> etc should be logged.</p>
<pre><code class="language-csharp">builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Trace);
</code></pre>
<p>In the Background Service, which in our case is named <strong>Worker</strong> we set different levels of span/traces.</p>
<p>First, define an <strong>ActivitySource</strong> called <strong>MyApp.Worker</strong> under the <strong>StartAsync</strong> operation.</p>
<pre><code class="language-csharp">private static readonly ActivitySource Activity = new("MyApp.Worker");
</code></pre>
<p>Next, define an <strong>Activity</strong> called <strong>Worker.Start</strong> and trace/spans under it</p>
<pre><code class="language-csharp">using var activity_start = Activity.StartActivity("Worker.Start", ActivityKind.Server);

activity_start.SetTag("worker.name", "Worker_1");
activity_start.SetTag("Activity Name", activity_start.DisplayName);
activity_start.SetTag("Source Name", activity_start.Source.Name);
</code></pre>
<p>and then under <strong>RunAsync</strong> event which is a method that runs as a Background service.</p>
<p>Note that we have a different <strong>Activity</strong> called <strong>Worker.Run</strong> where different sets of trace/spans are registered.</p>
<pre><code class="language-csharp">using var activity = Activity.StartActivity("Worker.Run", ActivityKind.Server);

 activity?.SetTag("receiver.name", "promptqueue");           
 activity?.SetTag("receiver.activity", "servicebusdata");
 activity?.SetTag("operation.type", "background-job");
 activity?.SetTag("operationstatus", "start");
 activity?.SetTag("servicebus.ingestion.start", "promptqueue");
</code></pre>
<p>Trace the token usage by the agent</p>
<pre><code class="language-csharp">AgentResponse response = await agent.RunAsync(message.Body.ToString(), session);     
Console.WriteLine(response.Text);
activity?.SetTag("InputTokeCount", response.Usage.InputTokenCount);
activity?.SetTag("OutputTokeCount", response.Usage.OutputTokenCount);
activity?.SetTag("TotalTokenCount", response.Usage.TotalTokenCount);
</code></pre>
<h3>Complete Code</h3>
<p><strong>Program.cs &gt;&gt;</strong></p>
<pre><code class="language-csharp">using Azure;
using Azure.AI.OpenAI;
using Azure.Messaging.ServiceBus;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Diagnostics;

internal class Program

{
    private async static Task Main(string[] args)

    {
        HostApplicationBuilder builder = Host.CreateApplicationBuilder();
        var configuration = new ConfigurationBuilder()
      .SetBasePath(Directory.GetCurrentDirectory())
      .AddJsonFile("appsettings.json", optional: false)
      .Build();

        var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

        builder.Services.AddKeyedChatClient("ChatClient", (sp =&gt; new AzureOpenAIClient(
                 new Uri(configuration["AppSettings:EndPoint"]), credential)
                     .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
   .AsIChatClient()));

        builder.Services.AddSingleton&lt;AIAgent&gt;(sp =&gt;
        {
            Func&lt;ChatClientAgentOptions&gt; func = () =&gt;
            {
                return new ChatClientAgentOptions
                {
                    ChatOptions = new ChatOptions
                    {

                        Instructions = "You are a helpful stock market analysis assistant"
                    }
                };

            };

            return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"), options: func());
        }

       );

        builder.Services.AddSingleton(sp =&gt;
        {
            return new ServiceBusClient(configuration["AppSettings:AzureQueue"], new ServiceBusClientOptions
            {
                TransportType = ServiceBusTransportType.AmqpTcp
            });
       });
        
        builder.Services.AddOpenTelemetry()
      .WithTracing(tracing =&gt;
      {
          tracing
              .SetSampler(new AlwaysOnSampler())
              .SetResourceBuilder(
              ResourceBuilder.CreateDefault()
              .AddService("MyApp")) 
              .AddSource("MyApp.Worker")                
              .AddOtlpExporter(options =&gt; options.Endpoint = new Uri("http://localhost:4317"))                                                                                              
              .AddHttpClientInstrumentation() 
              .AddConsoleExporter()
              .AddAzureMonitorTraceExporter(options =&gt;
              {
                  options.ConnectionString = "Application Insight Connection String";
              });
      });
        //docker ps
        builder.Logging.ClearProviders();
        builder.Logging.AddConsole();
        builder.Logging.SetMinimumLevel(LogLevel.Trace);
        builder.Services.AddHostedService&lt;Worker&gt;();
        using IHost host = builder.Build();
        await host.RunAsync().ConfigureAwait(false);
    }

    internal sealed class Worker(AIAgent agent, ServiceBusClient servicebusClient,IHostApplicationLifetime appLifetime, ILogger&lt;Worker&gt; logger, IHost host) : IHostedService
    {
        private AgentSession? session;
        private Task? backgroundTask;
        private static readonly ActivitySource Activity = new("MyApp.Worker");

        public async Task StartAsync(CancellationToken cancellationToken)
        {

            using var activity_start = Activity.StartActivity("Worker.Start", ActivityKind.Server);

           
                activity_start.SetTag("worker.name", "Worker_1");
                activity_start.SetTag("Activity Name", activity_start.DisplayName);
                activity_start.SetTag("Source Name", activity_start.Source.Name);
           

            session = await agent.CreateSessionAsync(cancellationToken);
            backgroundTask = RunAsync(appLifetime.ApplicationStopping);
        }


        public async Task RunAsync(CancellationToken cancellationToken)
        {
            await Task.Delay(1000, cancellationToken);

            var receiver = servicebusClient!.CreateReceiver("promptqueue");

            while (!cancellationToken.IsCancellationRequested)
            {
                using var activity = Activity.StartActivity("Worker.Run", ActivityKind.Server);
                activity?.SetTag("receiver.name", "promptqueue");           
                activity?.SetTag("receiver.activity", "servicebusdata");
                activity?.SetTag("operation.type", "background-job");
                activity?.SetTag("operationstatus", "start");
                activity?.SetTag("servicebus.ingestion.start", "promptqueue");

                ServiceBusReceivedMessage message = await receiver.ReceiveMessageAsync(cancellationToken: cancellationToken);

                activity?.SetTag("servicebus.ingestion.end", "promptqueue");

                if (message == null)
                {
                    continue;
                }
                Console.WriteLine("----------------------------------------------------------------------------------------------------------------------------");
                Console.WriteLine("");
                activity?.SetTag("AgentInput", message.Body.ToString());
                activity?.SetTag("SessionId", session.ToString());
                AgentResponse response = await agent.RunAsync(message.Body.ToString(), session);              
                Console.WriteLine(response.Text);
                activity?.SetTag("InputTokeCount", response.Usage.InputTokenCount);
                activity?.SetTag("OutputTokeCount", response.Usage.OutputTokenCount);
                activity?.SetTag("TotalTokenCount", response.Usage.TotalTokenCount);
                Console.WriteLine("");
                Console.WriteLine("----------------------------------------------------------------------------------------------------------------------------");
                await receiver.CompleteMessageAsync(message);
                activity?.SetTag("operationstatus", "end");
            }

        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            if (backgroundTask != null)
            {
                await backgroundTask;

            }
        }
    }

}
</code></pre>
<p>Run the app and check the Logs options in Application Insights and you will see the traces.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/598990dc-455d-4506-997d-5d695b749c4c.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/6b37eea3-0504-4800-a427-e852900f25c6.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/3bd63584-316c-4b6e-9af5-b192a89e98b2.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<h3>KQL Queries</h3>
<p>We can leverage KQL queries to query and fetch detailed insights from these trace logs.</p>
<pre><code class="language-yaml">union traces, requests, dependencies
| order by timestamp desc
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/a940da8d-fbe0-495b-9685-939b755fb278.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<p>Expanding <strong>customDimensions</strong> gets the token usage details</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7e3c07b5-ba0f-4a13-a873-56b6a4d09a68.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<pre><code class="language-yaml">requests
| where name == "Worker.Start"
| order by timestamp desc
</code></pre>
<p>Gets the the traces for event the <strong>Worker.Start</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/49328550-ca68-45dd-8ced-3f6b8c70c398.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<pre><code class="language-yaml">requests
| where customDimensions["worker.name"] == "Worker_1"
| order by timestamp desc
</code></pre>
<p>Trace details for worker <strong>Worker_1</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f034c543-258e-407a-8d5a-e8c8540e66b2.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<pre><code class="language-yaml">requests
| project timestamp,name,duration,success
| order by duration desc
</code></pre>
<p>Duration and success status</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/43b3dbc2-6dd8-4053-bc95-8c07c09927c8.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<pre><code class="language-yaml">requests
| where customDimensions["TotalTokenCount"] &gt;100
| order by timestamp desc
</code></pre>
<p>Details of requests that costs more than 100 tokens.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/622b3268-9e37-4e87-9464-2ddd37e92f82.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<pre><code class="language-yaml">requests
| where duration &gt;60000
| order by duration 
</code></pre>
<p>Gets the details of requests having a duration &gt;60000</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/55f56957-bb88-4191-89b7-b5f51d3d2769.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<h3>Aspire Dashboard</h3>
<p>Navigate to the <strong>Apsire</strong> dashboard thorough <a href="http://localhost:18888/traces">http://localhost:18888/traces</a> that was configured earlier</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/ea707b80-87a5-4273-8f32-3bf629ac6681.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/e0527936-894c-4eb7-b1ab-b7835ad74c6f.png" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<p><strong>Execution &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c1d6754f-61cf-4b3e-81f6-80f6d9daf4c3.gif" alt="Microsoft Agent Framework and Agent Observability" style="display:block;margin:0 auto" />

<h3><strong>Conclusion</strong></h3>
<p>Through this article, I demonstrated the most viable options by which Observability in Microsoft Agent Framework through Open Telemetry in Azure Application Insights and Aspire can be implemented.</p>
<p>I would personally prefer Observability implementation in MAF through Azure Application Insights because of strong KQL support with the flexibility of leveraging KQL queries which Aspire lacks. Added to that, Azure Application Insights also seamlessly integrates with Grafana to implement rich dashboard experience which I would possibly cover in another article.</p>
<p>I hope this article helps you get started with <strong>OpenTelemetry</strong> implementation for Microsoft Agent Framework.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[AG-UI Protocol In Microsoft Agent Framework]]></title><description><![CDATA[The Agent-User Interaction Protocol or commonly known as AG-UI is a protocol developed to expose AI agents to web/mobile frontends.
Think of AG-UI as HTTP protocol for AI Agents. With AG-UI its now po]]></description><link>https://www.azureguru.net/ag-ui-protocol-in-microsoft-agent-framework</link><guid isPermaLink="true">https://www.azureguru.net/ag-ui-protocol-in-microsoft-agent-framework</guid><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[llm]]></category><category><![CDATA[AG-UI]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[agents]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Wed, 17 Jun 2026 16:07:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/db122890-11c3-4cf8-b508-86b4eb00f0da.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The Agent-User Interaction Protocol or commonly known as AG-UI is a protocol developed to expose AI agents to web/mobile frontends.</p>
<p>Think of AG-UI as HTTP protocol for AI Agents. With AG-UI its now possible to call and render tools through agents invoked from your frontends. Before AG-UI all frontend calls to AI agents required custom integration but AG-UI has made it the integration pretty straightforward.</p>
<p>Most people get confused between AG-UI, MCP and A2A. The following table from <a href="https://docs.ag-ui.com/introduction">https://docs.ag-ui.com/introduction</a> clears the distinction between all the three protocols.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/242073ce-95e6-4843-906d-65d26b7c0741.png" alt="AG-UI protocol in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>In this article, I will not delve into the in-depth details of AG-UI. Instead, I will focus on its implementation. You can refer to the AG-UI documentation <a href="https://docs.ag-ui.com/introduction">here</a> and <a href="https://learn.microsoft.com/en-us/agent-framework/integrations/ag-ui/getting-started">here</a>.</p>
<p>There are two approaches through which AG-UI Integration with Microsoft Agent Framework can be implemented.</p>
<ul>
<li><p>Backend Tool Rendering</p>
</li>
<li><p>Frontend Tool Rendering</p>
</li>
</ul>
<p>In Backend Tool Rendering , the function tools are defined on the server and the tools are executed on the server through client calls. The client receives updates about tool execution progress and the results are streamed to the client in real time.</p>
<p>In Frontend Tool Rendering , the function tools are registered and defined on the client and the tools get executed in the client environment . The results are sent back to the server to be incorporate into the server responses.</p>
<p>In this article I will demonstrate implementation of server side tool rendering.</p>
<p>We will have two functions :</p>
<ul>
<li><p><strong>ReturnCityTemperature</strong> &gt;&gt; Returns temperature of a given city</p>
</li>
<li><p><strong>ReturnCountryCapital</strong> &gt;&gt; Returns capital city of a given country</p>
</li>
</ul>
<p>We will use a single agent to handle requests for both the methods.</p>
<h3><strong>SetUp</strong></h3>
<p><strong>AG-UI Server &gt;&gt;</strong></p>
<p>The first step is to create an AG-UI server.</p>
<p>Create a new ASP.NET core application and add the following packages</p>
<pre><code class="language-csharp">dotnet add package Azure
dotnet add package Azure.AI.OpenAI
dotnet add package Microsoft.Agents.AI
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging
dotnet add package Microsoft.Agents.AI
dotnet add package Microsoft.Agents.AI.Hosting.AGUI.AspNetCore
</code></pre>
<p>Add <strong>appsetting.json</strong> to the project</p>
<pre><code class="language-csharp">"AppSettings": { 
    "Chat_DeploymentName": "Deployment Name",
    "EndPoint": "Azure OpenAI endpoint",
    "ApiKey": "Azure OpenAI API key"
}
</code></pre>
<p>In <strong>launchSettings.json</strong>, configure the ports on which the server should listen.</p>
<pre><code class="language-csharp">{
  "profiles": {
    "AGUIServer": {
      "commandName": "Project",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "https://localhost:49408;http://localhost:49409"
    }
  }
}
</code></pre>
<p>In the above settings , the application is configured to listen on ports <strong>49408</strong> (HTTPS) and <strong>49409</strong> (HTTP). For this article, we will use <strong>49408</strong> on https.</p>
<h3><strong>Code</strong></h3>
<p>Now that we have all the underlying artifacts in place, add the following code to read the settings from <code>appsettings.json</code></p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p><strong>Create a DI container</strong></p>
<pre><code class="language-csharp"> ServiceCollection servicecollection = new ServiceCollection();
</code></pre>
<p><strong>Read the credentials and register</strong> <code>Chatclient</code></p>
<pre><code class="language-csharp">var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

servicecollection.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
   .AsIChatClient()
    )
);
</code></pre>
<p><strong>Register an AI Agent in the DI container</strong></p>
<pre><code class="language-csharp">servicecollection.AddScoped&lt;AIAgent&gt;(sp =&gt;
       {
           Func&lt;ChatClientAgentOptions&gt; func = () =&gt;

           {
               return new ChatClientAgentOptions
               {

                   ChatOptions = new ChatOptions
                   {
                      Instructions = "You are a helpful assistant.",
                   },
                    Id = "1",
                    Name = "HelpfulAgent"                  
               };

           };
           return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"), options: func());
       });
</code></pre>
<p>We define the required request and response objects for <strong>CityTemperature</strong> and <strong>CountryCapital</strong>.</p>
<p><strong>CityTemperatureSearchRequest &amp; CityTemperatureSearchResponse</strong> <strong>&gt;&gt;</strong></p>
<pre><code class="language-csharp"> public class CityTemperatureSearchRequest
 {
     public string City { get; set; }
 }

 public class CityTemperatureSearchResponse
 {
     public string City { get; set; }
     public string Temperature { get; set; }
 }
</code></pre>
<p><strong>CountryCapitalSearchRequest &amp; CountryCapitalSearchResponse &gt;&gt;</strong></p>
<pre><code class="language-csharp"> public class CountryCapitalSearchRequest
 {
     public string Country { get; set; }
 }

 public class CountryCapitalSearchResponse
 {
     public string Country { get; set; }
     public string Capital { get; set; }
 }
</code></pre>
<p>Next, create serialization metadata for both request and response types.</p>
<pre><code class="language-csharp">[JsonSerializable(typeof(CountryCapitalSearchRequest))]
[JsonSerializable(typeof(CountryCapitalSearchResponse))]
internal sealed partial class CountryCapitalSerializerContext : JsonSerializerContext;


[JsonSerializable(typeof(CityTemperatureSearchRequest))]
[JsonSerializable(typeof(CityTemperatureSearchResponse))]
internal sealed partial class CityTemperatureSerializerContext : JsonSerializerContext;
</code></pre>
<p>Register the above two serialization options and add the AIAgent support via AG-UI to the app.</p>
<pre><code class="language-csharp">WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
        builder.Services.AddHttpClient().AddLogging();
        builder.Services.ConfigureHttpJsonOptions(options =&gt; options.SerializerOptions.TypeInfoResolverChain.Add(CountryCapitalSerializerContext.Default));
        builder.Services.ConfigureHttpJsonOptions(options =&gt; options.SerializerOptions.TypeInfoResolverChain.Add(CityTemperatureSerializerContext.Default));
        builder.Services.AddAGUI();
</code></pre>
<p>Following delegates return the underlying data</p>
<p><strong>ReturnCountryCapital&gt;&gt;</strong></p>
<pre><code class="language-csharp"> public static Func&lt;CountryCapitalSearchRequest, CountryCapitalSearchResponse&gt; ReturnCountryCapital = (CountryCapitalSearchRequest) =&gt;

{
    switch (CountryCapitalSearchRequest.Country)
    {

        case "India":

            return new CountryCapitalSearchResponse
            {

                Country = "India",
                Capital = "New Delhi"
            };
            break;

        case "USA":

            return new CountryCapitalSearchResponse
            {

                Country = "USA",
                Capital = "Washington"
            };
            break;


        case "Germany":

            return new CountryCapitalSearchResponse
            {

                Country = "Germany",
                Capital = "Berlin"
            };
            break;

        case "Russia":

            return new CountryCapitalSearchResponse
            {

                Country = "Russia",
                Capital = "Moscow"
            };
            break;
    }

    return new CountryCapitalSearchResponse
    {

        Country = CountryCapitalSearchRequest.Country,
        Capital = "Unknown"
    };

};
</code></pre>
<p><strong>ReturnCityTemperature &gt;&gt;</strong></p>
<pre><code class="language-csharp">public static Func&lt;CityTemperatureSearchRequest, CityTemperatureSearchResponse&gt; ReturnCityTemperature = (CityTemperatureSearchRequest) =&gt;

{
    switch (CityTemperatureSearchRequest.City)
    {

        case "Mumbai":

            return new CityTemperatureSearchResponse
            {

                City = "Mumbai",
                Temperature = "40"
            };

            break;

        case "Pune":

            return new CityTemperatureSearchResponse
            {

                City = "Pune",
                Temperature = "41"
            };

            break;

        case "Delhi":

            return new CityTemperatureSearchResponse
            {

                City = "Delhi",
                Temperature = "42"
            };

            break;

        case "Chennai":

            return new CityTemperatureSearchResponse
            {

                City = "Chennai",
                Temperature = "43"
            };
            break;
    }

    return new CityTemperatureSearchResponse
    {

        City = CityTemperatureSearchRequest.City,
        Temperature = "Unknown"
    };

};
</code></pre>
<p>Register the above two delegates in the DI container as <strong>AIFunction</strong> with the corresponding <strong>SerializerOptions</strong> defined earlier.</p>
<pre><code class="language-csharp">  servicecollection.AddSingleton&lt;AIFunction&gt;(sp =&gt;
      {
          return AIFunctionFactory.Create(ReturnCityTemperature, new AIFunctionFactoryOptions { Name = "ReturnCityTemperature", Description = "Gets the current weather for a specific city", SerializerOptions = CityTemperatureSerializerContext.Default.Options });
      }

  );

  servicecollection.AddSingleton&lt;AIFunction&gt;(sp =&gt;
    {
        return AIFunctionFactory.Create(ReturnCountryCapital, new AIFunctionFactoryOptions { Name = "ReturnCountryCapital", Description = "Gets the capital city for a specific country.", SerializerOptions = CountryCapitalSerializerContext.Default.Options });
    }
);
</code></pre>
<p>Now create a DI container and fetch list of all <strong>AIFunction</strong> and <strong>ChatClientAgent</strong></p>
<pre><code class="language-csharp">
ServiceProvider serviceprovider = servicecollection.BuildServiceProvider();

var chatlient = serviceprovider.GetServices&lt;ChatClientAgent&gt;();

var aifunctions = serviceprovider.GetServices&lt;AIFunction&gt;();

List&lt;ChatClientAgent&gt; lstchatclient = new(chatlient);

List&lt;AITool&gt; functions = new(aifunctions);
</code></pre>
<p>Create an agent from the chatclient and register the ToolFunctions.</p>
<pre><code class="language-csharp"> var agent = lstchatclient[0].ChatClient.AsAIAgent(

     new ChatClientAgentOptions
     {
         ChatOptions = new ChatOptions
         {
             Tools = functions,
             Instructions = "You return capital city of a country and you also provide temperature of a city.You will use only the data provided to you and not use any external data"
         }
     }
  );
</code></pre>
<p>Expose the above MAF agent through an AG-UI endpoint.</p>
<pre><code class="language-csharp">WebApplication app = builder.Build();      
app.MapAGUI("/", agent);
await app.RunAsync();
</code></pre>
<p>Run the server and verify that it is listening on <a href="https://localhost:%7Bport">https://localhost:{port</a> number}.</p>
<p>In our case it will be <a href="https://localhost:49408">https://localhost:49408</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/80d8084d-c26f-4c7c-ad24-cca00fb7f94f.png" alt="AG-UI protocol in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Now that the server is up and running the next step is to configure a client that can invoke server side function tools.</p>
<p><strong>AG-UI Client &gt;&gt;</strong></p>
<p>Create a new console application project and add the following packages</p>
<pre><code class="language-csharp">dotnet add package Microsoft.Agents.AI;
dotnet add package Microsoft.Agents.AI.AGUI;
dotnet add package Microsoft.Extensions.AI;
</code></pre>
<p>In the Main method of <strong>Program.cs</strong>, create a <strong>AGUIChatClient</strong> through an <strong>HTTP client</strong> that references to the agent on the AGUI server at <a href="https://localhost:49408">https://localhost:49408</a></p>
<pre><code class="language-csharp"> string serverUrl = "https://localhost:49408";

 using HttpClient httpClient = new()
 {
     Timeout = TimeSpan.FromSeconds(60)
 };

 AGUIChatClient chatClient = new(httpClient, serverUrl);

 AIAgent agent = chatClient.AsAIAgent(
     name: "HelpfulAgent",
     description: "AG-UI Client Agent");

 AgentSession session = await agent.CreateSessionAsync();
</code></pre>
<p>Create a conversation history and send messages to the agent on the server.</p>
<pre><code class="language-csharp">  List&lt;ChatMessage&gt; messages =
  [
      new(ChatRole.System, "You are a helpful assistant.")
  ];

messages.Add(new ChatMessage(ChatRole.User, "What is temperature in Mumbai and what is capital of USA ?"));
</code></pre>
<p>With AG-UI in MAF, we can extend the client streaming to expose the following content types.</p>
<p><strong>FunctionCallContent</strong> &gt;&gt; It contains details of the invoked function including the function name and arguments used if any.</p>
<p><strong>FunctionResultContent</strong> &gt;&gt; It contains results of the function invocation and the results are exposed in json format.</p>
<p><strong>TextContent</strong> &gt;&gt; Contains the textual response generated by the agent.</p>
<p><strong>ErrorContent</strong> &gt;&gt; Contains errors if any.</p>
<pre><code class="language-csharp">await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
   {

       foreach (var content in update.Contents)
       {
           if (content is Microsoft.Extensions.AI.FunctionCallContent functioncallcontent)
           {
               Console.ForegroundColor = ConsoleColor.DarkCyan;
               Console.WriteLine($"\n[Function Call - Name: {functioncallcontent.Name}]");
               Console.ResetColor();

               foreach (var args in functioncallcontent.Arguments)
               {
                   Console.ForegroundColor = ConsoleColor.Cyan;
                   Console.WriteLine($"\n[Function Arguments - Argument Name : {args.Key} - Argument Value : {args.Value}");

                   Console.ResetColor();
               }
           }

           if (content is Microsoft.Extensions.AI.FunctionResultContent functionresultcontent)
           {
               Console.ForegroundColor = ConsoleColor.Red;
               Console.Write(($"\n[Function raw output : {functionresultcontent.Result}]"));
               Console.WriteLine();
               Console.ResetColor();
           }

           if (content is Microsoft.Extensions.AI.TextContent textContent)
           {                   
               Console.ForegroundColor = ConsoleColor.Green;
               Console.Write(textContent.Text);
               Console.ResetColor();
           }

           if (content is Microsoft.Extensions.AI.ErrorContent errorContent)
           {
               Console.ForegroundColor = ConsoleColor.Red;
               Console.WriteLine($"Error : {errorContent.Message}");
               Console.ResetColor();

           }
       }
   }
</code></pre>
<p>That's all. Test the client execution with a prompt .</p>
<pre><code class="language-html">What is the temperature in Delhi and capital of USA ?
</code></pre>
<p>Ensure that the server is up and running.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d99b3365-dcdc-44bb-9793-54403045d5a2.png" alt="AG-UI protocol in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p><strong>Execution &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9d424268-6bb1-42c9-995a-d3a71db48d97.gif" alt="" style="display:block;margin:0 auto" />

<h3><strong>Conclusion</strong></h3>
<p>In conclusion, AG-UI is a great step forward for front end tool interactions with agents deployed on server. It reduces the complexity of implementing custom integration for client interaction with agents running on server.</p>
<p>I hope this article helped you understand the caveats of implement AG-UI solution for Microsoft Agent Framework.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Human In the Loop(HITL) in Azure Durable Functions for Microsoft Agent Framework]]></title><description><![CDATA[My previous article focused on Multi Agent Orchestration in Azure Durable Functions for Microsoft Agent Framework.
The use case used in the article was pretty straightforward wherein there were two ag]]></description><link>https://www.azureguru.net/human-in-the-loop-hitl-in-azure-durable-functions-for-microsoft-agent-framework</link><guid isPermaLink="true">https://www.azureguru.net/human-in-the-loop-hitl-in-azure-durable-functions-for-microsoft-agent-framework</guid><category><![CDATA[AI]]></category><category><![CDATA[Azure Durable Functions]]></category><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[llm]]></category><category><![CDATA[ai functions]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Wed, 10 Jun 2026 17:29:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/4a89439e-137a-465d-a6f1-9cda88646ec9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My previous <a href="https://www.azureguru.net/multi-agent-orchestration-through-azure-durable-functions-in-microsoft-agent-framework">article</a> focused on Multi Agent Orchestration in Azure Durable Functions for Microsoft Agent Framework.</p>
<p>The use case used in the article was pretty straightforward wherein there were two agents .</p>
<ul>
<li><p>Content generating agent</p>
</li>
<li><p>Content review agent</p>
</li>
</ul>
<p>Content generated by the first agent is reviewed, modified and pushed forward for further processing by the second agent.</p>
<p>But such an straightforward approach might carry potential risks and might not be feasible in real life scenarios particularly for crucial business processes that adhere to strict compliances.</p>
<p>We wouldn't want agents to work autonomously and take decisions without proper human reviews . This is where <strong>Human In The Loop (HITL)</strong> plays an important role.</p>
<p>This article focusses on introducing <strong>Human In The Loop (HITL)</strong> concept for Azure Durable functions in MAF.</p>
<p>Also in the previous article, the user input was embedded in the code and there wasn't a mechanism to make the input dynamic . This article covers that aspect as well where the user input is dynamic and is sent while REST API invocation.</p>
<p>If you would like to skip the writeup you can view the flow walkthrough <a href="https://youtu.be/dK2j7v-Is7Y">here</a></p>
<p><strong>Flow &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9f21eabd-fa3f-4867-a906-37dff3e1baeb.png" alt="Microsoft Agent Framework Worflow and HITL" style="display:block;margin:0 auto" />

<p>The flow is pretty straightforward.</p>
<p>The entry point is an HTTP Trigger that accepts the input as <strong>HttpRequestData</strong> and creates an orchestration instance that invokes an Orchestration method called <strong>RunOrchestrationAsync.</strong></p>
<p>In <strong>RunOrchestrationAsync</strong> method, the agent creates the content on the topic provided and a notification is sent to the user(human) to approve or reject it. In the meantime the flow pauses for the human response.</p>
<p>Once the <strong>RunOrchestrationAsync</strong> receives the response from the user(human), the method invokes the <strong>PublishContent</strong> activity if approved else invokes the <strong>NotifyUseForRejection</strong> activity if rejected . This concludes the orchestration execution.</p>
<h3><strong>SetUp</strong></h3>
<p>To get started, create a new Azure Function project and apply settings covered in my previous <a href="https://www.azureguru.net/azure-durable-agents-in-microsoft-agent-framework-with-docker-durable-task-scheduler-dts-emulator#setup"><strong>article</strong></a> including setting up of the Docker DTS Emulator.</p>
<p>Ensure Docker DTS Emulator is up and navigate to <a href="http://localhost:8082/"><strong>http://localhost:8082/</strong></a> to ensure that it is running.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/15e6fdef-abc5-4086-80d5-4e3bd7477eec.png" alt="Microsoft Agent Framework Worflow and HITL" style="display:block;margin:0 auto" />

<h3><strong>Code</strong></h3>
<p>After the above artifacts are in place, add the following code to read the settings from <code>appsettings.json</code> in <strong>Program.cs</strong> of the project.</p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p>Read the credentials and register a <code>Chatclient</code></p>
<pre><code class="language-csharp">var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

ServiceCollection servicecollection = new();

builder.Services.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
                .AsIChatClient()
    )
);
</code></pre>
<p>In the next step, register the following agent in the hosted DI container.</p>
<p><strong>FootballContentCreatorAgent &gt;&gt;</strong></p>
<pre><code class="language-csharp">servicecollection.AddSingleton&lt;ChatClientAgent&gt;(sp =&gt;

{
    Func&lt;ChatClientAgentOptions&gt; func = () =&gt;
   {
       return new ChatClientAgentOptions
       {
           ChatOptions = new ChatOptions
           {
                Instructions = "You are a content creator.You are good at writing reviews for football club.Be concise and please stick to the topic.",
           },
           Name = "FootballContentCreatorAgent",
           Id = "1"
       };
   };

    return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"), options: func());

});
</code></pre>
<p>Build the service and fetch agents from the <strong>ServiceProvider</strong> as <strong>ChatClientAgent.</strong></p>
<pre><code class="language-csharp">ServiceProvider serviceProvider = servicecollection.BuildServiceProvider();

var agent = serviceProvider.GetServices&lt;ChatClientAgent&gt;();

List&lt;ChatClientAgents&gt; chatclientagent = new(agent);
</code></pre>
<p>Then add these agents as <strong>DurableAgent</strong> to the Azure Function worker.</p>
<pre><code class="language-csharp">using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =&gt; options.AddAIAgent(chatclientagent[0], timeToLive: TimeSpan.FromHours(1)))
.Build();

app.Run();
</code></pre>
<p>Create a new class file called <strong>FunctionTigger.cs</strong> and add two classes <strong>Input</strong> and <strong>HumanApproval</strong>.</p>
<p><strong>Input &gt;&gt;</strong></p>
<pre><code class="language-csharp">public class Input
 {
   [JsonPropertyName("input")]
   public string input { get; set; }
 }
</code></pre>
<p><strong>HumanApproval&gt;&gt;</strong></p>
<pre><code class="language-csharp"> public class HumanApproval
 {
     [JsonPropertyName("IsApproved")]
     public string IsApproved { get; set; }
 }
</code></pre>
<p><strong>Note</strong> : I have kept the structure pretty simple. You can add additional properties based on your use case. For ex , a <strong>Feedback</strong> property in the <strong>HumanApproval</strong><br />class that accepts feedback from the human(user) prior to an approval/rejection.</p>
<p>Also add a <strong>Record</strong> object called <strong>TextResponse</strong> that accepts AgentResponses as input.</p>
<pre><code class="language-csharp"> public record TextResponse(string Response);
</code></pre>
<p><strong>StartOrchestrationAsync &gt;&gt;</strong></p>
<p>As shown in the flowchart, <strong>StartOrchestrationAsync</strong> will be the method that acts as an entry point when the AzureFunction is triggered through an POST request to the HTTP endpoint.</p>
<pre><code class="language-csharp"> [Function(nameof(StartOrchestrationAsync))]
 public static async Task&lt;HttpResponseData&gt; StartOrchestrationAsync(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/run")] HttpRequestData req,
    [DurableClient] DurableTaskClient client)
 {
     var input = await req.ReadFromJsonAsync&lt;Input&gt;();
     string instanceid = await client.ScheduleNewOrchestrationInstanceAsync(orchestratorName: nameof(RunOrchestrationAsync), input: input);

   var response = req.CreateResponse(System.Net.HttpStatusCode.Accepted);
    
   return response;
 }
</code></pre>
<p>The endpoint for the orchestration in the above example is <strong>"hitl/run".</strong></p>
<p>A new orchestration instance is created in this method and which in turn invokes the <strong>RunOrchestrationAsync</strong> method.</p>
<p><strong>RunOrchestrationAsync &gt;&gt;</strong></p>
<pre><code class="language-csharp"> [Function(nameof(RunOrchestrationAsync))]
public static async Task RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
    DurableAIAgent FootballContentCreatorAgent = context.GetAgent("FootballContentCreatorAgent");

    var input = context.GetInput&lt;Input&gt;();

    AgentSession Session = await FootballContentCreatorAgent.CreateSessionAsync();

    AgentResponse&lt;TextResponse&gt; content = await FootballContentCreatorAgent.RunAsync&lt;TextResponse&gt;(
        message: input.input,
        session: Session);

    TextResponse agentresponse = content.Result;

    await context.CallActivityAsync(nameof(NotifyUserForApproval), agentresponse);


    HumanApproval humanResponse;

    humanResponse = await context.WaitForExternalEvent&lt;HumanApproval&gt;(
          eventName: "HumanApproval",
          timeout: TimeSpan.FromHours(1));

    if (humanResponse.IsApproved == "Yes")
    {
        await context.CallActivityAsync(nameof(PublishContent), agentresponse);       
        context.SetCustomStatus($"Content published successfully at {context.CurrentUtcDateTime.ToLongTimeString}");            

    }
    else
    {
        await context.CallActivityAsync(nameof(NotifyUserForRejection), agentresponse);
        context.SetCustomStatus("Content is rejected by human reviewer. Publishing content...");

    }
  
}
</code></pre>
<p><strong>RunOrchestrationAsync</strong> is method is the most important method that processes the user input.</p>
<p>Lets breakdown the above method :</p>
<p>We first create a new instance of <strong>DurableAIAgent</strong> from the context . The user input from the http endpoint is stored in the input variable of type <strong>T</strong> declared earlier.</p>
<pre><code class="language-csharp">DurableAIAgent FootballContentCreatorAgent = context.GetAgent("FootballContentCreatorAgent");
var input = context.GetInput&lt;Input&gt;();
</code></pre>
<p>Create a new <strong>AgentSession</strong>. Executing the agent returns an output of type <strong>AgentResponse</strong>.</p>
<p>Then retrieve the underlying <strong>TextResponse</strong> from the <strong>Result</strong> property into the variable <strong>agentresponse</strong>.</p>
<pre><code class="language-csharp">AgentSession Session = await FootballContentCreatorAgent.CreateSessionAsync();

AgentResponse&lt;TextResponse&gt; content = await FootballContentCreatorAgent.RunAsync&lt;TextResponse&gt;(
                message: input.input,
                session: Session);

TextResponse agentresponse = content.Result;
</code></pre>
<p>Execute <strong>CallActivityAsync</strong> that invokes a method named <strong>NotifyUserForApproval</strong> with agentresponse created above as the parameter to the method.</p>
<pre><code class="language-csharp">await context.CallActivityAsync(nameof(NotifyUserForApproval), agentresponse);
</code></pre>
<p>Next, the method waits for humanapproval event through <strong>WaitForExternalEvent</strong> for a timespan of one hour.</p>
<pre><code class="language-csharp">HumanApproval humanResponse = await context.WaitForExternalEvent&lt;HumanApproval&gt;(
eventName: "HumanApproval",
timeout: TimeSpan.FromHours(1));
</code></pre>
<p>Once the approval is approved/rejected the underlying methods are invoked through <strong>CallActivityAsync</strong> based on the type of input received.</p>
<pre><code class="language-csharp"> if (humanResponse.IsApproved == "Yes")
 {
     await context.CallActivityAsync(nameof(PublishContent), agentresponse);    
     context.SetCustomStatus($"Content published successfully at {context.CurrentUtcDateTime.ToLongTimeString}");
 }
 else
 {
     await context.CallActivityAsync(nameof(NotifyUserForRejection), agentresponse);
     context.SetCustomStatus("Content is rejected by human reviewer. Publishing content...");
 }
</code></pre>
<p><strong>NotifyUserForApproval &gt;&gt;</strong></p>
<p>In real life use case an email or other forms of notifications with an link to the endpoint should be sent to for approval or rejections. This link is generated from the <strong>HumanApprovalAsync</strong> method explained later in the execution flow.</p>
<pre><code class="language-csharp"> [Function(nameof(NotifyUserForApproval))]
 public async static Task&lt;string&gt; NotifyUserForApproval(
   [ActivityTrigger] TextResponse content,
   FunctionContext functionContext)
 {
     return $"Please review the following generated content{content}";
 }
</code></pre>
<p><strong>PublishContent &gt;&gt;</strong></p>
<pre><code class="language-csharp"> [Function(nameof(PublishContent))]
 public async static Task&lt;string&gt; PublishContent(
[ActivityTrigger] TextResponse content,
FunctionContext functionContext)
 {
     return $"The following content {content} is approved and is published";
 }
</code></pre>
<p><strong>NotifyUserForRejection&gt;&gt;</strong></p>
<pre><code class="language-csharp"> [Function(nameof(NotifyUserForRejection))]
 public async static Task&lt;string&gt; NotifyUserForRejection(
[ActivityTrigger] TextResponse content,
FunctionContext functionContext)
 {
     return $"The following content {content} is rejected ";
 }
</code></pre>
<p><strong>HumanApprovalAsync&gt;&gt;</strong></p>
<p>The approval/rejection endpoint with <strong>hitl/notification/{instanceId}</strong> is created in <strong>HumanApprovalAsync</strong> method and a response is returned with <strong>IsApproved</strong> value set based on the user input.</p>
<pre><code class="language-csharp"> [Function(nameof(HumanApprovalAsync))]
 public static async Task&lt;HttpResponseData&gt; HumanApprovalAsync(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/notification/{instanceId}")] HttpRequestData req, string instanceId,
    [DurableClient] DurableTaskClient client)
 {
     var humanapproval = await req.ReadFromJsonAsync&lt;HumanApproval&gt;();
    
     await client.RaiseEventAsync(instanceId, "HumanApproval", humanapproval);

     HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);

     return response;
 }
</code></pre>
<p>You might ask how would the user approve/reject the request.</p>
<p>This can be done through the <strong>HTTPost</strong> that invokes the notification endpoints created in the <strong>HumanApprovalAsync</strong> method. Below is the PowerShell call to the endpoints.</p>
<p><strong>HTTP POST request to the StartOrchestrationAsync method &gt;&gt;</strong></p>
<pre><code class="language-python">$body = @{
    input = "Write a review for the football club FC Barcelona"    
} | ConvertTo-Json

Invoke-RestMethod -Method Post `
    -Uri http://localhost:{Port no set in your launchsettings.json file}/api/hitl/run `
    -ContentType application/json `
    -Body $body
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/7d2278e5-5bf4-491d-9ee0-1945d03dd46e.png" alt="HITL in Azure Durable Functions in MAF" style="display:block;margin:0 auto" />

<p><strong>HTTP POST request to the HumanApprovalAsync method &gt;&gt;</strong></p>
<pre><code class="language-python">$json = '{"IsApproved":"Yes"}'

Invoke-RestMethod `
-Uri "http://localhost:{Port no set in your launchsettings.json file}/api/hitl/notification/{InstanceId from StartOrchestrationAsync method}" `
-Method Post `
-ContentType "application/json" `
-Body $json
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/122649d6-1865-4cdb-b641-026dd18153c5.png" alt="HITL in Azure Durable Functions in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p><strong>DTS Dashboard for User Approval &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/def1065f-82c4-4b0e-bce8-7b1a6e950ed7.png" alt="HITL in Azure Durable Functions in MAF" style="display:block;margin:0 auto" />

<p><strong>DTS Dashboard for User Rejection &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/8864082e-025d-4f89-a7db-ac2512caa6be.png" alt="HITL in Azure Durable Functions in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<h3>Execution</h3>
<p><a class="embed-card" href="https://youtu.be/dK2j7v-Is7Y">https://youtu.be/dK2j7v-Is7Y</a></p>

<h3>Conclusion</h3>
<p>Through this article, I tried to explore a simple implementation of Human-in-the-Loop (HITL) for Azure Durable Functions.</p>
<p>Although the sample focuses on a basic approval workflow it highlights the core concepts required to pause an orchestration, wait for human input, and resume execution based on the outcome.</p>
<p>I hope this article helps you understand the fundamentals and sets the required foundation for building more business centric HITL solutions in your applications.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Multi Agent Orchestration through Azure Durable  Functions in Microsoft Agent Framework]]></title><description><![CDATA[In my previous article, I introduced the process of setting up a single durable agent using the Azure Durable Framework and the Docker-based Durable Task Scheduler (DTS) Emulator.
This article will be]]></description><link>https://www.azureguru.net/multi-agent-orchestration-through-azure-durable-functions-in-microsoft-agent-framework</link><guid isPermaLink="true">https://www.azureguru.net/multi-agent-orchestration-through-azure-durable-functions-in-microsoft-agent-framework</guid><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[AI]]></category><category><![CDATA[Azure]]></category><category><![CDATA[#AzureFunctions ]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[llm]]></category><category><![CDATA[LLM's ]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Thu, 04 Jun 2026 11:27:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/77735ef3-de2e-462a-b896-80b30a8df010.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my previous <a href="https://www.azureguru.net/azure-durable-agents-in-microsoft-agent-framework-with-docker-durable-task-scheduler-dts-emulator">article</a>, I introduced the process of setting up a single durable agent using the Azure Durable Framework and the Docker-based Durable Task Scheduler (DTS) Emulator.</p>
<p>This article will be a step ahead and is focused on setting up a Multi Agent Orchestration based on the similar principal.</p>
<p>The use case in this article is a multi-agent workflow involving two agents.</p>
<p>The first agent, the Football Content Creator Agent is responsible for generating a review of a football club. The second agent, the Football Content Reviewer Agent evaluates and reviews the generated content and produces a more refined version of the original review generated by Content Creator Agent.</p>
<p>I had used a similar use case in one of my earlier article that deep dived into workflow orchestration pattern.</p>
<p><a href="https://www.azureguru.net/workflow-orchestration-patterns-in-microsoft-agent-framework#group-chat-orchestration-pattern">https://www.azureguru.net/workflow-orchestration-patterns-in-microsoft-agent-framework#group-chat-orchestration-pattern</a></p>
<blockquote>
<p>In this example, you could use the same agent for content creation and review but I used two separate agents just to give an idea on how execution of durable multi agents can be orchestrated .</p>
</blockquote>
<h3><strong>SetUp</strong></h3>
<p>To get started, create a new Azure Function project and apply settings covered in my previous <a href="https://www.azureguru.net/azure-durable-agents-in-microsoft-agent-framework-with-docker-durable-task-scheduler-dts-emulator#setup">article</a> including setting up of the Docker DTS Emulator.</p>
<p>Ensure Docker DTS Emulator is up and navigate to <a href="http://localhost:8082/"><strong>http://localhost:8082/</strong></a> to ensure that it is running.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/72056ed9-a2ed-453f-8c34-14c343a17e39.png" alt="Azure Durable  Functions in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<h3><strong>Code</strong></h3>
<p>After the above artifacts are in place, add the following code to read the settings from <code>appsettings.json</code> in <strong>Program.cs</strong> of the project.</p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p>Read the credentials and register a <code>Chatclient</code></p>
<pre><code class="language-csharp">var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

ServiceCollection servicecollection = new();

builder.Services.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
                .AsIChatClient()
    )
);
</code></pre>
<p>In the next step, register the following two <strong>AIAgents</strong> in the hosted DI container.</p>
<p><strong>FootballContentCreatorAgent &gt;&gt;</strong></p>
<pre><code class="language-csharp">servicecollection.AddSingleton&lt;ChatClientAgent&gt;(sp =&gt;

{
    Func&lt;ChatClientAgentOptions&gt; func = () =&gt;
   {
       return new ChatClientAgentOptions
       {
           ChatOptions = new ChatOptions
           {
                Instructions = "You are a content creator. You create content for football teams. Be concise and please stick to the topic.",
           },
           Name = "FootballContentCreatorAgent",
           Id = "1"
       };
   };

    return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"), options: func());

});
</code></pre>
<p><strong>FootballContentReviewerAgent &gt;&gt;</strong></p>
<pre><code class="language-csharp">servicecollection.AddSingleton&lt;ChatClientAgent&gt;(sp =&gt;

{
    Func&lt;ChatClientAgentOptions&gt; func = () =&gt;
   {
       return new ChatClientAgentOptions
       {
           ChatOptions = new ChatOptions
           {
               Instructions = "You are a content reviewer.You review created content for football teams and make necessary changes to it.Be concise and please stick to the topic.",
           },
           Name = "FootballContentReviewerAgent",
           Id = "2"
       };
   };
    return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"), options: func());

});
</code></pre>
<p>Build the service and fetch agents from the <strong>ServiceProvider</strong> as <strong>ChatClientAgent.</strong></p>
<pre><code class="language-csharp">ServiceProvider serviceProvider = servicecollection.BuildServiceProvider();

var agents = serviceProvider.GetServices&lt;ChatClientAgent&gt;();

List&lt;ChatClientAgents&gt; chatclientagent = new(agents);
</code></pre>
<p>Then add these agents as <strong>DurableAgent</strong> to the Azure Function worker.</p>
<pre><code class="language-csharp">using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =&gt; { options.AddAIAgents(chatclientagent).DefaultTimeToLive = TimeSpan.FromHours(1); })
.Build();

app.Run();
</code></pre>
<p>The time to live for the agents is configured to an hour.</p>
<h3>Orchestration</h3>
<p>Define a type record that holds the <strong>AgentResponse</strong> in a new class file called <strong>FunctionTrigger.cs</strong></p>
<pre><code class="language-csharp">public record TextResponse(string Response);
</code></pre>
<p>The orchestration occurs in two steps <strong>Start</strong> and <strong>Run.</strong></p>
<p>Start is the trigger point that defines the route , creates an orchestration instance for an <strong>DurableTaskClient</strong> and returns a response of type <strong>HttpResponseData.</strong></p>
<p><strong>StartOrchestrationAsync &gt;&gt;</strong></p>
<pre><code class="language-csharp"> [Function(nameof(StartOrchestrationAsync))]

public static async Task&lt;HttpResponseData&gt; StartOrchestrationAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "footballagent/run")] HttpRequestData req, [DurableClient] DurableTaskClient client)

 {

var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(orchestratorName: nameof(RunOrchestrationAsync));

     var response = req.CreateResponse(HttpStatusCode.Accepted);
         await response.WriteAsJsonAsync(new
         {
             message = "Orchestration started.",
             InstanceId = instanceId
         }
      );
     return response;
 }
</code></pre>
<p>Run is where an agent orchestration pipeline is triggered post creation of an orchestration instance through <strong>StartOrchestrationAsync</strong></p>
<p><strong>RunOrchestrationAsync &gt;&gt;</strong></p>
<pre><code class="language-csharp"> [Function(nameof(RunOrchestrationAsync))]
 public static async Task&lt;string&gt; RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
 {
     DurableAIAgent FootballContentCreatorAgent = context.GetAgent("FootballContentCreatorAgent");
     DurableAIAgent FootballContentReviewerAgent = context.GetAgent("FootballContentReviewerAgent");

     AgentSession Session = await FootballContentCreatorAgent.CreateSessionAsync();

     AgentResponse&lt;TextResponse&gt; initial = await FootballContentCreatorAgent.RunAsync&lt;TextResponse&gt;(
         message: "Summarize the glory of football club FC Barcelona",
         session: Session);

     AgentResponse&lt;TextResponse&gt; refined = await FootballContentReviewerAgent.RunAsync&lt;TextResponse&gt;(
      message: $"Improve and expand the review further while keeping it under 1000 words: {initial.Result.Response}",
      session: Session);

     return refined.Result.Response;
 }
</code></pre>
<p>In the code above through **TaskOrchestrationContext ,**we get a list of available agents from the context.</p>
<p>The <strong>AgentResponse</strong> output generated by <strong>FootballContentCreatorAgent</strong> acts as input to the <strong>FootballContentReviewerAgent.</strong> The input is reviewed and post modification the modified reviewed is returned as the final output.</p>
<p>Add the above two functions to the <strong>FunctionTrigger.cs</strong> file created earlier.</p>
<p>Our endpoint is <strong>"footballagent/run".</strong> Invoke it through Powershell.</p>
<pre><code class="language-yaml">Invoke-RestMethod -Method Post -Uri http://localhost:7001/api/footballagent/run
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d949c78e-fd6e-4d7e-ab99-d8137dc15e2d.png" alt="Azure Durable  Functions in Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>Post invocation , underlying artifacts are visible on the Durable Task Scheduler Dashboard.</p>
<p><strong>Orchestration &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/9f5bdadf-6b90-468b-b218-6bb54edc7feb.png" alt="Microsoft Agent Framework Azure Durable Functions" style="display:block;margin:0 auto" />

<p><strong>Entities &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/bae96325-b1d3-4708-8e13-a3b983d86fcd.png" alt="Microsoft Agent Framework Azure Durable Functions" style="display:block;margin:0 auto" />

<p><strong>Agents &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/cd81ede8-fd6e-4efa-bd33-542027db61ac.png" alt="Microsoft Agent Framework Azure Durable Functions" style="display:block;margin:0 auto" />

<p><strong>Execution &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/f6c1a39d-a091-49e3-b38f-30b150e36802.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>In this article, I tried to explore how Azure Durable Functions can be used to orchestrate a multi-agent execution within the Microsoft Agent Framework. I hope this article helps you understand how durable orchestrations can be leveraged to coordinate multi agent execution effectively.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item><item><title><![CDATA[Azure Durable Agents in Microsoft Agent Framework with Docker Durable Task Scheduler (DTS) Emulator ]]></title><description><![CDATA[Azure Durable Functions are an extension of Azure Functions that makes it easy to build long-running, stateful workflows in serverless environments.
Instead of managing state, retries, checkpoints and]]></description><link>https://www.azureguru.net/azure-durable-agents-in-microsoft-agent-framework-with-docker-durable-task-scheduler-dts-emulator</link><guid isPermaLink="true">https://www.azureguru.net/azure-durable-agents-in-microsoft-agent-framework-with-docker-durable-task-scheduler-dts-emulator</guid><category><![CDATA[Azure]]></category><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Azure Durable Functions]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Tue, 02 Jun 2026 10:25:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/adba863a-b66d-49bb-a80d-596d265d79fe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Azure Durable Functions are an extension of Azure Functions that makes it easy to build long-running, stateful workflows in serverless environments.</p>
<p>Instead of managing state, retries, checkpoints and recovery custom built logic, Azure Durable Functions automatically handles them behind the scenes.</p>
<p>You can think of it as a workflow coordinator that orchestrates multiple tasks, waits for external events, schedules timers, and resumes execution even after application restarts or failures. Such implementation make it ideal for business processes that requires <strong>Human-in-the-loop (HITL)</strong> that may run for minutes, hours, or even days.</p>
<p>Azure Durable Functions uses an event-sourcing model where every action is recorded as an event. When the workflow needs to resume, the orchestrator rebuilds its state by replaying these events without requiring developers to manage state and infrastructure concerns.</p>
<p>In this article I will demonstrate a very basic setup of an Azure Durable Agent in MAF through Docker Emulator for Durable Task Scheduler (DTS) that can run durable and stateful agents.</p>
<h3>SetUp</h3>
<p>Create a new Azure Function project and add the following references.</p>
<pre><code class="language-csharp">dotnet add package Azure;
dotnet add package Azure.AI.OpenAI;
dotnet add package Microsoft.Agents.AI;
dotnet add package Microsoft.Extensions.AI;
dotnet add package Microsoft.Extensions.Configuration;
dotnet add package Microsoft.Extensions.DependencyInjection;
dotnet add package Microsoft.Extensions.Hosting;
dotnet add package Microsoft.Agents.AI.Hosting.AzureFunctions--prerelase;
dotnet add package Microsoft.Azure.Functions.Worker.Builder;
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore;
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
dotnet add package  Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged;
</code></pre>
<p>Of the above ,ensure that you don't miss to reference the following two libraries in the project.</p>
<pre><code class="language-csharp">Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged;
</code></pre>
<p>Otherwise you will face issue that is highlighted in the following GitHub post.</p>
<p><a href="https://github.com/microsoft/agent-framework/issues/5927">https://github.com/microsoft/agent-framework/issues/5927</a></p>
<p>Not referencing the above two libraries, will result in you having to declare a dummy orchestrator.</p>
<pre><code class="language-csharp">public static class MyDummyOrchestrator
{
    [Function(nameof(MyDummyOrchestrator))]
    public static Task RunOrchestrator(
        [OrchestrationTrigger] TaskOrchestrationContext context)
    {
        return Task.CompletedTask;
    }
}
</code></pre>
<p>This is because the function worker fails to find an entry point of execution.</p>
<p>These are the major packages and version numbers that I have referenced in the project</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/2f899054-bc7f-4b17-8abc-55dc6cdc3d29.png" alt="Microsoft Agent Framework , Azure Durable Function" />

<p><strong>Docker DTS Emulator</strong> &gt;&gt;</p>
<p>Install Docker Desktop if you don't have on your system and then pull the Docker image containing the DTS emulator</p>
<pre><code class="language-yaml">docker pull mcr.microsoft.com/dts/dts-emulator:latest
</code></pre>
<p>Now run the emulator in the Docker desktop terminal</p>
<pre><code class="language-yaml">docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/dd5e6207-b9cb-447e-a787-a23c288ccf80.png" alt="Microsoft Agent Framework , Azure Durable Function, Docker Emulator" style="display:block;margin:0 auto" />

<p>You can change the port numbers if you want to.</p>
<p>Ensure that the Emulator is up and running.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/11ff7316-fd9e-4de4-9493-d72419f9863f.png" alt="Microsoft Agent Framework , Azure Durable Function, Docker Emulator" style="display:block;margin:0 auto" />

<p>Navigate to <a href="http://localhost:8082/">http://localhost:8082/</a> and you should see the DTS endpoints running on port number <em><strong>8082</strong></em>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/797fd604-150f-4063-bcdc-1c551d3a38fc.png" alt="Microsoft Agent Framework , Azure Durable Function, Docker Emulator" style="display:block;margin:0 auto" />

<p>Add <code>appsetting.json</code> to the project</p>
<pre><code class="language-csharp">"AppSettings": 
{ 
    "Chat_DeploymentName": "Deployment Name",
    "EndPoint": "Azure OpenAI endpoint",
    "ApiKey": "Azure OpenAI API key"
}
</code></pre>
<p><code>local.settings.json</code></p>
<pre><code class="language-yaml">{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
        "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
        "AZURE_OPENAI_ENDPOINT": "Azure OpenAI endpoint",
        "AZURE_OPENAI_DEPLOYMENT_NAME": "Deployment Name"
    }
}
</code></pre>
<p>In <code>DURABLE_TASK_SCHEDULER_CONNECTION_STRING</code> settings above, the Endpoint value should match the Endpoint that was set while configuring the Docker DTS Emulator.</p>
<p><code>launchSettings.json</code></p>
<pre><code class="language-yaml">{
  "profiles": {
    "Azure_Durable_SingleAgent": {
      "commandName": "Project",
      "commandLineArgs": "--port 7001",
      "launchBrowser": false
    }
  }
}
</code></pre>
<p><code>--port 7001</code> value will be used to send <strong>HttpRequest</strong> to the running Agent.</p>
<p><code>host.json</code></p>
<pre><code class="language-yaml">{
    "version": "2.0",
    "logging": {
        "logLevel": {
            "Microsoft.Agents.AI.DurableTask": "Information",
            "Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
            "DurableTask": "Information",
            "Microsoft.DurableTask": "Information"
        }
    },
    "extensions": {
        "durableTask": {
            "hubName": "default",
            "storageProvider": {
                "type": "AzureManaged",
                "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
            }
        }
    }
}
</code></pre>
<p><em>"DURABLE_TASK_SCHEDULER_CONNECTION_STRING"</em> is the keyword above.</p>
<p>If you are using Azure storage, then there is no need for <strong>connectionStringName</strong> and <strong>type</strong> settings but in case you are using Durable Task Scheduler (DTS) which is the recommended approach , then setting <strong>type</strong> and <strong>connectionStringName</strong> property is required.</p>
<p><code>serviceDependencies.json</code></p>
<pre><code class="language-yaml">{
  "dependencies": {
    "appInsights1": {
      "type": "appInsights"
    },
    "storage1": {
      "type": "storage",
      "connectionId": "AzureWebJobsStorage"
    }
  }
}
</code></pre>
<h3>Code</h3>
<p>Implementation is pretty straightforward.</p>
<p>After the above artifacts in place, add the following code to read the settings from <code>appsettings.json</code> in <strong>Program.cs</strong> of the project.</p>
<pre><code class="language-csharp">var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
</code></pre>
<p>Read the credentials and register a <code>Chatclient</code></p>
<pre><code class="language-csharp">var credential = new AzureKeyCredential(configuration["AppSettings:ApiKey"]);

ServiceCollection servicecollection = new();

builder.Services.AddKeyedChatClient(
    "ChatClient",
    (
        sp =&gt;
            new AzureOpenAIClient(new Uri(configuration["AppSettings:EndPoint"]), credential)
                .GetChatClient(configuration["AppSettings:Chat_DeploymentName"])
                .AsIChatClient()
    )
);
</code></pre>
<p>In the next step, register the <code>AIAgent</code> in the hosted DI container.</p>
<pre><code class="language-csharp">servicecollection.AddSingleton&lt;ChatClientAgent&gt;(sp =&gt;

{
    Func&lt;ChatClientAgentOptions&gt; func = () =&gt;
   {
       return new ChatClientAgentOptions
       {
           ChatOptions = new ChatOptions
           {
               Instructions = "You are good at explaining topics on history",
           },
           Name = "History",
           Id = "1"

       };
   };

    return new ChatClientAgent(sp.GetKeyedService&lt;IChatClient&gt;("ChatClient"), options: func());

});
</code></pre>
<p>Build the service and get the agent from the <strong>ServiceProvider</strong> as <strong>ChatClientAgent.</strong></p>
<pre><code class="language-csharp">ServiceProvider serviceProvider = servicecollection.BuildServiceProvider();

var agent = serviceProvider.GetServices&lt;ChatClientAgent&gt;();

List&lt;ChatClientAgent&gt; chatclientagent = new(agent);
</code></pre>
<p>Then add this agent as <strong>DurableAgent</strong> to the Azure Function worker.</p>
<pre><code class="language-csharp"> using IHost app = FunctionsApplication
 .CreateBuilder(args)
 .ConfigureFunctionsWebApplication()
 .ConfigureDurableAgents(options =&gt; options.AddAIAgent(chatclientagent[0], timeToLive: TimeSpan.FromHours(1)))
 .Build();

 app.Run();
</code></pre>
<p>The time to live of agent is configured to to an hour.</p>
<p>The agent name <strong>History</strong> set during defining the agent will act as the Durable Agent endpoint.</p>
<p>In our example the <strong>HttpRequest</strong> endpoint and the call will be as follows</p>
<pre><code class="language-javascript">Invoke-RestMethod -Method Post `
    -Uri  http://localhost:7001/api/agents/History/run `
    -ContentType text/plain `
    -Body "Tell me more about World war 2"
</code></pre>
<p>Port number <em><strong>7001</strong></em> comes from the set value in <code>launchSettings.json</code>.</p>
<p>That's all. Go ahead and invoke the call.</p>
<p>I invoked it through PowerShell</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/db73d4ef-62af-47d2-8f3f-525899121753.png" alt="Microsoft Agent Framework , Azure Durable Function, Docker Emulator" style="display:block;margin:0 auto" />

<p>You should see the Agent chat history and other details in the DTS Emulator Dashboard.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/ccff9327-cd2f-4f4c-8c64-0caaa52d57e8.png" alt="Microsoft Agent Framework , Azure Durable Function, Docker Emulator" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/aa5981d3-ed7c-4270-a720-a605b2024ce7.png" alt="Microsoft Agent Framework , Azure Durable Function, Docker Emulator" style="display:block;margin:0 auto" />

<h3><strong>Execution &gt;&gt;</strong></h3>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/6da8079d-1e59-4db6-8dfd-2e85902e89e3.gif" alt="Microsoft Agent Framework , Azure Durable Function, Docker Emulator" style="display:block;margin:0 auto" />

<h3><strong>Conclusion</strong></h3>
<p>Leveraging Azure Durable Function for state management of Agents is the most optimal approach and effective approach to build reliable long running workflows that can recover through restarts and process failures without the over head of implementation and maintaining custom processes.</p>
<p>Though the above set up is an example for a very simple use case, in a few upcoming articles I will touch base on more advance use cases.</p>
<p>Till then stay tuned !!!</p>
]]></content:encoded></item><item><title><![CDATA[Human-In-The-Loop (HITL) in Microsoft Agent Framework Workflow without ToolApprovalRequestContent ]]></title><description><![CDATA[The general approach for Human-In-The-Loop (HITL) in Microsoft Agent Framework is through ToolApprovalRequestContent driven by AIFunction invocation wrapped around ApprovalRequiredAIFunction.
But for ]]></description><link>https://www.azureguru.net/human-in-the-loop-hitl-in-microsoft-agent-framework-workflow-without-toolapprovalrequestcontent</link><guid isPermaLink="true">https://www.azureguru.net/human-in-the-loop-hitl-in-microsoft-agent-framework-workflow-without-toolapprovalrequestcontent</guid><category><![CDATA[microsoft agent framework]]></category><category><![CDATA[workflow]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[aitools]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Sachin Nandanwar]]></dc:creator><pubDate>Fri, 29 May 2026 12:40:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/d3512fea-729e-4d04-8684-495c2ee452b4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The general approach for Human-In-The-Loop (HITL) in Microsoft Agent Framework is through <strong>ToolApprovalRequestContent</strong> driven by <strong>AIFunction</strong> invocation wrapped around <strong>ApprovalRequiredAIFunction</strong>.</p>
<p>But for MAF workflows it is possible to achieve HITL without the need for wrapping your <strong>AIFunction</strong> with <strong>ApprovalRequiredAIFunction</strong>.</p>
<p>In this article we will be exploring the alternate approach</p>
<p>If you would like to know more about HITL through <strong>ApprovalRequiredAIFunction</strong>, for Agents, then you can refer to my article on the topic <a href="https://www.azureguru.net/human-in-the-loop-hitl-in-multi-agent-orchestration-in-microsoft-agent-framework">here</a> .</p>
<p>We will use the same use case that I used in my <a href="https://www.azureguru.net/human-in-the-loop-hitl-in-multi-agent-orchestration-in-microsoft-agent-framework">article</a> on MAF Workflows to demonstrate how HITL can be leveraged without <strong>ApprovalRequiredAIFunction</strong>.</p>
<h3><strong>Use Case</strong></h3>
<p>This was the flow for the use case used in my previous article on HITL</p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/94395484-9843-4826-a886-cc3836d02086.png" alt="Microsoft Agent Framework" style="display:block;margin:0 auto" />

<p>So, we had an input number <code>N</code> that requires to be identified as a Prime or a non Prime number. If its a Prime number then compute its square root and send the value through <strong>Route A</strong> .</p>
<p>If its not a Prime number then send it through <strong>Route B</strong> without its square root.</p>
<p>Now , lets make some changes to the flow to accommodate HITL .</p>
<p>How about we add HITL approval before the value of √N is dispatched to Route A ?</p>
<p>So the flow would be as follows :</p>
<pre><code class="language-csharp">                          typeDetectionExecutor
                                   |
           -----------------------------------------------
           |                       |                     |
           v                       v                     v
     PrimeNumber               NotPrimeNumber          Default
           |                       |                     |
           v                       v                     v
PrimeNo_SquareRootExecutor   Executor_RouteB    Unsure_Executor 
           |
           |
           v
      HITL Approval
           |
           |
           v
  -----------------
  |               |
Rejected       Approved
  |               |
  |               |
  v               v
Log Rejection Executor_RouteA
</code></pre>
<p>To achieve this we will use a combination of <strong>RequestPort</strong> and <strong>RequestInfoEvent</strong> .</p>
<p><strong>RequestPort &amp; RequestInfoEvent &gt;&gt;</strong></p>
<p>RequestPort can be defined as a channel through which executors can send and receive responses. But you might ask that it is already possible to do that through <strong>SendMessages</strong> or <strong>YieldsOutput</strong> .</p>
<p>The drawback with both <strong>SendMessages</strong> or <strong>YieldsOutput</strong> is that they cant emit responses as Events.</p>
<blockquote>
<p>Recall that <code>SendMessages</code> sends messages to all the connected executors while <code>YieldsOutput</code> sends the output to the caller and both do not emit responses.</p>
</blockquote>
<p>When an executor sends a message to RequestPort, it emits a RequestInfoEvent and then external inputs can listen to those RequestInfoEvent and then RequestInfoEvent sends it responses back to the workflow through RequestPort .</p>
<p>This is similar to <strong>ToolApprovalRequestContent</strong> but it requires an <strong>AIFunction</strong> wrapped in <strong>ApprovalRequiredAIFunction</strong> and <strong>AIFunction</strong> to be invoked through an agent.</p>
<p>With RequestPort, AIFunction invocation is not required as the workflow can read the input and receive output through RequestPort which is then emitted in form of RequestInfoEvent.</p>
<p>To implement this we will have to go back and make some changes to workflow edges and the <strong>PrimeNo_SendDetails_RouteA_Executor</strong> that routes the square root value to RouteA.</p>
<p>Refer above flowchart and code for <strong>PrimeNo_SendDetails_RouteA_Executor</strong> in my earlier article <a href="https://www.azureguru.net/deep-dive-into-workflow-execution-in-microsoft-agent-framework">here</a>.</p>
<h3>Code</h3>
<p>First, add a new property named <strong>IsApproved</strong> to the <strong>_Response</strong> object of the workflow.</p>
<p><strong>_Response &gt;&gt;</strong></p>
<p><em>Earlier version :</em></p>
<pre><code class="language-csharp">public sealed class _Response
{
    [JsonPropertyName("decision")]
    public string Decision { get; set; } = string.Empty;

    [JsonPropertyName("reason")]
    public string Reason { get; set; } = string.Empty;

    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;

    [JsonPropertyName("squareroot")]
    public double squareroot { get; set; } = 0.00;
}
</code></pre>
<p><em>Changed version :</em></p>
<pre><code class="language-csharp">public sealed class _Response
{
    [JsonPropertyName("decision")]
    public string Decision { get; set; } = string.Empty;

    [JsonPropertyName("reason")]
    public string Reason { get; set; } = string.Empty;

    [JsonPropertyName("id")]
    public string Id { get; set; } = string.Empty;   
   
    [JsonPropertyName("squareroot")]
     public double squareroot { get; set; } = 0.00;
    
    [JsonPropertyName("approved")]
    public string IsApproved { get; set; } = string.Empty; 
}
</code></pre>
<p><strong>RequestPort &gt;&gt;</strong></p>
<pre><code class="language-csharp"> RequestPort humanApprovalPort = RequestPort.Create&lt;_Response, _Response&gt;("Approval");
</code></pre>
<p>RequestPort instance is of type <strong>T&lt;TRequest,TResponse&gt;</strong>.</p>
<p>Output of <strong>PrimeNo_SquareRootExecutor</strong> which is of type _Response acts as <strong>TRequest</strong> for RequestPort and the output from RequestPort which is also of type _Response acts as an input to <strong>SendDetails_Executor_RouteA</strong>.</p>
<p><strong>SendDetails_Executor_RouteA &gt;&gt;</strong></p>
<p><em>Earlier version :</em></p>
<pre><code class="language-csharp">internal sealed class PrimeNo_SendDetails_RouteA_Executor() : Executor&lt;_Response&gt;("PrimeNo_SendDetails_RouteA_Executor")
{
    [YieldsOutput(typeof(string))]
    public override async ValueTask HandleAsync(_Response result, IWorkflowContext context, CancellationToken cancellationToken = default)
    {
        var number = await context.ReadStateAsync&lt;InputNumber&gt;(result.Id, scopeName: NumbervalueConstants.NumbervalueScope);
        await context.YieldOutputAsync($"Details for {number.Value} sent to Route A: {result.squareroot}", cancellationToken);

    }
}
</code></pre>
<p><em>Changed version :</em></p>
<pre><code class="language-csharp"> internal sealed class PrimeNo_SendDetails_RouteA_Executor() : Executor&lt;_Response&gt;("PrimeNo_SendDetails_RouteA_Executor")
 {
     [YieldsOutput(typeof(string))]

     public override async ValueTask HandleAsync(_Response result, IWorkflowContext context, CancellationToken cancellationToken = default)
     {
         var number = await context.ReadStateAsync&lt;InputNumber&gt;(result.Id, scopeName: NumbervalueConstants.NumbervalueScope);

         if (result.IsApproved == "Yes")
         {
             await context.YieldOutputAsync($"Request is approved !!! Details for {number.Value} sent to Route A with square root value of: {result.squareroot}", cancellationToken);
         }
         else
         {
             await context.YieldOutputAsync($"Request is rejected !!! Details of rejection for {number.Value} will be logged ", cancellationToken);
         }
         
     }
 }
</code></pre>
<p>The change above, is that in the changed version , we are reading the <strong>IsApproved</strong> property value which was set through <strong>RequestPort</strong> and based on the output from approval/rejection , a decision is made to route the square root value or log the rejection.</p>
<p>But, how to set the value for <strong>IsApproved</strong> property ?</p>
<p>This is done through <strong>ExternalResponse</strong> process where the executor pauses its execution awaiting an input from external system. But then you might wonder how to invoke the <strong>ExternalResponse</strong> process.This is done by adding the <strong>RequestPort</strong> as part of the workflow Edge.</p>
<p>A method named <strong>HandleExternalRequest</strong> is defined later in the article to that handles <strong>ExternalResponse</strong>.</p>
<p>Recall that earlier we declared RequestPort with name <strong>humanApprovalPort</strong>.</p>
<p><strong>Edges &gt;&gt;</strong></p>
<p><em>Earlier version :</em></p>
<pre><code class="language-csharp">.AddEdge(PrimeNo_SquareRootExecutor,SendDetails_Executor_RouteA)
.WithOutputFrom(SendDetails_Executor_RouteA, SendDetails_Executor_RouteB, SendEmail_Executor_Unsure);
</code></pre>
<p><em>Changed version :</em></p>
<pre><code class="language-csharp">.AddEdge(PrimeNo_SquareRootExecutor, humanApprovalPort)
.AddEdge(humanApprovalPort, SendDetails_Executor_RouteA)
.WithOutputFrom(SendDetails_Executor_RouteA, SendDetails_Executor_RouteB, SendEmail_Executor_Unsure);
</code></pre>
<p>In the workflow event we then invoke <strong>ExternalResponse</strong> that returns a response object which is then sent to the Executor.</p>
<p><strong>WorkflowEvent&gt;&gt;</strong></p>
<p><em>Earlier version :</em></p>
<pre><code class="language-csharp">var workflow = builder.Build();

string input = "Your Input Number/Letter";

await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, input));

  await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
  await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
  {
    if (evt is WorkflowOutputEvent outputEvent)
    {
     Console.WriteLine($"{outputEvent}");
    }
  }
</code></pre>
<p><em>Changed version :</em></p>
<pre><code class="language-csharp">var workflow = builder.Build();

string input = "Your Input Number/Letter";

await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, new ChatMessage(ChatRole.User, input));

  await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
  await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
  {
    if (evt is WorkflowOutputEvent outputEvent)
    {
     Console.WriteLine($"{outputEvent}");
    }    
    
    if (evt is RequestInfoEvent requestInputEvt)
    {
     ExternalResponse response =  HandleExternalRequest(requestInputEvt.Request);
   await run.SendResponseAsync(response);
   }
 
  }
</code></pre>
<p><strong>HandleExternalRequest &gt;&gt;</strong></p>
<pre><code class="language-csharp"> private static ExternalResponse HandleExternalRequest(ExternalRequest request)
 {
     if (request.TryGetDataAs&lt;_Response&gt;(out var response))
     {

         Console.WriteLine($"Would you like to approve/reject ? please reply Y to approve and N to reject");

         response.IsApproved = Convert.ToString(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) == true ? "Yes" : "No");

         return request.CreateResponse(response);
     }

     throw new NotSupportedException($"Request {request.PortInfo.RequestType} is not supported");
 }
</code></pre>
<p>As the output from <strong>PrimeNo_SquareRootExecutor</strong> is of type _Response, the _Response object becomes a part of ExternalRequest which in turn exposes the IsApproved property through the out variable <strong>response</strong> and post human interaction , the modified request is sent back as a response.</p>
<p><strong>Execution &gt;&gt;</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6693c62c166ee9c594cffda0/c86f3c67-313c-4fb6-997e-03a54bc36acb.gif" alt="" style="display:block;margin:0 auto" />

<h3>Conclusion</h3>
<p>Through this article I tried to explore a more unique approach where HITL does not necessarily had to be dependent on AIFunction invocation through an AIAgent.</p>
<p>Instead, we saw how human approval can be introduced at different stages of an workflow through simple call mechanism through RequestPort and the responses can be handled through ExternalResponse object.</p>
<p>Go ahead and give it a try.</p>
<p>Thanks for reading !!!</p>
]]></content:encoded></item></channel></rss>