<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ohalay</title>
    <description>The latest articles on DEV Community by ohalay (@ohalay).</description>
    <link>https://dev.to/ohalay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F631028%2Fb3242a62-7ee9-4354-8395-03166ed33226.jpeg</url>
      <title>DEV Community: ohalay</title>
      <link>https://dev.to/ohalay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ohalay"/>
    <language>en</language>
    <item>
      <title>Integrating MCP Tools with AWS Bedrock in an ASP.NET Core Minimal API</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Wed, 05 Nov 2025 19:09:04 +0000</pubDate>
      <link>https://dev.to/ohalay/integrating-mcp-tools-with-aws-bedrock-in-an-aspnet-core-minimal-api-1p88</link>
      <guid>https://dev.to/ohalay/integrating-mcp-tools-with-aws-bedrock-in-an-aspnet-core-minimal-api-1p88</guid>
      <description>&lt;p&gt;In this article, we'll build an ASP.NET Core Minimal API that integrates AWS Bedrock with MCP client capabilities, enabling the dynamic invocation of AI tools through standardized MCP interfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to work with LLM models
&lt;/h2&gt;

&lt;p&gt;There are several ways to interact with LLM models in .NET: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/code-library/latest/ug/csharp_3_bedrock-runtime_code_examples.html" rel="noopener noreferrer"&gt;Direct LLM provider client (API or SDK)&lt;/a&gt; - lower-level abstraction&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai" rel="noopener noreferrer"&gt;Microsoft.Extensions.AI&lt;/a&gt; - middle-level abstraction&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/overview/" rel="noopener noreferrer"&gt;Semantic Kernel&lt;/a&gt; - higher-level abstraction&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We'll use the mid-level abstraction (Microsoft.Extensions.AI), which balances simplicity and flexibility while keeping the implementation decoupled from specific model providers. To start working, we need to install dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.nuget.org/packages/Microsoft.Extensions.AI" rel="noopener noreferrer"&gt;AI&lt;/a&gt; extension itself&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nuget.org/packages/AWSSDK.Extensions.Bedrock.MEAI" rel="noopener noreferrer"&gt;Bedrock AI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then register &lt;code&gt;IChatClient&lt;/code&gt; abstraction&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDefaultAWSOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAWSOptions&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAddAWSService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAmazonBedrockRuntime&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IChatClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAmazonBedrockRuntime&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIChatClient&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;AsBuilder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;UseFunctionInvocation&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Add MCP client
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;IChatClient&lt;/code&gt; interface supports AI tools that extend its capabilities. We can dynamically load these tools from an MCP server.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transport&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClientTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://my-mcp-server-enpoint.com"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Mcp Client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mcpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;McpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mcpTools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mcpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ListToolsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A possible MCP server configuration looks like this: &lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/ohalay/add-the-mcp-server-to-the-aspnet-core-minimal-api-4331" class="crayons-story__hidden-navigation-link"&gt;Add the MCP server to the ASP.NET Core minimal API&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/ohalay" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F631028%2Fb3242a62-7ee9-4354-8395-03166ed33226.jpeg" alt="ohalay profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/ohalay" class="crayons-story__secondary fw-medium m:hidden"&gt;
              ohalay
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                ohalay
                
              
              &lt;div id="story-author-preview-content-2853827" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/ohalay" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F631028%2Fb3242a62-7ee9-4354-8395-03166ed33226.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;ohalay&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/ohalay/add-the-mcp-server-to-the-aspnet-core-minimal-api-4331" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 18 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/ohalay/add-the-mcp-server-to-the-aspnet-core-minimal-api-4331" id="article-link-2853827"&gt;
          Add the MCP server to the ASP.NET Core minimal API
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tutorial"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tutorial&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dotnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dotnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/ohalay/add-the-mcp-server-to-the-aspnet-core-minimal-api-4331" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/ohalay/add-the-mcp-server-to-the-aspnet-core-minimal-api-4331#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              2&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  Chat API
&lt;/h2&gt;

&lt;p&gt;Now, let’s combine everything into a single endpoint &lt;code&gt;/api/message&lt;/code&gt; that sends user messages to the LLM while using MCP tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChatRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IChatClient&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetResponseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChatRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ChatOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ModelId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"anthropic.claude-3-5-sonnet-20240620-v1:0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[..&lt;/span&gt; &lt;span class="n"&gt;mcpTools&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the end, to authenticate the user request to MCP, we will use an HTTP handler for our transport&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OnBehalfOfHttpHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHttpContextAccessor&lt;/span&gt; &lt;span class="n"&gt;httpContextAccessor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DelegatingHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;authorizationHeader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpContextAccessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorizationHeader&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;AuthenticationHeaderValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorizationHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;headerValue&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthenticationHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headerValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Solution Limitation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only Bedrock models supported by Microsoft.Extensions.AI can be used.&lt;/li&gt;
&lt;li&gt;Some models may not support tool invocation&lt;/li&gt;
&lt;li&gt;The MCP SDK is still in preview and subject to change&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By combining IChatClient with MCP tools, you create a modular and extensible architecture for AI integration in .NET.&lt;br&gt;
This approach lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep your code provider-agnostic&lt;/li&gt;
&lt;li&gt;Dynamically load new AI tools via MCP&lt;/li&gt;
&lt;li&gt;Securely delegate authentication using "on-behalf-of" tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As &lt;em&gt;MCP&lt;/em&gt; and &lt;em&gt;Microsoft.Extensions.AI&lt;/em&gt; evolve, this pattern positions your application to easily adopt new models, providers, and AI capabilities with minimal refactoring.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>dotnet</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Build Semantic Search in ASP.NET Core using PostgreSQL</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Mon, 06 Oct 2025 19:09:33 +0000</pubDate>
      <link>https://dev.to/ohalay/how-to-build-semantic-search-in-aspnet-core-using-postgresql-28m8</link>
      <guid>https://dev.to/ohalay/how-to-build-semantic-search-in-aspnet-core-using-postgresql-28m8</guid>
      <description>&lt;p&gt;I usually build simple applications that expose public APIs backed by PostgreSQL databases. But sometimes, it is not enough for the product. However, businesses often need more flexible and intelligent search capabilities than what PostgreSQL’s built-in search provides. Someone may argue that PostgreSQL supports full-text search. Yes, it's fine, but it's more for finding specific words or phrases. Semantic search focuses on understanding contextual meaning rather than exact keyword matches - and that’s what we’ll explore.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database side
&lt;/h3&gt;

&lt;p&gt;The first solution that comes to mind is Elasticsearch. It’s an excellent tool for our problem, but it introduces extra cost, additional infrastructure, and the need to learn and integrate a new technology. Since we already use PostgreSQL, we can simply enable the &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector extension&lt;/a&gt; for vector search. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;a href="//Pgvector.EntityFrameworkCore"&gt;NuGet&lt;/a&gt; to support vectors&lt;/li&gt;
&lt;li&gt;Configure a model to store and search vectors. I'm using the &lt;code&gt;ivfflat&lt;/code&gt; index type, which is optimised for approximate nearest neighbor search. And I'm enabling &lt;code&gt;vector_cosine_ops&lt;/code&gt; cosine distance similarity search.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasPostgresExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"vector"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TextEmbedding&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// all-minilm produces 384-dimensional vectors&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;HasColumnType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"vector(384)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
  &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Add index for vector similarity search&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ivfflat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasOperators&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"vector_cosine_ops"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Application part
&lt;/h3&gt;

&lt;p&gt;Now that the database is ready to work with vectors, let’s move to the application side. We'll register an embedding service, create an endpoint to store text embeddings, and finally build a semantic search endpoint.&lt;/p&gt;
&lt;h4&gt;
  
  
  1. Register the embedding service.
&lt;/h4&gt;

&lt;p&gt;Microsoft provides an abstraction layer for working with large language models via the &lt;code&gt;Microsoft.Extensions.AI&lt;/code&gt; NuGet package, so we don’t have to depend on a specific provider. I will use &lt;strong&gt;Ollama&lt;/strong&gt; - a free, open-source tool that simplifies running LLMs. NuGet &lt;code&gt;OllamaSharp&lt;/code&gt; to work in NET.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEmbeddingGenerator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;modelId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Ollama:EmbeddingModel"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Ollama:BaseUrl"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OllamaApiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  2. Create an endpoint to convert text to vectors and store them
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;EmbeddingDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;IEmbeddingGenerator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Convert request to vectors&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GenerateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dbModels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TextEmbedding&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Embedding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbModels&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TypedResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Created&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  3. The magic happens here - semantic search using cosine similarity
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/text/search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;EmbeddingDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;IEmbeddingGenerator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Convert query to vector&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GenerateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;queryEmbedding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Find most similar texts using cosine distance&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;similarTexts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextEmbeddings&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CosineDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryEmbedding&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TypedResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;similarTexts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Test local
&lt;/h3&gt;

&lt;p&gt;To run everything locally, we can use &lt;a href="https://ollama.com/library/all-minilm" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; for generating embeddings. This approach avoids cloud APIs (like OpenAI or Azure) and keeps everything self-contained.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama:latest&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;11434:11434"&lt;/span&gt;
  &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bin/bash"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;serve&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pull&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;all-minilm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;wait"&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;OLLAMA_KEEP_ALIVE="24h"&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ollama_data:/root/.ollama&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The rest of the stack - the API, PostgreSQL, and database migrations - can be managed within the same Docker Compose setup.&lt;/p&gt;
&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;By combining EF Core, PostgreSQL, and the pgvector extension, we can bring powerful semantic search capabilities directly into ASP.NET applications — without introducing new infrastructure.&lt;br&gt;
The approach is efficient, cost-effective, and fully compatible with familiar .NET tools. Thanks to the Microsoft.Extensions.AI abstraction, we can easily swap between local (Ollama) and cloud (OpenAI, Azure) embeddings. This setup is ideal for building document search, recommendation engines, similarity detection, and RAG-based applications.&lt;br&gt;
You can find the full implementation details on my 

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ohalay" rel="noopener noreferrer"&gt;
        ohalay
      &lt;/a&gt; / &lt;a href="https://github.com/ohalay/embedeting-poc" rel="noopener noreferrer"&gt;
        embedeting-poc
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Architecture&lt;/h1&gt;
&lt;/div&gt;


  &lt;div class="js-render-enrichment-target"&gt;
    &lt;div class="render-plaintext-hidden"&gt;
      &lt;pre&gt;flowchart LR
    subgraph Architecture
        API[API .NET]
        DB[(PostgreSQL using pgvector)]
        OLLAMA[Ollama LLM]
        MIGRATIONS[EF Migrations]
    end
    API &amp;lt;--&amp;gt; DB
    API &amp;lt;--&amp;gt; OLLAMA
    MIGRATIONS --&amp;gt; DB
&lt;/pre&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;span class="js-render-enrichment-loader d-flex flex-justify-center flex-items-center width-full"&gt;
    &lt;span&gt;
  
    &lt;span class="sr-only"&gt;Loading&lt;/span&gt;
&lt;/span&gt;
  &lt;/span&gt;


&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;API Endpoints&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;br&gt;
&lt;thead&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/thead&gt;
&lt;br&gt;
&lt;tbody&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;/text&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;List first 20 texts&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;/text&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Add texts (array of { content })&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;/text/search&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Vector search (?query=...)&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/tbody&gt;
&lt;br&gt;
&lt;/table&gt;&lt;/div&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Testing the API&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;You can use the provided &lt;a href="https://github.com/ohalay/embedeting-poc/%60Api/Api.http%60" rel="noopener noreferrer"&gt;.http&lt;/a&gt; file to easily test the API endpoints directly from Visual Studio Code or other compatible tools. This file contains example requests for adding, searching, and listing texts.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;EmbeddingPoC&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;A proof-of-concept .NET API for text embedding and semantic search using PostgreSQL with pgvector and Ollama for embedding generation.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Store and retrieve text embeddings in PostgreSQL using Entity Framework Core and pgvector.&lt;/li&gt;
&lt;li&gt;Generate embeddings via Ollama API.&lt;/li&gt;
&lt;li&gt;REST endpoints for:
&lt;ul&gt;
&lt;li&gt;Adding new texts and their embeddings.&lt;/li&gt;
&lt;li&gt;Searching for similar texts using vector similarity.&lt;/li&gt;
&lt;li&gt;Listing stored texts.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;Simply run:&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;docker-compose up --build
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ohalay/embedeting-poc" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;





</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>postgres</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>Add the MCP server to the ASP.NET Core minimal API</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Thu, 18 Sep 2025 14:37:43 +0000</pubDate>
      <link>https://dev.to/ohalay/add-the-mcp-server-to-the-aspnet-core-minimal-api-4331</link>
      <guid>https://dev.to/ohalay/add-the-mcp-server-to-the-aspnet-core-minimal-api-4331</guid>
      <description>&lt;p&gt;A running solution where you can add server middleware to ASP.NET Core minimal APIs&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Install MCP NuGet packages
&lt;/h3&gt;

&lt;p&gt;Install the MCP packages from NuGet. Use the latest stable versions where possible. Example package links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.nuget.org/packages/ModelContextProtocol" rel="noopener noreferrer"&gt;ModelContextProtocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nuget.org/packages/ModelContextProtocol.AspNetCore" rel="noopener noreferrer"&gt;ModelContextProtocol.AspNetCore&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Add the MCP server to your host
&lt;/h3&gt;

&lt;p&gt;Register the MCP server and transports during host building. &lt;code&gt;WithToolsFromAssembly()&lt;/code&gt; scans your assembly for &lt;code&gt;[McpServerToolType]&lt;/code&gt; classes and auto-registers tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMcpServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithHttpTransport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithToolsFromAssembly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapEndpoints&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Your own extension method, defined later&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapMcp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"api/mcp"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Expose MCP over HTTP&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Define MCP tools
&lt;/h3&gt;

&lt;p&gt;Mark the types that contain tools with the appropriate attribute &lt;code&gt;McpServerToolType&lt;/code&gt; and annotate tool methods &lt;code&gt;McpServerTool&lt;/code&gt;. Provide clear &lt;code&gt;Description&lt;/code&gt; text to improve discovery and documentation.&lt;/p&gt;

&lt;p&gt;Note: attribute names and exact signatures depend on the MCP C# SDK you're using.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerToolType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Endpoint&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEndpointRouteBuilder&lt;/span&gt; &lt;span class="nf"&gt;MapEndpoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IEndpointRouteBuilder&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Get list of users"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Produces&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PagedResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetUserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status200OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithOpenApi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"get_paged_list_users"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Get cursor-based paged list of users"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PagedResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetUserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ApplicationDbContext&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AsParameters&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;GetUsersRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetUsersRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateAndThrowAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToPagedAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Add authorization
&lt;/h3&gt;

&lt;p&gt;The Model Context Protocol provides &lt;a href="https://modelcontextprotocol.io/specification/draft/basic/authorization" rel="noopener noreferrer"&gt;authorization capabilities&lt;/a&gt;. In our API, we are using OAuth 2.1, and we will reuse it. Just add &lt;code&gt;RequireAuthorization&lt;/code&gt; to the MCP endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Add JWT authentication&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticationScheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddJwtBearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"jwt_authority"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Audience&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"jwt_audiences"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthorization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthorizationPolicyBuilder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;RequireAuthenticatedUser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapMcp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"api/mcp"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;RequireAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Test locally
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use the Model Context Protocol inspector app: &lt;a href="https://modelcontextprotocol.io/legacy/tools/inspector" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/legacy/tools/inspector&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Use your VS Code with Copilot: &lt;a href="https://code.visualstudio.com/docs/copilot/customization/mcp-servers" rel="noopener noreferrer"&gt;https://code.visualstudio.com/docs/copilot/customization/mcp-servers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Any other tool that supports MCP&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusions
&lt;/h3&gt;

&lt;p&gt;There’s a straightforward way to add MCP server support to an existing minimal API. We can reuse the same endpoint handlers and authorization logic, and dependency injection works as before. With this setup, the server can now expose both a REST API and MCP. The key point is to provide clear descriptions of tools and parameters to improve discovery and usability for LLMs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional references
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Model Context Protocol docs: &lt;a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/docs/getting-started/intro&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Getting-started blog post: &lt;a href="https://devblogs.microsoft.com/dotnet/build-a-model-context-protocol-mcp-server-in-csharp/" rel="noopener noreferrer"&gt;https://devblogs.microsoft.com/dotnet/build-a-model-context-protocol-mcp-server-in-csharp/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub C# SDK: &lt;a href="https://github.com/modelcontextprotocol/csharp-sdk/blob/main/README.md" rel="noopener noreferrer"&gt;https://github.com/modelcontextprotocol/csharp-sdk/blob/main/README.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>tutorial</category>
      <category>dotnet</category>
      <category>ai</category>
    </item>
    <item>
      <title>Integration Testing with .NET Aspire: Unified Local and Test environments with SQS, Lambda, and Postgres</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Tue, 24 Jun 2025 05:50:44 +0000</pubDate>
      <link>https://dev.to/ohalay/integration-testing-with-net-aspire-unified-local-and-test-environments-with-sqs-lambda-and-1g5e</link>
      <guid>https://dev.to/ohalay/integration-testing-with-net-aspire-unified-local-and-test-environments-with-sqs-lambda-and-1g5e</guid>
      <description>&lt;p&gt;Our application is an ASP.NET Core API, Postage DB using Entity Framework, and SQS Lambda handler. The main idea is to configure an application using Aspire for local development, and the same configuration for integration testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution structure
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ApiService&lt;/strong&gt; - ASP.NET Core API that persists data to Postgres and sends a message to AWS SQS. EF Core code-first approach for persistent abstraction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; - AWS Lambda that is triggered based on an SQS message&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AppHost&lt;/strong&gt; - Describes and runs resources for an application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt; - xUnit tests that use the Aspire app host to run and test &lt;code&gt;ApiService&lt;/code&gt; and &lt;code&gt;Lambda&lt;/code&gt; projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Aspire Configuration
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DB: Postgres container, PgAdmin container, and DB Migrate using &lt;code&gt;ef 
core&lt;/code&gt; tool
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Postgres configuration
var postgres = builder.AddPostgres(AspireResources.Postgres)
  .WithLifetime(ContainerLifetime.Persistent)
  .WithPgAdmin(c =&amp;gt; c.WithLifetime(ContainerLifetime.Persistent));

var db = postgres.AddDatabase(AspireResources.PostgresDb);

// DB migration
var migrator = builder
  .AddExecutable(
    "ef-db-migrations",
    "dotnet",
    "../AspirePoc.ApiService",
    "ef", "database", "update", "--no-build"
  )
  .WaitFor(db)
  .WithReference(db);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;AWS: LocalStack container, Lambda emulator, Lambda handler
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// LocalStack configuration
var awsConfig = builder.AddAWSSDKConfig()
  .WithRegion(RegionEndpoint.EUCentral1);

var localStack = builder.AddLocalStack(AspireResources.LocalStack)
  .WithEnvironment("AWS_DEFAULT_REGION", awsConfig.Region!.SystemName);

// AWS Lambda function configuration
builder.AddAWSLambdaFunction&amp;lt;Projects.AspirePoc_Lambda&amp;gt;(
  AspireResources.Lambda,
  "AspirePoc.Lambda::AspirePoc.Lambda.Function::FunctionHandler")
  .WithReference(awsConfig);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;API: Api service with connection to DB and LocalStack
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// API
var url = $"http://sqs.eu-central-1.localhost.localstack.cloud:4566/000000000000/{AspireResources.LocalStackResources.SqsName}";
var apiService = builder.AddProject&amp;lt;Projects.AspirePoc_ApiService&amp;gt;(AspireResources.Api)
  .WithReference(localStack)
  .WithReference(awsConfig)
  .WithReference(db)
  .WithEnvironment("SqsUrl", url)
  .WaitForCompletion(migrator);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Integration test implementation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;xUnit fixture&lt;/strong&gt; - Start up the Aspire test host before tests run
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[assembly: AssemblyFixture(typeof(AspireFixture))]

namespace AspirePoc.Tests.Infrastructure;

public class AspireFixture : IAsyncLifetime
{
  public IDistributedApplicationTestingBuilder AppHost { get; private set; }
  public DistributedApplication? App { get; private set; }

  public async ValueTask InitializeAsync()
  {
    AppHost = await DistributedApplicationTestingBuilder
      .CreateAsync&amp;lt;Projects.AspirePoc_ApiService&amp;gt;();

    AppHost.Services.ConfigureHttpClientDefaults(clientBuilder =&amp;gt;
    {
      clientBuilder.AddStandardResilienceHandler();
    });

    // Remove useless resources for testing
    RemoveNotNeededResourcesForTesting();

    // Change config for testing
    ModifyResourcesForTesting();

    // Set AWS credentials for testing
    SetupTestDependencies();

    App = await AppHost.BuildAsync();
    await App.StartAsync();

    // Wait for the API service to be healthy
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
    await App.ResourceNotifications
      .WaitForResourceHealthyAsync(AspireResources.Api, cts.Token);
  }

  public async ValueTask DisposeAsync()
  {
    if (App is not null)
    {
      await App.DisposeAsync();
    }
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Configure test host and dependencies(Http client, DbContext, LambdaClient) to arrange tests
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AppHost.Services
  .AddSingleton&amp;lt;IAmazonLambda&amp;gt;(_ =&amp;gt; new AmazonLambdaClient(
  new BasicAWSCredentials("mock-access-key", "mock-secret-access-key"),
  new AmazonLambdaConfig
  {
    ServiceURL = AppHost.GetLambdaEmulatorUrl(),
  }))
  .AddSingleton(_ =&amp;gt;
  {
    var connectionString = AppHost.GetConnectionString()
      .GetAwaiter().GetResult();
    var npqConnBuilder = new NpgsqlConnectionStringBuilder(connectionString)
    {
      IncludeErrorDetail = true
    };

    var source = new NpgsqlDataSourceBuilder(npqConnBuilder.ToString())
      .Build();

    return new AppDbContext(
      new DbContextOptionsBuilder&amp;lt;AppDbContext&amp;gt;()
      .UseNpgsql(source)
      .Options);
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;API Test example
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Fact]
public async Task Get_WeatherForecast_ShouldReturnResponse()
{
  // Arrange
  dbContext.Add(new WeatherForecast(new DateOnly(), 25, "Sunny"));
  await dbContext.SaveChangesAsync(TestContext.Current.CancellationToken);

  // Act
  var response = await sut.GetFromJsonAsync&amp;lt;WeatherForecast[]&amp;gt;(
    "/weatherforecast",
    TestContext.Current.CancellationToken);

  // Assert
  response.ShouldNotBeEmpty();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Lambda handler test example
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Fact]
public async Task InvokeLambda_SQSLambda_ShouldAccepted()
{
  var request = new InvokeRequest
  {
    FunctionName = AspireResources.Lambda,
    InvocationType = InvocationType.Event,
    Payload = @"{
                 ""Records"": [
                   { ""body"": ""Test message"" }
                 ]
                }"
    };
    // Act
    var response = await lambdaClient.InvokeAsync(
      request,
      TestContext.Current.CancellationToken);

    // Assert
    response.StatusCode.ShouldBe(202);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The result appears in the Aspire dashboard and &lt;a href="https://github.com/ohalay/aspire-poc" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdyh2b0nc2p5b09qne5ue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdyh2b0nc2p5b09qne5ue.png" alt="Aspire dashboard" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>aspire</category>
      <category>aws</category>
      <category>testing</category>
    </item>
    <item>
      <title>aspire</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Mon, 23 Jun 2025 09:48:32 +0000</pubDate>
      <link>https://dev.to/ohalay/aspire-3l37</link>
      <guid>https://dev.to/ohalay/aspire-3l37</guid>
      <description></description>
      <category>motivation</category>
    </item>
    <item>
      <title>We don't need DynamoDB when we have Aurora Postgres</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Fri, 14 Mar 2025 09:30:20 +0000</pubDate>
      <link>https://dev.to/ohalay/we-dont-need-dynamodb-when-we-have-aurora-postgres-2n8b</link>
      <guid>https://dev.to/ohalay/we-dont-need-dynamodb-when-we-have-aurora-postgres-2n8b</guid>
      <description>&lt;p&gt;Aurora Postgres has many cool features. Let's compare them to &lt;a href="https://aws.amazon.com/dynamodb/features/" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt; and review them. While I'm writing this article, &lt;em&gt;the latest version of Postgres - v17.4&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Key-value and document data models&lt;/strong&gt; - We don't need to talk about key-value. It is more interested in "how to store document data?". Postregs has a special type to sore JOSN data - &lt;a href="https://www.postgresql.org/docs/current/datatype-json.html" rel="noopener noreferrer"&gt;JSONB&lt;/a&gt;. It was introduced in version 9.4 more than 10 years ago. Using &lt;a href="https://www.npgsql.org/efcore/mapping/json.html?tabs=fluent-api%2Cjsondocument" rel="noopener noreferrer"&gt;EF Core 8&lt;/a&gt; we don't even need to think of serialization/deserialization - just use it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serverless that scales to zero&lt;/strong&gt; - AWS introduced scaling to 0 capacity with &lt;a href="https://aws.amazon.com/blogs/database/introducing-scaling-to-0-capacity-with-amazon-aurora-serverless-v2/" rel="noopener noreferrer"&gt;Amazon Aurora Serverless v2&lt;/a&gt;.  It is an infrastructure-level configuration, but keep in mind a cold start during resuming.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ACID transactions&lt;/strong&gt; - nice joke 😄 &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Active-active replication with global tables&lt;/strong&gt; - &lt;a href="https://aws.amazon.com/blogs/database/using-pgactive-active-active-replication-extension-for-postgresql-on-amazon-rds-for-postgresql/" rel="noopener noreferrer"&gt;here&lt;/a&gt; is an AWS blog post on configuring active-active replication for Postgres. It is an infrastructure-level configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DynamoDB Streams as part of an event-driven architecture&lt;/strong&gt; -  postgress logical replication. A Short article about AWS RDS configuration &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_MultiAZDBCluster_LogicalRepl.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;. The .Net &lt;code&gt;Npgsql&lt;/code&gt; data provider has abstraction for &lt;a href="https://www.npgsql.org/doc/replication.html#logical-replication" rel="noopener noreferrer"&gt;logical replication&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Secondary indexes&lt;/strong&gt; - postgres jsonb type support &lt;a href="https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING" rel="noopener noreferrer"&gt;Generalized Inverted Index.&lt;/a&gt; and can efficiently search using then.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Time to live&lt;/strong&gt; - postgres &lt;a href="https://github.com/citusdata/pg_cron" rel="noopener noreferrer"&gt;pg_cron&lt;/a&gt; extension that we can use to schedule row delition. But before that, we need to &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL_pg_cron.html" rel="noopener noreferrer"&gt;configure rds&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bulk import from S3&lt;/strong&gt; - easy, just &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PostgreSQL.S3Import.html" rel="noopener noreferrer"&gt;add permission&lt;/a&gt; and go on.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, do you want to &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL-Lambda.html" rel="noopener noreferrer"&gt;trigger aws lambda from RDS&lt;/a&gt;? No problem, we can do that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Before choosing DynamoDB for your project, I recommend creating a Proof of Concept (PoC) using Aurora Postgres to validate your non-functional requirements (NFRs). With its mature feature set and broad support for complex use cases, Aurora Postgres can often be an excellent alternative for many workloads, saving you from introducing the complexity of a NoSQL database when a relational solution might meet your needs just as well.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>postgres</category>
      <category>aws</category>
      <category>dynamodb</category>
    </item>
    <item>
      <title>Visualize Asp Net Core metrics with Grafana</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Mon, 03 Feb 2025 08:48:39 +0000</pubDate>
      <link>https://dev.to/ohalay/visualize-asp-net-core-metrics-with-grafana-2dpd</link>
      <guid>https://dev.to/ohalay/visualize-asp-net-core-metrics-with-grafana-2dpd</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Spoiler&lt;/em&gt;&lt;/strong&gt;: In this article, we’ll walk through how to collect and visualize metrics from an ASP.NET Core application using OpenTelemetry, Prometheus, and Grafana. We’ll configure the necessary services and see how to monitor performance metrics in a real-time dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collect metrics with OTLP
&lt;/h3&gt;

&lt;p&gt;A metric is a measurement of your application’s behavior that we aggregate over a given time period and capture at runtime. OpenTelemetry (OTLP) is an industry standard for collecting and exporting metrics. Here’s an example of how to set it up in your ASP.NET Core application &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics" rel="noopener noreferrer"&gt;metrics&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;appBuilder.Services.AddOpenTelemetry()
    .ConfigureResource(builder =&amp;gt; builder
        .AddService(serviceName: "my_service_name"))
    .WithMetrics(builder =&amp;gt; builder
        .AddAspNetCoreInstrumentation() // metric collector
    );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Produce metrics with Prometheus
&lt;/h3&gt;

&lt;p&gt;Once you've collected metrics with OpenTelemetry, you can export them using any supported OpenTelemetry exporter. To export them to Prometheus, we’ll add the &lt;code&gt;.AddPrometheusExporter()&lt;/code&gt; method to the metric configuration. It works as a pull model, with a scrap date from the application. That means, an application should provide an endpoint with metrics for scrapping. To do that we need to add middleware &lt;code&gt;app.MapPrometheusScrapingEndpoint()&lt;/code&gt;. Prometheus scrapes this &lt;code&gt;/metrics&lt;/code&gt; endpoint at regular intervals to collect the application’s time-series data &lt;a href="https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md" rel="noopener noreferrer"&gt;metrics&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# HELP http_requests_total The total number of HTTP requests.
# TYPE http_requests_total counter
http_requests_total{method="post",code="200"} 1027 1395066363000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Define Grafana charts
&lt;/h3&gt;

&lt;p&gt;The dotnet team created a Grafana &lt;a href="https://grafana.com/orgs/dotnetteam/dashboards" rel="noopener noreferrer"&gt;charts&lt;/a&gt; for the built-in Asp Net Core metrics. We can also search the community &lt;a href="https://grafana.com/search/?term=asp+net+core" rel="noopener noreferrer"&gt;charts&lt;/a&gt; published in Grafana. We just need to import it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Run locally
&lt;/h3&gt;

&lt;p&gt;We will use docker-compose with AspNet Core, Prometheus, and Grafana containers. We download Grafana charts and add them to the container (&lt;em&gt;grafana&lt;/em&gt; folder). We also configured the Prometheus scrab job, &lt;code&gt;prometheus.yml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  httpprometheus:
    image: ${DOCKER_REGISTRY-}httpprometheus
    build:
      context: HttpPrometheus
      dockerfile: Dockerfile
    environment:
      - OTEL_RESOURCE_ATTRIBUTES=service.namespace=demo,deployment.environment=dev

  prometheus:
    image: prom/prometheus
    restart: always
    ports:
      - 9090:9090
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro

  grafana: 
    image: grafana/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=P@ssw0rd
    restart: always
    ports:
      - 3000:3000
    volumes:
      - ./grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Source code
&lt;/h3&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ohalay" rel="noopener noreferrer"&gt;
        ohalay
      &lt;/a&gt; / &lt;a href="https://github.com/ohalay/HttpPrometheus" rel="noopener noreferrer"&gt;
        HttpPrometheus
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Vsualize Asp Net Core metrics with Promethous and Grafana&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;An example of Asp Net Core metric configuration using Promethous and Grafana. Also docker-compose was provided to run locally.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;.NET8&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ohalay/HttpPrometheus" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h3&gt;
  
  
  Help Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/docs/metrics/getting-started-aspnetcore/README.md" rel="noopener noreferrer"&gt;https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/docs/metrics/getting-started-aspnetcore/README.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md" rel="noopener noreferrer"&gt;https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/orgs/dotnetteam/dashboards" rel="noopener noreferrer"&gt;https://grafana.com/orgs/dotnetteam/dashboards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/grafana/dashboards/17706-asp-net-otel-metrics/" rel="noopener noreferrer"&gt;https://grafana.com/grafana/dashboards/17706-asp-net-otel-metrics/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnetcore</category>
      <category>tutorial</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Dotnet AsyncApi specification for Avro Kafka channel</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Mon, 16 Sep 2024 07:19:44 +0000</pubDate>
      <link>https://dev.to/ohalay/dotnet-asyncapi-specification-for-avro-kafka-channel-31ib</link>
      <guid>https://dev.to/ohalay/dotnet-asyncapi-specification-for-avro-kafka-channel-31ib</guid>
      <description>&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;How do we document Event-Driven Architecture (EDA) and provide discovery for events and transport?&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;Generate &lt;a href="https://www.asyncapi.com/docs/reference/specification/v3.0.0" rel="noopener noreferrer"&gt;AsyncAPI specifications&lt;/a&gt; document - it is defacto standard for EDA as OpenAPI for HTTP API. This document may be used by a lot of tools to visualize and share specifications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://studio.asyncapi.com/#introduction" rel="noopener noreferrer"&gt;AsynAPI studio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://editor-next.swagger.io" rel="noopener noreferrer"&gt;Swagger&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://backstage.io/docs/features/software-catalog/descriptor-format/#kind-api" rel="noopener noreferrer"&gt;BackStage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;Our ASP .Net Core application publishes events to Kafka using &lt;a href="https://avro.apache.org/docs/1.11.1/specification/" rel="noopener noreferrer"&gt;avro&lt;/a&gt; serialization protocol. There is a library that can help to build &lt;a href="https://github.com/LEGO/AsyncAPI.NET" rel="noopener noreferrer"&gt;AsyncApi&lt;/a&gt; specification. A stable version &lt;code&gt;v5.2.4&lt;/code&gt;(when the article was published) does not support &lt;em&gt;Avro&lt;/em&gt; schema, but the new beta version (&lt;code&gt;6.0.0-beta.97&lt;/code&gt;) - has implementation. &lt;/p&gt;

&lt;h4&gt;
  
  
  1. We will start with the high-level document
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var document = new AsyncApiDocument()
{
    Info = new AsyncApiInfo
    {
        Title = "My AsyncAPI",
        Version = "0.0.1"
    }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Next, we will create components(Kafka messages with Avro schema). Also, there is an option to reference the existing Avro file schema.
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document.Components = new AsyncApiComponents
{
    Messages = new Dictionary&amp;lt;string, AsyncApiMessage&amp;gt;
    {
        ["MyAvroMessage"] = new AsyncApiMessage
        {
            SchemaFormat = "application/vnd.apache.avro;version=1.9.0",
            Payload = new AsyncApiAvroSchemaPayload(new AvroRecord
            {
                Doc = "Doc for event",
                Name = "MyAvroMessage",
                Namespace = "Test.MyAvroMessage",
                Fields = [
                    new AvroField {
                        Doc = "Doc for field",
                        Name = "Id",
                        Type = AvroPrimitiveType.String,
                    }
                ]
            })
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Reference messages to channels(Kafka topic)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;document.Channels["my.kafka.topic"] = new AsyncApiChannel
{
    Subscribe = new AsyncApiOperation
    {
        Message = [
           new AsyncApiMessage
           {
               Reference = new AsyncApiReference
               {
                   Id = "MyAvroMessage",
                   Type = ReferenceType.Message,
               }
           }
        ]
    }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4. Serialize document to AsyncApi specification
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var serializedDoc = document.Serialize(
    AsyncApiVersion.AsyncApi2_0,
    AsyncApiFormat.Yaml
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  5. In the end, we will have the next specification
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;asyncapi: 2.6.0
info:
  title: My AsyncAPI
  version: 0.0.1
channels:
  my.kafka.topic:
    subscribe:
      message:
        $ref: '#/components/messages/MyAvroMessage'
components:
  messages:
    MyAvroMessage:
      payload:
        type: record
        name: MyAvroMessage
        namespace: Test.MyAvroMessage
        doc: Doc for event
        fields:
          - name: Id
            type: string
            doc: Doc for field
      schemaFormat: application/vnd.apache.avro;version=1.9.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  6. Finally, visualization
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcujeciooakfibfojzad9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcujeciooakfibfojzad9.png" alt="Image description" width="800" height="674"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;With AsyncApi spec document we can share it via endpoint as OpenAPI document and use a tool to visualize&lt;/li&gt;
&lt;li&gt;We can build an Avro scheme from scratch or reference schema file&lt;/li&gt;
&lt;li&gt;We can provide information for Kafka server and bindings, but it is not our case&lt;/li&gt;
&lt;li&gt;We can even generate clients for some programming languages, but that is also not our case &lt;/li&gt;
&lt;li&gt;Currently &lt;em&gt;AsyncAPI.NET&lt;/em&gt; don't support &lt;code&gt;3.0.0&lt;/code&gt; specification&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Help links
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.asyncapi.com/docs/tutorials" rel="noopener noreferrer"&gt;https://www.asyncapi.com/docs/tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://avro.apache.org/docs/1.11.1/specification/" rel="noopener noreferrer"&gt;https://avro.apache.org/docs/1.11.1/specification/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/LEGO/AsyncAPI.NET" rel="noopener noreferrer"&gt;https://github.com/LEGO/AsyncAPI.NET&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>dotnet</category>
      <category>tutorial</category>
      <category>kafka</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>CloudWatch OTLP metrics exporter using .Net Lambda</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Sat, 27 Jul 2024 08:13:09 +0000</pubDate>
      <link>https://dev.to/ohalay/otel-lambda-net-8-using-cdk-3ki4</link>
      <guid>https://dev.to/ohalay/otel-lambda-net-8-using-cdk-3ki4</guid>
      <description>&lt;p&gt;We will configure the open telemetry protocol(OTLP) metrics exporter to AWS CloudWatch using AWS Lambda with .NET 8 runtime. We will start from the infrastructure using AWS CDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lambda infrastructure
&lt;/h2&gt;

&lt;p&gt;To support Lambda for open telemetry, we need to configure the next thing. Add a lambda layer for &lt;a href="https://aws-otel.github.io/" rel="noopener noreferrer"&gt;AWS Distro for OpenTelemetry&lt;/a&gt;(ADOT); layer versions are in GitHub &lt;a href="https://aws-otel.github.io/docs/getting-started/lambda/lambda-dotnet" rel="noopener noreferrer"&gt;repository&lt;/a&gt;. Also, add &lt;code&gt;collector.yaml&lt;/code&gt; path to the environment variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const string version = "ver-0-115-0:2";
var lambda = new Function("otlp-lambda", new FunctionProps
{
  ...
  Runtime = Runtime.DOTNET_8,
  Environment = new Dictionary&amp;lt;string, string&amp;gt;
  {
    ["OPENTELEMETRY_COLLECTOR_CONFIG_URI"] = "/var/task/collector.yaml";
     },
  Layers = [LayerVersion.FromLayerVersionArn(
     this,
     "otel-lambda-layer",
     Fn.Join("", ["arn:aws:lambda:", Aws.REGION, $":901920570463:layer:aws-otel-collector-arm64-{version}"]))
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Collector config
&lt;/h2&gt;

&lt;p&gt;The collector file is a &lt;a href="https://opentelemetry.io/docs/concepts/components/#collector" rel="noopener noreferrer"&gt;configuration&lt;/a&gt; to receive, process, and export telemetry data. An important thing is &lt;code&gt;NodeName&lt;/code&gt; that we will path later, during OTLP configuration. &lt;a href="https://aws-otel.github.io/docs/getting-started/cloudwatch-metrics#getting-started" rel="noopener noreferrer"&gt;ADOT collector&lt;/a&gt; configuration here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;receivers:
  otlp:
    protocols:
      grpc:
      http:
exporters:
  debug:
    verbosity: normal
  awsemf:
    log_group_name: '/aws/lambda/{NodeName}'
service:
  pipelines:
    metrics:
      receivers: [ otlp ]
      exporters: [ awsemf ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  .NET part
&lt;/h2&gt;

&lt;p&gt;First of all, we need to register open telemetry metrics that we want to collect and add an exporter. &lt;code&gt;AWS_LAMBDA_FUNCTION_NAME&lt;/code&gt; - environment variable that is managed by AWS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.AddOpenTelemetry()
  .WithMetrics(builder =&amp;gt;
  {
    var lambdaName = Environment.GetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME")
      ?? "unknown";
    var resourceBuilder = ResourceBuilder.CreateDefault()
      .AddService("otlp-metrics-sample")
      .AddAttributes([new KeyValuePair&amp;lt;string, object&amp;gt;("NodeName", lambdaName!)]);

    builder
    .SetResourceBuilder(resourceBuilder)
    .AddMeter(api.test.operation);
    .AddOtlpExporter((_, readerOptions) =&amp;gt;
    {
        readerOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
    })
  });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we need to integrate metrics with the DI container &lt;code&gt;.AddMetrics()&lt;/code&gt;.&lt;br&gt;
The last thing - record our metric&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.MapPost("api/metric", (IMeterFactory meterFactory) =&amp;gt;
{
  var metter = meterFactory.Create("api.test.operation");
  var instrument = metter.CreateCounter&amp;lt;int&amp;gt;("sample_counter");
  instrument.Add(1);

  return Results.Ok();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CloudWatch
&lt;/h2&gt;

&lt;p&gt;ADOT collector will create a separate stream in the AWS Lambda log group with the prefix &lt;code&gt;otel-stream-&lt;/code&gt; and put all metrics there. After that, we can use those metrics with CloudWatch tools(dashboards, alarms, etc).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"OTelLib": "api.test.operation",
"Version": "1",
"_aws": {
  "CloudWatchMetrics": [
    {
      "Namespace": "otlp-metrics-sample",
      "Dimensions": [
        [ "OTelLib" ]
      ],
      "Metrics": [
        {
          "Name": "sample_counter"
        }
      ]
    }
  ],
  "Timestamp": 1722233676723
},
"sample_counter": 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open telemetry  - language agnostic protocol, and using the built-in exporter, we may easily migrate from AWS CloudWatch to other supported exporters. &lt;/li&gt;
&lt;li&gt;Using .NET abstraction &lt;code&gt;IMeterFactory&lt;/code&gt; for OTLP, we may easily migrate from AWS Lambda to other computed services(Any Cloud, On-Prem, etc), with no vendor lock. &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Help links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws-otel.github.io/docs/getting-started/lambda/lambda-dotnet#lambda-layer" rel="noopener noreferrer"&gt;https://aws-otel.github.io/docs/getting-started/lambda/lambda-dotnet#lambda-layer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws-otel.github.io/docs/getting-started/dotnet-sdk/manual-instr" rel="noopener noreferrer"&gt;https://aws-otel.github.io/docs/getting-started/dotnet-sdk/manual-instr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws-otel.github.io/docs/getting-started/cloudwatch-metrics" rel="noopener noreferrer"&gt;https://aws-otel.github.io/docs/getting-started/cloudwatch-metrics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-instrumentation" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-instrumentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>lambda</category>
      <category>aws</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Implement REST async request-reply pattern in .Net 8</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Thu, 15 Feb 2024 11:14:28 +0000</pubDate>
      <link>https://dev.to/ohalay/implement-rest-async-request-reply-pattern-in-net-ed7</link>
      <guid>https://dev.to/ohalay/implement-rest-async-request-reply-pattern-in-net-ed7</guid>
      <description>&lt;p&gt;Decouple backend processing from a frontend host, where backend processing needs to be asynchronous, but the frontend still needs a clear response. More details are in the &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/async-request-reply" rel="noopener noreferrer"&gt;article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ToDo API
&lt;/h2&gt;

&lt;p&gt;I'm going to show implementation based on ToDo API. We have REST methods to get ToDo(s). The typical implementation - gets ToDo item by ID and gets all ToDo items&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;group.MapGet("/", (Store store) =&amp;gt; store.Todos.Values.ToArray());
group.MapGet("/{id}", (string id, Store store) =&amp;gt;
  store.Todos.TryGetValue(id, out var todo)
    ? Results.Ok(todo)
    : Results.NotFound());
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;More interested POST method, because it is asynchronous:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept a request and generate jobId&lt;/li&gt;
&lt;li&gt;Create a job and send it to the queue&lt;/li&gt;
&lt;li&gt;Return 202 accepted status code with response header &lt;code&gt;Location&lt;/code&gt; URL where get the result and &lt;code&gt;RetryAfter&lt;/code&gt; to delay next request.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;group.MapPost("/", async (Todo model, 
   BackgroundJobQueue queue,
   Store store, HttpResponse response) =&amp;gt;
{
    model = model with { Id = Guid.NewGuid().ToString() };
    var job = new Job(
     Guid.NewGuid().ToString(),
     DateTime.UtcNow,
     JobStatus.InProgress,
     $"/api/todos/{model.Id}"
    );

    store.Jobs.Add(job.Id, job);

    Func&amp;lt;CancellationToken, ValueTask&amp;gt; workItem = async (token) =&amp;gt;
    {
      await Task.Delay(TimeSpan.FromSeconds(10), token);
      store.Todos.Add(model.Id, model);

    };
    await queue.QueueJobAsync(job.Id, workItem);

    response.Headers.RetryAfter = TimeSpan.FromSeconds(2).ToString();
    response.Headers.Location = $"/api/jobs/{job.Id}";
    response.StatusCode = StatusCodes.Status202Accepted;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Background service
&lt;/h2&gt;

&lt;p&gt;In this example, I'm using ASP Net Core build-in &lt;code&gt;BackgroundService&lt;/code&gt; as a job processor and &lt;code&gt;Channel&lt;/code&gt; as an in-process queue. But it may be any job processor like hangfire, lambda, etc. Also, it may be any queue in-process or hosted. Just take into account all the pros and cons related to queues and job processors.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
  while (!stoppingToken.IsCancellationRequested)
  {
    var (id, jobTask) = await queue.DequeueJobAsync(stoppingToken);
    if (!store.Jobs.TryGetValue(id, out var job))
      return;

    try
    {
      await jobTask(stoppingToken);
      job = job with { CompletedAt = DateTime.UtcNow, Status = JobStatus.Completed };
    }
    catch (Exception ex)
    {
      job = job with { CompletedAt = DateTime.UtcNow, Error = ex.Message, Status = JobStatus.Failed };
    }

    store.Jobs[id] = job;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Job API
&lt;/h2&gt;

&lt;p&gt;This is a job status API that redirects to &lt;code&gt;Location&lt;/code&gt; where get the job completion results or returns the job with an error when it is failed. Also, return the job with &lt;code&gt;RetryAfter&lt;/code&gt; header when it is still processing.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;group.MapGet("/{id}", (string id, Store store, HttpResponse httpResponse) =&amp;gt;
{
  if (!store.Jobs.TryGetValue(id, out var job) || job is null)
    return Results.NotFound();

  var okResult = () =&amp;gt;
  {
    httpResponse.Headers.RetryAfter = TimeSpan.FromSeconds(5).ToString();
    return Results.Ok(job);
  };

  return job.Status switch
  {
    JobStatus.Completed =&amp;gt; Results.Redirect(job.Location),
    JobStatus.InProgress =&amp;gt; okResult(),
    JobStatus.Failed =&amp;gt; Results.BadRequest(job),
    _ =&amp;gt; throw new NotImplementedException(),
  };
});

return group;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Test using the .http file
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.http&lt;/code&gt; files are a simple way to quickly invoke your API endpoints&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get all ToDo items&lt;/li&gt;
&lt;li&gt;Create a new ToDo&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;Location&lt;/code&gt; to get job status&lt;/li&gt;
&lt;li&gt;Get ToDo item by &lt;code&gt;id&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET {{base_url}}/api/todos

###
# @name createTodo
POST {{base_url}}/api/todos
Content-Type: application/json

{
    "dueBy": "2024-02-14",
    "title": "Write an article"
}

###
@jobLocation = {{createTodo.response.headers.Location}}

# @name job
GET {{base_url}}{{jobLocation}}
Accept: application/json

###
@todoId = {{job.response.body.$.id}}

GET {{base_url}}/api/todos/{{todoId}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I provided an example, of how to implement the &lt;em&gt;async request-reply pattern&lt;/em&gt;. Important things such as queues and job processors should be chosen related to the requirements. Also, do not forget to expire job items in the DB. Btw API clients should use status codes and headers 😎. And finally, the source code in the GitHub repository&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ohalay" rel="noopener noreferrer"&gt;
        ohalay
      &lt;/a&gt; / &lt;a href="https://github.com/ohalay/async-request-reply" rel="noopener noreferrer"&gt;
        async-request-reply
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/aa75d37fe2e844f037001d77478fe404800347c9a14ed52913598ce5cf8313d9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f7265706f2d73697a652f6f68616c61792f6173796e632d726571756573742d7265706c79"&gt;&lt;img src="https://camo.githubusercontent.com/aa75d37fe2e844f037001d77478fe404800347c9a14ed52913598ce5cf8313d9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f7265706f2d73697a652f6f68616c61792f6173796e632d726571756573742d7265706c79" alt="GitHub repo size"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/0d65d92bc580dd277e338bec5d3cfda99ccc3f65d15e072178f1e562570ffa35/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732f6f68616c61792f6173796e632d726571756573742d7265706c79"&gt;&lt;img src="https://camo.githubusercontent.com/0d65d92bc580dd277e338bec5d3cfda99ccc3f65d15e072178f1e562570ffa35/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732f6f68616c61792f6173796e632d726571756573742d7265706c79" alt="GitHub contributors"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/e25d53ec280b07ac042d68427a5ccdf055a1619b394443295c1a32790775985c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6f68616c61792f6173796e632d726571756573742d7265706c793f7374796c653d736f6369616c"&gt;&lt;img src="https://camo.githubusercontent.com/e25d53ec280b07ac042d68427a5ccdf055a1619b394443295c1a32790775985c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6f68616c61792f6173796e632d726571756573742d7265706c793f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/f1a71c85488c8a7426d8c2a1364ef161902fcee02f9e8863abdb50a6109d4df3/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6f68616c61792f6173796e632d726571756573742d7265706c793f7374796c653d736f6369616c"&gt;&lt;img src="https://camo.githubusercontent.com/f1a71c85488c8a7426d8c2a1364ef161902fcee02f9e8863abdb50a6109d4df3/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6f68616c61792f6173796e632d726571756573742d7265706c793f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Asynchronous Request-Reply pattern implementation&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;Decouple backend processing from a frontend host, where backend processing needs to be asynchronous, but the frontend still needs a clear response&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;.NET8&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ohalay/async-request-reply" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  Help links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/async-request-reply" rel="noopener noreferrer"&gt;Async Request-Reply pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-8.0&amp;amp;tabs=visual-studio#queued-background-tasks" rel="noopener noreferrer"&gt;Queued background job&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=humao.rest-client" rel="noopener noreferrer"&gt;Rest client .http file&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>backend</category>
      <category>tutorial</category>
      <category>architecture</category>
    </item>
    <item>
      <title>GitLab feature management for .Net</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Thu, 17 Aug 2023 16:01:24 +0000</pubDate>
      <link>https://dev.to/ohalay/gitlab-feature-management-for-net-1a6p</link>
      <guid>https://dev.to/ohalay/gitlab-feature-management-for-net-1a6p</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;Provide a mechanism for continued delivery together with continued development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Use trunk-based development and feature toggles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;Before starting to work with a feature, we need to create a new feature flag in GitLab and disable it. After that, all development should be branched with a feature toggle. When the feature is developed and deployed we can enable a feature in GitLab for the environment and it will activate during runtime. After the next release, we delete a feature flag and clean up the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitLab feature management
&lt;/h3&gt;

&lt;p&gt;GitLab provides a built-in solution for feature flags where we can create, add strategies, and manage. GitLab uses &lt;a href="https://www.getunleash.io/"&gt;unleash&lt;/a&gt; client(we can also use direct GitLab API) to work with features. We can install &lt;em&gt;unleash&lt;/em&gt; NuGet package from &lt;a href="https://github.com/Unleash/unleash-client-dotnet"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iKGmBX29--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dpvgryfw4meygpgzuu24.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iKGmBX29--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dpvgryfw4meygpgzuu24.png" alt="Image description" width="800" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During development, I found two weird things&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;UnleashSettings&lt;/code&gt; has a specific property for &lt;em&gt;Environment&lt;/em&gt;, but the client works only when we set the GitLab environment name to &lt;strong&gt;AppName&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It is hard to understand where we should set &lt;strong&gt;Instance ID&lt;/strong&gt; in &lt;code&gt;UnleashSettings&lt;/code&gt;. In GitHub example - &lt;em&gt;ProjectId&lt;/em&gt;, but property that work &lt;strong&gt;InstanceTag&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var unleash = new DefaultUnleash(new UnleashSettings
{
   AppName = "environment_name" 
   UnleashApi = new Uri("API URL in GitLab config"),
   InstanceTag = "Instance ID in GitLab config",
   SendMetricsInterval = null,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Environments
&lt;/h3&gt;

&lt;p&gt;Important GitLab feature - &lt;strong&gt;environments&lt;/strong&gt;. We deploy our solution to specific environments and we can activate/deactivate feature flags per environment. &lt;/p&gt;

&lt;h3&gt;
  
  
  Microsoft feature management
&lt;/h3&gt;

&lt;p&gt;Unleash client is good enough, but we want to use &lt;br&gt;
&lt;a href="https://github.com/Unleash/unleash-client-dotnet"&gt;dotnet feature management&lt;/a&gt;, which has great integration with ASP.NET Core. To do that we should implement &lt;code&gt;IFeatureDefinitionProvider&lt;/code&gt; for GitLab&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal class GitLabFeatureProvider : IFeatureDefinitionProvider
{
  private IUnleash unleash;
  public GitLabFeatureProvider(IUnleash unleash)
    =&amp;gt; this.unleash = unleash;

  public async IAsyncEnumerable&amp;lt;FeatureDefinition&amp;gt; GetAllFeatureDefinitionsAsync()
  {
    await Task.CompletedTask;
    foreach (var feature in this.unleash.FeatureToggles)
    {
        yield return CreateFeatureDefinition(feature.Name, feature.Enabled);
    }
  }

  public Task&amp;lt;FeatureDefinition&amp;gt; GetFeatureDefinitionAsync(string featureName)
  {
    var active = unleash.IsEnabled(featureName);
    return Task.FromResult(CreateFeatureDefinition(featureName, active));
  }

  private static FeatureDefinition CreateFeatureDefinition(string name, bool active)
  {
    return new FeatureDefinition
    {
      Name = name,
      EnabledFor = active
        ? new[] { new FeatureFilterConfiguration { Name = "AlwaysOn" } }
        : Array.Empty&amp;lt;FeatureFilterConfiguration&amp;gt;()
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using feature management
&lt;/h3&gt;

&lt;p&gt;In the end, we need to register feature management&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.AddFeatureManagement()
.AddSingleton&amp;lt;IFeatureDefinitionProvider, GitLabFeatureProvider&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;inject &lt;code&gt;IFeatureManager&lt;/code&gt; into our solution and use it&lt;br&gt;
&lt;code&gt;await feature.IsEnabledAsync(nameof(FeatureFlags.myfeature))&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;We manage our features during runtime using GitLab feature flags per environment&lt;/li&gt;
&lt;li&gt;We can rollback feature during runtime&lt;/li&gt;
&lt;li&gt;We develop and deploy without impact to the system and end users.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Help links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://trunkbaseddevelopment.com"&gt;https://trunkbaseddevelopment.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ee/operations/feature_flags.html"&gt;https://docs.gitlab.com/ee/operations/feature_flags.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/FeatureManagement-Dotnet"&gt;https://github.com/microsoft/FeatureManagement-Dotnet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>learning</category>
      <category>gitlab</category>
      <category>featureflag</category>
    </item>
    <item>
      <title>AWS DynamoDB stream batch processing lambda using CDK</title>
      <dc:creator>ohalay</dc:creator>
      <pubDate>Mon, 31 Jul 2023 08:13:50 +0000</pubDate>
      <link>https://dev.to/ohalay/aws-dynamodb-stream-batch-processing-lambda-using-cdk-46n5</link>
      <guid>https://dev.to/ohalay/aws-dynamodb-stream-batch-processing-lambda-using-cdk-46n5</guid>
      <description>&lt;h2&gt;
  
  
  Simple lambda
&lt;/h2&gt;

&lt;p&gt;The most common mechanism create lambda - install &lt;a href="https://aws.amazon.com/visualstudio/" rel="noopener noreferrer"&gt;AWS Toolkit&lt;/a&gt;, which will add AWS project templates. After that, choose the lambda template with a DynamoDB trigger, and that is all you need. But what about infrastructure?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void FunctionHandler(DynamoDBEvent dynamoEvent)
{
  foreach (var record in dynamoEvent.Records)
  {   
    // TODO: Add business logic processing the record.Dynamodb
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  CDK
&lt;/h2&gt;

&lt;p&gt;Infrastructure should live together with code because it has the same lifetime. Also, infrastructure should be easily reproducible in other environments. CDK is IaC that will solve these problems and we can write infrastructure using our lovely C# language.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class DynamoDbLambda : Stack
{
  internal DynamoDbLambda(Construct scope, string id, IStackProps props) 
     : base(scope, id, props)
  {
    var dynamoDbTable = new Table(this, "testTable", new TableProps
    {
      BillingMode = BillingMode.PAY_PER_REQUEST,
      PartitionKey = new Attribute { Name = "Pk", Type = AttributeType.STRING },
      SortKey = new Attribute { Name = "Sk", Type = AttributeType.STRING },
    });

    var dynamoDbLambda = new Function(this, "dynamoDbLambda", new FunctionProps
    {
      Runtime = Runtime.DOTNET_6,
      MemorySize = 256,
      Handler = "Serverless.DynamoDbLambda::Serverless.DynamoDbLambda.LambdaHandler::Handle",
      Code = Code.FromAsset("Serverless.DynamoDbLambda/", new AssetOptions
      {
        Bundling = buildOption
      }),
    });

    dynamoDbLambda.AddEventSource(new DynamoEventSource(dynamoDbTable, new DynamoEventSourceProps
    {
      StartingPosition = StartingPosition.TRIM_HORIZON,
    }));
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Taking into account, that we are using DynamoDB &lt;a href="https://aws.amazon.com/blogs/compute/creating-a-single-table-design-with-amazon-dynamodb/" rel="noopener noreferrer"&gt;single table design&lt;/a&gt;, we will receive stream for all tables, but we want to listen to only а special table...&lt;/p&gt;
&lt;h2&gt;
  
  
  Filters
&lt;/h2&gt;

&lt;p&gt;AWS lambda source has the possibility &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.Tutorial2.html" rel="noopener noreferrer"&gt;filter Dynamo DB stream&lt;/a&gt;. CDK syntax is a little bit weird, but it is just JSON representations.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Filters = new[]
{
  FilterCriteria.Filter(new Dictionary&amp;lt;string, object&amp;gt; {
    ["dynamodb"] = new Dictionary&amp;lt;string, object&amp;gt;
    {
      ["Keys"] = new Dictionary&amp;lt;string, object&amp;gt;
      {
        ["Pk"] = new Dictionary&amp;lt;string, object&amp;gt;
        {
          ["S"] = new[]{"prefix","MyTableName" }
        }
      }
    }
  })
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;All good, only one thing... handle failed records.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fails and retries
&lt;/h2&gt;

&lt;p&gt;There's no easy way to handle failed records. Because we can have a lot of reasons for failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bugs in lambda code &lt;/li&gt;
&lt;li&gt;Unavailable services &lt;/li&gt;
&lt;li&gt;Db structure changed
We may retry and in case of failure send failed record to SQS.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var deadLetterQueue = new Queue(this, "deadLetterQueue");
dynamoDbLambda.AddEventSource(
  new DynamoEventSource(dynamoDbTable, new DynamoEventSourceProps
  {
    OnFailure = new SqsDlq(deadLetterQueue),
    RetryAttempts = 5,
  }));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So far so good, but how to work with partial batch failure?&lt;/p&gt;
&lt;h2&gt;
  
  
  Batch processing
&lt;/h2&gt;

&lt;p&gt;Instead of returning &lt;code&gt;void&lt;/code&gt; from the lambda handler, we can report failed items and lambda will handle partial failure. Add this option to CDK &lt;code&gt;ReportBatchItemFailures = true&lt;/code&gt; and modify the lambda itself.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public async Task&amp;lt;StreamsEventResponse&amp;gt; Handle(DynamoDBEvent events)
{
  var tasks = events.Records
   .Select(new Executor().Execute)
   .ToList();

  var failedItems = new List&amp;lt;BatchItemFailure&amp;gt;();

  try
  {
    await Task.WhenAll(tasks);
  }
  catch
  {
    failedItems = tasks
      .Select((task, index) =&amp;gt; new { task, index })
      .Where(x =&amp;gt; !x.task.IsCompletedSuccessfully)
      .Select(x =&amp;gt; new BatchItemFailure 
      {
        ItemIdentifier = events.Records[x.index].EventID 
      })
      .ToList();
  }

  return new StreamsEventResponse {BatchItemFailures = failedItems};
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And finally, the source code in the GitHub repository &lt;code&gt;add-dynamo-db-lambda&lt;/code&gt; branch&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ohalay" rel="noopener noreferrer"&gt;
        ohalay
      &lt;/a&gt; / &lt;a href="https://github.com/ohalay/serverless-containers" rel="noopener noreferrer"&gt;
        serverless-containers
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a href="https://github.com/ohalay/serverless-containers/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/ohalay/serverless-containers/actions/workflows/ci.yml/badge.svg" alt="Build&amp;amp;Test"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/45b9fd726d12bf29a2055df343a417d98d8dbfbf51d0dc3ca624aa0d4a63a309/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f7265706f2d73697a652f6f68616c61792f7365727665726c6573732d636f6e7461696e657273"&gt;&lt;img src="https://camo.githubusercontent.com/45b9fd726d12bf29a2055df343a417d98d8dbfbf51d0dc3ca624aa0d4a63a309/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f7265706f2d73697a652f6f68616c61792f7365727665726c6573732d636f6e7461696e657273" alt="GitHub repo size"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/486305afbffaaf4bc5cf06cc785be2f91413124f24efaf161e6831f929c9fe28/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732f6f68616c61792f7365727665726c6573732d636f6e7461696e657273"&gt;&lt;img src="https://camo.githubusercontent.com/486305afbffaaf4bc5cf06cc785be2f91413124f24efaf161e6831f929c9fe28/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732f6f68616c61792f7365727665726c6573732d636f6e7461696e657273" alt="GitHub contributors"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/034370bf0ac3a5297cb4f815447f0585b99957e47f83d09ed50bfea2731f9e93/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6f68616c61792f7365727665726c6573732d636f6e7461696e6572733f7374796c653d736f6369616c"&gt;&lt;img src="https://camo.githubusercontent.com/034370bf0ac3a5297cb4f815447f0585b99957e47f83d09ed50bfea2731f9e93/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6f68616c61792f7365727665726c6573732d636f6e7461696e6572733f7374796c653d736f6369616c" alt="GitHub stars"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/3bc013def37d0c345ee2219dc12dbf9da1a4c6e79c11ecdc323f45039c7fc7e5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6f68616c61792f7365727665726c6573732d636f6e7461696e6572733f7374796c653d736f6369616c"&gt;&lt;img src="https://camo.githubusercontent.com/3bc013def37d0c345ee2219dc12dbf9da1a4c6e79c11ecdc323f45039c7fc7e5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f666f726b732f6f68616c61792f7365727665726c6573732d636f6e7461696e6572733f7374796c653d736f6369616c" alt="GitHub forks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Serverless integration tests&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;A public feed with available products that updates every day&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Business problem&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Integration tests for serverless solution&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Implementation&lt;/h2&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;S3 public buckets with available documents&lt;/li&gt;
&lt;li&gt;Lambda updates document&lt;/li&gt;
&lt;/ol&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ohalay/serverless-containers" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>serverless</category>
      <category>dotnet</category>
      <category>dynamodb</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
