<?xml version='1.0' encoding='UTF-8'?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:blogger="http://schemas.google.com/blogger/2008" xmlns:georss="http://www.georss.org/georss" xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr="http://purl.org/syndication/thread/1.0" version="2.0"><channel><atom:id>tag:blogger.com,1999:blog-7365737872932202828</atom:id><lastBuildDate>Sat, 25 Oct 2025 18:28:55 +0000</lastBuildDate><category>asp.net core</category><category>asp.net mvc</category><category>jqgrid</category><category>azure functions</category><category>jquery</category><category>http</category><category>asp.net core mvc</category><category>azure</category><category>httpclient</category><category>performance</category><category>ajax</category><category>asp.net</category><category>ndjson</category><category>push notifications</category><category>blazor</category><category>extensibility</category><category>microfrontends</category><category>web push</category><category>webassembly</category><category>webhooks</category><category>websocket</category><category>.net</category><category>devops</category><category>http2</category><category>iac</category><category>infrastructure as code</category><category>jquery ui</category><category>server-sent events</category><category>aes128gcm</category><category>antiforgerytoken</category><category>async-streaming</category><category>book</category><category>entity-framework</category><category>grid</category><category>helper</category><category>jquery validation</category><category>json</category><category>response compression</category><category>review</category><category>security</category><category>server push</category><category>server timing</category><category>sse</category><category>web components</category><category>aks</category><category>async-streams</category><category>asynchronous</category><category>azure cosmos db</category><category>changefeed</category><category>compression</category><category>cors</category><category>cosmos db</category><category>cosmosdb</category><category>crud</category><category>csp</category><category>encryption</category><category>eventsource</category><category>file upload</category><category>graphql</category><category>httpclientfactory</category><category>isolated worker</category><category>kubernetes</category><category>redis</category><category>transformation</category><category>treeview</category><category>wasi</category><category>wasm</category><category>web api</category><category>web push protocol</category><category>xml</category><category>xslt</category><category>yarp</category><category>.net 5</category><category>WebPI</category><category>ajaxcontroltoolkit</category><category>akamai</category><category>algorithms</category><category>api-documentation</category><category>authentication</category><category>authorization</category><category>azure container apps</category><category>azure databricks</category><category>azure storage</category><category>background processing</category><category>brotli</category><category>c#</category><category>change feed</category><category>clear site data</category><category>client hints</category><category>client ip</category><category>client-side validation</category><category>cloudflare</category><category>concurrent requests</category><category>conditional requests</category><category>configuration</category><category>container groups</category><category>container instances</category><category>containers</category><category>content security policy</category><category>continuous delivery</category><category>continuous deployment</category><category>continuous integration</category><category>continuous monitoring</category><category>continuous operations</category><category>continuous testing</category><category>cross-origin resource sharing</category><category>cryptography</category><category>dapr</category><category>diagnostics</category><category>docfx</category><category>documentation-generator</category><category>dotnet</category><category>durable functions</category><category>error handling</category><category>expect-ct</category><category>export</category><category>fetch api</category><category>file icons</category><category>forwarded headers</category><category>github</category><category>head</category><category>healthchecks</category><category>hijacking</category><category>hsts</category><category>http headers</category><category>httphandler</category><category>https</category><category>iis</category><category>import</category><category>integration testing</category><category>isolated</category><category>javascriptmvc</category><category>jeditable</category><category>jquery ui progressbar</category><category>jqueryui dialog</category><category>json-rpc</category><category>lazy-loading</category><category>lib.web.mvc</category><category>middleware</category><category>mongodb</category><category>monitoring</category><category>neon</category><category>network error logging</category><category>ops</category><category>pem</category><category>post tunneling</category><category>proxy</category><category>pub-sub</category><category>push api</category><category>range requests</category><category>rate limiting</category><category>reporting api</category><category>reporting-api</category><category>requests limits</category><category>response buffering</category><category>response caching</category><category>rethinkdb</category><category>rpc</category><category>rsocket</category><category>runtime scaling</category><category>save-data</category><category>server-to-client</category><category>sidecar</category><category>signalr</category><category>slight</category><category>ssl acceleration</category><category>ssl offloading</category><category>static files</category><category>streams api</category><category>subgrid</category><category>subprotocol negotiation</category><category>sys.services.authenticationservice</category><category>target-based scaling</category><category>testcontainers</category><category>tinymce</category><category>toolkitscriptmanager</category><category>trailers</category><category>treegrid</category><category>xss</category><title>Yet another developer blog</title><description></description><link>http://www.tpeczek.com/</link><managingEditor>noreply@blogger.com (Tomasz Pęczek)</managingEditor><generator>Blogger</generator><openSearch:totalResults>142</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-1304634620752734117</guid><pubDate>Mon, 10 Feb 2025 21:26:00 +0000</pubDate><atom:updated>2025-02-11T15:01:41.343+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure</category><category domain="http://www.blogger.com/atom/ns#">dotnet</category><category domain="http://www.blogger.com/atom/ns#">neon</category><title>Exploring Neon as a Serverless Postgres Alternative for .NET Applications on Azure - Part 1 (Simple ASP.NET Core on App Service)</title><description>&lt;p&gt;&lt;a href=&quot; https://neon.tech/?refcode=44WD03UH&quot;&gt;Neon&lt;/a&gt;, a serverless Postgres platform, was recently brought to my attention. It comes with  the promise of some  interesting features like scale-to-zero, on-demand autoscaling, point-in-time restore and time travel with up to 30 days retention, instant read replicas, and probably the most unique, branching (which allows you to quickly branch your schema and data to create isolated development, test, or other purpose environments).&lt;/p&gt;
&lt;p&gt;That&#39;s a lot of stuff to play with, but before I jumped in, I wanted to see how Neon integrated with my tech stack of choice - .NET, Azure, and GitHub. I decided to start with something simple - an ASP.NET Core application hosted on Azure App Service. Since I like to build things from the ground up, my first step was infrastructure.&lt;/p&gt;
&lt;h2 id=&quot;deploying-the-infrastructure&quot;&gt;Deploying the Infrastructure&lt;/h2&gt;
&lt;p&gt;Neon has three deployment options that might be of interest to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a Neon project hosted in the AWS region&lt;/li&gt;
&lt;li&gt;Create a Neon project hosted in the Azure region&lt;/li&gt;
&lt;li&gt;Deploy a Neon organization as an &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/partner-solutions/neon?WT.mc_id=DT-MVP-5002979&quot;&gt;Azure Native ISV Service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first two options are fully managed by Neon, you grab the connection details from the Neon console and start hacking. But I wanted to explore the third option because it provides the tightest integration from an Azure perspective (including SSO and unified billing), and that&#39;s what the organizations I work with are usually looking for.&lt;/p&gt;
&lt;p&gt;As a strong IaC advocate, I also didn&#39;t want to deploy Neon through the Azure Portal or CLI, I wanted to use Bicep. Thanks to the native Azure integration, the Neon organization comes with its own resource type - &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/templates/neon.postgres/organizations?WT.mc_id=DT-MVP-5002979&quot;&gt;&lt;code&gt;Neon.Postgres/organizations&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource neonOrganization &#39;Neon.Postgres/organizations@2024-08-01-preview&#39; = {
  name: &#39;neon-net-applications-on-azure&#39;
  location: location
  properties: {
    companyDetails: { }
    marketplaceDetails: {
      subscriptionId: subscription().id
      offerDetails: {
        publisherId: &#39;neon1722366567200&#39;
        offerId: &#39;neon_serverless_postgres_azure_prod&#39;
        planId: &#39;neon_serverless_postgres_azure_prod_free&#39;
        termUnit: &#39;P1M&#39;
        termId: &#39;gmz7xq9ge3py&#39;
      }
    }
    partnerOrganizationProperties: {
      organizationName: &#39;net-applications-on-azure&#39;
    }
    userDetails: {
      upn: &#39;userId@domainName&#39;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You may ask, how did I get the values for the properties? Well, they are not documented (yet, I&#39;m assuming) and I had to resort to reverse engineering (inspecting the template of the manually deployed resource).&lt;/p&gt;
&lt;p&gt;The next step is to create a project. Everything in Neon (branches, databases, etc.) lives inside a project. Neon projects don&#39;t have an Azure resource representation, so I had to change the tool. I could create a project through the UI, but since I still wanted to have repeatability (something I could later reuse in a GitHub Actions workflow later), I decided to use Neon CLI. I still had to visit the UI (thanks to SSO I could just click on a link available on the resource &lt;em&gt;Overview&lt;/em&gt; blade) to get myself an &lt;a href=&quot;https://neon.tech/docs/manage/api-keys&quot;&gt;API key&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;export NEON_API_KEY=&amp;lt;neon_api_key&amp;gt;

neon projects create \
    --name simple-asp-net-core-on-app-service \
    --region-id azure-westus3 \
    --output json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output includes connection details for the created database. I don&#39;t want to manage secrets manually if I don&#39;t have to, so I decided to quickly create a key vault and put them there.&lt;/p&gt;
&lt;p&gt;The last missing elements of my intended infrastructure were a managed identity, a container registry, an app service plan, and an app service. Below is the final diagram.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0OssiHC2iwP7LDlAbIyfXMVgEDtI8tNU7naUum8-iif0pSnEpCQpObmaLRhGDyBo6mbo-PSm_cVY3B1tn_h-OgLWYAe4fqhr_RtRJrB6WNji1_vzY7cSAHfHXx6BAyidDe5WLbWJrQzLV2F2yzdNtd2dOYzrsYfHGczD4OSdS6xblKegjeHz8iFDZvEg/s1600/SIMPLE~1.PNG&quot; alt=&quot;Infrastructure for simple ASP.NET Core application on Azure App Service (managed identity, key vault, container registry, app service plan, and app service) and using Neon deployed as Azure Native ISV Service&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;seeding-the-database&quot;&gt;Seeding the Database&lt;/h2&gt;
&lt;p&gt;With all the infrastructure in place, I could start building my ASP.NET Core application. I wanted something simple, something that just displayed data, as my goal here was to see if Neon could be a drop-in replacement for Posgres from a code perspective. But to display data, you have to have data. So I needed to seed the database. I decided to take the simplest approach I could think of - using the built-in seeding capabilities of the Entity Framework Core. I had two options to choose from: &lt;a href=&quot;https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding?WT.mc_id=DT-MVP-5002979#use-seeding-method&quot;&gt;&lt;code&gt;UseSeeding&lt;/code&gt;/&lt;code&gt;UseAsyncSeeding&lt;/code&gt; method&lt;/a&gt; or &lt;a href=&quot;https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding?WT.mc_id=DT-MVP-5002979#model-managed-data&quot;&gt;model managed data&lt;/a&gt;. I chose the latter and I quickly created a database context with two sets and a bunch of entities to add.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class StarWarsDbContext : DbContext
{
    public DbSet&amp;lt;Character&amp;gt; Characters { get; private set; }

    public DbSet&amp;lt;Planet&amp;gt; Planets { get; private set; }

    public StarWarsDbContext(DbContextOptions&amp;lt;StarWarsDbContext&amp;gt; options)
    : base(options)
    { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&amp;lt;Planet&amp;gt;(builder =&amp;gt;
        {
            builder.Property(x =&amp;gt; x.Name).IsRequired();
            builder.HasData(
                new Planet { Id = 1, Name = &quot;Tatooine&quot;, ... },
                ...
            );
        });

        modelBuilder.Entity&amp;lt;Character&amp;gt;(builder =&amp;gt;
        {
            builder.Property(x =&amp;gt; x.Name).IsRequired();
            builder.HasData(
                new Character { Id = 1, Name = &quot;Luke Skywalker&quot;, ... },
                ...
            );
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I needed to register this database context as a service in my application. For the provider I used &lt;code&gt;Npgsql.EntityFrameworkCore.PostgreSQL&lt;/code&gt;in the spirit of the drop-in replacement approach. With the connection string in the key vault and a managed identity in place to authenticate against that key vault, I could use &lt;code&gt;DefaultAzureCredential&lt;/code&gt; and &lt;code&gt;SecretClient&lt;/code&gt; to configure the provider and restrict my application settings to the key vault URI (yes, I choose this option even in demos).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext&amp;lt;StarWarsDbContext&amp;gt;(options =&amp;gt;
{
    var keyVaultSecrettClient = new SecretClient(
        new Uri(builder.Configuration[&quot;KEY_VAULT_URI&quot;]),
        new DefaultAzureCredential()
    );
    options.UseNpgsql(keyVaultSecrettClient.GetSecret(&quot;neon-connection-string&quot;).Value.Value);
});

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last thing to do here was to trigger the model creation code. This only needs to happen once, and as this is a demo, it can be ugly. I used an old trick of getting the database context as part of the startup and calling &lt;code&gt;EnsureCreated&lt;/code&gt; (please don&#39;t do this in serious applications).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

var app = builder.Build();

using (var serviceScope = app.Services.GetRequiredService&amp;lt;IServiceScopeFactory&amp;gt;().CreateScope())
{
    var context = serviceScope.ServiceProvider.GetRequiredService&amp;lt;StarWarsDbContext&amp;gt;();
    context.Database.EnsureCreated();
}

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All the pieces are now in place. It&#39;s time to wrap things up.&lt;/p&gt;
&lt;h2 id=&quot;completing-and-deploying-the-application&quot;&gt;Completing and Deploying the Application&lt;/h2&gt;
&lt;p&gt;For the application to be complete, it needed some UI. Since I didn&#39;t have an idea for anything special, I did something I used to do a lot in the past - a jqGrid based table that was quickly set up with the help of my old project &lt;a href=&quot;https://github.com/tpeczek/Lib.AspNetCore.Mvc.JqGrid&quot;&gt;Lib.AspNetCore.Mvc.JqGrid&lt;/a&gt; (I&#39;ve basically copied some stuff from its &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.JqGrid&quot;&gt;demo&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;As you&#39;ve probably guessed from the infrastructure, my intention from the start was to deploy this application as a container. So my last step was to add a docker file, build, push, and voilà. I was able to navigate to the application, browse and sort the data.&lt;/p&gt;
&lt;p&gt;Everything worked as expected and I consider my first experiment with Neon a success 😊.&lt;/p&gt;
&lt;h2 id=&quot;thoughts&quot;&gt;Thoughts&lt;/h2&gt;
&lt;p&gt;They say to never publish part one if you don&#39;t have part two ready. I don&#39;t have part two ready, but I really think I&#39;ll write one, I just don&#39;t know when 😉. That&#39;s because Neon really got me interested.&lt;/p&gt;
&lt;p&gt;In this post I wanted to check out the basics and set the stage for digging deeper. While writing it, I&#39;ve also created a &lt;a href=&quot;https://github.com/tpeczek/demo-neon-for-net-applications-on-azure&quot;&gt;repository&lt;/a&gt; where you can find ready-to-deploy infrastructure and application. I&#39;ve created GitHub Actions workflows for deploying a Neon organization, creating a Neon project, and deploying the solution itself. All you need to do to play with it is clone and provide credentials 🙂.&lt;/p&gt;</description><link>http://www.tpeczek.com/2025/02/exploring-neon-as-serverless-postgres.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0OssiHC2iwP7LDlAbIyfXMVgEDtI8tNU7naUum8-iif0pSnEpCQpObmaLRhGDyBo6mbo-PSm_cVY3B1tn_h-OgLWYAe4fqhr_RtRJrB6WNji1_vzY7cSAHfHXx6BAyidDe5WLbWJrQzLV2F2yzdNtd2dOYzrsYfHGczD4OSdS6xblKegjeHz8iFDZvEg/s72-c/SIMPLE~1.PNG" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-2520358221106669221</guid><pubDate>Tue, 26 Nov 2024 12:20:00 +0000</pubDate><atom:updated>2024-11-26T13:20:57.999+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure functions</category><category domain="http://www.blogger.com/atom/ns#">isolated worker</category><category domain="http://www.blogger.com/atom/ns#">monitoring</category><title>Monitoring C# Azure Functions in the Isolated Worker Model - Infrastructure &amp; Configuration Deep Dive</title><description>&lt;p&gt;Not so long ago my colleague reached out with a question &quot;Are there any known issues with &lt;code&gt;ITelemetryInitializer&lt;/code&gt; in Azure Functions?&quot;. This question started a discussion about the monitoring configuration for C# Azure Functions in the isolated worker model. At some point in that discussion I stated &quot;Alright, you&#39;ve motivated me, I&#39;ll make a sample&quot;. When I sat down to do that, I started wondering which scenario should I cover and my conclusion was that there are several things I should go through... So here I am.&lt;/p&gt;
&lt;h2 id=&quot;setting-the-scene&quot;&gt;Setting the Scene&lt;/h2&gt;
&lt;p&gt;Before I start describing how we can monitor an isolated worker model C# Azure Function, allow me to introduce you to the one we are going to use for this purpose. I want to start with as basic setup as possible. This is why the initial &lt;code&gt;Program.cs&lt;/code&gt; will contain only four lines.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .Build();

host.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;host.json&lt;/code&gt; will also be minimal.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-json&quot;&gt;{
    &quot;version&quot;: &quot;2.0&quot;,
    &quot;logging&quot;: {
        &quot;logLevel&quot;: {
            &quot;default&quot;: &quot;Information&quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the function itself, I&#39;ve decided to go for the Fibonacci sequence implementation as it can easily generate a ton of logs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class FibonacciSequence
{
    private readonly ILogger&amp;lt;FibonacciSequence&amp;gt; _logger;

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

    [Function(nameof(FibonacciSequence))]
    public async Task&amp;lt;HttpResponseData&amp;gt; Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, &quot;get&quot;, Route = &quot;fib/{index:int}&quot;)]
        HttpRequestData request,
        int index)
    {
        _logger.LogInformation(
            $&quot;{nameof(FibonacciSequence)} function triggered for {{index}}.&quot;,
            index
        );

        var response = request.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add(&quot;Content-Type&quot;, &quot;text/plain; charset=utf-8&quot;);

        await response.WriteStringAsync(FibonacciSequenceRecursive(index).ToString());

        return response;
    }

    private int FibonacciSequenceRecursive(int index)
    {
        int fibonacciNumber = 0;

        _logger.LogInformation(&quot;Calculating Fibonacci sequence for {index}.&quot;, index);

        if (index &amp;lt;= 0)
        {
            fibonacciNumber = 0;
        }
        else if (index &amp;lt;= 1)
        {
            fibonacciNumber = 1;
        }
        else
        {
            fibonacciNumber = 
              FibonacciSequenceRecursive(index - 1)
              + FibonacciSequenceRecursive(index - 2);
        }

        _logger.LogInformation(
            &quot;Calculated Fibonacci sequence for {index}: {fibonacciNumber}.&quot;,
            index,
            fibonacciNumber
        );

        return fibonacciNumber;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a recursive implementation which in our case has the added benefit of being crashable on demand 😉.&lt;/p&gt;
&lt;p&gt;Now we can start capturing the signals this function will produce after deployment.&lt;/p&gt;
&lt;h2 id=&quot;simple-and-limited-option-for-specific-scenarios-file-system-logging&quot;&gt;Simple and Limited Option for Specific Scenarios - File System Logging&lt;/h2&gt;
&lt;p&gt;What we can capture in a minimal deployment scenario is logs coming from our function. What do I understand by a minimal deployment scenario? The bare minimum that Azure Function requires is a storage account, app service plan, and function app. The function can push all its logs into that storage account. Is this something I would recommend for a production scenario? Certainly not. It&#39;s only logs (and in production you will want metrics) and works reasonably only for Azure Functions deployed on Windows (for production I would suggest Linux due to better cold start performance or lower pricing for dedicated plan). But it may be the right option for some development scenarios (when you want to run some work, download the logs and analyze them locally). So, how to achieve this? Let&#39;s start with some Bicep snippets for the required infrastructure. First, we need to deploy a storage account.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource storageAccount &#39;Microsoft.Storage/storageAccounts@2023-05-01&#39; = {
  name: &#39;stwebjobs${uniqueString(resourceGroup().id)}&#39;
  location: resourceGroup().location
  sku: {
    name: &#39;Standard_LRS&#39;
  }
  kind: &#39;Storage&#39;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need an app service plan. It must be a Windows one (the below snippet creates a &lt;em&gt;Windows Consumption Plan&lt;/em&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource appServicePlan &#39;Microsoft.Web/serverfarms@2024-04-01&#39; = {
  name: &#39;plan-monitored-function&#39;
  location: resourceGroup().location
  sku: {
    name: &#39;Y1&#39;
    tier: &#39;Dynamic&#39;
  }
  properties: {
    computeMode: &#39;Dynamic&#39;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the actual service doing the work, the function app.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource functionApp &#39;Microsoft.Web/sites@2024-04-01&#39; = {
  name: &#39;func-monitored-function&#39;
  location: resourceGroup().location
  kind: &#39;functionapp&#39;
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      netFrameworkVersion: &#39;v8.0&#39;
      appSettings: [
        {
          name: &#39;AzureWebJobsStorage&#39;
          value: &#39;DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}&#39;
        }
        {
          name: &#39;WEBSITE_CONTENTAZUREFILECONNECTIONSTRING&#39;
          value: &#39;DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}&#39;
        }
        {
          name: &#39;WEBSITE_CONTENTSHARE&#39;
          value: &#39;func-monitored-function&#39;
        }
        {
          name: &#39;FUNCTIONS_EXTENSION_VERSION&#39;
          value: &#39;~4&#39;
        }
        {
          name: &#39;FUNCTIONS_WORKER_RUNTIME&#39;
          value: &#39;dotnet-isolated&#39;
        }
      ]
    }
    httpsOnly: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A couple of words about this function app. The &lt;code&gt;kind: &#39;functionapp&#39;&lt;/code&gt; indicates that this is a Windows function app that creates the requirement of setting &lt;code&gt;netFrameworkVersion&lt;/code&gt; to desired .NET version. To make this an isolated worker model function, the &lt;code&gt;FUNCTIONS_EXTENSION_VERSION&lt;/code&gt; is set to &lt;code&gt;~4&lt;/code&gt; and &lt;code&gt;FUNCTIONS_WORKER_RUNTIME&lt;/code&gt; to &lt;code&gt;dotnet-isolated&lt;/code&gt;. When it comes to all the settings referencing the storage account, your attention should go to &lt;code&gt;WEBSITE_CONTENTAZUREFILECONNECTIONSTRING&lt;/code&gt; and &lt;code&gt;WEBSITE_CONTENTSHARE&lt;/code&gt; - those indicate the file share that will be created for this function app in the storage account (this is where the logs will go).&lt;/p&gt;
&lt;p&gt;After deployment, the resulting infrastructure should be as below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_iJvUHWJAAvLFHhOOkGzTO92CB2o3UF5qKHcXLy3BqaVgldC2EkjvgiLZAwhL-XGDx1Zgol4BZz6YH5cay4s3GyXbZteYGthJvN0oP0_IrX_HiIY5XYHNhNc3iEt6c-Q4fv5HeWae9c4mHlXdK3mfjWfxKJVq3FLPbsXmA2fprs9SpFqb94B2GR9yu_8/s1600/azure-functions-with-file-system-logging.png&quot; alt=&quot;Infrastructure for Azure Functions with file system logging (storage account, app service plan, and function app)&quot;&gt;&lt;/p&gt;
&lt;p&gt;What remains is configuring the file system logging in the &lt;code&gt;host.json&lt;/code&gt; file of our function. The default behavior is to generate log files only when the function is being debugged using the Azure portal. We want them to be generated always.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-json&quot;&gt;{
    &quot;version&quot;: &quot;2.0&quot;,
    &quot;logging&quot;: {
        &quot;fileLoggingMode&quot;: &quot;always&quot;,
        &quot;logLevel&quot;: {
            &quot;default&quot;: &quot;Information&quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you deploy the code and execute some requests, you will be able to find the log files in the defined file share (the path is &lt;code&gt;/LogFiles/Application/Functions/Host/&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-txt&quot;&gt;2024-11-17T15:33:39.339 [Information] Executing &#39;Functions.FibonacciSequence&#39; ...
2024-11-17T15:33:39.522 [Information] FibonacciSequence function triggered for 6.
2024-11-17T15:33:39.526 [Information] Calculating Fibonacci sequence for 6.
2024-11-17T15:33:39.526 [Information] Calculating Fibonacci sequence for 5.
...
2024-11-17T15:33:39.527 [Information] Calculated Fibonacci sequence for 5: 5.
...
2024-11-17T15:33:39.528 [Information] Calculated Fibonacci sequence for 4: 3.
2024-11-17T15:33:39.528 [Information] Calculated Fibonacci sequence for 6: 8.
2024-11-17T15:33:39.566 [Information] Executed &#39;Functions.FibonacciSequence&#39; ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;for-production-scenarios-entra-id-protected-application-insights&quot;&gt;For Production Scenarios - Entra ID Protected Application Insights&lt;/h2&gt;
&lt;p&gt;As I said, I wouldn&#39;t use file system logging for production. Very often it&#39;s not even enough for development. This is why Application Insights are considered a default these days. But before we deploy one, we can use the fact that we no longer new Windows and change the deployment to be a Linux based one. First I&#39;m going to change the app service plan to a &lt;em&gt;Linux Consumption Plan&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource appServicePlan &#39;Microsoft.Web/serverfarms@2024-04-01&#39; = {
  ...
  kind: &#39;linux&#39;
  properties: {
    reserved: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With a different app service plan, the function app can be changed to a Linux one, which means changing the kind to &lt;code&gt;functionapp,linux&lt;/code&gt; and replacing &lt;code&gt;netFrameworkVersion&lt;/code&gt; with the corresponding &lt;code&gt;linuxFxVersion&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource functionApp &#39;Microsoft.Web/sites@2024-04-01&#39; = {
  ...
  kind: &#39;functionapp,linux&#39;
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      linuxFxVersion: &#39;DOTNET-ISOLATED|8.0&#39;
      ...
    }
    httpsOnly: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is also a good chance that you no longer need the file share (although the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-functions/storage-considerations?WT.mc_id=DT-MVP-5002979&quot;&gt;documentation&lt;/a&gt; itself is inconsistent whether it&#39;s needed when running Linux functions on the Elastic Premium plan) so the &lt;code&gt;WEBSITE_CONTENTAZUREFILECONNECTIONSTRING&lt;/code&gt; and &lt;code&gt;WEBSITE_CONTENTSHARE&lt;/code&gt; can simply be removed. No requirement for the file share creates one more improvement opportunity - we can drop credentials for the blob storage (&lt;code&gt;AzureWebJobsStorage&lt;/code&gt;) as this connection can use managed identity. I&#39;m going to use a user-assigned managed identity because I often prefer them and they usually cause more trouble to set up 😉. To do so, we need to create one and grant it the &lt;em&gt;Storage Blob Data Owner&lt;/em&gt; role for the storage account.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource managedIdentity &#39;Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31&#39; = {
  name: &#39;id-monitored-function&#39;
  location: resourceGroup().location
}

resource storageBlobDataOwnerRoleDefinition &#39;Microsoft.Authorization/roleDefinitions@2022-04-01&#39; existing = {
  name: &#39;b7e6dc6d-f1e8-4753-8033-0f276bb0955b&#39; // Storage Blob Data Owner
  scope: subscription()
}

resource storageBlobDataOwnerRoleAssignment &#39;Microsoft.Authorization/roleAssignments@2022-04-01&#39; = {
  scope: storageAccount
  name: guid(storageAccount.id, managedIdentity.id, storageBlobDataOwnerRoleDefinition.id)
  properties: {
    roleDefinitionId: storageBlobDataOwnerRoleDefinition.id
    principalId: managedIdentity.properties.principalId
    principalType: &#39;ServicePrincipal&#39;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The change to the function app is only about assigning the managed identity and replacing &lt;code&gt;AzureWebJobsStorage&lt;/code&gt; with &lt;code&gt;AzureWebJobsStorage__accountName&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource functionApp &#39;Microsoft.Web/sites@2024-04-01&#39; = {
  ...

  ...
  properties: {
    ...
    siteConfig: {
      ...
      appSettings: [
        {
          name: &#39;AzureWebJobsStorage__accountName&#39;
          value: storageAccount.name
        }
        ...
      ]
    }
    httpsOnly: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Enough detouring (although it wasn&#39;t without a purpose 😜) - it&#39;s time to deploy Application Insights. As the classic Application Insights are retired, we are going to create a workspace-based instance.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource logAnalyticsWorkspace &#39;Microsoft.OperationalInsights/workspaces@2023-09-01&#39; = {
  name: &#39;log-monitored-function&#39;
  location: resourceGroup().location
  properties: {
    sku: { 
      name: &#39;PerGB2018&#39; 
    }
  }
}

resource applicationInsights &#39;Microsoft.Insights/components@2020-02-02&#39; = {
  name: &#39;appi-monitored-function&#39;
  location: resourceGroup().location
  kind: &#39;web&#39;
  properties: {
    Application_Type: &#39;web&#39;
    WorkspaceResourceId: logAnalyticsWorkspace.id
    DisableLocalAuth: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You might have noticed that I&#39;ve set &lt;code&gt;DisableLocalAuth&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;. This is a security improvement. It enforces authentication by &lt;em&gt;Entra ID&lt;/em&gt; for ingestion and as a result, makes &lt;code&gt;InstrumentationKey&lt;/code&gt; a resource identifier instead of a secret. This is nice and we can easily handle it because we already have managed identity in place (I told you it had a purpose 😊). All we need to do is grant the &lt;em&gt;Monitoring Metrics Publisher&lt;/em&gt; role to our managed identity.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource monitoringMetricsPublisherRoleDefinition &#39;Microsoft.Authorization/roleDefinitions@2022-04-01&#39; existing = {
  name: &#39;3913510d-42f4-4e42-8a64-420c390055eb&#39; // Monitoring Metrics Publisher
  scope: subscription()
}

resource monitoringMetricsPublisherRoleAssignment &#39;Microsoft.Authorization/roleAssignments@2022-04-01&#39; = {
  scope: applicationInsights
  name: guid(applicationInsights.id, managedIdentity.id, monitoringMetricsPublisherRoleDefinition.id)
  properties: {
    roleDefinitionId: monitoringMetricsPublisherRoleDefinition.id
    principalId: managedIdentity.properties.principalId
    principalType: &#39;ServicePrincipal&#39;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding two application settings definitions to our function app will tie the services together.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource functionApp &#39;Microsoft.Web/sites@2024-04-01&#39; = {
  ...
  properties: {
    ...
    siteConfig: {
      ...
      appSettings: [
        ...
        {
          name: &#39;APPLICATIONINSIGHTS_CONNECTION_STRING&#39;
          value: applicationInsights.properties.ConnectionString
        }
        {
          name: &#39;APPLICATIONINSIGHTS_AUTHENTICATION_STRING&#39;
          value: &#39;ClientId=${managedIdentity.properties.clientId};Authorization=AAD&#39;
        }
        ...
      ]
    }
    httpsOnly: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Are we done with the infrastructure? Not really. There is one more useful thing that is often forgotten - enabling storage logs. There are some important function app data in there so it would be nice to monitor it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource storageAccountBlobService &#39;Microsoft.Storage/storageAccounts/blobServices@2023-05-01&#39; existing = {
  name: &#39;default&#39;
  parent: storageAccount
}

resource storageAccountDiagnosticSettings &#39;Microsoft.Insights/diagnosticSettings@2021-05-01-preview&#39; = {
  name: &#39;${storageAccount.name}-diagnostic&#39;
  scope: storageAccountBlobService
  properties: {
    workspaceId: logAnalyticsWorkspace.id
    logs: [
      {
        category: &#39;StorageWrite&#39;
        enabled: true
      }
    ]
    metrics: [
      {
        category: &#39;Transaction&#39;
        enabled: true
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we are done and our infrastructure should look like below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgUJnh-IuO3BTDOM3EdRbr7P1C75n6JO5J8l3VZO9RnmrrVZlq3BzZUebxAqqQzpLWo-1xsNkoGknE_uY0S2zqD3SCtFRoib4yvj4EjrNFG_mnf1tgMz6NLeeysf8_0B5fW6DZNX-OIFW8vHsnC9f89BtvvGO1fh1sdNUcCy9EysgNoduTXzUWGVh2hJqg/s1600/azure-functions-with-application-insights.png&quot; alt=&quot;Infrastructure for Azure Functions with Application Insights (managed identity, application insights, log workspace, storage account, app service plan, and function app)&quot;&gt;&lt;/p&gt;
&lt;p&gt;Once we deploy our function and make some requests, thanks to the codeless monitoring by the host and relaying worker logs through the host, we can find the traces in Application Insights.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmRaqy2hBcCz2kyPjNXV4sQPV2IrscHKmKljpb0F6iPlLrIc45KCD4e4fcPtwbtWHkZ-MdOffH2i_UvsGh1K1Hb578DX9G7i408NUphAtfKp8RIBhhBl7aS9nL7L-dczdbOLZ9jJ_UZkHTtrwuJbIwDv3bI_PubEBAhNzaD2IBZp8amyT3jN-2qgxC7dM/s1600/relayed-worker-logs-in-application-insights.png&quot; alt=&quot;Relayed worker logs ingested through host codeless monitoring&quot;&gt;&lt;/p&gt;
&lt;p&gt;You may be asking what I mean by &lt;em&gt;&quot;relaying worker logs through the host&quot;&lt;/em&gt;? You probably remember that in the case of C# Azure Functions in the isolated worker model, we have two processes: functions host and isolated worker. Azure Functions wants to be helpful and by default, it sends the logs from the worker process to the functions host process, which then sends them to Application Insights.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSBk_xKsiHQHqiUD7gG9X6xUlgqQZBakg7-kfA_oEuVtQqOPiE0SQSxv1paT-8742lsxcUbFX3hDDx4sJ7ULkb_aOjlJXv62SrHsk1OxQrz8u510YSyW4LFOq_ueuK0-Qv0r_8ri5cThQMTYrtKtOJPP4AseVgBTwN_DpruSRMu6FkKM-0-Nie9KmLWT8/s1600/azure-functions-relaying-worker-logs-through-the-host.png&quot; alt=&quot;Azure Functions relaying worker logs through the host&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is nice, but may not be exactly what you want. You may want to split the logs so you can treat them separately (for example by configuring different default log levels for the host and the worker). To achieve that you must explicitly set up Application Insights integration in the worker code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services =&amp;gt; {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

host.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The telemetry from the worker can now be controlled in the code, while from the host through the &lt;code&gt;host.json&lt;/code&gt;. But if you deploy this, you will find no telemetry from the worker in Application Insights. You can still see your logs in the &lt;em&gt;Log stream&lt;/em&gt;. You will also see a lot of those:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-txt&quot;&gt;Azure.Identity: Service request failed.
Status: 400 (Bad Request)

Content:
{&quot;statusCode&quot;:400,&quot;message&quot;:&quot;Unable to load the proper Managed Identity.&quot;,&quot;correlationId&quot;:&quot;...&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s Application Insights SDK not being able to use the user-assigned managed identity. Azure Functions runtime uses the &lt;code&gt;APPLICATIONINSIGHTS_AUTHENTICATION_STRING&lt;/code&gt; setting to provide a user-assigned managed identity OAuth token to the Application Insights but none of that (at the time of writing this) happens when we set up the integration explicitly. That said, we can do it ourselves. The parser for the setting is available in the &lt;code&gt;Microsoft.Azure.WebJobs.Logging.ApplicationInsights&lt;/code&gt; so we can mimic the host implementation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;using TokenCredentialOptions =
    Microsoft.Azure.WebJobs.Logging.ApplicationInsights.TokenCredentialOptions;

var host = new HostBuilder()
    ...
    .ConfigureServices(services =&amp;gt; {
        services.Configure&amp;lt;TelemetryConfiguration&amp;gt;(config =&amp;gt;
        {
            string? authenticationString =
                Environment.GetEnvironmentVariable(&quot;APPLICATIONINSIGHTS_AUTHENTICATION_STRING&quot;);

            if (!String.IsNullOrEmpty(authenticationString))
            {
                var tokenCredentialOptions = TokenCredentialOptions.ParseAuthenticationString(
                    authenticationString
                );
                config.SetAzureTokenCredential(
                    new ManagedIdentityCredential(tokenCredentialOptions.ClientId)
                );
            }
        });
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

host.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The telemetry should now reach the Application Insights without a problem.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPJ35FDPtaLob7hxRZMQfK4XoyYM_UlLmMvP0CnghyhZD8JaIsXBr3VfOAHGuKfzet7lAlvf-zkYTwrFWFYkC1q59wm_joRbYU2YFaW3IiqtWSz6hnexFKl-TcuH381lbMTCXr9aNh6jJpDoYi57Ud8_bflDw97swo57Z9hmwLd0WIfGWWO4_jEvMv56g/s1600/azure-functions-emitting-worker-logs-directly.png&quot; alt=&quot;Azure Functions emitting worker logs directly&quot;&gt;&lt;/p&gt;
&lt;p&gt;Be cautious. As Application Insights SDK now controls emitting the logs you must be aware of its &lt;em&gt;opinions&lt;/em&gt;. It so happens that it tries to optimize by default and adds a logging filter that sets the minimum log level to &lt;em&gt;Warning&lt;/em&gt;. You may want to get rid of that (be certain to make it after &lt;code&gt;ConfigureFunctionsApplicationInsights&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

var host = new HostBuilder()
    ...
    .ConfigureServices(services =&amp;gt; {
        ...
        services.ConfigureFunctionsApplicationInsights();
        services.Configure&amp;lt;LoggerFilterOptions&amp;gt;(options =&amp;gt;
        {
            LoggerFilterRule? sdkRule = options.Rules.FirstOrDefault(rule =&amp;gt;
                rule.ProviderName == typeof(Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider).FullName
            );

            if (sdkRule is not null)
            {
                options.Rules.Remove(sdkRule);
            }
        });
    })
    .Build();

host.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;modifying-application-map-in-application-insights&quot;&gt;Modifying Application Map in Application Insights&lt;/h2&gt;
&lt;p&gt;The default application map is not impressive - it will simply show your function app by its resource name.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgf40-tJWNi4AOhWfscHb-svDOL1SHw_GMhcIqYFhCirGiFzVfsNBKwerK6hPtR8doLysl-f2gVD9y5mVxSTGYSEZkEL7IE4kDCYYubxit6Hbr83MdIvFIYnkOhQ-EQ4G8M1A4VVfQGemR3ARVwYhu8egab9FTfOuqRbmEvpxiK2UwbjEPHEEZrNdMCvhM/s1600/application-insights-default-application-map.png&quot; alt=&quot;Default application map in Application Insights&quot;&gt;&lt;/p&gt;
&lt;p&gt;As function apps rarely exist in isolation, we often want to present them in a more meaningful way on the map by providing a cloud role. The simplest way to do this is through the &lt;code&gt;WEBSITE_CLOUD_ROLENAME&lt;/code&gt; setting.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource functionApp &#39;Microsoft.Web/sites@2024-04-01&#39; = {
  ...
  properties: {
    ...
    siteConfig: {
      ...
      appSettings: [
        ...
        {
          name: &#39;WEBSITE_CLOUD_ROLENAME&#39;
          value: &#39;InstrumentedFunctions&#39;
        }
        ...
      ]
    }
    ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will impact both the host and the work. With explicit Application Insights integration, we can separate the two (if there is such a need) and change the cloud role for the worker. This is done in the usual way, by registering an &lt;code&gt;ITelemetryInitializer&lt;/code&gt;. The only important detail is the place of the registration - it needs to be after &lt;code&gt;ConfigureFunctionsApplicationInsights&lt;/code&gt; as Azure Functions are adding their own initializers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class IsolatedWorkerTelemetryInitializer : ITelemetryInitializer
{
    public void Initialize(ITelemetry telemetry)
    {
        telemetry.Context.Cloud.RoleName = &quot;InstrumentedFunctionsIsolatedWorker&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

var host = new HostBuilder()
    ...
    .ConfigureServices(services =&amp;gt; {
        ...
        services.ConfigureFunctionsApplicationInsights();
        services.AddSingleton&amp;lt;ITelemetryInitializer, IsolatedWorkerTelemetryInitializer&amp;gt;();
        services.Configure&amp;lt;LoggerFilterOptions&amp;gt;(options =&amp;gt;
        {
            ...
        });
    })
    .Build();

host.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting map will look as below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjB432aidxf6s6XwxqLkvHarlR3pb2Sy2F3TcbbVnaqTxl6c7ULivUwyx-x9phyphenhypheneGaWNGPovbf-Rnf3hGG8fEh93gaK3JOnPL6D_GNZMI4Hw5tmqJXcIBYcwyD0tj9V6Ead56qJ5td0-_9l1Gkyn6xsFwPK_LnHSfG1VUKdCBfbX9WQVdWik5FvXP_4M0I/s1600/application-insights-modified-application-map.png&quot; alt=&quot;Modified application map in Application Insights&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;monitoring-for-scaling-decisions&quot;&gt;Monitoring for Scaling Decisions&lt;/h2&gt;
&lt;p&gt;This is currently in preview, but you can enable emitting scale controller logs (for a chance to understand the scaling decisions). This is done with a single setting (&lt;code&gt;SCALE_CONTROLLER_LOGGING_ENABLED&lt;/code&gt;) that takes a value in the &lt;code&gt;:&lt;/code&gt; format. The possible values for destination are &lt;code&gt;Blob&lt;/code&gt; and &lt;code&gt;AppInsights&lt;/code&gt;, while the verbosity can be &lt;code&gt;None&lt;/code&gt;, &lt;code&gt;Verbose&lt;/code&gt;, and &lt;code&gt;Warning&lt;/code&gt;. The &lt;code&gt;Verbose&lt;/code&gt; seems to be the most useful one when you are looking for understanding as it&#39;s supposed to provide reason for changes in the worker count, and information about the triggers that impact those decisions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource functionApp &#39;Microsoft.Web/sites@2024-04-01&#39; = {
  ...
  properties: {
    ...
    siteConfig: {
      ...
      appSettings: [
        ...
        {
          name: &#39;SCALE_CONTROLLER_LOGGING_ENABLED&#39;
          value: &#39;AppInsights:Verbose&#39;
        }
        ...
      ]
    }
    ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;is-this-exhaustive-&quot;&gt;Is This Exhaustive?&lt;/h2&gt;
&lt;p&gt;Certainly not 🙂. There are other things that you can fiddle with. You can have a very granular configuration of different log levels for different categories. You can fine-tune aggregation and sampling. When you are playing with all those settings, please remember that you&#39;re looking for the right balance between the gathered details and costs. My advice is not to configure monitoring for the worst-case scenario (when you need every detail) as that often isn&#39;t financially sustainable. Rather aim for a lower amount of information and change the settings to gather more if needed (a small hint - you can override &lt;code&gt;host.json&lt;/code&gt; settings through application settings without deployment).&lt;/p&gt;</description><link>http://www.tpeczek.com/2024/11/monitoring-c-azure-functions-in.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_iJvUHWJAAvLFHhOOkGzTO92CB2o3UF5qKHcXLy3BqaVgldC2EkjvgiLZAwhL-XGDx1Zgol4BZz6YH5cay4s3GyXbZteYGthJvN0oP0_IrX_HiIY5XYHNhNc3iEt6c-Q4fv5HeWae9c4mHlXdK3mfjWfxKJVq3FLPbsXmA2fprs9SpFqb94B2GR9yu_8/s72-c/azure-functions-with-file-system-logging.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-3693878604342508492</guid><pubDate>Tue, 24 Sep 2024 08:57:00 +0000</pubDate><atom:updated>2024-09-24T10:57:26.186+02:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">asp.net core</category><category domain="http://www.blogger.com/atom/ns#">async-streaming</category><category domain="http://www.blogger.com/atom/ns#">blazor</category><category domain="http://www.blogger.com/atom/ns#">json</category><category domain="http://www.blogger.com/atom/ns#">ndjson</category><title>ASP.NET Core 9 and IAsyncEnumerable - Async Streaming JSON and NDJSON From Blazor WebAssembly</title><description>&lt;p&gt;I&#39;ve been exploring &lt;a href=&quot;https://www.tpeczek.com/p/working-with-asynchronous-streaming.html&quot;&gt;working with asynchronous streaming data sources over HTTP&lt;/a&gt; for quite some time on this blog. I&#39;ve been playing with async streamed JSON and NDJSON. I&#39;ve been streaming and receiving streams in ASP.NET Core and console applications. But there is one scenario I haven&#39;t touched yet - streaming from Blazor WebAssembly. Why? Simply it wasn&#39;t possible.&lt;/p&gt;
&lt;p&gt;Streaming objects from Blazor WebAssembly requires two things. First is the support for streaming upload in browsers Fetch API. The Fetch API did support streams for response bodies for quite some time (my first post using it is from 2019) but non-experimental support for streams as request bodies come to Chrome, Edge, and Safari in 2022 (and it is yet to come to Firefox as far as I can tell). Still, it&#39;s been available for two years and I haven&#39;t explored it yet? Well, I did, more than a year ago, with &lt;a href=&quot;https://github.com/tpeczek/Demo.Ndjson.AsyncStreams/pull/10&quot;&gt;NDJSON and pure Javascript&lt;/a&gt;, where I used &lt;a href=&quot;https://www.tpeczek.com/2021/05/receiving-json-objects-stream-ndjson-in.html&quot;&gt;the ASP.NET Core endpoint&lt;/a&gt; I created long ago. But here we are talking about Blazor WebAssembly, which brings us to the second requirement - the browser API needs to be surfaced in Blazor. This is &lt;a href=&quot;https://github.com/dotnet/runtime/issues/36634&quot;&gt;finally happening with .NET 9&lt;/a&gt; and now I can fully explore it.&lt;/p&gt;
&lt;h2 id=&quot;async-streaming-ndjson-from-blazor-webassembly&quot;&gt;Async Streaming NDJSON From Blazor WebAssembly&lt;/h2&gt;
&lt;p&gt;I&#39;ve decided to start with NDJSON because I already have my own building blocks for it (that mentioned ASP.NET Core endpoint, and &lt;code&gt;NdjsonAsyncEnumerableContent&lt;/code&gt; coming from one of my &lt;a href=&quot;https://github.com/tpeczek/Ndjson.AsyncStreams&quot;&gt;packages&lt;/a&gt;). If things wouldn&#39;t work I would be able to easily debug them. &lt;/p&gt;
&lt;p&gt;I&#39;ve quickly put together a typical code using &lt;code&gt;HttpClient&lt;/code&gt;. I just had to make sure I&#39;ve enabled streaming upload by calling &lt;code&gt;SetBrowserRequestStreamingEnabled&lt;/code&gt; on the request instance.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;private async Task PostWeatherForecastsNdjsonStream()
{
    ...

    try
    {
        ....

        HttpRequestMessage request = new HttpRequestMessage(
            HttpMethod.Post, &quot;api/WeatherForecasts/stream&quot;);
        request.Content = new NdjsonAsyncEnumerableContent&amp;lt;WeatherForecast&amp;gt;(
            StreamWeatherForecastsAsync());
        request.SetBrowserRequestStreamingEnabled(true);

        using HttpResponseMessage response = await Http.SendAsync(request, cancellationToken);

        response.EnsureSuccessStatusCode();
    }
    finally
    {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It worked! No additional changes and no issues to resolve. I could see the items being logged by my ASP.NET Core service as they were streamed.&lt;/p&gt;
&lt;p&gt;After this initial success, I could move to a potentially more challenging scenario - async streaming JSON.&lt;/p&gt;
&lt;h2 id=&quot;async-streaming-json-from-blazor-webassembly&quot;&gt;Async Streaming JSON From Blazor WebAssembly&lt;/h2&gt;
&lt;p&gt;In theory, async streaming JSON should also just work. .NET has the support for &lt;code&gt;IAsyncEnumerable&lt;/code&gt; built into &lt;code&gt;JsonSerializer.SerializeAsync&lt;/code&gt; so &lt;code&gt;JsonContent&lt;/code&gt; should just use it. Well, there is no better way than to try, so I&#39;ve quickly changed the code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;private async Task PostWeatherForecastsJsonStream()
{
    ...

    try
    {
        ...

        HttpRequestMessage request = new HttpRequestMessage(
            HttpMethod.Post, &quot;api/WeatherForecasts/stream&quot;);
        request.Content = JsonContent.Create(StreamWeatherForecastsAsync());
        request.SetBrowserRequestStreamingEnabled(true);

        using HttpResponseMessage response = await Http.SendAsync(request, cancellationToken);

        response.EnsureSuccessStatusCode();
    }
    finally
    {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To my surprise, it also worked! I couldn&#39;t test it against my ASP.NET Core service because it didn&#39;t support it, but I quickly created a dumb endpoint that would simply dump whatever was incoming.&lt;/p&gt;
&lt;p&gt;Now, why was I surprised? Because this is in fact a platform difference - this behavior seems to be unique for the &lt;em&gt;&quot;browser&quot;&lt;/em&gt; platform. I did attempt the same in a desktop application earlier and &lt;code&gt;HttpClient&lt;/code&gt; would buffer the request - I had to create a simple &lt;a href=&quot;https://github.com/tpeczek/Demo.Ndjson.AsyncStreams/blob/main/Demo.Ndjson.AsyncStreams.Net.Http/JsonAsyncEnumerableContent.cs&quot;&gt;&lt;code&gt;HttpContent&lt;/code&gt;&lt;/a&gt; that would wrap the underlying stream and flush on write. I think I like the Blazor WebAssembly behavior better and I&#39;m happy that I didn&#39;t have to do the same here.&lt;/p&gt;
&lt;p&gt;Ok, but if I can&#39;t handle the incoming stream nicely in ASP.NET Core, it&#39;s not really that useful. So I&#39;ve tried to achieve that as well.&lt;/p&gt;
&lt;h2 id=&quot;receiving-async-streamed-json-in-asp-net-core&quot;&gt;Receiving Async Streamed JSON in ASP.NET Core&lt;/h2&gt;
&lt;p&gt;Receiving async streamed JSON is a little bit more tricky. &lt;code&gt;JsonSerializer&lt;/code&gt; has a dedicated method (&lt;code&gt;DeserializeAsyncEnumerable&lt;/code&gt;) to deserialize an incoming JSON stream into an &lt;code&gt;IAsyncEnumerable&lt;/code&gt; instance. The built-in &lt;code&gt;SystemTextJsonInputFormatter&lt;/code&gt; doesn&#39;t use that method, it simply uses &lt;code&gt;JsonSerializer.DeserializeAsync&lt;/code&gt; with request body as an input. That shouldn&#39;t be a surprise, it is the standard way to deserialize incoming JSON. To support async streamed JSON, a custom formatter is required. What is more, to be sure that this custom formatter will be used, it can&#39;t be just added - it has to replace the built-in one. But that would mean that the custom formatter has to have all the functionality of the built-in one (if we want to support not only async streamed JSON). Certainly, not something I wanted to reimplement, so I&#39;ve decided to simply inherit from &lt;code&gt;SystemTextJsonInputFormatter&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class SystemTextStreamedJsonInputFormatter : SystemTextJsonInputFormatter
{
    ...

    public override Task&amp;lt;InputFormatterResult&amp;gt; ReadRequestBodyAsync(InputFormatterContext context)
    {
        return base.ReadRequestBodyAsync(context);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, for the actual functionality, the logic has to branch based on the type of the model as we want to use &lt;code&gt;DeserializeAsyncEnumerable&lt;/code&gt; only if it&#39;s &lt;code&gt;IAsyncEnumerable&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class SystemTextStreamedJsonInputFormatter : SystemTextJsonInputFormatter
{
    ...

    public override Task&amp;lt;InputFormatterResult&amp;gt; ReadRequestBodyAsync(InputFormatterContext context)
    {
        if (context.ModelType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable&amp;lt;&amp;gt;))
        {
            ...
        }

        return base.ReadRequestBodyAsync(context);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s not the end of challenges (and reflection). &lt;code&gt;JsonSerializer.DeserializeAsyncEnumerable&lt;/code&gt; is a generic method. That means we have to get the actual type of values, based on it construct the right method, and call it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class SystemTextStreamedJsonInputFormatter : SystemTextJsonInputFormatter
{
    ...

    public override Task&amp;lt;InputFormatterResult&amp;gt; ReadRequestBodyAsync(InputFormatterContext context)
    {
        if (context.ModelType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable&amp;lt;&amp;gt;))
        {
            MethodInfo deserializeAsyncEnumerableMethod = typeof(JsonSerializer)
                .GetMethod(
                    nameof(JsonSerializer.DeserializeAsyncEnumerable),
                    [typeof(Stream), typeof(JsonSerializerOptions), typeof(CancellationToken)])
                .MakeGenericMethod(context.ModelType.GetGenericArguments()[0]);

            return Task.FromResult(InputFormatterResult.Success(
                deserializeAsyncEnumerableMethod.Invoke(null, [
                    context.HttpContext.Request.Body,
                    SerializerOptions,
                    context.HttpContext.RequestAborted
                ])
            ));
        }

        return base.ReadRequestBodyAsync(context);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The implementation above is intentionally ugly, so it doesn&#39;t hide anything and can be understood more easily. You can find a cleaner one, which also performs some constructed methods caching, in the &lt;a href=&quot;https://github.com/tpeczek/Demo.Ndjson.AsyncStreams/blob/main/Demo.Ndjson.AsyncStreams.AspNetCore.Mvc/Formatters/SystemTextStreamedJsonInputFormatter.cs&quot;&gt;demo repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now that we have the formatter, we can create a setup for &lt;code&gt;MvcOptions&lt;/code&gt; that will replace the &lt;code&gt;SystemTextJsonInputFormatter&lt;/code&gt; with a custom implementation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class SystemTextStreamedJsonMvcOptionsSetup : IConfigureOptions&amp;lt;MvcOptions&amp;gt;
{
    private readonly IOptions&amp;lt;JsonOptions&amp;gt;? _jsonOptions;
    private readonly ILogger&amp;lt;SystemTextJsonInputFormatter&amp;gt; _inputFormatterLogger;

    public SystemTextStreamedJsonMvcOptionsSetup(
        IOptions&amp;lt;JsonOptions&amp;gt;? jsonOptions,
        ILogger&amp;lt;SystemTextJsonInputFormatter&amp;gt; inputFormatterLogger)
    {
        _jsonOptions = jsonOptions;
        _inputFormatterLogger = inputFormatterLogger;
    }

    public void Configure(MvcOptions options)
    {
        options.InputFormatters.RemoveType&amp;lt;SystemTextJsonInputFormatter&amp;gt;();
        options.InputFormatters.Add(
            new SystemTextStreamedJsonInputFormatter(_jsonOptions?.Value, _inputFormatterLogger)
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A small convenience method to register that setup.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal static class SystemTextStreamedJsonMvcBuilderExtensions
{
    public static IMvcBuilder AddStreamedJson(this IMvcBuilder builder)
    {
        ...

        builder.Services.AddSingleton
            &amp;lt;IConfigureOptions&amp;lt;MvcOptions&amp;gt;, SystemTextStreamedJsonMvcOptionsSetup&amp;gt;();

        return builder;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we can configure the application to use it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers()
            ...
            .AddStreamedJson();

        ...
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It gets the job done. As the formatter for NDJSON expects a different content type (&lt;code&gt;application/x-ndjson&lt;/code&gt;) the content negotiation also works and I could asynchronously stream NDJSON and JSON to the same endpoint. Honestly, I think it&#39;s cool.&lt;/p&gt;
&lt;h2 id=&quot;afterthoughts&quot;&gt;Afterthoughts&lt;/h2&gt;
&lt;p&gt;One question that arises at this point is &quot;Which one to use?&quot;. I prefer NDJSON here. Why? Both require custom formatters and both formatters must use reflection. But in the case of NDJSON that is isolated only to the requests coming with a dedicated content type. To support async streamed JSON, the initial model type check has to happen on every request with JSON-related content type. Also I don&#39;t feel great with replacing the built-in formatter like that. On top of that, NDJSON enables clean cancellation as the endpoint is receiving separate JSON objects. In the case of JSON that is a single JSON collection of objects that will not be terminated properly and it will trip the deserialization.&lt;/p&gt;
&lt;p&gt;But this is just my opinion. You can grab the &lt;a href=&quot;https://github.com/tpeczek/Demo.Ndjson.AsyncStreams&quot;&gt;demo&lt;/a&gt; and play with it, or use one of my &lt;a href=&quot;https://github.com/tpeczek/Ndjson.AsyncStreams&quot;&gt;NDJON packages&lt;/a&gt; and experiment with it yourself. That&#39;s the best way to choose the right approach for your scenario.&lt;/p&gt;</description><link>http://www.tpeczek.com/2024/09/aspnet-core-9-and-iasyncenumerable.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-6028191154407920725</guid><pubDate>Tue, 05 Mar 2024 09:22:00 +0000</pubDate><atom:updated>2024-03-05T10:22:50.014+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure functions</category><category domain="http://www.blogger.com/atom/ns#">extensibility</category><category domain="http://www.blogger.com/atom/ns#">isolated worker</category><title>Azure Functions Extensibility - Extensions and Isolated Worker Model</title><description>&lt;p&gt;I&#39;ve been exploring the subject of Azure Functions extensibility on this blog for quite some time. I&#39;ve touched on subjects directly related to creating extensions and their anatomy, but also some peripheral ones.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2018/11/azure-functions-20-extensibility.html&quot;&gt;Azure Functions Extensibility - Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2018/11/azure-functions-20-extensibility_20.html&quot;&gt;Azure Functions Extensibility - Triggers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2024/02/azure-functions-extensibility-runtime.html&quot;&gt;Azure Functions Extensibility - Runtime Scaling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2019/01/azure-functions-20-extensibility.html&quot;&gt;Azure Functions Extensibility - Extending Extensions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have always written from the perspective of the in-process model, but since 2020 there has been a continued evolution when it comes to the preferred model for .NET-based function apps. The isolated worker model, first introduced with .NET 5, has been gaining parity and becoming the leading vehicle for making new .NET versions available with Azure Functions. In August 2023 Microsoft &lt;a href=&quot;https://techcommunity.microsoft.com/t5/apps-on-azure-blog/net-on-azure-functions-august-2023-roadmap-update/ba-p/3910098?wt.mc_id=DT-MVP-5002979&quot;&gt;announced&lt;/a&gt; the intention for .NET 8 to be the last LTS release to receive in-process model support. So the question comes, does it invalidate all that knowledge about Azure Functions extensibility? The short answer is no. But before I go into details, I need to cover the common ground.&lt;/p&gt;
&lt;h2 id=&quot;isolated-worker-model-in-a-nutshell&quot;&gt;Isolated Worker Model in a Nutshell&lt;/h2&gt;
&lt;p&gt;.NET was always a little bit special in Azure Functions. It shouldn&#39;t be a surprise. After all, it&#39;s Microsoft technology and there was a desire for the integration to be efficient and powerful. So even when Azure Functions v2 brought the separation between the host process and language worker process, .NET-based function apps were running in the host process. This had performance benefits (no communication between processes) and allowed .NET functions apps to leverage the full capabilities of the host, but started to become a bottleneck when the pace of changes in the .NET ecosystem accelerated. There were more and more conflicts between the assemblies that developers wanted to use in the apps and the ones used by the host. There was a delay in making new .NET versions available because the host had to be updated. Also, there were things that the app couldn&#39;t do because it was coupled with the host. Limitations like those were reasons for bringing Azure Functions .NET Worker to life.&lt;/p&gt;
&lt;p&gt;At the same time, Microsoft didn&#39;t want to take away all the benefits that .NET developers had when working with Azure Functions. The design had to take performance and developers&#39; experience into account. So how does Azure Functions .NET Worker work? In simplification, it&#39;s an ASP.NET Core application that receives inputs and provides outputs to the host over gRPC (which is more performant than HTTP primitives used in the case of &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-functions/functions-custom-handlers?wt.mc_id=DT-MVP-5002979&quot;&gt;custom handlers&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8CLf3FJxWA0xgxjlIBwbAOdtzMWs_HBOM8WQOAdpuPzTaDTW1ERhg1Rx3HIkvdQg4jvRpC0pssO8mMyOf1lsyTyn0Ast_7P-GSyTVJf5BhCJSw4DP99Ne1XHzttYTihW3uaP-JqR0siejfUFoOea7k-La9qX54LKT_Q-cMdCPMKIaTUhuXc2blSIw7xw/s1600/azure-functions-extensibility-in-isolated-worker-model-01.png&quot; alt=&quot;Azure Functions .NET Worker overview&quot;&gt;&lt;/p&gt;
&lt;p&gt;The request and response payloads are also pretty well hidden. Developers have been given a new binding model with required attributes available through &lt;code&gt;*.Azure.Functions.Worker.Extensions.*&lt;/code&gt; packages. But if the actual bindings activity happens in the host, what do those new packages provide? And what is their relation with the &lt;code&gt;*.Azure.WebJobs.Extensions.*&lt;/code&gt; packages?&lt;/p&gt;
&lt;h2 id=&quot;worker-extensions-and-webjobs-extensions&quot;&gt;Worker Extensions and WebJobs Extensions&lt;/h2&gt;
&lt;p&gt;The well-hidden truth is that the worker extension packages are just a bridge to the in-process extension packages. It means that if you want to create a new extension or understand how an existing one works, you should start with an extension for the in-process model. The worker extensions are mapped to the in-process ones through an assembly-level attribute, which takes the name of the package and version to be used as parameters.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;[assembly: ExtensionInformation(&quot;RethinkDb.Azure.WebJobs.Extensions&quot;, &quot;0.6.0&quot;)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The integration is quite seamless. During the build, the Azure Functions tooling will use NuGet to install the needed in-process extension package, it doesn&#39;t have to be referenced. Of course that has its drawbacks (tight coupling to a specific version and more challenges during debugging). So, the final layout of the packages can be represented as below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkM5Z3PUZ3_kLVx3pT303BnQDMVWkacivKfQPtxHeVX4nzBom2eEwClkc0KWt-kVnPg1K0CQmEfOecTPviKzSQPTan0tz9tNh6P7farURFBpZ6Bfvrob386Bko9o1Mml2TsCixHLSQ2tq8Lgmv9U6uSrjX__KD2teCDdteuqzl6_i-vvAkjOMsFLym0QI/s1600/azure-functions-extensibility-in-isolated-worker-model-02.png&quot; alt=&quot;Azure Functions .NET Worker Extensions and WebJobs Extensions relation overview&quot;&gt;&lt;/p&gt;
&lt;p&gt;What ensures the cooperation between those two packages running in two different processes are the binding attributes.&lt;/p&gt;
&lt;h2 id=&quot;binding-attributes&quot;&gt;Binding Attributes&lt;/h2&gt;
&lt;p&gt;In the case of the in-process model extensions we have two types of attributes - one for bindings and one for trigger. In the case of the isolated worker model, there are three - for input binding, for output binding, and for trigger.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class RethinkDbInputAttribute : InputBindingAttribute
{
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public sealed class RethinkDbOutputAttribute : OutputBindingAttribute
{
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public sealed class RethinkDbTriggerAttribute : TriggerBindingAttribute
{
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The isolated worker model attributes are used in two ways. One is for developers, who use them to decorate their functions and provide needed settings. The other is for the worker, which uses them as data transfer objects. They are being serialized and transferred as metadata. On the host side, they are being deserialized to the corresponding in-process model extension attribute. The input and output attributes will be deserialized to the binding attribute, and the trigger will be deserialized to the trigger. This means that we need to ensure that the names of properties which we want to support are matching.&lt;/p&gt;
&lt;p&gt;Implementing the attributes and decorating the functions with them is all we need to make it work. This will give us support for POCOs as values (the host and worker will take care of serialization, transfer over gRPC, and deserialization). But what if we want something more than POCO?&lt;/p&gt;
&lt;h2 id=&quot;beyond-poco-inputs-with-converters&quot;&gt;Beyond POCO Inputs With Converters&lt;/h2&gt;
&lt;p&gt;It&#39;s quite common for in-process extensions to support binding data provided using types from specific service SDKs (for example &lt;code&gt;CosmosClient&lt;/code&gt; in the case of Azure Cosmos DB). That kind of binding data is not supported out-of-the-box by isolated worker extensions as they can&#39;t be serialized and transferred. But there is a way for isolated worker extensions to go beyond POCOs - input converters.&lt;/p&gt;
&lt;p&gt;Input converters are classes that implement the &lt;code&gt;IInputConverter&lt;/code&gt; interface. This interface defines a single method, which is supposed to return a conversation result. Conversation result can be one of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Unhandled&lt;/code&gt; (the converter did not act on the input)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Succeeded&lt;/code&gt; (conversion was successful and the result is included)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Failed&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The converter should check if it&#39;s being used with an extension it supports (the name that has been used for isolated extensions registration will be provided as part of the model binding data) and if the incoming content is in supported format. The converter can also be decorated with multiple &lt;code&gt;SupportedTargetType&lt;/code&gt; attributes to narrow its scope.&lt;/p&gt;
&lt;p&gt;Below is a sample template for an input converter.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;[SupportsDeferredBinding]
[SupportedTargetType(typeof(...))]
[SupportedTargetType(typeof(...))]
internal class RethinkDbConverter : IInputConverter
{
    private const string RETHINKDB_EXTENSION_NAME = &quot;RethinkDB&quot;;
    private const string JSON_CONTENT_TYPE = &quot;application/json&quot;;

    ...

    public RethinkDbConverter(...)
    {
        ...
    }

    public async ValueTask&amp;lt;ConversionResult&amp;gt; ConvertAsync(ConverterContext context)
    {
        ModelBindingData modelBindingData = context?.Source as ModelBindingData;

        if (modelBindingData is null)
        {
            return ConversionResult.Unhandled();
        }

        try
        {
            if (modelBindingData.Source is not RETHINKDB_EXTENSION_NAME)
            {
                throw new InvalidOperationException($&quot;Unexpected binding source.&quot;);
            }

            if (modelBindingData.ContentType is not JSON_CONTENT_TYPE)
            {
                throw new InvalidOperationException($&quot;Unexpected content-type.&quot;);
            }

            object result = context.TargetType switch
            {
                // Here you can use modelBindingData.Content,
                // any injected services, etc.
                // to prepare the value.
                ...
            };


            return ConversionResult.Success(result);
        }
        catch (Exception ex)
        {
            return ConversionResult.Failed(ex);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Input converters can be applied to input and trigger binding attributes by simply decorating them with an attribute (we should also define the fallback behavior policy).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;[InputConverter(typeof(RethinkDbConverter))]
[ConverterFallbackBehavior(ConverterFallbackBehavior.Default)]
public class RethinkDbInputAttribute : InputBindingAttribute
{
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding input converters to the extensions moves some of the logic from host to worker. It may mean that the worker will be now establishing connections to the services or performing other operations. This will most likely create a need to register some dependencies, read configuration, and so on. Such things are best done at a function startup.&lt;/p&gt;
&lt;h2 id=&quot;participating-in-the-function-app-startup&quot;&gt;Participating in the Function App Startup&lt;/h2&gt;
&lt;p&gt;Extensions for the isolated worker model can implement a startup hook. It can be done by creating a public class with a parameterless constructor, that derives from &lt;code&gt;WorkerExtensionStartup&lt;/code&gt;. This class also has to be registered through an assembly-level attribute. Now we can override the &lt;code&gt;Configure&lt;/code&gt; method and register services and middlewares. The mechanic is quite similar to its equivalent for in-process extension.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;[assembly: WorkerExtensionStartup(typeof(RethinkDbExtensionStartup))]

namespace Microsoft.Azure.Functions.Worker
{
    public class RethinkDbExtensionStartup : WorkerExtensionStartup
    {
        public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder)
        {
            if (applicationBuilder == null)
            {
                throw new ArgumentNullException(nameof(applicationBuilder));
            }

            ...
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;the-conclusion&quot;&gt;The Conclusion&lt;/h2&gt;
&lt;p&gt;The isolated worker model doesn&#39;t invalidate what we know about the Azure Functions extensions, on the contrary, it builds another layer on top of that knowledge. Sadly, there are limitations in the supported data types for bindings which come from the serialization and transfer of data between the host and the worker. Still, in my opinion, the benefits of the new model can outweigh those limitations.&lt;/p&gt;
&lt;p&gt;If you are looking for a working sample to learn and explore, you can find one &lt;a href=&quot;https://github.com/tpeczek/RethinkDb.Azure&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</description><link>http://www.tpeczek.com/2024/03/azure-functions-extensibility.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8CLf3FJxWA0xgxjlIBwbAOdtzMWs_HBOM8WQOAdpuPzTaDTW1ERhg1Rx3HIkvdQg4jvRpC0pssO8mMyOf1lsyTyn0Ast_7P-GSyTVJf5BhCJSw4DP99Ne1XHzttYTihW3uaP-JqR0siejfUFoOea7k-La9qX54LKT_Q-cMdCPMKIaTUhuXc2blSIw7xw/s72-c/azure-functions-extensibility-in-isolated-worker-model-01.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-5557502819831846633</guid><pubDate>Thu, 22 Feb 2024 12:28:00 +0000</pubDate><atom:updated>2024-02-22T13:28:27.434+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure functions</category><category domain="http://www.blogger.com/atom/ns#">extensibility</category><category domain="http://www.blogger.com/atom/ns#">runtime scaling</category><category domain="http://www.blogger.com/atom/ns#">target-based scaling</category><title>Azure Functions Extensibility - Runtime Scaling</title><description>&lt;p&gt;In the past, I&#39;ve written posts that explored the core aspects of creating Azure Functions extensions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2018/11/azure-functions-20-extensibility.html&quot;&gt;Azure Functions Extensibility - Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2018/11/azure-functions-20-extensibility_20.html&quot;&gt;Azure Functions Extensibility - Triggers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this post, I want to dive into how extensions implement support for the Azure Functions scaling model.&lt;/p&gt;
&lt;p&gt;The Azure Functions scaling model (as one can expect) has evolved over the years. The first model is incremental scaling handled by the &lt;em&gt;scale controller&lt;/em&gt; (a dedicated Azure Functions component). The scale controller monitors the rate of incoming events and makes magical (following the rule that every sophisticated enough technology is indistinguishable from magic) decisions about whether to add or remove a worker. The workers count can change only by one and at a specific rate. This mechanism has some limitations.&lt;/p&gt;
&lt;p&gt;The first problem is the inability to scale in network-isolated deployments. In such a scenario, the scale controller doesn&#39;t have access to the services and can&#39;t monitor events - only the function app (and as a consequence the extensions) could. As a result, toward the end of 2019, the SDK provided support for extensions to vote in scale in and scale out decisions.&lt;/p&gt;
&lt;p&gt;The second problem is the scaling process clarity and rate. Change by one worker at a time driven by complex heuristics is not perfect. This led to the introduction of target-based scaling at the beginning of 2023. Extensions become able to implement their own algorithms for requesting a specific number of workers and scale up by four instances at a time.&lt;/p&gt;
&lt;p&gt;So, how do those two mechanisms work and can be implemented? To answer this question I&#39;m going to describe them from two perspectives. One will be how it is being done by a well-established extension - Azure Cosmos DB bindings for Azure Functions. The other will be a sample implementation in my extension that I&#39;ve used previously while writing on Azure Functions extensibility - &lt;a href=&quot;https://github.com/tpeczek/RethinkDb.Azure&quot;&gt;RethinkDB bindings for Azure Functions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But first things first. Before we can talk about the actual logic driving the scaling behaviors, we need the right SDK and the right classes in place.&lt;/p&gt;
&lt;h2 id=&quot;runtime-and-target-based-scaling-support-in-azure-webjobs-sdk&quot;&gt;Runtime and Target-Based Scaling Support in Azure WebJobs SDK&lt;/h2&gt;
&lt;p&gt;As I&#39;ve mentioned above, support for extensions to participate in the scaling model has been introduced gradually to the Azure WebJobs SDK. At the same time, the team makes an effort to introduce changes in a backward-compatible manner. Support for voting in scaling decisions came in version 3.0.14 and for target-based scaling in 3.0.36, but you can have an extension that is using version 3.0.0 and it will work on the latest Azure Functions runtime. You can also have an extension that uses the latest SDK and doesn&#39;t implement the scalability mechanisms. You need to know that they are there and choose to utilize them.&lt;/p&gt;
&lt;p&gt;This is reflected in the official extensions as well. The Azure Cosmos DB extension has implemented support for runtime scaling in version 3.0.5 and target-based scaling in 4.1.0 (you can find some tables that are gathering version numbers, for example in the section about &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-functions/functions-networking-options?wt.mc_id=DT-MVP-5002979#premium-plan-with-virtual-network-triggers&quot;&gt;virtual network triggers&lt;/a&gt;). This means, that if your function app is using lower versions of extensions, it won&#39;t benefit from these capabilities.&lt;/p&gt;
&lt;p&gt;So, regardless if you&#39;re implementing your extension or you&#39;re just using an existing one, the versions of dependencies matter. However, if you&#39;re implementing your extension, you will need some classes.&lt;/p&gt;
&lt;h2 id=&quot;classes-needed-to-implement-scaling-support&quot;&gt;Classes Needed to Implement Scaling Support&lt;/h2&gt;
&lt;p&gt;The Azure Functions scaling model revolves around triggers. After all, it&#39;s their responsibility to handle the influx of events. As you may know (for example from my previous post 😉), the heart of a trigger is a listener. This is where all the heavy lifting is being done and this is where we can inform the runtime that our trigger supports scalability features. We can do so by implementing two interfaces: &lt;code&gt;IScaleMonitorProvider&lt;/code&gt; and &lt;code&gt;ITargetScalerProvider&lt;/code&gt;. They are respectively tied to runtime scaling and target-based scaling. If they are implemented, the runtime will use the properties they define to obtain the actual logic implementations.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbTriggerListener : IListener, IScaleMonitorProvider, ITargetScalerProvider
{
    ...

    private readonly IScaleMonitor&amp;lt;RethinkDbTriggerMetrics&amp;gt; _rethinkDbScaleMonitor;
    private readonly ITargetScaler _rethinkDbTargetScaler;

    ...

    public IScaleMonitor GetMonitor()
    {
        return _rethinkDbScaleMonitor;
    }

    public ITargetScaler GetTargetScaler()
    {
        return _rethinkDbTargetScaler;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the snippets above you can notice one more class - &lt;code&gt;RethinkDbTriggerMetrics&lt;/code&gt;. It derives from the &lt;code&gt;ScaleMetrics&lt;/code&gt; class and is used for capturing the values on which the decisions are being made.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbTriggerMetrics : ScaleMetrics
{
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;IScaleMonitor&lt;/code&gt; implementation contributes to the runtime scaling part of the scaling model. Its responsibilities are to provide snapshots of the metrics and to vote in the scaling decisions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbScaleMonitor : IScaleMonitor&amp;lt;RethinkDbTriggerMetrics&amp;gt;
{
    public ScaleMonitorDescriptor Descriptor =&amp;gt; ...;

    public Task&amp;lt;RethinkDbTriggerMetrics&amp;gt; GetMetricsAsync()
    {
        ...
    }

    Task&amp;lt;ScaleMetrics&amp;gt; IScaleMonitor.GetMetricsAsync()
    {
        ...
    }

    public ScaleStatus GetScaleStatus(ScaleStatusContext&amp;lt;RethinkDbTriggerMetrics&amp;gt; context)
    {
        ...
    }

    public ScaleStatus GetScaleStatus(ScaleStatusContext context)
    {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ITargetScaler&lt;/code&gt; implementation is responsible for the target-based scaling. It needs to focus only on one thing - calculating the desired worker count.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbTargetScaler : ITargetScaler
{
    public TargetScalerDescriptor TargetScalerDescriptor =&amp;gt; ...;

    public Task&amp;lt;TargetScalerResult&amp;gt; GetScaleResultAsync(TargetScalerContext context)
    {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As both of these implementations are tightly tied with a specific trigger, it is typical to instantiate them in the listener constructor.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbTriggerListener : IListener, IScaleMonitorProvider, ITargetScalerProvider
{
    ...

    public RethinkDbTriggerListener(...)
    {
        ...

        _rethinkDbScaleMonitor = new RethinkDbScaleMonitor(...);
        _rethinkDbTargetScaler = new RethinkDbTargetScaler(...);
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are almost ready to start discussing the details of scaling logic, but before we do that, we need to talk about gathering metrics. The decisions need to be based on something.&lt;/p&gt;
&lt;h2 id=&quot;gathering-metrics&quot;&gt;Gathering Metrics&lt;/h2&gt;
&lt;p&gt;Yes, the decisions need to be based on something and the quality of the decisions will depend on the quality of metrics you can gather. So a lot depends on how well the service, for which the extension is being created, is prepared to be monitored.&lt;/p&gt;
&lt;p&gt;Azure Cosmos DB is well prepared. The Azure Cosmos DB bindings for Azure Functions implement the trigger on top of the Azure Cosmos DB change feed processor and use the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-use-change-feed-estimator?wt.mc_id=DT-MVP-5002979&quot;&gt;change feed estimator&lt;/a&gt; to gather the metrics. This gives access to quite an accurate estimate of the remaining work. As an additional metric, the extension is gathers the number of leases for the container that the trigger is observing.&lt;/p&gt;
&lt;p&gt;RethinkDB is not so well prepared. It seems like change feed provides only one metric - buffered items count.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbTriggerMetrics : ScaleMetrics
{
    public int BufferedItemsCount { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, the metric can only be gathered while iterating the change feed. This forces an intermediary between the listener and the scale monitor (well you could use the scale monitor directly, but it seemed ugly to me).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbMetricsProvider
{
    public int CurrentBufferedItemsCount { get; set; }

    public RethinkDbTriggerMetrics GetMetrics()
    {
        return new RethinkDbTriggerMetrics { BufferedItemsCount = CurrentBufferedItemsCount };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the same instance of this intermediary can be used by the listener to provide the value.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbTriggerListener : IListener, IScaleMonitorProvider, ITargetScalerProvider
{
    ...

    private readonly RethinkDbMetricsProvider _rethinkDbMetricsProvider;

    ...

    public RethinkDbTriggerListener(...)
    {
        ...

        _rethinkDbMetricsProvider = new RethinkDbMetricsProvider();
        _rethinkDbScaleMonitor = new RethinkDbScaleMonitor(..., _rethinkDbMetricsProvider);

        ...
    }

    ...

    private async Task ListenAsync(CancellationToken listenerStoppingToken)
    {
        ...

        while (!listenerStoppingToken.IsCancellationRequested
               &amp;amp;&amp;amp; (await changefeed.MoveNextAsync(listenerStoppingToken)))
        {
            _rethinkDbMetricsProvider.CurrentBufferedItemsCount = changefeed.BufferedItems.Count;
            ...
        }

        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the scale monitor to read it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbScaleMonitor : IScaleMonitor&amp;lt;RethinkDbTriggerMetrics&amp;gt;
{
    ...

    private readonly RethinkDbMetricsProvider _rethinkDbMetricsProvider;

    ...

    public RethinkDbScaleMonitor(..., RethinkDbMetricsProvider rethinkDbMetricsProvider)
    {
        ...

        _rethinkDbMetricsProvider = rethinkDbMetricsProvider;

        ...
    }

    public Task&amp;lt;RethinkDbTriggerMetrics&amp;gt; GetMetricsAsync()
    {
        return Task.FromResult(_rethinkDbMetricsProvider.GetMetrics());
    }

    Task&amp;lt;ScaleMetrics&amp;gt; IScaleMonitor.GetMetricsAsync()
    {
        return Task.FromResult((ScaleMetrics)_rethinkDbMetricsProvider.GetMetrics());
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have the metrics now, it is finally time to make some decisions.&lt;/p&gt;
&lt;h2 id=&quot;voting-to-scale-in-or-scale-out&quot;&gt;Voting to Scale In or Scale Out&lt;/h2&gt;
&lt;p&gt;An extension can cast one of three votes: to scale out, to scale in, or neutral. This is where the intimate knowledge of the service that the extension is created for comes into play.&lt;/p&gt;
&lt;p&gt;As I&#39;ve already written, the Azure Cosmos DB change feed processor is well prepared to gather metrics about it. It is also well prepared to be consumed in a &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/change-feed-processor?wt.mc_id=DT-MVP-5002979#dynamic-scaling&quot;&gt;scalable&lt;/a&gt; manner. It can distribute the work among multiple compute instances, by balancing the number of leases owned by each compute instance. It will also adjust the number of leases based on throughput and storage. This is why the Azure Cosmos DB bindings are tracking the number of leases as one of the metrics - it&#39;s an upper limit for the number of workers. So the extension utilizes all this knowledge and employs the following algorithm to cast a vote:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If there are no metrics gathered yet, cast a neutral vote.&lt;/li&gt;
&lt;li&gt;If the current number of leases is greater than the number of workers, cast a scale-in vote.&lt;/li&gt;
&lt;li&gt;If there are less than five metrics samples, cast a neutral vote.&lt;/li&gt;
&lt;li&gt;If the ratio of workers to remaining items is less than 1 per 1000, cast a scale-out vote.&lt;/li&gt;
&lt;li&gt;If there are constantly items waiting to be processed and the number of workers is smaller than the number of leases, cast a scale-out vote.&lt;/li&gt;
&lt;li&gt;If the trigger source has been empty for a while, cast a scale-in vote.&lt;/li&gt;
&lt;li&gt;If there has been a continuous increase across the last five samples in items to be processed, cast a scale-out vote.&lt;/li&gt;
&lt;li&gt;If there has been a continuous decrease across the last five samples in items to be processed, cast a scale-in vote.&lt;/li&gt;
&lt;li&gt;If none of the above has happened, cast a neutral vote.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;RethinkDB is once again lacking here. It looks like its change feed is not meant to be processed in parallel at all. This leads to an interesting edge case, where we never want to scale beyond a single instance.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbScaleMonitor : IScaleMonitor&amp;lt;RethinkDbTriggerMetrics&amp;gt;
{
    ...

    public ScaleStatus GetScaleStatus(ScaleStatusContext&amp;lt;RethinkDbTriggerMetrics&amp;gt; context)
    {
        return GetScaleStatus(
            context.WorkerCount,
            context.Metrics?.ToArray()
        );
    }

    public ScaleStatus GetScaleStatus(ScaleStatusContext context)
    {
        return GetScaleStatus(
            context.WorkerCount, 
            context.Metrics?.Cast&amp;lt;RethinkDbTriggerMetrics&amp;gt;().ToArray()
        );
    }

    private ScaleStatus GetScaleStatus(int workerCount, RethinkDbTriggerMetrics[] metrics)
    {
        ScaleStatus status = new ScaleStatus
        {
            Vote = ScaleVote.None
        };

        // RethinkDB change feed is not meant to be processed in paraller.
        if (workerCount &amp;gt; 1)
        {
            status.Vote = ScaleVote.ScaleIn;

            return status;
        }

        return status;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;requesting-desired-number-of-workers&quot;&gt;Requesting Desired Number of Workers&lt;/h2&gt;
&lt;p&gt;Being able to tell if you want more workers or less is great, being able to tell how many workers you want is even better. Of course, there is no promise the extension will get the requested number (even in the case of target-based scaling the scaling happens with a maximum rate of four instances at a time), but it&#39;s better than increasing and decreasing by one instance. Extensions can also use this mechanism to participate in &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-functions/functions-concurrency?wt.mc_id=DT-MVP-5002979#dynamic-concurrency&quot;&gt;dynamic concurrency&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is exactly what the Azure Cosmos DB extension is doing. It divides the number of remaining items by the value of the &lt;code&gt;MaxItemsPerInvocation&lt;/code&gt; trigger setting (the default is 100). The result is capped by the number of leases and that&#39;s the desired number of workers.&lt;/p&gt;
&lt;p&gt;We already know  that in the case of RethinkDB, it&#39;s even simpler - we always want just one worker.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RethinkDbTargetScaler : ITargetScaler
{
    ...

    public Task&amp;lt;TargetScalerResult&amp;gt; GetScaleResultAsync(TargetScalerContext context)
    {
        return Task.FromResult(new TargetScalerResult
        {
            TargetWorkerCount = 1
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it. The runtime scaling implementation is now complete.&lt;/p&gt;
&lt;h2 id=&quot;consequences-of-runtime-scaling-design&quot;&gt;Consequences of Runtime Scaling Design&lt;/h2&gt;
&lt;p&gt;Creating extensions for Azure Functions is not something commonly done. Still, there is a value in understanding their anatomy and how they work as it is often relevant also when you are creating function apps.&lt;/p&gt;
&lt;p&gt;The unit of scale for Azure Functions is the entire functions app. At the same time, the scaling votes and the desired number of workers are delivered at a trigger level. This means that you need to be careful when creating function apps with multiple triggers. If the triggers will be aiming for completely different scaling decisions it may lead to undesired scenarios. For example, one trigger may constantly want to scale in, while another will want to scale out. As you could notice throughout this post, some triggers have hard limits on how far they can scale to not waste resources (even two Azure Cosmos DB triggers may have a different upper limit because the lease containers they are attached to will have different numbers of leases available). This all should be taken into account while designing the function app and trying to foresee how it will scale.&lt;/p&gt;</description><link>http://www.tpeczek.com/2024/02/azure-functions-extensibility-runtime.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-5098970047446691928</guid><pubDate>Tue, 09 Jan 2024 11:09:00 +0000</pubDate><atom:updated>2024-01-09T12:09:24.262+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">.net</category><category domain="http://www.blogger.com/atom/ns#">aks</category><category domain="http://www.blogger.com/atom/ns#">azure</category><category domain="http://www.blogger.com/atom/ns#">kubernetes</category><category domain="http://www.blogger.com/atom/ns#">slight</category><category domain="http://www.blogger.com/atom/ns#">wasi</category><category domain="http://www.blogger.com/atom/ns#">wasm</category><category domain="http://www.blogger.com/atom/ns#">webassembly</category><title>Experimenting With .NET &amp; WebAssembly - Running .NET Based Slight Application On WASM/WASI Node Pool in AKS</title><description>&lt;p&gt;A year ago I wrote about &lt;a href=&quot;https://www.tpeczek.com/2022/12/experimenting-with-net-webassembly.html&quot;&gt;running a .NET based Spin application on a WASI node pool in AKS&lt;/a&gt;. Since then the support for WebAssembly in AKS hasn&#39;t changed. We still have the same preview, supporting the same versions of two ContainerD shims: Spin and Slight (a.k.a. SpiderLightning). So why am I coming back to the subject? The broader context has evolved.&lt;/p&gt;
&lt;p&gt;With WASI preview 2, the ecosystem is embracing the &lt;a href=&quot;https://github.com/WebAssembly/component-model&quot;&gt;component model&lt;/a&gt; and standardized APIs. When I was experimenting with Spin, I leveraged WAGI (WebAssembly Gateway Interface) which allowed me to be ignorant about the Wasm runtime context. Now I want to change that, cross the barrier and dig into direct interoperability between .NET and Wasm.&lt;/p&gt;
&lt;p&gt;Also, regarding the mentioned APIs, one of the emerging ones is &lt;a href=&quot;https://github.com/WebAssembly/wasi-cloud-core&quot;&gt;&lt;code&gt;wasi-cloud-core&lt;/code&gt;&lt;/a&gt; which aims to provide a generic way for WASI applications to interact with services. This proposal is not yet standardized but it has an experimental host implementation which happens to be Slight. By running a .NET based Slight application I want to get a taste of what that API might bring.&lt;/p&gt;
&lt;p&gt;Last but not least, .NET 8 has brought a new way of building .NET based Wasm applications with a &lt;code&gt;wasi-experimental&lt;/code&gt; workload. I want to build something with it and see where the .NET support for WASI is heading.&lt;/p&gt;
&lt;p&gt;So, this &quot;experiment&quot; has multiple angles and brings together a bunch of different things. How am I going to start? In the usual way, by creating a project.&lt;/p&gt;
&lt;h2 id=&quot;creating-a-net-8-wasi-wasm-project&quot;&gt;Creating a .NET 8 &lt;code&gt;wasi-wasm&lt;/code&gt; Project&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;wasi-experimental&lt;/code&gt; workload is optional, so we need to install it before we can create a project.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;dotnet workload install wasi-experimental
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also doesn&#39;t bundle the &lt;a href=&quot;https://github.com/WebAssembly/wasi-sdk&quot;&gt;WASI SDK&lt;/a&gt; (the &lt;a href=&quot;https://github.com/SteveSandersonMS/dotnet-wasi-sdk&quot;&gt;WASI SDK for .NET 7&lt;/a&gt; did), so we have to install it ourselves. The releases of WASI SDK available on GitHub contain binaries for different platforms. All you need to do is download the one appropriate for yours, extract it, and create the WASI_SDK_PATH environment variable pointing to the output. The version I&#39;ll be using here is &lt;a href=&quot;https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-20&quot;&gt;20.0&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With the prerequisites in place, we can create the project.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;dotnet new wasiconsole -o Demo.Wasm.Slight
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, if you run &lt;code&gt;dotnet build&lt;/code&gt; and inspect the output folder, you can notice that it contains &lt;em&gt;Demo.Wasm.Slight.dll&lt;/em&gt;, other managed DLLs, and &lt;em&gt;dotnet.wasm&lt;/em&gt;. This is the default output, where the &lt;em&gt;dotnet.wasm&lt;/em&gt; is responsible for loading the Mono runtime and then loading the functionality from DLLs. This is not what we want. We want a single file. To achieve that we need to modify the project file by adding the &lt;code&gt;WasmSingleFileBundle&lt;/code&gt; property (in my opinion this should be the default).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-xml&quot;&gt;&amp;lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&amp;gt;
  &amp;lt;PropertyGroup&amp;gt;
    ...
    &amp;lt;WasmSingleFileBundle&amp;gt;true&amp;lt;/WasmSingleFileBundle&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you run &lt;code&gt;dotnet build&lt;/code&gt; after this modification, you will find &lt;em&gt;Demo.Wasm.Slight.wasm&lt;/em&gt; in the output. Exactly what we want.&lt;/p&gt;
&lt;p&gt;But, before we can start implementing the actual application, we need to work on the glue between the .NET and WebAssembly so we can interact with the APIs provided by Slight from C#.&lt;/p&gt;
&lt;h2 id=&quot;from-webassembly-idl-to-c-&quot;&gt;From WebAssembly IDL To C#&lt;/h2&gt;
&lt;p&gt;The imports and exports in WASI APIs are described in &lt;a href=&quot;https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md&quot;&gt;Wasm Interface Type (WIT)&lt;/a&gt; format. This format is an IDL which is a foundation behind tooling for the WebAssembly Component Model.&lt;/p&gt;
&lt;p&gt;WIT aims at being a developer-friendly format, but writing bindings by hand is not something that developers expect. This is where &lt;a href=&quot;https://github.com/bytecodealliance/wit-bindgen&quot;&gt;&lt;code&gt;wit-bindgen&lt;/code&gt;&lt;/a&gt; comes into the picture. It&#39;s a (still young) binding generator for languages that are compiled into WebAssembly. It currently supports languages like Rust, C/C++, or Java. The &lt;a href=&quot;https://github.com/bytecodealliance/wit-bindgen/issues/713&quot;&gt;C# support&lt;/a&gt; is being actively worked on and we can expect that at some point getting C# bindings will be as easy as running a single command (or even simpler as Steve Sanderson is already &lt;a href=&quot;https://github.com/SteveSandersonMS/wasm-component-sdk&quot;&gt;experimenting with making it part of the toolchain&lt;/a&gt;) but for now, it&#39;s too limited and we will have to approach things differently. What we can use is the C/C++ support.&lt;/p&gt;
&lt;p&gt;There is one more challenge on our way. The current version of &lt;code&gt;wit-bindgen&lt;/code&gt; is meant for WASI preview 2. Meanwhile, a lot of existing WIT definitions and WASI tooling around native languages are using WASI preview 1. This is exactly the case when it comes to the Slight implementation available in the AKS preview. To handle that we need an old (and I mean old) version of &lt;code&gt;wit-bindgen&lt;/code&gt;. I&#39;m using version &lt;a href=&quot;https://github.com/bytecodealliance/wit-bindgen/releases/tag/v0.2.0&quot;&gt;v0.2.0&lt;/a&gt;. Once you install it, you can generate C/C++ bindings for the desired imports and exports. Slight in version 0.1.0 provides a &lt;a href=&quot;https://github.com/deislabs/spiderlightning/blob/v0.1.0/docs/primer.md#spiderlightning-capabilities&quot;&gt;couple of capabilities&lt;/a&gt;, but I&#39;ve decided to start with just one, the HTTP Server. That means we need imports for &lt;code&gt;http.wit&lt;/code&gt; and exports for &lt;code&gt;http-handler.wit&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;wit-bindgen c --import ./wit/http.wit --out-dir ./native/
wit-bindgen c --export ./wit/http-handler.wit --out-dir ./native/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we have the C/C++ bindings, we can configure the build to include those files in the arguments passed to Clang. For this purpose, we can use the &lt;a href=&quot;https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-your-build#choose-between-adding-properties-to-a-props-or-targets-file&quot;&gt;&lt;code&gt;.targets&lt;/code&gt;&lt;/a&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-xml&quot;&gt;&amp;lt;Project&amp;gt;
  &amp;lt;ItemGroup&amp;gt;
    &amp;lt;_WasmNativeFileForLinking Include=&quot;$(MSBuildThisFileDirectory)\..\native\*.c&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need to implement the C# part of the interop. For the main part, it&#39;s like any other interop you might have ever done. You can use generators or ask for help from a friendly AI assistant and it will get you through the code for types and functions. However, there is one specific catch. The &lt;code&gt;DllImportAttribute&lt;/code&gt; requires passing a library name for the P/Invoke generator, but we have no library. The solution is as simple as surprising (at least to me), we can provide the name of any library that the P/Invoke generator knows about.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal static class HttpServer
{
    // Any library name that P/Invoke generator knows
    private const string LIBRARY_NAME = &quot;libSystem.Native&quot;;

    [DllImport(LIBRARY_NAME)]
    internal static extern unsafe void http_server_serve(WasiString address,
        uint httpRouterIndex, out WasiExpected&amp;lt;uint&amp;gt; ret0);

    [DllImport(LIBRARY_NAME)]
    internal static extern unsafe void http_server_stop(uint httpServerIndex,
        out WasiExpected&amp;lt;uint&amp;gt; ret0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With all the glue in place, we can start implementating the HTTP server capability.&lt;/p&gt;
&lt;h2 id=&quot;implementing-the-slight-http-server-capability&quot;&gt;Implementing the Slight HTTP Server Capability&lt;/h2&gt;
&lt;p&gt;In order for a Slight application to start accepting HTTP requests, the application needs to call the &lt;code&gt;http_server_serve&lt;/code&gt; function to which it must provide the address it wants to listen on and the index of a router that defines the supported routes. I&#39;ve decided to roll out to simplest implementation I could think of to start testing things out - the &lt;code&gt;HttpRouter&lt;/code&gt; and &lt;code&gt;HttpServer&lt;/code&gt; classes which only allow for calling the serve function (no support for routing).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class HttpRouter
{
    private uint _index;

    public uint Index =&amp;gt; _index;

    private HttpRouter(uint index)
    {
        _index = index;
    }

    public static HttpRouter Create()
    {
        http_router_new(out WasiExpected&amp;lt;uint&amp;gt; expected);

        if (expected.IsError)
        {
            throw new Exception(expected.Error?.ErrorWithDescription.ToString());
        }

        return new HttpRouter(expected.Result.Value);
    }
}

internal static class HttpServer
{
    private static uint? _index;

    public static void Serve(string address)
    {
        if (_index.HasValue)
        {
            throw new Exception(&quot;The server is already running!&quot;);
        }

        HttpRouter router = HttpRouter.Create();
        http_server_serve(
            WasiString.FromString(address),
            router.Index,
            out WasiExpected&amp;lt;uint&amp;gt; expected
        );

        if (expected.IsError)
        {
            throw new Exception(expected.Error?.ErrorWithDescription.ToString());
        }

        _index = expected.Result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allowed me to implement a simplistic application, a one-liner.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;HttpServer.Serve(&quot;0.0.0.0:80&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It worked! Every request results in 404, because there is no routing, but it worked. So, how to add support for routing?&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;http_router_*&lt;/code&gt; functions for defining routes expect two strings - one for the route and one for the handler. This suggests that the handler should be an exported symbol that Slight will be able to call. I went through the bindings exported for &lt;code&gt;http-handler.wit&lt;/code&gt; and I&#39;ve found a function that is being exported as &lt;code&gt;handle-http&lt;/code&gt;. That function seems to be what we are looking for. It performs transformations to/from the request and response objects and calls a &lt;code&gt;http_handler_handle_http&lt;/code&gt; function which has only a definition. So it looks like the &lt;code&gt;http_handler_handle_http&lt;/code&gt; implementation is a place for the application logic. To test this theory, I&#39;ve started by implementing a simple route registration method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class HttpRouter
{
    ...

    private static readonly WasiString REQUEST_HANDLER = WasiString.FromString(&quot;handle-http&quot;);

    ...

    public HttpRouter RegisterRoute(HttpMethod method, string route)
    {
        WasiExpected&amp;lt;uint&amp;gt; expected;

        switch (method)
        {
            case HttpMethod.GET:
                http_router_get(_index, WasiString.FromString(route), REQUEST_HANDLER,
                    out expected);
                break;
            case HttpMethod.PUT:
                http_router_put(_index, WasiString.FromString(route), REQUEST_HANDLER,
                    out expected);
                break;
            case HttpMethod.POST:
                http_router_post(_index, WasiString.FromString(route), REQUEST_HANDLER,
                    out expected);
                break;
            case HttpMethod.DELETE:
                http_router_delete(_index, WasiString.FromString(route), REQUEST_HANDLER,
                    out expected);
                break;
            default:
                throw new NotSupportedException($&quot;Method {method} is not supported.&quot;);
        }

        if (expected.IsError)
        {
            throw new Exception(expected.Error?.ErrorWithDescription.ToString());
        }

        return new HttpRouter(expected.Result.Value)
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next I&#39;ve registered some catch-all routes and implemented the &lt;code&gt;HandleRequest&lt;/code&gt; method as the .NET handler. It would still return a 404, but it would be mine 404.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal static class HttpServer
{
    ...

    public static void Serve(string address)
    {
        ...

        HttpRouter router = HttpRouter.Create()
                            .RegisterRoute(HttpMethod.GET, &quot;/&quot;)
                            .RegisterRoute(HttpMethod.GET, &quot;/*&quot;);
        http_server_serve(
            WasiString.FromString(address),
            router.Index,
            out WasiExpected&amp;lt;uint&amp;gt; expected
        );

        ...
    }

    private static unsafe void HandleRequest(ref HttpRequest request,
        out WasiExpected&amp;lt;HttpResponse&amp;gt; result)
    {
        HttpResponse response = new HttpResponse(404);
        response.SetBody($&quot;Handler Not Found ({request.Method} {request.Uri.AbsolutePath})&quot;);

        result = new WasiExpected&amp;lt;HttpResponse&amp;gt;(response);
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s time for some C. I went through the Mono WASI C driver and found two functions that looked the right tools for the job: &lt;code&gt;lookup_dotnet_method&lt;/code&gt; and &lt;code&gt;mono_wasm_invoke_method_ref&lt;/code&gt;. The implementation didn&#39;t seem overly complicated.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-c&quot;&gt;#include &amp;lt;string.h&amp;gt;
#include &amp;lt;wasm/driver.h&amp;gt;
#include &quot;http-handler.h&quot;

MonoMethod* handle_request_method;

void mono_wasm_invoke_method_ref(MonoMethod* method, MonoObject** this_arg_in,
                                 void* params[], MonoObject** _out_exc, MonoObject** out_result);

void http_handler_handle_http(http_handler_request_t* req,
                              http_handler_expected_response_error_t* ret0)
{
    if (!handle_request_method)
    {
        handle_request_method = lookup_dotnet_method(
            &quot;Demo.Wasm.Slight&quot;,
            &quot;Demo.Wasm.Slight&quot;,
            &quot;HttpServer&quot;,
            &quot;HandleRequest&quot;,
        -1);
    }

    void* method_params[] = { req, ret0 };
    MonoObject* exception;
    MonoObject* result;
    mono_wasm_invoke_method_ref(handle_request_method, NULL, method_params, &amp;amp;exception, &amp;amp;result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But it didn&#39;t work. The thrown exception suggested that the Mono runtime wasn&#39;t loaded. I went back to studying Mono to learn how it is being loaded. What I&#39;ve learned is that during compilation a &lt;code&gt;_start()&lt;/code&gt; function is being generated. This function performs the steps necessary to load the Mono runtime and wraps the entry point to the .NET code. I could call it, but this would mean going through the &lt;code&gt;Main&lt;/code&gt; method and retriggering &lt;code&gt;HttpServer.Serve&lt;/code&gt;, which was doomed to fail. I needed to go a level lower. By reading the code of the &lt;code&gt;_start()&lt;/code&gt; function I&#39;ve learned that it calls the &lt;code&gt;mono_wasm_load_runtime&lt;/code&gt; function. Maybe I could as well?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-c&quot;&gt;...

int mono_runtime_loaded = 0;

...

void http_handler_handle_http(http_handler_request_t* req,
                              http_handler_expected_response_error_t* ret0)
{
    if (!mono_runtime_loaded) {
        mono_wasm_load_runtime(&quot;&quot;, 0);

        mono_runtime_loaded = 1;
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now it worked. But I wasn&#39;t out of the woods yet. What I&#39;ve just learned meant that to provide dedicated handlers for routes I couldn&#39;t rely on registering dedicated methods as part of the &lt;code&gt;Main&lt;/code&gt; method flow. I could only register the routes and the handlers needed to be discoverable later, in a new context, with static &lt;code&gt;HandleRequest&lt;/code&gt; as the entry point. My thoughts went in the direction of a poor man&#39;s attribute-based routing, so I&#39;ve started with an attribute for decorating handlers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class HttpHandlerAttribute: Attribute
{
    public HttpMethod Method { get; }

    public string Route { get; }

    public HttpHandlerAttribute(HttpMethod method, string route)
    {
        Method = method;
        Route = route;
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A poor man&#39;s implementation of an attribute-based routing must have an ugly part and it is reflection. To register the routes (and later match them with handlers), the types must be scanned for methods with the attributes. In a production solution, it would be necessary to narrow the scan scope but as this is just a small demo I&#39;ve decided to keep it simple and scan the whole assembly for static, public and non-public, methods decorated with the attribute. The code supports adding multiple attributes to a single method just because it&#39;s simpler than putting proper protections in place. As you can probably guess, I did override the &lt;code&gt;Equals&lt;/code&gt; and &lt;code&gt;GetHashCode&lt;/code&gt; implementations in the attribute to ensure it behaves nicely as a dictionary key.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class HttpRouter
{
    ...

    private static readonly Type HTTP_HANDLER_ATTRIBUTE_TYPE = typeof(HttpHandlerAttribute);

    private static Dictionary&amp;lt;HttpHandlerAttribute, MethodInfo&amp;gt;? _routes;

    ...

    private static void DiscoverRoutes()
    {
        if (_routes is null)
        {
            _routes = new Dictionary&amp;lt;HttpHandlerAttribute, MethodInfo&amp;gt;();

            foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
            {
                foreach(MethodInfo method in type.GetMethods(BindingFlags.Static |
                                                             BindingFlags.Public |
                                                             BindingFlags.NonPublic))
                {
                    foreach (object attribute in method.GetCustomAttributes(
                             HTTP_HANDLER_ATTRIBUTE_TYPE, false))
                    {
                        _routes.Add((HttpHandlerAttribute)attribute, method);
                    }
                }
            }
        }
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the reflection stuff (mostly) out of the way, I could implement a method that can be called to register all discovered routes and a method to invoke a handler for a route. This implementation is not &quot;safe&quot;. I don&#39;t do any checks on the reflected &lt;code&gt;MethodInfo&lt;/code&gt; to ensure that the method has a proper signature. After all, I can only hurt myself here.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class HttpRouter
{
    ...

    internal HttpRouter RegisterRoutes()
    {
        DiscoverRoutes();

        HttpRouter router = this;
        foreach (KeyValuePair&amp;lt;HttpHandlerAttribute, MethodInfo&amp;gt; route in _routes)
        {
            router = router.RegisterRoute(route.Key.Method, route.Key.Route);
        }

        return router;
    }

    internal static HttpResponse? InvokeRouteHandler(HttpRequest request)
    {
        DiscoverRoutes();

        HttpHandlerAttribute attribute = new HttpHandlerAttribute(request.Method,
                                                                  request.Uri.AbsolutePath);
        MethodInfo handler = _routes.GetValueOrDefault(attribute);

        return (handler is null) ? null : (HttpResponse)handler.Invoke(null,
                                                                     new object[] { request });
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What remained was small modifications to the &lt;code&gt;HttpServer&lt;/code&gt; to use the new methods.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal static class HttpServer
{
    ...

    public static void Serve(string address)
    {
        ...

        HttpRouter router = HttpRouter.Create()
                            .RegisterRoutes();
        http_server_serve(
            WasiString.FromString(address),
            router.Index,
            out WasiExpected&amp;lt;uint&amp;gt; expected
        );

        ...
    }

    private static unsafe void HandleRequest(ref HttpRequest request,
        out WasiExpected&amp;lt;HttpResponse&amp;gt; result)
    {
        HttpResponse? response = HttpRouter.InvokeRouteHandler(request);

        if (!response.HasValue)
        {
            response = new HttpResponse(404);
            response.Value.SetBody(
                $&quot;Handler Not Found ({request.Method} {request.Uri.AbsolutePath})&quot;
            );
        }

        result = new WasiExpected&amp;lt;HttpResponse&amp;gt;(response.Value);
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To test this out, I&#39;ve created two simple handlers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;[HttpHandler(HttpMethod.GET, &quot;/hello&quot;)]
internal static HttpResponse HandleHello(HttpRequest request)
{
    HttpResponse response = new HttpResponse(200);
    response.SetHeaders(new[] { KeyValuePair.Create(&quot;Content-Type&quot;, &quot;text/plain&quot;) });
    response.SetBody($&quot;Hello from Demo.Wasm.Slight!&quot;);

    return response;
}

[HttpHandler(HttpMethod.GET, &quot;/goodbye&quot;)]
internal static HttpResponse HandleGoodbye(HttpRequest request)
{
    HttpResponse response = new HttpResponse(200);
    response.SetHeaders(new[] { KeyValuePair.Create(&quot;Content-Type&quot;, &quot;text/plain&quot;) });
    response.SetBody($&quot;Goodbye from Demo.Wasm.Slight!&quot;);

    return response;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it. Certainly not complete, certainly not optimal, most likely buggy, and potentially leaking memory. But it works and can be deployed to the cloud.&lt;/p&gt;
&lt;h2 id=&quot;running-a-slight-application-in-wasm-wasi-node-pool&quot;&gt;Running a Slight Application in WASM/WASI Node Pool&lt;/h2&gt;
&lt;p&gt;To deploy our Slight application to the cloud, we need an AKS Cluster with a WASM/WASI node pool. The process of setting it up hasn&#39;t changed since my &lt;a href=&quot;https://www.tpeczek.com/2022/12/experimenting-with-net-webassembly.html&quot;&gt;previous post&lt;/a&gt; and you can find all the necessary steps there. Here we can start with dockerizing our application.&lt;/p&gt;
&lt;p&gt;As we are dockerizing a Wasm application, the final image should be from &lt;em&gt;scratch&lt;/em&gt;. In the case of a Slight application, it should contain two elements: &lt;em&gt;app.wasm&lt;/em&gt; and &lt;em&gt;slightfile.toml&lt;/em&gt;. The &lt;em&gt;slightfile.toml&lt;/em&gt; is a configuration file and its main purpose is to define and provide options for the capabilities needed by the application. In our case, that&#39;s just the HTTP Server capability.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-toml&quot;&gt;specversion = &quot;0.1&quot;

[[capability]]
name = &quot;http&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;em&gt;app.wasm&lt;/em&gt; file is our application. It should have this exact name and be placed at the root of the image. To be able to publish the application, the &lt;code&gt;build&lt;/code&gt; stage in our &lt;em&gt;Dockerfile&lt;/em&gt; must install the same prerequisites as we did for local development (WASI SDK and &lt;code&gt;wasi-experimental&lt;/code&gt; workload).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-Dockerfile&quot;&gt;FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

RUN curl https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/wasi-sdk-20.0-linux.tar.gz -L --output wasi-sdk-20.0-linux.tar.gz
RUN tar -C /usr/local/lib -xvf wasi-sdk-20.0-linux.tar.gz
ENV WASI_SDK_PATH=/usr/local/lib/wasi-sdk-20.0

RUN dotnet workload install wasi-experimental

WORKDIR /src
COPY . .
RUN dotnet publish --configuration Release

FROM scratch

COPY --from=build /src/bin/Release/net8.0/wasi-wasm/AppBundle/Demo.Wasm.Slight.wasm ./app.wasm
COPY --from=build /src/slightfile.toml .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the infrastructure in place and the image pushed to the container registry, all that is needed is a deployment manifest for Kubernetes resources. It is the same as it was for a Spin application, the only difference is the &lt;code&gt;kubernetes.azure.com/wasmtime-slight-v1&lt;/code&gt; node selector.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: &quot;wasmtime-slight-v1&quot;
handler: &quot;slight&quot;
scheduling:
  nodeSelector:
    &quot;kubernetes.azure.com/wasmtime-slight-v1&quot;: &quot;true&quot;
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: slight-with-dotnet-8
spec:
  replicas: 1
  selector:
    matchLabels:
      app: slight-with-dotnet-8
  template:
    metadata:
      labels:
        app: slight-with-dotnet-8
    spec:
      runtimeClassName: wasmtime-slight-v1
      containers:
        - name: slight-with-dotnet-8
          image: crdotnetwasi.azurecr.io/slight-with-dotnet-8:latest
          command: [&quot;/&quot;]
---
apiVersion: v1
kind: Service
metadata:
  name: slight-with-dotnet-8
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  selector:
    app: slight-with-dotnet-8
  type: LoadBalancer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After applying the above manifest, you can get the service IP with &lt;code&gt;kubectl get svc&lt;/code&gt; or from the Azure Portal and execute some requests.&lt;/p&gt;
&lt;h2 id=&quot;webassembly-cloud-net-and-future-&quot;&gt;WebAssembly, Cloud, .NET, and Future?&lt;/h2&gt;
&lt;p&gt;Everything I toyed with here is either a preview or experimental, but it&#39;s a glimpse into where WebAssembly in the Cloud is heading.&lt;/p&gt;
&lt;p&gt;If I dare to make a prediction, I think that when the wasi-cloud-core proposal reaches a mature enough phase, we will see it supported on AKS (probably it will replace the Spin and Slight available in the current preview). The support for WASI in .NET will also continue to evolve and we will see a non-experimental SDK once the specification gets stable.&lt;/p&gt;
&lt;p&gt;For now, we can keep experimenting and exploring. If you want to kick-start your own exploration with what I&#39;ve described in this post, the source code is &lt;a href=&quot;https://github.com/tpeczek/demo-dotnet-on-aks-wasi-node-pool&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;postscript&quot;&gt;Postscript&lt;/h2&gt;
&lt;p&gt;Yes, I know that this post is already a little bit long, but I wanted to mention one more thing.&lt;/p&gt;
&lt;p&gt;When I was wrapping this post, I read the &lt;a href=&quot;https://devblogs.microsoft.com/dotnet/extending-web-assembly-to-the-cloud/&quot;&gt;&quot;Extending WebAssembly to the Cloud with .NET&quot;&lt;/a&gt; and learned that Steve Sanderson has also built some &lt;a href=&quot;https://github.com/SteveSandersonMS/spiderlightning-dotnet/tree/main/sample&quot;&gt;Slight samples&lt;/a&gt;. Despite that, I&#39;ve decided to publish this post. I had two main reasons for that. First,I dare to think that there is valuable knowledge in this dump of thoughts of mine. Second, those samples take a slightly different direction than mine, and I believe that the fact that you can arrive at different destinations from the same origin is one of the beautiful aspects of software development - something worth sharing.&lt;/p&gt;</description><link>http://www.tpeczek.com/2024/01/experimenting-with-net-webassembly.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-6340462120240203656</guid><pubDate>Tue, 31 Oct 2023 12:03:00 +0000</pubDate><atom:updated>2023-10-31T13:03:09.233+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure functions</category><category domain="http://www.blogger.com/atom/ns#">integration testing</category><category domain="http://www.blogger.com/atom/ns#">testcontainers</category><title>Azure Functions Integration Testing With Testcontainers</title><description>&lt;p&gt;As developers, we want reassurance that our code functions as expected. We also want to be given that reassurance as fast as possible. This is why we are writing automated tests. We also desire for those tests to be easy to run on our machine or in the worst-case scenario on the build agent as part of the continuous integration pipeline. This is something that can be challenging to achieve when it comes to Azure Functions.&lt;/p&gt;
&lt;p&gt;Depending on the language we are using for our Azure Functions, the challenges can be different. Let&#39;s take .NET (I guess I haven&#39;t surprised anyone with that choice) as an example. In the case of .NET, we can write any &lt;em&gt;unit tests&lt;/em&gt; we want (I will deliberately avoid trying to define what is that &lt;em&gt;unit&lt;/em&gt;). But the moment we try to move to &lt;em&gt;integration tests&lt;/em&gt;, things get tricky. If our Azure Function is using the in-process model, we have an option of crafting a &lt;em&gt;system under test&lt;/em&gt; based on the WebJobs host which will be good enough for some scenarios. If our Azure Function is using the isolated worker model there are only two options: accept that our tests will integrate only to a certain level and implement &lt;em&gt;Test Doubles&lt;/em&gt; or wait for the Azure Functions team to &lt;a href=&quot;https://github.com/Azure/azure-functions-dotnet-worker/issues/281&quot;&gt;implement a test worker&lt;/a&gt;. This is all far from perfect.&lt;/p&gt;
&lt;p&gt;To work around at least some of the above limitations, I&#39;ve adopted with my teams a different approach for Azure Functions integration testing - we&#39;ve started using &lt;a href=&quot;https://testcontainers.com/&quot;&gt;&lt;em&gt;Testcontainers&lt;/em&gt;&lt;/a&gt;. Testcontainers is a framework for defining through code throwaway, lightweight instances of containers, to be used in test context.&lt;/p&gt;
&lt;p&gt;We initially adopted this approach for .NET Azure Functions, but I know that teams creating Azure Functions in different languages also started using it. This is possible because the approach is agnostic to the language in which the functions are written (and the tests can be written using any language/framework supported by Testcontainers).&lt;/p&gt;
&lt;p&gt;In this post, I want to share with you the core parts of this approach. It starts with creating a Dockerfile for your Azure Functions.&lt;/p&gt;
&lt;h2 id=&quot;creating-a-dockerfile-for-an-azure-functions-container-image&quot;&gt;Creating a Dockerfile for an Azure Functions Container Image&lt;/h2&gt;
&lt;p&gt;You may already have a Dockerfile for your Azure Functions (for example if you decided to host them in Azure Container Apps or Kubernetes). From my experience, that&#39;s usually not the case. That means you need to create a Dockerfile. There are two options for doing that. You can use &lt;em&gt;Azure Functions Core Tools&lt;/em&gt; and call &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-functions/functions-core-tools-reference?tabs=v2#func-init&quot;&gt;&lt;code&gt;func init&lt;/code&gt;&lt;/a&gt; with the &lt;code&gt;--docker-only&lt;/code&gt; option, or you can create the Dockerfile manually. The Dockerfile is different for every language, so until you gain experience I suggest using the command. Once you are familiar with the structure, you will very likely end up with a modified template that you will be reusing with small adjustments. The one below is my example for .NET Azure Functions using the isolated worker model.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-Dockerfile&quot;&gt;FROM mcr.microsoft.com/dotnet/sdk:7.0 AS installer-env
ARG RESOURCE_REAPER_SESSION_ID=&quot;00000000-0000-0000-0000-000000000000&quot;
LABEL &quot;org.testcontainers.resource-reaper-session&quot;=$RESOURCE_REAPER_SESSION_ID

WORKDIR /src
COPY function-app/ ./function-app/

RUN dotnet publish function-app \
    --output /home/site/wwwroot

FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated7.0

ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

COPY --from=installer-env [&quot;/home/site/wwwroot&quot;, &quot;/home/site/wwwroot&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What&#39;s probably puzzling you right now is that label based on the provided argument. This is something very specific to Testcontainers. The above Dockerfile is for a multi-stage build, so it will generate intermediate layers. Testcontainers has a concept of &lt;em&gt;Resource Reaper&lt;/em&gt; which job is to remove Docker resources once they are no longer needed. This label is needed for the Resource Reaper to be able to track those intermediate layers.&lt;/p&gt;
&lt;p&gt;Once we have the Dockerfile we can create the test context.&lt;/p&gt;
&lt;h2 id=&quot;creating-a-container-instance-in-test-context&quot;&gt;Creating a Container Instance in Test Context&lt;/h2&gt;
&lt;p&gt;The way you create the test context depends on the testing framework you are going to use and the isolation strategy you want for that context. My framework of choice is xUnit. When it comes to the isolation strategy, it depends 😉. That said, the one I&#39;m using most often is test class. For xUnit that translates to class fixture. You can probably guess that there are also requirements when it comes to the context lifetime management. After all, we will be spinning containers and that takes time. That&#39;s why the class fixture must implement &lt;code&gt;IAsyncLifetime&lt;/code&gt; to provide support for asynchronous operations.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class AzureFunctionsTestcontainersFixture : IAsyncLifetime
{
    ...

    public AzureFunctionsTestcontainersFixture()
    { 
        ...
    }

    public async Task InitializeAsync()
    {
        ...
    }

    public async Task DisposeAsync()
    {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a couple of things that we need to do here. The first is creating an image based on our Dockerfile. For this purpose, we can use &lt;code&gt;ImageFromDockerfileBuilder&lt;/code&gt;. The minimum we need to provide is the location of the Dockerfile (directory and file name). Testcontainers provides us with some handy helpers for getting the solution, project, or Git directory. We also want to set that &lt;code&gt;RESOURCE_REAPER_SESSION_ID&lt;/code&gt; argument.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class AzureFunctionsTestcontainersFixture : IAsyncLifetime
{
    private readonly IFutureDockerImage _azureFunctionsDockerImage;

    public AzureFunctionsTestcontainersFixture()
    {
        _azureFunctionsDockerImage = new ImageFromDockerfileBuilder()
            .WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory(), String.Empty)
            .WithDockerfile(&quot;AzureFunctions-Testcontainers.Dockerfile&quot;)
            .WithBuildArgument(
                 &quot;RESOURCE_REAPER_SESSION_ID&quot;,
                 ResourceReaper.DefaultSessionId.ToString(&quot;D&quot;))
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _azureFunctionsDockerImage.CreateAsync();

        ...
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the image in place, we can create a container instance. This will require reference to the image, port binding, and wait strategy. Port binding is something that Testcontainers can almost completely handle for us. We just need to tell which port to bind and the host port can be assigned randomly. The wait strategy is quite important. This is how the framework knows that the container instance is available. We have a lot of options here: port availability, specific message in log, command completion, file existence, successful request, or &lt;code&gt;HEALTHCHECK&lt;/code&gt;. What works great for Azure Functions is a successful request to its default page.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class AzureFunctionsTestcontainersFixture : IAsyncLifetime
{
    private readonly IFutureDockerImage _azureFunctionsDockerImage;

    public IContainer AzureFunctionsContainerInstance { get; private set; }

    ...

    public async Task InitializeAsync()
    {
        await _azureFunctionsDockerImage.CreateAsync();

        AzureFunctionsContainerInstance = new ContainerBuilder()
            .WithImage(_azureFunctionsDockerImage)
            .WithPortBinding(80, true)
            .WithWaitStrategy(
                Wait.ForUnixContainer()
                .UntilHttpRequestIsSucceeded(r =&amp;gt; r.ForPort(80)))
            .Build();
        await AzureFunctionsContainerInstance.StartAsync();
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last missing part is the cleanup. We should nicely dispose the container instance and the image.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class AzureFunctionsTestcontainersFixture : IAsyncLifetime
{
    private readonly IFutureDockerImage _azureFunctionsDockerImage;

    public IContainer AzureFunctionsContainerInstance { get; private set; }

    ...

    public async Task DisposeAsync()
    {
        await AzureFunctionsContainerInstance.DisposeAsync();

        await _azureFunctionsDockerImage.DisposeAsync();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we are ready to write some tests.&lt;/p&gt;
&lt;h2 id=&quot;implementing-integration-tests&quot;&gt;Implementing Integration Tests&lt;/h2&gt;
&lt;p&gt;At this point, we can start testing our function. We need a test class using our class fixture.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class AzureFunctionsTests : IClassFixture&amp;lt;AzureFunctionsTestcontainersFixture&amp;gt;
{
    private readonly AzureFunctionsTestcontainersFixture _azureFunctionsTestcontainersFixture;

    public AzureFunctions(AzureFunctionsTestcontainersFixture azureFunctionsTestcontainersFixture)
    {
        _azureFunctionsTestcontainersFixture = azureFunctionsTestcontainersFixture;
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now for the test itself, let&#39;s assume that the function has an HTTP trigger. To build the URL of our function we can use the &lt;code&gt;Hostname&lt;/code&gt; provided by the container instance and acquire the host port by calling &lt;code&gt;.GetMappedPublicPort&lt;/code&gt;. This means that the test only needs to create an instance of &lt;code&gt;HttpClient&lt;/code&gt;, make a request, and assert the desired aspects of the response. The simplest test I could think of was to check for a status code indicating success.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class AzureFunctionsTests : IClassFixture&amp;lt;AzureFunctionsTestcontainersFixture&amp;gt;
{
    private readonly AzureFunctionsTestcontainersFixture _azureFunctionsTestcontainersFixture;

    ...

    [Fact]
    public async Task Function_Request_ReturnsResponseWithSuccessStatusCode()
    {
        HttpClient httpClient = new HttpClient();
        var requestUri = new UriBuilder(
            Uri.UriSchemeHttp,
            _azureFunctionsTestcontainersFixture.AzureFunctionsContainerInstance.Hostname,
            _azureFunctionsTestcontainersFixture.AzureFunctionsContainerInstance.GetMappedPublicPort(80),
            &quot;api/function&quot;
        ).Uri;

        HttpResponseMessage response = await httpClient.GetAsync(requestUri);

        Assert.True(response.IsSuccessStatusCode);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And Voila. This will run on your machine (assuming you have Docker) and in any CI/CD environment which build agents have Docker pre-installed (for example Azure DevOps or GitHub).&lt;/p&gt;
&lt;h2 id=&quot;adding-dependencies&quot;&gt;Adding Dependencies&lt;/h2&gt;
&lt;p&gt;What I&#39;ve shown you so far covers the scope of the function itself. This is already beneficial because it allows for verifying if dependencies are registered properly or if the middleware pipeline behaves as expected. But Azure Functions rarely exist in a vacuum. There are almost always dependencies and Testcontainers can help us with those dependencies as well. There is a wide set of &lt;a href=&quot;https://testcontainers.com/modules/&quot;&gt;preconfigured implementations&lt;/a&gt; that we can add to our test context. A good example can be storage. In the majority of cases, storage is required to run the function itself. For local development, Azure Functions are using the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?WT.mc_id=DT-MVP-5002979&quot;&gt;Azurite&lt;/a&gt; emulator and we can do the same with Testcontainers as it is available as a ready-to-use &lt;a href=&quot;https://testcontainers.com/modules/azurite/&quot;&gt;module&lt;/a&gt;. To add it to the context you just need to reference the proper NuGet package and add a couple of lines of code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class AzureFunctionsTestcontainersFixture : IAsyncLifetime
{
    ...

    public AzuriteContainer AzuriteContainerInstance { get; private set; }

    ...

    public async Task InitializeAsync()
    {
        AzuriteContainerInstance = new AzuriteBuilder().Build();
        await AzuriteContainerInstance.StartAsync();

        ...
    }

    public async Task DisposeAsync()
    {
        ...

        await AzuriteContainerInstance.DisposeAsync();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need to point Azure Functions to use this Azurite container by setting the &lt;code&gt;AzureWebJobsStorage&lt;/code&gt; parameter.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class AzureFunctionsTestcontainersFixture : IAsyncLifetime
{
    ...

    public async Task InitializeAsync()
    {
        ...

        AzureFunctionsContainerInstance = new ContainerBuilder()
            ...
            .WithEnvironment(&quot;AzureWebJobsStorage&quot;, AzuriteContainerInstance.GetConnectionString())
            ...
            .Build();

        ...
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it. Having Azurite in place also enables testing functions that use triggers and bindings based on Azure Storage. There are also ready-to-use modules for Redis, Azure Cosmos DB, Azure SQL Edge, MS SQL, Kafka, or RabbitMQ. So there is quite good out-of-the-box coverage for potential Azure Functions dependencies. Some other dependencies can be covered by creating containers yourself (for example with an unofficial &lt;a href=&quot;https://github.com/pmcilreavy/AzureEventGridSimulator&quot;&gt;Azure Event Grid simulator&lt;/a&gt;). That said, some dependencies can be only satisfied by a real thing (at least for &lt;a href=&quot;https://github.com/Azure/azure-service-bus/issues/223&quot;&gt;now&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id=&quot;a-powerful-tool-in-your-toolbox&quot;&gt;A Powerful Tool in Your Toolbox&lt;/h2&gt;
&lt;p&gt;Is Testcontainers a solution for every integration problem - no. Should Testcontainers be your default choice when thinking about integration tests - also no. But it is a very powerful tool and you should be familiar with it, so you can use it when appropriate.&lt;/p&gt;</description><link>http://www.tpeczek.com/2023/10/azure-functions-integration-testing.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-5075869972314954380</guid><pubDate>Tue, 10 Oct 2023 11:54:00 +0000</pubDate><atom:updated>2023-10-10T13:54:38.234+02:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">.net</category><category domain="http://www.blogger.com/atom/ns#">azure storage</category><category domain="http://www.blogger.com/atom/ns#">change feed</category><category domain="http://www.blogger.com/atom/ns#">cosmos db</category><category domain="http://www.blogger.com/atom/ns#">mongodb</category><title>Revisiting Various Change Feeds Consumption in .NET</title><description>&lt;p&gt;The first time I wrote about change feeds consumption from .NET (ASP.NET Core to be more precise) was back in &lt;a href=&quot;https://www.tpeczek.com/2018/05/exposing-rethinkdb-changefeed-from.html&quot;&gt;2018&lt;/a&gt; in the context of RethinkDB. It was always a very powerful concept. Having access to an ordered flow of information about changes to items is a low-entry enabler for various event-driven, stream processing, or data movement scenarios. As a result, over the years, this capability (with various name variations around the words change, stream, and feed) has found its way to many databases and sometimes even other storage services. The list includes (but is not limited to) MongoDB, RavenDB, Cosmos DB, DynamoDB or Azure Blob Storage (in preview).&lt;/p&gt;
&lt;p&gt;As I was cleaning up and updating a &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.Changefeed&quot;&gt;demo application&lt;/a&gt; that shows how to consume and expose various change feeds from ASP.NET Core, I decided to write down some notes to refresh the content from my previous posts.&lt;/p&gt;
&lt;h2 id=&quot;iasyncenumerable-as-universal-change-feed-abstraction&quot;&gt;IAsyncEnumerable as Universal Change Feed Abstraction&lt;/h2&gt;
&lt;p&gt;When I started working with change feeds over 5 years ago, I initially didn&#39;t put them behind any abstraction. I like to think that I was smart and avoided premature generalization. The abstraction came after a couple of months when I could clearly see that I was implementing the same concepts through similar components in different projects where teams were using RethinkDB, MongoDB, or Cosmos DB. The abstraction that I started advocating back then looked usually like this.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public interface IChangeFeed&amp;lt;T&amp;gt;
{
    T CurrentChange { get; }

    Task&amp;lt;bool&amp;gt; MoveNextAsync(CancellationToken cancelToken = default(CancellationToken));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In retrospect, I&#39;m happy with this abstraction, because around two or more years later, when those teams and projects started to adopt C# 8 and .NET Core 3 (or later versions), refactoring all those implementations was a lot easier. C# 8 has brought async streams, a natural programming model for asynchronous streaming data sources. Asynchronous streaming data source is exactly what change feeds are and modeling them through &lt;code&gt;IAsyncEnumerable&lt;/code&gt; results in nice and clean consumption patterns. This is why currently I advocate for using &lt;code&gt;IAsyncEnumerable&lt;/code&gt; as a universal change feed abstraction. The trick to properly using that abstraction is defining the right change representation to be returned. That should depend on change feed capabilities and actual needs in a given context. Not all change feeds are the same. Some of them can provide information on all operations performed on an item, and some only on a subset. Some can provide old and new value, and some only the old. Your representation of change should consider all that. In the samples ahead I&#39;m avoiding this problem by reducing the change representation to the changed version of the item.&lt;/p&gt;
&lt;h2 id=&quot;azure-cosmos-db-change-feed&quot;&gt;Azure Cosmos DB Change Feed&lt;/h2&gt;
&lt;p&gt;Azure Cosmos DB change feed is the second (after the RethinkDB one) I&#39;ve been writing about in the past. It&#39;s also the one which consumption has seen the most evolution through time.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.tpeczek.com/2018/08/exposing-cosmos-db-change-feed-from.html&quot;&gt;first consumption model&lt;/a&gt; was quite complicated. It required going through partition key ranges, building document change feed queries for them, and then obtaining enumerators. This whole process required managing its state, which resulted in non-trivial code. It&#39;s good that it has been deprecated as part of Azure Cosmos DB .NET SDK V2, and it&#39;s going out of support in August 2024.&lt;/p&gt;
&lt;p&gt;Azure Cosmos DB .NET SDK V3 has brought the &lt;a href=&quot;https://www.tpeczek.com/2020/01/exposing-cosmos-db-change-feed-from.html&quot;&gt;second consumption model&lt;/a&gt; based on &lt;em&gt;change feed processor&lt;/em&gt;. The whole inner workings of consuming the change feed have been enclosed within a single class, which reduced the amount of code required. But &lt;em&gt;change feed processor&lt;/em&gt; has its oddities. It requires an additional container - a lease container that deals with previously described state management. This is beneficial in complex scenarios as it allows for coordinated processing by multiple workers, but becomes an unnecessary complication for simple scenarios. It also provides only a push-based programming model. The consumer must provide a delegate to receive changes. Once again this is great for certain scenarios, but leads to awkward implementation when you want to abstract change feed as a stream.&lt;/p&gt;
&lt;p&gt;The story doesn&#39;t end there, version 3.20.0 of Azure Cosmos DB .NET SDK has introduced the third consumption model based on &lt;em&gt;change feed iterator&lt;/em&gt;. It provides a pull-based alternative to &lt;em&gt;change feed processor&lt;/em&gt; for scenarios where it&#39;s more appropriate. With the &lt;em&gt;change feed iterator&lt;/em&gt; the control over the pace of consuming the changes is given back to the consumer. State management is also optional, but it&#39;s the consumer&#39;s responsibility to persist continuation tokens if necessary. Additionally, the &lt;em&gt;change feed iterator&lt;/em&gt; brings the option of obtaining a change feed for a specific partition key.&lt;/p&gt;
&lt;p&gt;The below snippet shows a very simple consumer implementation of the &lt;em&gt;change feed iterator&lt;/em&gt; model - no state management, just starting the consumption from a certain point in time and waiting one second before polling for new changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public async IAsyncEnumerable&amp;lt;T&amp;gt; FetchFeed(
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    FeedIterator&amp;lt;T&amp;gt; changeFeedIterator = _container.GetChangeFeedIterator&amp;lt;T&amp;gt;(
        ChangeFeedStartFrom.Time(DateTime.UtcNow),
        ChangeFeedMode.LatestVersion
    );

    while (changeFeedIterator.HasMoreResults &amp;amp;&amp;amp; !cancellationToken.IsCancellationRequested)
    {
        FeedResponse&amp;lt;T&amp;gt; changeFeedResponse = await changeFeedIterator
            .ReadNextAsync(cancellationToken);

        if (changeFeedResponse.StatusCode == HttpStatusCode.NotModified)
        {
            await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
        }
        else
        {
            foreach (T item in changeFeedResponse)
            {
                yield return item;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;mongodb-change-feed&quot;&gt;MongoDB Change Feed&lt;/h2&gt;
&lt;p&gt;MongoDB is probably the most popular NoSQL choice among the teams that I&#39;ve been working with, which doesn&#39;t use cloud PaaS databases for their needs. Among its many features, it has quite powerful change feed (a.k.a. &lt;em&gt;Change Streams&lt;/em&gt;) capability.&lt;/p&gt;
&lt;p&gt;The incoming change information can cover a wide spectrum of &lt;a href=&quot;https://www.mongodb.com/docs/manual/reference/change-events/&quot;&gt;operations&lt;/a&gt; which can come from a single collection, database, or entire deployment. If the operation relates to a document, the change feed can provide the current version, the previous version, and the delta. There is also support for resume tokens which can be used to manage state if needed.&lt;/p&gt;
&lt;p&gt;One unintuitive thing when it comes to MongoDB change feed is that it&#39;s only available when you are running a &lt;em&gt;replica set&lt;/em&gt; or a &lt;em&gt;sharded cluster&lt;/em&gt;. This doesn&#39;t mean that you have to run a cluster. You can run a single instance as a replica set (even in a container), you just need the right configuration (you will find a workflow that handles such a deployment to Azure Container Instances in the &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.Changefeed&quot;&gt;demo repository&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The consumption of MongoDB change feed is available through the &lt;code&gt;Watch&lt;/code&gt; and &lt;code&gt;WatchAsync&lt;/code&gt; methods available on &lt;code&gt;IMongoCollection&lt;/code&gt;, &lt;code&gt;IMongoDatabase&lt;/code&gt;, and &lt;code&gt;IMongoClient&lt;/code&gt; instances. The below snippet watches a single collection and configures the change feed to return the current version of the document. You can also provide a pipeline definition when calling &lt;code&gt;Watch&lt;/code&gt; or &lt;code&gt;WatchAsync&lt;/code&gt; to filter the change feed (for example to monitor only specific operation types).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public async IAsyncEnumerable&amp;lt;T&amp;gt; FetchFeed(
    [EnumeratorCancellation]CancellationToken cancellationToken = default)
{
    IAsyncCursor&amp;lt;ChangeStreamDocument&amp;lt;T&amp;gt;&amp;gt; changefeed = await _collection.WatchAsync(
        new ChangeStreamOptions { FullDocument = ChangeStreamFullDocumentOption.UpdateLookup },
        cancellationToken: cancellationToken
    );

    while (!cancellationToken.IsCancellationRequested)
    {
        while (await changefeed.MoveNextAsync(cancellationToken))
        {
            IEnumerator&amp;lt;ChangeStreamDocument&amp;lt;T&amp;gt;&amp;gt;  changefeedCurrentEnumerator = changefeed
                .Current.GetEnumerator();

            while (changefeedCurrentEnumerator.MoveNext())
            {
                if (changefeedCurrentEnumerator.Current.OperationType
                    == ChangeStreamOperationType.Insert)
                {
                    yield return changefeedCurrentEnumerator.Current.FullDocument;
                }

                ...
            }
        }

        await Task.Delay(_moveNextDelay, cancellationToken);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;azure-blob-storage-change-feed&quot;&gt;Azure Blob Storage Change Feed&lt;/h2&gt;
&lt;p&gt;Azure Blob Storage is the odd one on this list because it&#39;s an object storage, not a database. Its change feed provides information about changes to blobs and blobs metadata in an entire storage account. Under the hood the change feed is implemented as a special container (yes it&#39;s visible, yes you can take a look) which is being created once you enable it. As it is a container you should consider the configuration of the retention period as it will affect your costs.&lt;/p&gt;
&lt;p&gt;There is one more important aspect of Azure Blob Storage change feed when considering its usage - latency. It&#39;s pretty slow. It can take minutes for changes to appear.&lt;/p&gt;
&lt;p&gt;From the consumption perspective, it follows the enumerator approach. You can obtain the enumerator by calling &lt;code&gt;BlobChangeFeedClient.GetChangesAsync&lt;/code&gt;. The enumerator is not infinite, it will return the changes currently available and once you process them you have to poll for new ones. This makes managing the continuation tokens required even for a local state. What is unique is that you can request changes within a specified time window.&lt;/p&gt;
&lt;p&gt;The change feed supports six events in the latest schema version. In addition to expected ones like created or deleted, there are some interesting ones like tier changed. The information never contains the item, which shouldn&#39;t be surprising as in the context of object storage this would be quite risky.&lt;/p&gt;
&lt;p&gt;The below snippet streams the change feed by locally managing the continuation token and for changes that represent blob creation, it downloads the current version of the item.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public async IAsyncEnumerable&amp;lt;T&amp;gt; FetchFeed(
    [EnumeratorCancellation]CancellationToken cancellationToken = default)
{
    string? continuationToken = null;

    TokenCredential azureCredential = new DefaultAzureCredential();

    BlobServiceClient blobServiceClient = new BlobServiceClient(_serviceUri, azureCredential);
    BlobChangeFeedClient changeFeedClient = _blobServiceClient.GetChangeFeedClient();

    while (!cancellationToken.IsCancellationRequested)
    {
        IAsyncEnumerator&amp;lt;Page&amp;lt;BlobChangeFeedEvent&amp;gt;&amp;gt; changeFeedEnumerator = changeFeedClient
            .GetChangesAsync(continuationToken)
            .AsPages()
            .GetAsyncEnumerator();

        while (await changeFeedEnumerator.MoveNextAsync())
        {
            foreach (BlobChangeFeedEvent changeFeedEvent in changeFeedEnumerator.Current.Values)
            {
                if ((changeFeedEvent.EventType == BlobChangeFeedEventType.BlobCreated)
                    &amp;amp;&amp;amp; changeFeedEvent.Subject.StartsWith($&quot;/blobServices/default/containers/{_container}&quot;))
                {
                    BlobClient createdBlobClient = new BlobClient(
                        changeFeedEvent.EventData.Uri,
                        azureCredential);

                    if (await createdBlobClient.ExistsAsync())
                    {
                        MemoryStream blobContentStream =
                            new MemoryStream((int)changeFeedEvent.EventData.ContentLength);
                        await createdBlobClient.DownloadToAsync(blobContentStream);
                        blobContentStream.Seek(0, SeekOrigin.Begin);

                        yield return JsonSerializer.Deserialize&amp;lt;T&amp;gt;(blobContentStream);
                    }
                }
            }

            continuationToken = changeFeedEnumerator.Current.ContinuationToken;
        }

        await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;there-is-more&quot;&gt;There Is More&lt;/h2&gt;
&lt;p&gt;The above samples are in no way exhaustive. They don&#39;t show all the features of given change feeds and they don&#39;t show all the change feeds out there. But they are a good start, this is why  I&#39;ve been evolving them for the past five years.&lt;/p&gt;</description><link>http://www.tpeczek.com/2023/10/revisiting-various-change-feeds.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-4717310053772857696</guid><pubDate>Tue, 05 Sep 2023 11:08:00 +0000</pubDate><atom:updated>2023-09-05T13:08:24.584+02:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure</category><category domain="http://www.blogger.com/atom/ns#">container groups</category><category domain="http://www.blogger.com/atom/ns#">container instances</category><category domain="http://www.blogger.com/atom/ns#">containers</category><category domain="http://www.blogger.com/atom/ns#">dapr</category><category domain="http://www.blogger.com/atom/ns#">sidecar</category><title>Deploying a Dapr Sidecar to Azure Container Instances</title><description>&lt;p&gt;Containers have become one of the main, if not the main, ways to modularize, isolate, encapsulate, and package applications in the cloud. The sidecar pattern allows for taking this even further by allowing the separation of functionalities like monitoring, logging, or configuration from the business logic. This is why I recommend that the teams who are adopting containers adopt sidecars as well. One of my preferred suggestions is Dapr which can bring early value by providing abstractions for message broker integration, encryption, observability, secret management, state management, or configuration management.&lt;/p&gt;
&lt;p&gt;To my surprise, many conversations starting around adopting sidecars quickly deviate to &quot;we should set up a Kubernetes cluster&quot;. It&#39;s almost like there are only two options out there - you either run a standalone container or you need Kubernetes for anything more complicated. This is not the case. There are multiple ways to run containers and you should choose the one which is most suitable for your current context. Many of those options will give you more sophisticated features like sidecars or init containers while your business logic is still in a single container. Sidecars give here an additional benefit of enabling later evolution to more complex container hosting options without requirements for code changes.&lt;/p&gt;
&lt;p&gt;In the case of Azure, such a service that enables adopting sidecars at an early stage is Azure Container Instances.&lt;/p&gt;
&lt;h2 id=&quot;quick-reminder-azure-container-instances-can-host-container-groups&quot;&gt;Quick Reminder - Azure Container Instances Can Host Container Groups&lt;/h2&gt;
&lt;p&gt;Azure Container Instances provides a managed approach for running containers in a serverless manner, without orchestration. What I&#39;ve learned is that a common misconception is that Azure Container Instances can host only a single container. That is not exactly the truth, Azure Container Instances can host a &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/container-instances/container-instances-container-groups?WT.mc_id=DT-MVP-5002979&quot;&gt;container group&lt;/a&gt; (if you&#39;re using Linux containers 😉).&lt;/p&gt;
&lt;p&gt;A container group is a collection of containers scheduled on the same host and sharing lifecycle, resources, or local network. The container group has a single public IP address, but the publicly exposed ports can forward to ports exposed on different containers. At the same time, all the containers within the group can reach each other via localhost. This is what enables the sidecar pattern.&lt;/p&gt;
&lt;p&gt;How to create a container group? There are three options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;With ARM/Bicep&lt;/li&gt;
&lt;li&gt;With Azure CLI by using YAML file&lt;/li&gt;
&lt;li&gt;With Azure CLI by using Docker compose file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&#39;ll go with Bicep here. The &lt;code&gt;Microsoft.ContainerInstance&lt;/code&gt; namespace contains only a single type which is &lt;code&gt;containerGroups&lt;/code&gt;. This means that from ARM/Bicep perspective there is no difference if you are deploying a standalone container or a container group - there is a &lt;code&gt;containers&lt;/code&gt; list available as part of the resource properties where you specify the containers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource containerGroup &#39;Microsoft.ContainerInstance/containerGroups@2023-05-01&#39; = {
  name: CONTAINER_GROUP
  location: LOCATION
  ...
  properties: {
    sku: &#39;Standard&#39;
    osType: &#39;Linux&#39;
    ...
    containers: [
      ...
    ]
    ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How about a specific example? I&#39;ve mentioned that Dapr is one of my preferred sidecars, so I&#39;m going to use it here.&lt;/p&gt;
&lt;h2 id=&quot;running-dapr-in-self-hosted-mode-within-a-container-group&quot;&gt;Running Dapr in Self-Hosted Mode Within a Container Group&lt;/h2&gt;
&lt;p&gt;Dapr has several hosting options. It can be self-hosted with Docker, Podman, or without containers. It can be hosted in Kubernetes with first-class integration. It&#39;s also available as a serverless offering - part of Azure Container Apps. The option interesting us in the context of Azure Container Instances is self-hosted with Docker, but from that list, you could pick up how Dapr enables easy evolution from Azure Container Instances to Azure Container Apps, Azure Kubernetes Services or non-Azure Kubernetes clusters.&lt;/p&gt;
&lt;p&gt;But before we will be ready to deploy the container group, we need some infrastructure around it. We should start with a resource group, container registry and managed identity.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-powershell&quot;&gt;az group create -l $LOCATION -g $RESOURCE_GROUP
az acr create -n $CONTAINER_REGISTRY -g $RESOURCE_GROUP --sku Basic
az identity create -n $MANAGED_IDENTITY -g $RESOURCE_GROUP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We will be using the managed identity for role-based access control where possible, so we should reference it as the identity of the container group in our Bicep template.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource managedIdentity &#39;Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31&#39; existing = {
  name: MANAGED_IDENTITY
}

resource containerGroup &#39;Microsoft.ContainerInstance/containerGroups@2023-05-01&#39; = {
  ...
  identity: {
    type: &#39;UserAssigned&#39;
    userAssignedIdentities: {
      &#39;${managedIdentity.id}&#39;: {}
    }
  }
  properties: {
    ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Dapr sidecar requires a components directory. It&#39;s a folder that will contain YAML files with components definitions. To provide that folder to the Dapr sidecar container, we have to mount it as a volume. Azure Container Instances supports &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/container-instances/container-instances-volume-azure-files?WT.mc_id=DT-MVP-5002979&quot;&gt;mounting an Azure file share&lt;/a&gt; as a volume, so we have to create one.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-powershell&quot;&gt;az storage account create -n $STORAGE_ACCOUNT -g $RESOURCE_GROUP --sku Standard_LRS
az storage share create -n daprcomponents --account-name $STORAGE_ACCOUNT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The created Azure file share needs to be added to the list of volumes that can be mounted by containers in the group. Sadly, the integration between Azure Container Instances and Azure file share doesn&#39;t support role-based access control, an access key has to be used.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;...

resource storageAccount &#39;Microsoft.Storage/storageAccounts@2022-09-01&#39; existing = {
  name: STORAGE_ACCOUNT
}

resource containerGroup &#39;Microsoft.ContainerInstance/containerGroups@2023-05-01&#39; = {
  ...
  properties: {
    ...
    volumes: [
      {
        name: &#39;daprcomponentsvolume&#39;
        azureFile: {
          shareName: &#39;daprcomponents&#39;
          storageAccountKey: storageAccount.listKeys().keys[0].value
          storageAccountName: storageAccount.name
          readOnly: true
        }
      }
    ]
    ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need to assign the &lt;code&gt;AcrPull&lt;/code&gt; role to the managed identity so it can access the container registry.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-powershell&quot;&gt;az role assignment create --assignee $MANAGED_IDENTITY_OBJECT_ID \
    --role AcrPull \
    --scope &quot;/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP/providers/Microsoft.ContainerRegistry/registries/$CONTAINER_REGISTRY&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;m skipping the creation of the image for the application with the business logic, pushing it to the container registry, adding its definition to the &lt;code&gt;containers&lt;/code&gt; list, and exposing needed ports from the container group - I want to focus on the Dapr sidecar.&lt;/p&gt;
&lt;p&gt;In this example, I will be grabbing the &lt;code&gt;daprd&lt;/code&gt; image from the Docker Registry.&lt;/p&gt;
&lt;p&gt;The startup command for the sidecar is &lt;code&gt;./daprd&lt;/code&gt;. We need to provide a &lt;code&gt;--resources-path&lt;/code&gt; parameter which needs to point to the path where the &lt;code&gt;daprcomponentsvolume&lt;/code&gt; will be mounted. I&#39;m also providing the &lt;code&gt;--app-id&lt;/code&gt; parameter. This parameter is mostly used for service invocation (it won&#39;t be the case here and I&#39;m not providing &lt;code&gt;--app-port&lt;/code&gt;) but Dapr is using it also in different scenarios (for example as partition key for some state stores).&lt;/p&gt;
&lt;p&gt;Two ports need to be exposed from this container (not publicly): &lt;code&gt;3500&lt;/code&gt; is the default HTTP endpoint port and &lt;code&gt;50001&lt;/code&gt; is the default gRPC endpoint port. There is an option to change both ports through configuration if they need to be taken by some other container.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;resource containerGroup &#39;Microsoft.ContainerInstance/containerGroups@2023-05-01&#39; = {
  ...
  properties: {
    ...
    containers: [
      ...
      {
        name: &#39;dapr-sidecar&#39;
        properties: {
          image: &#39;daprio/daprd:1.10.9&#39;
          command: [ &#39;./daprd&#39;, &#39;--app-id&#39;, &#39;APPLICATION_ID&#39;, &#39;--resources-path&#39;, &#39;./components&#39;]
          volumeMounts: [
            {
              name: &#39;daprcomponentsvolume&#39;
              mountPath: &#39;./components&#39;
              readOnly: true
            }
          ]
          ports: [
            { 
              port: 3500
              protocol: &#39;TCP&#39;
            }
            { 
              port: 50001
              protocol: &#39;TCP&#39;
            }
          ]
          ...
        }
      }
    ]
    ...
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;ve omitted the &lt;code&gt;resources&lt;/code&gt; definition for brevity.&lt;/p&gt;
&lt;p&gt;Now the Bicep template can be deployed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-powershell&quot;&gt;az deployment group create -g $RESOURCE_GROUP -f container-group-with-dapr-sidecar.bicep
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The below diagram visualizes the final state after the deployment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh1UiAcIO8l04HphgPJxLFB2ayIDeDwun7uoD66XJt0U5YUuUcPFt0jfIE551ISUJC4wQDaN0LeLJAF_2ezFTt75MQejpBQb8gMNX_KuGLT5-OjIY0g_5_LWABtoGoWkpTfagnRyX1BRNcMahxw5YilmrPiu6thtgc-4u7aQY54l-AzdNVq5AXXHmTNoAU/s1600/container-instance-with-dapr-integrations.png&quot; alt=&quot;Diagram of Azure Container Instances hosting a container group including application container and Dapr sidecar integrated with Azure Container Registry and having Azure file share mounted as volume with Dapr components definitions.&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;configuring-a-dapr-component&quot;&gt;Configuring a Dapr Component&lt;/h2&gt;
&lt;p&gt;We have a running Dapr sidecar, but we have yet to make it truly useful. To be able to use APIs provided by Dapr, we have to provide the mentioned earlier components definitions which will provide implementation for those APIs. As we already have a storage account as part of our infrastructure, a state store component seems like a good choice. Dapr supports quite an extensive &lt;a href=&quot;https://docs.dapr.io/reference/components-reference/supported-state-stores/&quot;&gt;list of stores&lt;/a&gt;, out of which two are based on Azure Storage: Azure Blob Storage and Azure Table Storage. Let&#39;s use the Azure Table Storage one.&lt;/p&gt;
&lt;p&gt;First I&#39;m going to create a table. This is not a required step, the component can do it for us, but let&#39;s assume we want to seed some data manually before the deployment.&lt;/p&gt;
&lt;p&gt;Second, the more important operation is granting needed permissions to the storage account. Dapr has very good support for &lt;a href=&quot;https://docs.dapr.io/developing-applications/integrations/azure/azure-authentication/authenticating-azure/&quot;&gt;authenticating to Azure&lt;/a&gt; which includes managed identities and role-based access control, so I&#39;m just going to assign the &lt;em&gt;Storage Table Data Reader&lt;/em&gt; role to our managed identity for the scope of the storage account.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-powershell&quot;&gt;az storage table create -n $TABLE_NAME --account-name $STORAGE_ACCOUNT
az role assignment create --assignee $MANAGED_IDENTITY_OBJECT_ID \
    --role &quot;Storage Table Data Contributor&quot; \
    --scope &quot;/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last thing we need is the &lt;a href=&quot;https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-azure-tablestorage/&quot;&gt;component definition&lt;/a&gt;. The component type we want is &lt;code&gt;state.azure.tablestorage&lt;/code&gt;. The name is what we will be using when making calls with a Dapr client. As we are going to use managed identity for authenticating, we should provide &lt;code&gt;accountName&lt;/code&gt;, &lt;code&gt;tableName&lt;/code&gt;, and &lt;code&gt;azureClientId&lt;/code&gt; as metadata. I&#39;m additionally setting &lt;code&gt;skipCreateTable&lt;/code&gt; because I created the table earlier and the component will fail on an attempt to create it once again.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: state.table.&amp;lt;TABLE_NAME&amp;gt;
spec:
  type: state.azure.tablestorage
  version: v1
  metadata:
  - name: accountName
    value: &amp;lt;STORAGE_ACCOUNT&amp;gt;
  - name: tableName
    value: &amp;lt;TABLE_NAME&amp;gt;
  - name: azureClientId
    value: &amp;lt;Client ID of MANAGED_IDENTITY&amp;gt;
  - name: skipCreateTable
    value: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file with the definition needs to be uploaded to the file share which is mounted as the components directory. The Azure Container Instances need to be restarted for the component to be loaded. We can quickly verify if it has been done by taking a look at logs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-txt&quot;&gt;time=&quot;2023-08-31T21:25:22.5325911Z&quot;
level=info
msg=&quot;component loaded. name: state.table.&amp;lt;TABLE_NAME&amp;gt;, type: state.azure.tablestorage/v1&quot;
app_id=APPLICATION_ID
instance=SandboxHost-638291138933285823
scope=dapr.runtime
type=log
ver=1.10.9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can start &lt;a href=&quot;https://docs.dapr.io/developing-applications/building-blocks/state-management/state-management-overview/&quot;&gt;managing your state&lt;/a&gt; with a Dapr client for your language of choice or with HTTP API if one doesn&#39;t exist.&lt;/p&gt;
&lt;h2 id=&quot;the-power-of-abstraction-decoupling-and-flexibility&quot;&gt;The Power of Abstraction, Decoupling, and Flexibility&lt;/h2&gt;
&lt;p&gt;As you can see, the needed increase in complexity (when compared to a standalone container hosted in Azure Container Instances) is not that significant. At the same time, the gain is. Dapr allows us to abstract all the capabilities it provides in the form of &lt;a href=&quot;https://docs.dapr.io/concepts/building-blocks-concept/&quot;&gt;building blocks&lt;/a&gt;. It also decouples the capabilities provided by building blocks from the components providing implementation. We can change Azure Table Storage to Azure Cosmos DB if it better suits our solution, or to AWS DynamoDB if we need to deploy the same application to AWS. We also now have the flexibility of evolving our solution when the time comes to use a more sophisticated container offering - we just need to take Dapr with us.&lt;/p&gt;</description><link>http://www.tpeczek.com/2023/09/deploying-dapr-sidecar-to-azure.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh1UiAcIO8l04HphgPJxLFB2ayIDeDwun7uoD66XJt0U5YUuUcPFt0jfIE551ISUJC4wQDaN0LeLJAF_2ezFTt75MQejpBQb8gMNX_KuGLT5-OjIY0g_5_LWABtoGoWkpTfagnRyX1BRNcMahxw5YilmrPiu6thtgc-4u7aQY54l-AzdNVq5AXXHmTNoAU/s72-c/container-instance-with-dapr-integrations.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-1458654489071666460</guid><pubDate>Tue, 01 Aug 2023 10:50:00 +0000</pubDate><atom:updated>2023-08-01T12:50:34.342+02:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure</category><category domain="http://www.blogger.com/atom/ns#">continuous monitoring</category><category domain="http://www.blogger.com/atom/ns#">continuous operations</category><category domain="http://www.blogger.com/atom/ns#">devops</category><category domain="http://www.blogger.com/atom/ns#">iac</category><category domain="http://www.blogger.com/atom/ns#">infrastructure as code</category><title>DevOps Practices for Azure Infrastructure - Continuous Operations &amp; Continuous Monitoring</title><description>&lt;p&gt;This series on implementing DevOps practices for Azure infrastructure is nearing its conclusion. The last part remaining is completing the operations side of the loop.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgC2t0-uOwZyh4sUE8sO_S96SHTiINcP18_cIV2y2JnS7W37rW3OwKckiGs3zY8Ionzim4M_K0UDJAP0e21Y0n9NRjt8T0iYJZs5HbSK98rEXh_dmtc8n5RyNpP0Gb_ELogdRh2XVP7LW5VXm1j-dmcXGIrbuGt_y0teo8PCCKr7I7g1FppPB_dFTIMDE0/s1600/devops-pipeline-tools.png&quot; alt=&quot;DevOps Pipeline With Tools for Create, Verify, Package, and Release Stages&quot;&gt;&lt;/p&gt;
&lt;p&gt;This brings focus to the last two practices on our list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Continuous Planning&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure.html&quot;&gt;Continuous Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure_27.html&quot;&gt;Continuous Delivery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure_27.html&quot;&gt;Continuous Deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/07/devops-practices-for-azure.html&quot;&gt;Continuous Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Continuous Operations&lt;/li&gt;
&lt;li&gt;Continuous Monitoring&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;continuous-operations-continuous-monitoring&quot;&gt;Continuous Operations &amp;amp; Continuous Monitoring&lt;/h2&gt;
&lt;p&gt;The &lt;em&gt;Continuous Operations&lt;/em&gt; and &lt;em&gt;Continuous Monitoring&lt;/em&gt; practices are closely tied together. They jointly serve the goal of ensuring the overall reliability, resiliency, and security of solutions. The majority of capabilities supporting that goal are within the scope of Continuous Operations practice and cover aspects like &lt;em&gt;compliance enforcement&lt;/em&gt;, &lt;em&gt;cost management&lt;/em&gt;, &lt;em&gt;proactive maintenance&lt;/em&gt;, &lt;em&gt;security posture management&lt;/em&gt;, and &lt;em&gt;intelligence-driven responses&lt;/em&gt; to operational and security events. That said, most of those capabilities can&#39;t be achieved without capabilities coming from Continuous Monitoring practice. There can be no cost management without &lt;em&gt;cost tracking&lt;/em&gt;. There is no way to have proactive maintenance and intelligence-driven responses without gathering &lt;em&gt;observability signals&lt;/em&gt;, configuring &lt;em&gt;alerts&lt;/em&gt;, and building &lt;em&gt;dashboards&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Organizations usually have the capabilities covered by Continuous Operations and Continuous Monitoring already established, but often they are not aligned with DevOps cultural philosophies. This means that implementing those practices is often about addressing gaps around automation, collaboration, continuous feedback, and continuous improvement.&lt;/p&gt;
&lt;p&gt;But before we start addressing those gaps, it&#39;s worth making sure that the capabilities have been established on the right foundations, as Azure provides a wide range of services to support us here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Azure Policy&lt;/em&gt; for compliance enforcement.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Azure Monitor&lt;/em&gt; with its insights, visualization, analytics, and response stack for gathering observability signals, configuring alerts, and building dashboards.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Microsoft Defender for Cloud&lt;/em&gt; for workload protection and security posture management.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Azure Sentinel&lt;/em&gt; for security information and event management (SIEM) as well as security orchestration, automation, and response (SOAR).&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Azure Automation&lt;/em&gt; and &lt;em&gt;Azure Logic Apps&lt;/em&gt; for automating event-based intelligence-driven responses and orchestrating proactive maintenance.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Microsoft Cost Management and Billing&lt;/em&gt; for cost tracking and management.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With the right foundations in place, we can focus on aspects that make the difference between &quot;being DevOps&quot; and &quot;not being DevOps&quot;. The most crucial one is ensuring that everyone has access to information on how the part they are responsible for is behaving in production.&lt;/p&gt;
&lt;h2 id=&quot;all-teams-need-observability-signals&quot;&gt;All Teams Need Observability Signals&lt;/h2&gt;
&lt;p&gt;As you may remember from the post on &lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure_27.html&quot;&gt;Continuous Delivery and Continuous Deployment&lt;/a&gt;, at certain sizes solutions often start moving from centralized ownership to being owned by multiple independent applications teams and an environment team. This dynamics needs to be reflected in monitoring architecture as well. A single, centralized monitoring service, although needed by the environment team, may not be sufficient. This is why mature monitoring implementations utilize resource-context observability signals and granular insights, visualization, and analytics workspaces from which the signals are later centrally aggregated. This approach enables every application team to have direct access to its signals, configure alerts and build dashboards, while the environment team still has visibility into the whole picture.&lt;/p&gt;
&lt;p&gt;This approach also enables democratization when it comes to the tools itself. The native observability stack in Azure is provided by Azure Monitor, but it no longer means that application teams are fully limited to Application Insights. If they prefer they can use Prometheus and Grafana for metrics (which is great when they need to be more cloud agnostic and looking at adopting &lt;em&gt;Open Telemetry&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnvlyLjblNfQl80KN15zBvquvqMF9eYv-x2sn0jHgo8hUatUmA70XNWfjPDvTd0yKAqq9Ca-_aww51C4piTBX83-BGBTKUOIY5Arja3llfT_QURpjheY6uvamaPMKJO-TCCgwRng3u7oGkQbrUkrQIhgwKPefixN_Wd0aYlYQAtnVemMPkqsw5I-QE2jo/s1600/Democratized%20Monitoring%20Architecture.png&quot; alt=&quot;Diagram representing a democratized monitoring architecture with dedicated workspaces for every team and teams using different tools&quot;&gt;&lt;/p&gt;
&lt;p&gt;Of course, such a democratized monitoring architecture cannot be left without governance. There need to be rules around observability signals granularity, retention, and archiving data to cool-tier storage. Otherwise, we can be very unpleasantly surprised by the cost of our monitoring implementation.&lt;/p&gt;
&lt;p&gt;Automated responses should also be exporting proper context information to the respective tools - because part of automated response should be creating a proper item in the collaboration tool to ensure continuous feedback. What item should that be? That depends on the event category.&lt;/p&gt;
&lt;h2 id=&quot;operational-events-should-create-issues&quot;&gt;Operational Events Should Create Issues&lt;/h2&gt;
&lt;p&gt;From the infrastructure perspective, there are usually two main types of operational events that are potentially interesting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Resources events&lt;/em&gt; like creation, deletion, or modification&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Alerts&lt;/em&gt; defined in Azure Monitor&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The usage of resource events often covers adding special tags, granting permissions to special groups, or reacting to delete/create/update operation fails.&lt;/p&gt;
&lt;p&gt;Alerts are usually raised when the measured state of the system deviates from what is considered a baseline. To name just a few examples, this can mean networking issues, an erroneously stopped VM, or a resource reaching its capacity.&lt;/p&gt;
&lt;p&gt;The remediation for every resource event or alert can be different. In some cases, the remediation can be fully automated (restarting a VM, truncating tables in a database, or increasing RUs for Azure Cosmos DB). In some cases, all that is needed is just a notification to deal with the problem at the earliest convenience (failure to delete a resource). There are also those cases that require waking up an engineer immediately (networking issues).&lt;/p&gt;
&lt;p&gt;In &lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure.html&quot;&gt;the first post of this series&lt;/a&gt;, I wrote that the cornerstone of implementing DevOps practices for Azure infrastructure is infrastructure as code and the Git ecosystem used for collaboration. This means that regardless if the remediation is fully automated or an engineer needs to be engaged, part of the process should be issue creation (if the remediation has been already performed that issue can be closed and exist just for tracking purposes). In the stack I&#39;ve chosen for this series, the Git ecosystem is GitHub. Integrating GitHub issue creation into the response workflow is not a huge challenge, because there is a ready-to-use &lt;a href=&quot;https://learn.microsoft.com/en-us/connectors/github?WT.mc_id=DT-MVP-5002979&quot;&gt;GitHub connector for Azure Logic Apps&lt;/a&gt;. So, if we consider alerts, this means that we can build an automated response flow by using &lt;em&gt;Azure Monitor Alerts&lt;/em&gt;, &lt;em&gt;Azure Monitor Action Group&lt;/em&gt;, and &lt;em&gt;Azure Logic Apps&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgnW7LxhdISZJ6jWhPwhSIluFWF_juzT10n4XWb_80sqo5Y_ETGusQDkyLJr6la5d9tYYjZ1ZmZHy8z5OwkDVHkqHMBI2sAHwljEcXudyGsU2RZTLmdDCyfbPpINe2qcRVIvLflJ-ZRNRjyUe_DdseaoxwNRGM1lh96b41THkVKxRak1Li-kj3nBHXIVg/s1600/alert-automated-response-flow.png&quot; alt=&quot;Diagram representing an automated response flow for an alert raised in Azure Monitor which uses Azure Logic App to create issues in GitHub and perform remediation action&quot;&gt;&lt;/p&gt;
&lt;p&gt;Almost identical flow can be built for the resources events if we use &lt;em&gt;Azure Event Grid&lt;/em&gt; in place of Azure Monitor (as Azure Event Grid supports resource groups and subscriptions as sources).&lt;/p&gt;
&lt;p&gt;This is the approach that should be applied to ensure collaboration and continuous feedback when it comes to operational events, how about security events?&lt;/p&gt;
&lt;h2 id=&quot;security-events-should-create-vulnerabilities&quot;&gt;Security Events Should Create Vulnerabilities&lt;/h2&gt;
&lt;p&gt;Security events have a specific lifecycle that falls under the responsibility of the organization&#39;s &lt;em&gt;Security Operations Center (SOC)&lt;/em&gt;. It&#39;s the SOC that uses available observability signals, CVE alerting platforms like &lt;em&gt;OpenCVE&lt;/em&gt;, and other tools to detect, investigate, and remediate threats. In the case of Azure, Azure Sentinel is the one-stop shop to build and automate this responsibility.&lt;/p&gt;
&lt;p&gt;That said, SOC usually deals with the immediate remediation of a threat. For example, SOC operator or automation may determine that to mitigate a threat a specific resource needs to be isolated because a new CVE has been disclosed. The only action performed will be isolation - the responsibility for mitigating the CVE is with the application or environment team. In such cases, the SOC operator or automation should report the specific vulnerability with context and findings in the collaboration tool. When using GitHub as the Git ecosystem for collaboration, a great way to report such vulnerabilities may be through security advisories.&lt;/p&gt;
&lt;p&gt;Security advisories facilitate the process of reporting, discussing, and fixing vulnerabilities. &lt;a href=&quot;https://docs.github.com/en/code-security/security-advisories/repository-security-advisories/creating-a-repository-security-advisory&quot;&gt;Creating security advisories&lt;/a&gt; requires &lt;em&gt;admin&lt;/em&gt; or &lt;em&gt;security manager&lt;/em&gt; role within the repository, so the integration must be designed properly to avoid excessive permissions within the organization. My approach is to &lt;a href=&quot;https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps&quot;&gt;create a GitHub App&lt;/a&gt;. GitHub Apps use OAuth 2.0 and can act on behalf of a user, which in this case will be SOC operator or automation. To make the creation of security advisories available directly from Azure Sentinel, I expose a webhook from the GitHub App which can be called by a Playbook.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaeN_31GhxWu6QJ1ekEsucfVDgH7bgnnrmK-ll8EsJKL1hsvtnw0q_SrIONfQkFNlvOuhs0jdXuETi7ckN9h3LA7PM9T5d1LKMDv-hcpsregurZFeH8OzerDgi9WEk4CCg-AB-x7GC3FQi3ht_g9kzXjUOmtG37J6bZEU526OAyTHkEjEqZE8GcfX1CzM/s1600/security-advisory-creation-flow.png&quot; alt=&quot;Diagram representing a security advisory creation flow through Azure Sentinel Playbook and GitHub App&quot;&gt;&lt;/p&gt;
&lt;p&gt;Providing automated tools which don&#39;t require context switching from the SOC perspective removes roadblocks, which is crucial for the adoption of collaboration and continuous feedback between otherwise disconnected teams. This is the true spirit of DevOps.&lt;/p&gt;
&lt;h2 id=&quot;infrastructure-drift-detection&quot;&gt;Infrastructure Drift Detection&lt;/h2&gt;
&lt;p&gt;There is one capability in the context of &lt;em&gt;Continuous Monitoring&lt;/em&gt; and &lt;em&gt;Continuous Operations&lt;/em&gt;, which is very specific to infrastructure - detecting drift.&lt;/p&gt;
&lt;p&gt;As I have shown through the series, if we want to implement DevOps practices for Azure infrastructure, the infrastructure should be changed only through modifying and deploying its code. The repository should be the single source of truth. But sometimes, when there is pressure, stress, or time constraints (for example when solving a critical issue) engineers do take shortcuts and modify the infrastructure directly. It&#39;s not that big of an issue if such an engineer will later reflect the changes in the infrastructure code. But humans are humans and they sometimes forget. This can cause the environment to drift from its source of truth and creates potential risks from applied change being reverted after the deployment to deployment failures. This is why detecting drift is important.&lt;/p&gt;
&lt;p&gt;Infrastructure drift detection is a complex problem. Depending on chosen stack there are different tools you can use to make it as sophisticated as you need. Here, as an example, I&#39;m going to show a mechanism that can be set up quickly based on the stack I&#39;ve already used throughout this series. It&#39;s far from perfect, but it&#39;s a good start. It&#39;s using the &lt;code&gt;what-if&lt;/code&gt; command, which I&#39;ve already been using for creating previews of changes as part of &lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure.html&quot;&gt;Continuous Integration&lt;/a&gt; implementation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;az deployment group what-if \
    --resource-group rg-devops-practices-sample-application-prod \
    --template-file applications/sample-application/application.bicep \
    --mode Complete \
    --no-pretty-print
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You may notice two differences between the usage of &lt;code&gt;what-if&lt;/code&gt; for previews and drift detection.&lt;/p&gt;
&lt;p&gt;The first difference is the &lt;em&gt;Complete&lt;/em&gt; deployment mode. The difference between &lt;em&gt;Incremental&lt;/em&gt; (the default) and &lt;em&gt;Complete&lt;/em&gt; deployment modes is that in the case of the second resources that exist in the resource group but aren&#39;t specified in the template will be deleted instead of ignored.&lt;/p&gt;
&lt;p&gt;The second difference is the output format. For the previews, I wanted something human-readable, but here I prefer something which will be easy to process programmatically. Providing the &lt;code&gt;--no-pretty-print&lt;/code&gt; switch changes the output format to JSON. Below you can see a snippet of it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-json&quot;&gt;{
  &quot;changes&quot;: [
    {
      &quot;after&quot;: null,
      &quot;before&quot;: {
        &quot;name&quot;: &quot;kvsampleapplication&quot;,
        ...
      },
      &quot;changeType&quot;: &quot;Delete&quot;,
      ...
    },
    {
      &quot;after&quot;: {
        &quot;name&quot;: &quot;id-sampleapplication-gd3f7mnjwpuyu&quot;,
        ...
      },
      &quot;before&quot;: {
        &quot;name&quot;: &quot;id-sampleapplication-gd3f7mnjwpuyu&quot;,
        ...
      },
      &quot;changeType&quot;: &quot;NoChange&quot;,
      ...
    },
    ...
  ],
  &quot;error&quot;: null,
  &quot;status&quot;: &quot;Succeeded&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our attention should focus on the &lt;code&gt;changeType&lt;/code&gt; property. It provides information on what will happen with the resource after the deployment. The possible values are: &lt;em&gt;Create&lt;/em&gt;, &lt;em&gt;Delete&lt;/em&gt;, &lt;em&gt;Ignore&lt;/em&gt;, &lt;em&gt;NoChange&lt;/em&gt;, &lt;em&gt;Modify&lt;/em&gt;, and &lt;em&gt;Deploy&lt;/em&gt;. Create, Delete, and NoChange are self-explanatory. The Ignore value should not be present in the case of Complete deployment mode unless limits (number of nested templates or expanding time) have been reached - in such case, it will mean that the resource hasn&#39;t been evaluated. Modify and Deploy are tricky. They mean that the properties of the resource will be changed after the deployment. Unfortunately, the &lt;em&gt;Resource Manager&lt;/em&gt; is not perfect here and those two can give false positive predictions. This is why this technique is far from perfect - the only drift that can be reliably detected are missing resources or resources which shouldn&#39;t exist. But, as I said, it&#39;s a good start as we can quickly create a GitHub Actions workflow that will be performing the detection. Let&#39;s start by checking out the deployed tag and connecting to Azure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

env:
  TAG: sample-application-v1.0.0

jobs:
  drift-detection:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
    - name: Checkout
      uses: actions/checkout@v3
      with:
        ref: ${{ env.TAG }}
    - name: Azure Login
      uses: azure/login@v1
      with:
        ...
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The next step is to run a script that will call &lt;code&gt;what-if&lt;/code&gt; and process the results to create an array of detected changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

env:
  ...
  RESOURCE_GROUP: &#39;rg-devops-practices-sample-application-prod&#39;

jobs:
  drift-detection:
    ...
    steps:
    ...
    - name: Detect infrastructure drift
      shell: pwsh
      run: |
        $issues = @()

        $drift = az deployment group what-if `
          --resource-group $env:RESOURCE_GROUP `
          --template-file applications/sample-application/application.bicep `
          --mode Complete `
          --no-pretty-print | ConvertFrom-Json

        foreach ($change in $drift.Changes)
        {
          switch ($change.changeType)
          {
            &#39;Create&#39;
            {
              $issues += @{
                ResourceName = $change.after.name
                Description = &#39;Defined resource doesn&#39;&#39;t exist&#39;
              }
            }
            &#39;Delete&#39;
            {
              $issues += @{
                ResourceName = $change.before.name
                Description = &#39;Undefined resource exists&#39;
              }
            }
          }
        }

        &#39;DRIFT_ISSUES&amp;lt;&amp;gt; $env:GITHUB_ENV
        $issues | ConvertTo-Json -AsArray &amp;gt;&amp;gt; $env:GITHUB_ENV
        &#39;EOF&#39; &amp;gt;&amp;gt; $env:GITHUB_ENV
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Having all the changes gathered, we can use the proven &lt;a href=&quot;https://github.com/marketplace/actions/github-script&quot;&gt;script action&lt;/a&gt; to create an issue for every detected change.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

jobs:
  drift-detection:
    ...
    permissions:
      ...
      issues: write
    steps:
    ...
    - name: Report detected infrastructure drift
      uses: actions/github-script@v6
      with:
        script: |
          const issues = JSON.parse(process.env.DRIFT_ISSUES);
          for (const issue of issues) {
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: &#39;[DRIFT DETECTED] &#39; + issue.Description + &#39; (&#39; + issue.ResourceName + &#39;)&#39;
            });
          }
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can have this action running regularly. It will be creating nice issues like the one in the screenshot and will give us some start in drift detection.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhxKdn0hSxDoXM5eXnapp8TVZDTp3MF9iijrOw7jumeI6VLBV3p09GFDs8eHHd4bLwNriYcozzl0hRKlSiEX0mExeoek6xSJE0Q6LSF42Pp5Hhs7PtdEtVCQxP9lOFVlsuU1i2RI83jYBvOHdEeQcKIqCwTEim6WeZQE0oDg8RxjPoiTpG99RbKMwo62oY/s1600/drift-detection-issue.png&quot; alt=&quot;Drift Detection (Sample Application) workflow - created issue for undefined resource&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-journey-never-ends&quot;&gt;The Journey Never Ends&lt;/h2&gt;
&lt;p&gt;With Continuous Operations and Continuous Monitoring practices, we have closed the loop.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBG9sAszGTLFjI7YZMv9yESEDsiNWF2tLBH_2gpostPWwnSaJiW7hO7L3_4XoUuvewgQNuQoYY9eJXc5vos-_XyuzIpd7_1upiZCSjTNxNMFrwoomznAnPC3N4MXmkJ5PF_LGjE-F0EQJ7XLbSVjSxwldFPD3zUDoTYbZEumxVfbajhq9lUkgydWhRCs4/s1600/devops-pipeline-tools.png&quot; alt=&quot;DevOps Pipeline With Tools for Create, Verify, Package, Release, Operate, and Monitor Stages&quot;&gt;&lt;/p&gt;
&lt;p&gt;But the nature of a loop is that an end is also the beginning. The implementation of DevOps is never &quot;done&quot;. It&#39;s a direct consequence of its core cultural philosophies: continuous feedback and continuous improvement. Regardless of how your initial implementation will look, you should constantly evaluate it in the context of the ecosystem around and evolve. This will mean modifying the implementation of already established practices, but also implementing new complementary ones (like &lt;em&gt;Continuous Learning&lt;/em&gt; or &lt;em&gt;Continuous Documentation&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;The goal of this series was to draw the overall picture and provide examples that will bring that picture to life. The accompanying &lt;a href=&quot;https://github.com/tpeczek/demo-devops-practices-for-azure-infrastructure&quot;&gt;repository&lt;/a&gt; contains working workflows that can kickstart your journey.&lt;/p&gt;</description><link>http://www.tpeczek.com/2023/08/devops-practices-for-azure.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgC2t0-uOwZyh4sUE8sO_S96SHTiINcP18_cIV2y2JnS7W37rW3OwKckiGs3zY8Ionzim4M_K0UDJAP0e21Y0n9NRjt8T0iYJZs5HbSK98rEXh_dmtc8n5RyNpP0Gb_ELogdRh2XVP7LW5VXm1j-dmcXGIrbuGt_y0teo8PCCKr7I7g1FppPB_dFTIMDE0/s72-c/devops-pipeline-tools.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-5903928949952130037</guid><pubDate>Tue, 11 Jul 2023 12:11:00 +0000</pubDate><atom:updated>2023-08-27T21:15:46.118+02:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure</category><category domain="http://www.blogger.com/atom/ns#">continuous testing</category><category domain="http://www.blogger.com/atom/ns#">devops</category><category domain="http://www.blogger.com/atom/ns#">iac</category><category domain="http://www.blogger.com/atom/ns#">infrastructure as code</category><title>DevOps Practices for Azure Infrastructure - Continuous Testing</title><description>&lt;p&gt;So far, as part of my series on implementing DevOps practices for Azure infrastructure, I&#39;ve walked through &lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure.html&quot;&gt;Continuous Integration&lt;/a&gt;, &lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure_27.html&quot;&gt;Continuous Delivery, and Continuous Deployment&lt;/a&gt;. In many conversations I had around implementing DevOps I&#39;ve heard an opinion that once you have CI/CD (or CI/CD/CD) you have DevOps. That&#39;s not true. DevOps is about a continuous loop of feedback, automation, collaboration, and improvement. As you can see in the picture below, those three practices give only about half of that loop and cover mostly the development side.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgj_U2rjBnenPoR6Eqgr1zAPeQUrSrssNN1-_JnYls7l4P2KEEYz8mcJ6mUljO6pit9fCIq2BcIyqj4PMVPVhogKbVZjPGSmy5lPN5WGJUuCGLDKUBXc2BfzSPgJmKHjfNmTok5hFs463_0k2SVLxtTTsSYuuSFrO2jun4rNb3cRQ3tNND2JnOVRoCF2xY/s1600/devops-pipeline-tools.png&quot; alt=&quot;DevOps Pipeline With Tools for Create, Verify, Package, and Release Stages&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is why there are more practices on the list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Continuous Planning&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure.html&quot;&gt;Continuous Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure_27.html&quot;&gt;Continuous Delivery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure_27.html&quot;&gt;Continuous Deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Continuous Testing&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/08/devops-practices-for-azure.html&quot;&gt;Continuous Operations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/08/devops-practices-for-azure.html&quot;&gt;Continuous Monitoring&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To complete the loop and speak about complete DevOps implementation, it&#39;s time to start implementing practices that provide feedback from the deployed environment to the teams and automate operations concerns. In this post, I&#39;m going to discuss Continuous Testing.&lt;/p&gt;
&lt;p&gt;The goal of Continuous Testing is to ensure quality at different stages of the development life cycle. It&#39;s a practice that applies to both sides of the loop. We have already encountered it as part of the Continuous Integration practice. It&#39;s sometimes present as part of Continuous Delivery (for example running specific tests when versions of modules referenced from the environment repository are being updated) and it should be present as part of Continuous Deployment and later in the form of after-deployment tests. The after-deployment tests are what I want to focus on.&lt;/p&gt;
&lt;p&gt;Discussing tests often revolves around discussing two aspects: tools to be used for implementation and types of tests to be implemented. Those are two main ingredients used to create a test strategy (of course a mature test strategy covers much more, but it&#39;s a discussion for a different occasion). Let&#39;s first consider the tools.&lt;/p&gt;
&lt;p&gt;There are no real special requirements when choosing tools for infrastructure tests. As long as the stack allows calling APIs it should be sufficient. Very often the applications teams are using the same tools for testing the infrastructure tied to the application which they are using for testing the application itself. The environment teams, on the other hand, are looking for specific tools which fit the ecosystem they are familiar with. A popular choice when it comes to Azure is &lt;a href=&quot;https://pester.dev/&quot;&gt;Pester&lt;/a&gt;, a test framework for Powershell. I&#39;m going to use it for examples here.&lt;/p&gt;
&lt;p&gt;What types of tests should you consider implementing? There are two which I consider a must-have - smoke tests and negative tests.&lt;/p&gt;
&lt;h2 id=&quot;smoke-tests&quot;&gt;Smoke Tests&lt;/h2&gt;
&lt;p&gt;Smoke tests should be the first tests to verify the deployment. Their goal is to quickly provide feedback on crucial functions of the system without delving into finer details. Their implementation should be fast and simple. A typical smoke test is a verification if a host is responsive, which in Pester is just a couple of lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;param(
  [Parameter(Mandatory)]
  [ValidateNotNullOrEmpty()]
  [string] $HostName
)

Describe &#39;Application Host&#39; {
    It &#39;Serves pages over HTTPS&#39; {
      $request = [System.Net.WebRequest]::Create(&quot;https://$HostName/&quot;)
      $request.AllowAutoRedirect = $false
      $request.GetResponse().StatusCode |
        Should -Be 200 -Because &quot;It&#39;s responsive&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that we are not trying to determine if the hosted application is healthy beyond getting a successful status code - we are testing infrastructure and the application hasn&#39;t been deployed yet.&lt;/p&gt;
&lt;p&gt;Running smoke tests should be the first job in our post-deployment workflow. GitHub-hosted runners come with Pester, which means that running the tests is just two lines of Powershell.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;jobs:
  smoke-tests:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Run Pester Tests
      shell: pwsh
      run: |
        $container = New-PesterContainer `
          -Path &#39;applications/sample-application/tests/smoke-tests.ps1&#39; `
          -Data @{ HostName = &#39;${{ env.APPLICATION_HOST_NAME }}&#39; }
        Invoke-Pester -Container $container -CI
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, running the tests is not a challenge. But running the tests is not the goal by itself. The goal is to properly react when tests fail. What should we do when smoke tests fail? There are two options we can choose from: roll back or roll forward. For smoke tests, we should almost always aim for rolling back. After all a crucial function of our system is not working and reverting it to the previous stable version is usually the quickest way to fix this. Of course roll back may not always be possible and then you are left with roll forward as the only option. Still, you should aim for this to be an edge case.&lt;/p&gt;
&lt;p&gt;The situation with negative tests is a little bit different.&lt;/p&gt;
&lt;h2 id=&quot;negative-tests&quot;&gt;Negative Tests&lt;/h2&gt;
&lt;p&gt;While smoke tests are providing feedback if crucial functions of the system are working as expected, negative tests are there to provide feedback on how the system will behave in invalid scenarios. A good example can be unencrypted requests over HTTP. They are not secure and we want to disable them at the host level by configuring a redirect. A negative test to verify that can look like below.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;param(
  [Parameter(Mandatory)]
  [ValidateNotNullOrEmpty()]
  [string] $HostName
)

Describe &#39;Application Host&#39; {
    It &#39;Does not serves pages over HTTP&#39; {
      $request = [System.Net.WebRequest]::Create(&quot;http://$HostName/&quot;)
      $request.AllowAutoRedirect = $false
      $request.GetResponse().StatusCode | 
        Should -Be  301 -Because &quot;Redirect is forced&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As negative tests should be independent of smoke tests and still considered important, the typical approach is to run them in parallel with smoke tests.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;jobs:
  smoke-tests:
    ...
  negative-tests:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Run Pester Tests
      shell: pwsh
      run: |
        $container = New-PesterContainer `
          -Path &#39;applications/sample-application/tests/negative-tests.ps1&#39; `
          -Data @{ HostName = &#39;${{ env.APPLICATION_HOST_NAME }}&#39; }
        Invoke-Pester -Container $container -CI
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is often a discussion about how to decide if something is a negative test or a smoke test. The distinction should be based on the impact. Taking our two examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Host not being responsive is catastrophic, we can&#39;t provide any service for our users.&lt;/li&gt;
&lt;li&gt;Host responding to HTTP requests is something we can live with for a moment. There is secondary protection in the application code and in our industry context, it&#39;s only a recommendation, not a requirement.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of course, context matters and what is a negative test in one situation might be a critical smoke test in another. The key aspect is that negative tests failing don&#39;t have to mean rolling back. The system can still provide a valuable service for users. This is why the strategy in case of negative tests is often to roll forward, fix the issue as soon as possible, and perform another deployment.&lt;/p&gt;
&lt;h2 id=&quot;other-after-deployment-tests&quot;&gt;Other After-Deployment Tests&lt;/h2&gt;
&lt;p&gt;Smoke and negative tests are crucial, but they are only scratching the surface to provide initial feedback as soon as possible. They should be followed by different types of tests which go into previously ignored finer details. Depending on the needs you should implement functional, integration, or other types of tests.&lt;/p&gt;
&lt;p&gt;You also shouldn&#39;t limit running tests only to the deployment moment. Infrastructure is much more fragile than application code, so you should continuously run at least the key tests to ensure that everything is working as expected. You should also adopt health checks (yes they are usually considered part of monitoring, but sophisticated health checks are often complex tests) to notice when something becomes unavailable or starts to misbehave. You can also go a step further and continuously test how your system will behave when something goes down by adopting chaos engineering.&lt;/p&gt;
&lt;h2 id=&quot;chaos-engineering&quot;&gt;Chaos Engineering&lt;/h2&gt;
&lt;p&gt;Chaos engineering is testing through experimenting. You can think of it as a form of exploratory testing. It&#39;s about discovering and building confidence in system resilience by exploring its reactions to infrastructure failures. &lt;/p&gt;
&lt;p&gt;The level of &#39;chaotic-ness&#39; can be very different. &lt;a href=&quot;https://github.com/netflix/chaosmonkey&quot;&gt;Chaos Monkey&lt;/a&gt;, probably the most famous chaos engineering tool, randomly terminates virtual machines and containers, but there are more structured approaches. The methodical approach to chaos engineering starts by defining a &lt;em&gt;steady state&lt;/em&gt;, a measurable set of system characteristics that indicates normal behavior. That steady state is a base of a &lt;em&gt;hypothesis&lt;/em&gt; that the system will continue in the same state after the experiment. To prove or disprove that hypothesis, an &lt;em&gt;experiment&lt;/em&gt; is designed. The design of the experiment should include faults and their targets. Once the design is complete, the experiment is executed by injecting the faults into the environment and capturing the output state. The output state is being &lt;em&gt;verified&lt;/em&gt; against the steady state. If the hypothesis has been disproven, the output state should be used for &lt;em&gt;learning&lt;/em&gt; and &lt;em&gt;improvement&lt;/em&gt; of the system. If the hypothesis has been proven, it&#39;s time to design a new experiment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgx2Qom7dEf97Y9TyCShxPZXk0X7bE4SvFg6BS3n0GG4UQvr-8WbjjYXVoTyVWY0iP3TNRIVvSx7uJwQi2tdUgJDAHTkfTxI4lDttUhLd1VvqlGGTWtzsnCRGs1pwrmUamtr7sFW_nc5Eyu_IiS6mBKDwqbi62eZJwCjYHmmLuj-rJof_kr_EUPhu6jkLs/s1600/chaos-engineering.png&quot; alt=&quot;Chaos Engineering Process (Loop: Steady State, Hypothesis, Design Experiment, Inject Faults, Verify &amp;amp; Learn, Improve)&quot;&gt;&lt;/p&gt;
&lt;p&gt;Despite being around for several years, the tooling ecosystem for chaos engineering wasn&#39;t growing as rapidly as one could wish for. That was until 2020 when AWS announced &lt;a href=&quot;https://aws.amazon.com/fis/&quot;&gt;AWS Fault Injection Simulator&lt;/a&gt;. About a year later Microsoft followed by announcing a public preview of &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/chaos-studio?WT.mc_id=DT-MVP-5002979&quot;&gt;Azure Chaos Studio&lt;/a&gt;. Adopting chaos engineering through a managed service has become an option.&lt;/p&gt;
&lt;p&gt;What does Azure Chaos Studio offer? Currently (still in preview) it provides ~30 &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/chaos-studio/chaos-studio-fault-library?WT.mc_id=DT-MVP-5002979&quot;&gt;faults and actions&lt;/a&gt; which can be applied to ~10 &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/chaos-studio/chaos-studio-fault-providers?WT.mc_id=DT-MVP-5002979&quot;&gt;targets&lt;/a&gt;. What is interesting is that Azure Chaos Studio has two types of faults: service-direct and agent-based. The service-direct run directly against resources, while agent-based enable in-guest failures on virtual machines (for example high CPU).&lt;/p&gt;
&lt;p&gt;How to adopt Azure Chaos Studio? The service provides capabilities to create experiments through ARM or Bicep. There is also &lt;a href=&quot;https://learn.microsoft.com/en-us/rest/api/chaosstudio?WT.mc_id=DT-MVP-5002979&quot;&gt;REST API&lt;/a&gt; which can be used to create, manage, and run experiments. Those capabilities can be used to implement an architecture similar to the following, with continuous experiment execution (1, 2, 3, and 4).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOBTDWQsfdBgu03HLjW0xGr7kVARIAn0sqYgoZ1-TVhSNtcNKLKWvw2OOhPfwI46dFT3Tx0lesux3XtWNj58PRcj7MouxoqPC4LRBIJyoQGlVe9jhrntM0Syc8IXbE5hATK28V6tT-68rgM7QcktbHfZ3umZ8qw8algMB3WX3CZPTc1NZLhNUrHk4P_8g/s1600/chaos-engineering-architecture.png&quot; alt=&quot;Chaos Engineering Architecture Based On GitHub&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;not-there-yet&quot;&gt;Not There Yet&lt;/h2&gt;
&lt;p&gt;With Continuous Testing we have moved a little bit toward the right side of the loop, as part of this practice starts to provide us with feedback from the living system that we can use in our cycle of improvement. Still, there is a significant portion missing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgC2t0-uOwZyh4sUE8sO_S96SHTiINcP18_cIV2y2JnS7W37rW3OwKckiGs3zY8Ionzim4M_K0UDJAP0e21Y0n9NRjt8T0iYJZs5HbSK98rEXh_dmtc8n5RyNpP0Gb_ELogdRh2XVP7LW5VXm1j-dmcXGIrbuGt_y0teo8PCCKr7I7g1FppPB_dFTIMDE0/s1600/devops-pipeline-tools.png&quot; alt=&quot;DevOps Pipeline With Tools for Create, Verify, Package, and Release Stages&quot;&gt;&lt;/p&gt;
&lt;p&gt;There are practices that I haven&#39;t touched yet, which are focusing on the missing part - &lt;em&gt;Continuous Operations&lt;/em&gt; and &lt;em&gt;Continuous Monitoring&lt;/em&gt;. It&#39;s quite likely that they are already present in your organization, just not providing feedback to the loop. This is the journey further and I intend to go there in the next post.&lt;/p&gt;
&lt;p&gt;You can find samples for some of the aspects I&#39;m discussing here on &lt;a href=&quot;https://github.com/tpeczek/demo-devops-practices-for-azure-infrastructure&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</description><link>http://www.tpeczek.com/2023/07/devops-practices-for-azure.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgj_U2rjBnenPoR6Eqgr1zAPeQUrSrssNN1-_JnYls7l4P2KEEYz8mcJ6mUljO6pit9fCIq2BcIyqj4PMVPVhogKbVZjPGSmy5lPN5WGJUuCGLDKUBXc2BfzSPgJmKHjfNmTok5hFs463_0k2SVLxtTTsSYuuSFrO2jun4rNb3cRQ3tNND2JnOVRoCF2xY/s72-c/devops-pipeline-tools.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-7011756510786474279</guid><pubDate>Tue, 27 Jun 2023 07:06:00 +0000</pubDate><atom:updated>2023-08-27T21:15:08.562+02:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure</category><category domain="http://www.blogger.com/atom/ns#">continuous delivery</category><category domain="http://www.blogger.com/atom/ns#">continuous deployment</category><category domain="http://www.blogger.com/atom/ns#">devops</category><category domain="http://www.blogger.com/atom/ns#">iac</category><category domain="http://www.blogger.com/atom/ns#">infrastructure as code</category><title>DevOps Practices for Azure Infrastructure - Continuous Delivery &amp; Continuous Deployment</title><description>&lt;p&gt;In my &lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure.html&quot;&gt;previous post&lt;/a&gt;, I started the journey of implementing &lt;em&gt;DevOps&lt;/em&gt; practices for infrastructure. I&#39;ve proposed implementation for &lt;em&gt;Continuous Integration&lt;/em&gt; practice, which covers the &lt;em&gt;Create&lt;/em&gt; and &lt;em&gt;Verify&lt;/em&gt; stages of the DevOps pipeline.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcJbRiWRqsqCfK6dqzkSzN1UL_Aj3yvCPknat2esTFI3v_WkyXeSCASur9kP0tGR8HCQvaQ95UDPNQLnFQTv8u4Cllvr-rXrBZ_SRauXlJYUXhqgNeefZDiX06lJEXlNKYRn7fOkW_ij9a2iVo_Ucx4SHGADMxQgy2OkpdVrI3Z4fs7Lc8Iic560Z8/s1600/devops-pipeline-tools.png&quot; alt=&quot;DevOps Pipeline With Tools for Create and Verify Stages&quot;&gt;&lt;/p&gt;
&lt;p&gt;But Continuous Integration is just the first of several practices which should be implemented for a complete pipeline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Continuous Planning&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure.html&quot;&gt;Continuous Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Continuous Delivery&lt;/li&gt;
&lt;li&gt;Continuous Deployment&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/07/devops-practices-for-azure.html&quot;&gt;Continuous Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/08/devops-practices-for-azure.html&quot;&gt;Continuous Operations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/08/devops-practices-for-azure.html&quot;&gt;Continuous Monitoring&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this post, I want to focus on &lt;em&gt;Continuous Delivery&lt;/em&gt; and &lt;em&gt;Continuous Deployment&lt;/em&gt; practices which are there to pick up where Continuous Integration has finished and continue through the &lt;em&gt;Package&lt;/em&gt; and &lt;em&gt;Release&lt;/em&gt; stages of the pipeline.&lt;/p&gt;
&lt;h2 id=&quot;continuous-delivery-vs-continuous-deployment&quot;&gt;Continuous Delivery vs. Continuous Deployment&lt;/h2&gt;
&lt;p&gt;Quite often, when I&#39;m discussing &lt;em&gt;Software Development Life Cycle&lt;/em&gt; with teams, there is confusion around Continuous Delivery and Continuous Deployment. Teams will often say that they are doing CI/CD and when I ask about the CD part the terms Continuous Delivery and Continuous Deployment are being used interchangeably. A lot of marketing &lt;em&gt;&quot;What is DevOps&quot;&lt;/em&gt; articles also don&#39;t help by confusing the terms. So what is the difference?&lt;/p&gt;
&lt;p&gt;In short, Continuous Delivery is about making artifacts ready for deployment and Continuous Deployment is about actually deploying them. That seems to be quite a clear separation, so why the confusion? Because in the real world, they often blend. In an ideal scenario, when the Continuous Integration workflow is finished, the deployment workflow can kick off automatically and get the changes to the production. In such a scenario, the Continuous Delivery may not be there, and if it is there it will be considered an implicit part of Continuous Deployment. This is where the terms are often misused - the separation is not clear. Continuous Delivery exists in explicit form only when there is some kind of handover or different step between &lt;em&gt;&quot;packaging&quot;&lt;/em&gt; and &lt;em&gt;&quot;deployment&quot;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Why am I discussing this here? Because when it comes to infrastructure, especially for solutions of considerable size, there often is a need for separated Continuous Delivery and Continuous Deployment. Where does this need come from? From different responsibilities. For large solutions, there is infrastructure responsible for the overall environment and infrastructure tied to specific applications. That means multiple teams are owning different parts of the infrastructure and working on it independently. But from a governance and security perspective, there is often a desire to treat the entire infrastructure as one. Properly implemented Continuous Delivery and Continuous Deployment can solve this conflict, but before I move to discuss the practices I need to extend the context by discussing the repositories structure for such solutions.&lt;/p&gt;
&lt;h2 id=&quot;structuring-repositories&quot;&gt;Structuring Repositories&lt;/h2&gt;
&lt;p&gt;How repositories of your solutions are structured has an impact on the implementation of your DevOps practices. Very often projects start small with a monorepo structure.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjRLbltr_x4xnNWKppvexWXTljPQGLs0qpFNyguYnWLAZsoPdxPdcrkNaXp0wJ_5WOJX3MX2O-waLvzMLTXzDyjB9kw7KGMy23i496GqHquqsElv_yfwn2E0R3kP34I_zX89OChJ7Z9rlFc5lO7jfoyYrKQlizPugP7E41ViJVOB0iRqp4bRb5N2JHcjhg/s1600/monorepo.png&quot; alt=&quot;Monorepo&quot;&gt;&lt;/p&gt;
&lt;p&gt;There is nothing wrong with monorepo structure. It can be the only structure you will ever need. The main benefit of monorepo is that it&#39;s simple. All your code lives in one place, you can iterate fast, it&#39;s easy to govern and you can implement just a single set of DevOps practices. But there is a point at which those advantages are becoming limitations. This point comes when the solutions grow to consist of multiple applications owned by different teams. Sooner or later those teams start to ask for some level of independence. They want to have a little bit different governance rules (which better suit their culture) and they don&#39;t want to be blocked by work being done by other teams. Sometimes just the size of monorepo becomes a productivity issue. This is where the decoupling of the monorepo starts. Usually, the first step is that new applications are being created in their own repositories. Later, the existing ones are moved out from the monorepo. The outcome is multiple, independent repositories.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhJ3WpJWQ2q-2wKWN46jUvd9tA-6WFNw-UZYwsEFoaGOALjuM07kvYeh2qxBuo4dBhgMNNOtuhaxWcC9eszIf8QpNh8GIECcNt6BbcJGi7azIctcF-0qTuQ7HZyT_25J3O_kGjkvRqBdMlNhck4RHpSiCeDCx_omHejQIqcXwB3AN2dOdrvbTADajL6iNg/s1600/repo-per-app.png&quot; alt=&quot;Repositories per Application Without Dedicated Environment Repository&quot;&gt;&lt;/p&gt;
&lt;p&gt;But, this structure has some problems of its own. There is no longer a single source of truth that would represent the entire solution. Establishing governance rules which are required for the entire solution is harder. There are multiple sources of deployments which means access to the environment from multiple places, which means increased security risk. There is a need for balance between those aspects and teams needs in areas of flexibility and productivity. A good option for such a balance is having applications repositories and a dedicated environment repository.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi_u77nMztAZma2u0bXtdM-qcy4zjjITDdGvCA3dgBocQnESMditldJ20BZCye1N5pARF-n5CtjjNsXvk2UKPPVjlvpxAdIhNo2L5tNu_pm91jnW3SNB0rP268FPNdMZ19Mw_p3fUBrohRlTXGitovNPp7BctYrxfCTlPAHXC26i0PwRLswHPbhgKb3iqE/s1600/repo-for-environment-and-per-app.png&quot; alt=&quot;Repositories per Application and Dedicated Environment Repository&quot;&gt;&lt;/p&gt;
&lt;p&gt;The environment repository is the single source of truth. It&#39;s also the place to apply required governance rules and the only source of deployments. It can also hold the shared infrastructure. The applications repositories are owned by the applications teams and contain the source code for the application as well as the infrastructure code tied to it. This is the structure we will focus on because this is the structure that requires Continuous Delivery and Continuous Deployment. The applications teams should implement Continuous Delivery for packaging the infrastructure to be used by the environment repository, while the team responsible for the environment repository should implement Continuous Deployment. Let&#39;s start with Continuous Delivery.&lt;/p&gt;
&lt;h2 id=&quot;continuous-delivery-for-applications-infrastructure&quot;&gt;Continuous Delivery for Applications Infrastructure&lt;/h2&gt;
&lt;p&gt;The first idea for Continuous Delivery implementation can be simply copying the infrastructure code to the environment repository. The allure of this approach is that the environment repository will contain the complete code of the infrastructure removing any kind of context switching. The problem is that now the same artifacts live in two places and the sad truth is that when the same artifacts live in two places sooner or later something is going to get messed up. So, instead of copying the infrastructure code a better approach is to establish links from the environment module to the applications modules.&lt;/p&gt;
&lt;p&gt;Options for linking from the environment module to applications modules strongly depend on chosen infrastructure as code tooling. Some tools support a wide variety of sources for the links starting with linking directly to git repositories (so Continuous Delivery can be as simple as creating a tag and updating a reference in the environment module). In rare cases, when there is no support for linking by the tooling, you can always use git submodules.&lt;/p&gt;
&lt;p&gt;In the case of Bicep, there is one interesting option - using &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/private-module-registry?WT.mc_id=DT-MVP-5002979&quot;&gt;Azure Container Registry&lt;/a&gt;. This option can be attractive for two reasons. One is the possibility to create a private, isolated registry. The other is treating infrastructure the same way we would treat containers (so if you are using containers, both are treated similarly).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhUW0_KmI4DMtCyNnlDkpUieifdkkNwEsZn8MMRa3Y7ka6fF9-V1MXOnAtjys6-TbOIuO4sQI-wdqYum6FGSgmAOZmpJ1NvSsfdneQ4BuSsBO3Gidg5_wMggkTnVvlrRo1VbefoAfXCKVgNSceh8rEgjTLkNY5uhDG0jqhGtYpiAnOoM0fqfyItwz2uAe0/s1600/repo-for-environment-and-per-app-with-modules.png&quot; alt=&quot;Repositories per Application and Dedicated Environment Repository With Links for Modules&quot;&gt;&lt;/p&gt;
&lt;p&gt;The publishing of bicep files is available through the &lt;code&gt;az bicep publish&lt;/code&gt; command. We can create a workflow around this command. A good trigger for this workflow may be the creation of a tag. We can even extract the version from the tag name which we will later use to publish the module.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;name: Continuous Delivery (Sample Application)
on:
  push:
    tags:
    - &quot;sample-application-v[0-9]+.[0-9]+.[0-9]+&quot;

...

jobs:
  publish-infrastructure-to-registry:
    runs-on: ubuntu-latest
    steps:
    - name: Extract application version from tag
      run: |
        echo &quot;APPLICATION_VERSION=${GITHUB_REF/refs\/tags\/sample-application-v/}&quot; &amp;gt;&amp;gt; $GITHUB_ENV
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now all that needs to be done is checking-out the repository, connecting to Azure, and pushing the module.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

env:
  INFRASTRUCTURE_REGISTRY: &#39;crinfrastructuremodules&#39;

jobs:
  publish-infrastructure-to-registry:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
    ...
    - name: Checkout
      uses: actions/checkout@v3
    - name: Azure Login
      uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    - name: Publish application Bicep to infrastructure registry
      run: |
        bicep publish  \
          applications/sample-application/application.bicep  \
          --target br:${INFRASTRUCTURE_REGISTRY}.azurecr.io/infrastructure/applications/sample-application:${APPLICATION_VERSION}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The linking itself is to be done in the environment infrastructure Bicep file. The &lt;code&gt;module&lt;/code&gt; syntax allows the module path to be either a local file or a file in a registry. This is the part that applications teams will be contributing to the environment repository - the module definition.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bicep&quot;&gt;...

resource sampleApplicationResourceGroupReference &#39;Microsoft.Resources/resourceGroups@2022-09-01&#39; = {
  name: &#39;rg-devops-practices-sample-application-prod&#39;
  location: environmentLocation
}

module sampleApplicationResourceGroupModule &#39;br:crinfrastructuremodules.azurecr.io/infrastructure/applications/sample-application:1.0.0&#39; = {
  name: &#39;rg-devops-practices-sample-application-rg&#39;
  scope: resourceGroup(sampleApplicationResourceGroupReference.name)
}

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the Continuous Deployment practice for the environment can be implemented.&lt;/p&gt;
&lt;h2 id=&quot;continuous-deployment-for-environment-infrastructure&quot;&gt;Continuous Deployment for Environment Infrastructure&lt;/h2&gt;
&lt;p&gt;There are two deployment strategies that you may have heard of in the context of deploying infrastructure: &lt;em&gt;push-based&lt;/em&gt; deployment and &lt;em&gt;pull-based&lt;/em&gt; deployment.&lt;/p&gt;
&lt;p&gt;The push-based deployment is what one could call a classic approach to deployment. You implement a workflow that pushes the changes to the environment. That workflow is usually triggered as a result of changes to the code.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgaaQPWW0zJGZiyGTYNdoYv6pUWBlhegmylD7cuSYxYnibsQG4GVtGah-Qn7D5Oi-y3MD6Y2X-OHGUukInW8AF2TIC1QvOLk3JSKMA0kIAhSBkEjIfneotAvNXQYNoH9T5g7xU21WbV6JRBPOVF_RE5jsBFMH4wdKPFt_g2VYsEzCwz0ae3aYW-8gynaOo/s1600/push-based-deployment-strategy.png&quot; alt=&quot;The Push-based Deployment Strategy&quot;&gt;&lt;/p&gt;
&lt;p&gt;The pull-based deployment strategy is the newer approach. It introduces an &lt;em&gt;operator&lt;/em&gt; in place of the workflow. The operator monitors the repository and the environment and reconciles any differences to maintain the infrastructure as described in the environment repository. That means it will not only react to changes done to the code but also changes applied directly to the environment protecting it from drifting (at least in theory).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgNBvlnQ_RHtyldoWsVP0-NYKPfyf6lcWcBQCW7qtG5yuwPD9MtthC44siSpaMhcGknGXtmHHNURceCpQKGr_xnRUAPgxholg8fHLmHLxt9XSqj3gY2eNoI6wbpMF-rINg7_UstQ4FAvjRV12byphYYbs1_0N1k19sj6cO6P8fZ71hGMatPjzoFPVyZZws/s1600/pull-based-deployment-strategy.png&quot; alt=&quot;The Pull-based Deployment Strategy&quot;&gt;&lt;/p&gt;
&lt;p&gt;The pull-based deployment strategy has found the most adoption in the Kubernetes space with two ready-to-use operators (&lt;em&gt;Flux&lt;/em&gt; and &lt;em&gt;Argo CD&lt;/em&gt;). When it comes to general Azure infrastructure, the push-based strategy is still the way to go, although there is a way to have a pull-based deployment for Azure resources that are tied to applications hosted in Kubernetes. &lt;a href=&quot;https://azure.github.io/azure-service-operator/&quot;&gt;Azure Service Operator&lt;/a&gt; for Kubernetes provides &lt;em&gt;Custom Resource Definitions&lt;/em&gt; for deploying Azure resources, enabling a unified experience for application teams.&lt;/p&gt;
&lt;p&gt;In the scope of this post, I&#39;m going to stick with a typical push-based deployment, which means checking-out the repository, connecting to Azure, and deploying infrastructure based on the Bicep file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;name: Continuous Deployment (Environment)
...

jobs:
  deploy-environment:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Azure Login
      uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    - name: Deploy Environment
      uses: azure/arm-deploy@v1
      with:
        scope: &#39;subscription&#39;
        region: &#39;westeurope&#39;
        template: &#39;environment/environment.bicep&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;the-journey-continues&quot;&gt;The Journey Continues&lt;/h2&gt;
&lt;p&gt;The same as with the previous post, this one also just scratches the surface. There are many variations possible, depending on your needs. It can also by further automated - for example the Continuous Delivery implementation can be automatically creating a pull request to the environment repository. Part of the DevOps culture is continuous improvement and that also means improving the practices implementations itself.&lt;/p&gt;
&lt;p&gt;Our journey is also not over yet, there are a couple more practices I would like to explore in the next posts, so the loop is complete.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgj_U2rjBnenPoR6Eqgr1zAPeQUrSrssNN1-_JnYls7l4P2KEEYz8mcJ6mUljO6pit9fCIq2BcIyqj4PMVPVhogKbVZjPGSmy5lPN5WGJUuCGLDKUBXc2BfzSPgJmKHjfNmTok5hFs463_0k2SVLxtTTsSYuuSFrO2jun4rNb3cRQ3tNND2JnOVRoCF2xY/s1600/devops-pipeline-tools.png&quot; alt=&quot;DevOps Pipeline With Tools for Create, Verify, Package, and Release Stages&quot;&gt;&lt;/p&gt;
&lt;p&gt; If you want to play with the workflows, they are sitting on &lt;a href=&quot;https://github.com/tpeczek/demo-devops-practices-for-azure-infrastructure&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</description><link>http://www.tpeczek.com/2023/06/devops-practices-for-azure_27.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcJbRiWRqsqCfK6dqzkSzN1UL_Aj3yvCPknat2esTFI3v_WkyXeSCASur9kP0tGR8HCQvaQ95UDPNQLnFQTv8u4Cllvr-rXrBZ_SRauXlJYUXhqgNeefZDiX06lJEXlNKYRn7fOkW_ij9a2iVo_Ucx4SHGADMxQgy2OkpdVrI3Z4fs7Lc8Iic560Z8/s72-c/devops-pipeline-tools.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-634270584827131338</guid><pubDate>Mon, 12 Jun 2023 13:59:00 +0000</pubDate><atom:updated>2023-08-27T21:14:16.385+02:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure</category><category domain="http://www.blogger.com/atom/ns#">continuous integration</category><category domain="http://www.blogger.com/atom/ns#">devops</category><category domain="http://www.blogger.com/atom/ns#">iac</category><category domain="http://www.blogger.com/atom/ns#">infrastructure as code</category><title>DevOps Practices for Azure Infrastructure  - Continuous Integration</title><description>&lt;p&gt;The generally adopted definition of &lt;em&gt;DevOps&lt;/em&gt; methodology says that it&#39;s a combination of cultural philosophies, practices, and tools that increases an organization’s ability to deliver solutions. That&#39;s very broad. So broad, that initial adoption in the case of many organizations has focused on applying it only to application code. This has led to the naming of several additional methodologies to either further blend DevOps with additional areas or focus on previously neglected aspects: &lt;em&gt;DevSecOps&lt;/em&gt;, &lt;em&gt;FinOps&lt;/em&gt;, &lt;em&gt;DataOps&lt;/em&gt;, &lt;em&gt;GitOps&lt;/em&gt;, or &lt;em&gt;MLOps&lt;/em&gt;. But, regardless of the flavor, the core remains the same. This is why, although I&#39;m writing about DevOps in the context of infrastructure, I have avoided using GitOps in the title.&lt;/p&gt;
&lt;p&gt;If you look for a definition of GitOps, you may find statements like &lt;em&gt;&quot;GitOps is a process of automating IT infrastructure using infrastructure as code and software development best practices&quot;&lt;/em&gt; or &lt;em&gt;&quot;GitOps is a subset of DevOps&quot;&lt;/em&gt;. In reality, the term has been strongly tied to a specific ecosystem and specific tools. I don&#39;t want to fight with those associations. Instead, I want to focus on the essence - applying DevOps practices to infrastructure.&lt;/p&gt;
&lt;h2 id=&quot;devops-practices&quot;&gt;DevOps Practices&lt;/h2&gt;
&lt;p&gt;DevOps practices are a way to bring DevOps cultural philosophies (collaboration, automation, continuous feedback, continuous improvement, etc.) to life. They are used to implement all the stages of the DevOps pipeline:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjG80EZCZkHLsjeIOGWfPG0YAa2_XFNYevuEwBBoUk9wob-LpV4WaEB6F2YQuSNUX8NXS73IYFUDYtLS3j3pSTLliUyMHEUmJQBDmbe8jVDGKOdEbihOirH_T261JWH98wU1jXPcZSsauk8UzejSKJrvvttXXvNC4xz8xOlSiVI59uu1s-2jsbL4wMe/s1600/devops-pipeline.png&quot; alt=&quot;DevOps Pipeline&quot;&gt;&lt;/p&gt;
&lt;p&gt;You may find slightly different lists of those practices, this is the one I prefer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Continuous Planning&lt;/li&gt;
&lt;li&gt;Continuous Integration&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure_27.html&quot;&gt;Continuous Delivery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/06/devops-practices-for-azure_27.html&quot;&gt;Continuous Deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/07/devops-practices-for-azure.html&quot;&gt;Continuous Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/08/devops-practices-for-azure.html&quot;&gt;Continuous Operations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2023/08/devops-practices-for-azure.html&quot;&gt;Continuous Monitoring&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this post, I want to focus on &lt;em&gt;Continuous Integration&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;continuous-integration-practice&quot;&gt;Continuous Integration Practice&lt;/h2&gt;
&lt;p&gt;On some occasions, I&#39;ve heard an opinion that Continuous Integration is about merging the changes. The truth is that correctly implemented Continuous Integration practice covers the &lt;em&gt;Create&lt;/em&gt; and &lt;em&gt;Verify&lt;/em&gt; stages of the DevOps pipeline.&lt;/p&gt;
&lt;p&gt;It shouldn&#39;t be a surprise that the cornerstone of the Create stage in the case of infrastructure is infrastructure as code, the tooling used for development, and the Git ecosystem used for collaboration. This is something that is already widely adopted with many options to choose from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Terraform, Bicep, or Pulumi just to name some popular infrastructure as code options.&lt;/li&gt;
&lt;li&gt;GitHub, Azure DevOps, or GitLab as potential Git ecosystems.&lt;/li&gt;
&lt;li&gt;VS Code, Neovim, or JetBrains Fleet as possible development environments.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The above list is in no way exhaustive. I also don&#39;t aim at discussing the superiority of one tool over another. That said, discussing the Verify stage, which is the more challenging part of Continuous Integration practice, will be better done with specific examples. This is why I must choose a stack and I&#39;m going to choose Bicep (as I&#39;m writing this in the context of Azure) and GitHub (because it has some nice features which will make my life easier).&lt;/p&gt;
&lt;p&gt;So, once we have our infrastructure as code created, what should we consider as verification from the perspective of Continuous Integration? In the beginning, I quoted a statement saying that GitOps is about using software development best practices in the process of automating infrastructure. What would be the first thing one would do with an application code to verify it? Most likely build it.&lt;/p&gt;
&lt;h2 id=&quot;building-and-linting-for-infrastructure-code&quot;&gt;Building and Linting for Infrastructure Code&lt;/h2&gt;
&lt;p&gt;Building or compiling application code is the first step of the Verify stage. In the software development context, it&#39;s sometimes thought of as a way to generate the binaries (and it is), but it&#39;s also verifying if the code is syntactically correct. In the context of IaC, it means checking for the correct use of language keywords and that resources are defined according to the requirements for their type. This is something that IaC tooling should always support out of the box. Bicep provides this capability through &lt;code&gt;az bicep build&lt;/code&gt; command, which we can simply run as a step in a workflow.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

jobs:
  build-and-lint:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Build and lint Bicep
      run: |
        az bicep build --file applications/sample-application/application.bicep
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;az bicep build&lt;/code&gt; command also performs a second activity, which is closely tied to building/compiling - it runs &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/linter?WT.mc_id=DT-MVP-5002979&quot;&gt;linter&lt;/a&gt; over the template. The goal of linting is to help enforce best practices and coding standards based on defined rules. Best practices and coding standards are something that sometimes needs to be tailored to a specific team and organization, this is why Bicep allows for the configuration of rules severity through the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-config-linter?WT.mc_id=DT-MVP-5002979&quot;&gt;&lt;code&gt;bicepconfig.json&lt;/code&gt;&lt;/a&gt; file. Possible options are &lt;code&gt;Error&lt;/code&gt;, &lt;code&gt;Warning&lt;/code&gt;, &lt;code&gt;Info&lt;/code&gt;, and &lt;code&gt;Off&lt;/code&gt;. By default, the majority of rules are set to either &lt;code&gt;Warning&lt;/code&gt; or &lt;code&gt;Off&lt;/code&gt;. The typical adjustment which I almost always do is bumping &lt;code&gt;No unused parameters&lt;/code&gt; to &lt;code&gt;Error&lt;/code&gt; and enabling &lt;code&gt;Use recent API versions&lt;/code&gt; (as it is &lt;code&gt;Off&lt;/code&gt; by default).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-json&quot;&gt;{
    &quot;analyzers&quot;: {
        &quot;core&quot;: {
            &quot;enabled&quot;: true,
            &quot;rules&quot;: {
                ...
                &quot;no-unused-params&quot;: {
                    &quot;level&quot;: &quot;error&quot;
                },
                ...
                &quot;use-recent-api-versions&quot;: {
                     &quot;level&quot;: &quot;warning&quot;
                },
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;bicepconfig.json&lt;/code&gt; file should be committed to the repository, which will ensure that the local development environment will pick up the same configuration. This includes VS Code (if the Bicep extension is installed), enabling immediate feedback for engineers (in the spirit of DevOps cultural philosophies). Of course, engineers can ignore that feedback or simply use tooling which doesn&#39;t provide it, but then the &lt;code&gt;Build and lint Bicep&lt;/code&gt; step of the integration workflow will catch it and give them that feedback.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwpTVFm_9fU88cFlJknSAL57qU-1E9YBI8t57qhNa8zrpIl8K_CyHkdHUHieIGxYu9z2IRp9UCcalRq95jtekAIquV8sWbrk-uh0_vpiJiqonXZ2atPV_vyuNzfHPA7EYauGfTMcaPU0fF3Z8nNr0tzxKQWaLkk40HD4Fa5Ku6xuUflrkMot-g5TXk/s1600/continuous-integration-build-and-lint.png&quot; alt=&quot;Continuous Integration (Sample Application) workflow - output of Build and lint Bicep step&quot;&gt;&lt;/p&gt;
&lt;p&gt;If everything is correct, the workflow should move to the next phase, which doesn&#39;t mean we should be done with looking at the code itself. Following the software development best practices, the next phase should be static analysis.&lt;/p&gt;
&lt;h2 id=&quot;static-analysis-for-infrastructure-code&quot;&gt;Static Analysis for Infrastructure Code&lt;/h2&gt;
&lt;p&gt;Application code is usually scanned with tools like SonarQube, Veracode, Snyk, or GitHub&#39;s own CodeQL to detect potential vulnerabilities or bad patterns. The same should be done for infrastructure code and there are ready-to-use tools for that like &lt;a href=&quot;https://github.com/Checkmarx/kics&quot;&gt;KICS&lt;/a&gt; or &lt;a href=&quot;https://github.com/bridgecrewio/checkov&quot;&gt;Checkov&lt;/a&gt;. They are both designed to detect security vulnerabilities, compliance issues, and misconfigurations in our IaC. They both come with a huge set of configurable rules and the capability to create your own.&lt;/p&gt;
&lt;p&gt;I prefer KICS, especially the way it can be integrated with GitHub. Checkmarx, the company behind KICS, provides a ready-to-use action. The support for Bicep is &quot;indirect&quot; - KICS supports ARM so the analysis has to be done after the build step. There is also small preparation needed as the directory for output should be created. Still, adding KICS-based static analysis to the workflow is only about 10 lines.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

jobs:
  build-lint-and-static-analysis:
    runs-on: ubuntu-latest
    steps:
    ...
    - name: Create static analysis results folder
      run: |
        mkdir -p static-analysis-results
    - name: Perform KICS static analysis
      id: kics
      uses: checkmarx/kics-github-action@v1.6.3
      with:
        path: &#39;applications/sample-application/&#39;
        fail_on: &#39;high,medium&#39;
        output_path: &#39;static-analysis-results&#39;
        output_formats: &#39;json,sarif&#39;
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above analysis step will fail if any issues with severity high or medium are detected. Similarly to the build step, the feedback will be provided through workflow output.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh3oPALKUyjOU7vbkCU5OMccQO2MzCjOG0bVvEZxmLieKKk4RNNvqIoBOD0sPY9h1bmQnH7joLWdhZfwM27mJXlnrGSGXUFGEMOYwvyzzwbvInaZvGujY3WFuRVSKwfHjf30fhnga4vMgUUJAHTmrvncGhjRcUnAwnqrX4gG5QrJalh0ASRfAGQPwdr/s1600/continuous-integration-static-analysis.png&quot; alt=&quot;Continuous Integration (Sample Application) workflow - output of Perform KICS static analysis step&quot;&gt;&lt;/p&gt;
&lt;p&gt;But KICS integration is even more powerful than that. As you may have noticed I&#39;ve configured output formats from the analysis to be &lt;em&gt;JSON&lt;/em&gt; and &lt;em&gt;SARIF&lt;/em&gt;. SARIF is a standardized format for sharing static analysis results and it can be used to &lt;a href=&quot;https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning&quot;&gt;integrate with the code scanning feature of GitHub Advanced Security&lt;/a&gt;. Once again we can use an existing action (this time provided by GitHub) to upload the SARIF file. The only tricky part is to put a proper condition on the upload step so the results are pushed also when the analysis step fails due to the severity of detected issues.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

jobs:
  build-lint-and-static-analysis:
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write
    steps:
    ...
    - name: Upload KICS static analysis results
      if: always() &amp;amp;&amp;amp; (steps.kics.outcome == &#39;success&#39; || steps.kics.outcome == &#39;failure&#39;)
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: &#39;static-analysis-results/results.sarif&#39;
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thanks to this, the issues will be available in the Code Scanning section of the repository Security tab. This will provide alerts for those issues, the ability to triage them, and audit for taken actions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJOCT_l5bURch7w14jD8_7Nt9BM2B9Tzc6S5qxgNqZ0IlrEoCDBR6Drm8_vYbt7FpXII3n-68-ewCvcf_WoijrMZnGP480efjtX_v-W2FKq3jb9J89llRkybMw-_5aFOTj_9i5X7vPMQ69A3oz3PuXs6iooG37GT8gacNV07tCHAOTvYm-GPyM2Yht/s1600/continuous-integration-code-scanning-results.png&quot; alt=&quot;Continuous Integration (Sample Application) workflow - KICS static analysis results in the Code Scanning section of the repository Security tab&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now we can say that we have looked at the code enough as part of the integration workflow. In the case of software development, we would probably run now some unit tests. In the case of infrastructure, the equivalent at this stage is testing if the template will deploy successfully.&lt;/p&gt;
&lt;h2 id=&quot;preflight-validation-for-infrastructure-code&quot;&gt;Preflight Validation for Infrastructure Code&lt;/h2&gt;
&lt;p&gt;We have verified that the template will build probably and we have removed all important vulnerabilities and misconfigurations. Sadly, this doesn&#39;t guarantee that the template will deploy. There may be some policies or conditions on the environment, which are not reflected in any of the checks. To make sure that the template will deploy, we need to perform a preflight validation against the environment. This capability is provided differently by different ecosystems, in the case of Bicep and ARM it comes as &lt;em&gt;Validate&lt;/em&gt; deployment mode. This means that we can add another job to our workflow which will establish a connection to Azure and test the deployment.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

jobs:
  ...
  preflight-validation:
    needs: build-lint-and-static-analysis
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Azure Login
      uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    - name: Perform preflight validation
      uses: azure/arm-deploy@v1
      with:
        scope: &#39;resourcegroup&#39;
        resourceGroupName: &#39;rg-devops-practices-sample-application-sandbox&#39;
        template: &#39;applications/sample-application/application.bicep&#39;
        deploymentMode: &#39;Validate&#39;
        failOnStdErr: false
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will catch issues like duplicated storage account names (or simple cases where the name is too long) without actually deploying anything.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsjTQQvJdbgt7BYrUx49Sj1buv_y3Mor6bYz2cwCpr70ZEzqS52a_S4mqHeJlKU6iDu9dHyKHVufjLEkMGsSZb1dKZoetQA6wNzGKGHBajeaFLNjrwLQ944VNccpKhLqO09IbDdxn2zw2GVhmax1QVm1J89SH-u-Fn6CcYy8_84cQRZBkWcwEIfmfH/s1600/continuous-integration-preflight-validation.png&quot; alt=&quot;Continuous Integration (Sample Application) workflow - output of Perform preflight validation step&quot;&gt;&lt;/p&gt;
&lt;p&gt;What&#39;s next? Well, there is one common software development practice that we haven&#39;t touched yet - pull requests and code reviews. This subject recently caused some heated discussions. There are opinions that if you have an explicit code review step in your process it&#39;s not true Continuous Integration. There are also opinions that code reviews are perfectly fine. My opinion is that it&#39;s part of team culture. If your team has asynchronous culture, then doing code reviews through pull requests may be the correct way. If your team is collocated or strongly collaborates online, using pair or mob programming instead of code reviews may be the best. We can also detach the discussion around pull requests from the discussion around code reviews. I know teams that are relying on pair programming in place of code reviews but still use pull requests (automatically closed) for tracking purposes. And when we are talking pull requests in the context of infrastructure code, there is one challenge - it&#39;s hard to understand the actual change just by looking at code diff (especially after some time). This is why generating a preview of changes as part of the integration workflow can be extremely beneficial.&lt;/p&gt;
&lt;h2 id=&quot;preview-of-infrastructure-changes&quot;&gt;Preview of Infrastructure Changes&lt;/h2&gt;
&lt;p&gt;Infrastructure as code tooling usually provides a method to generate a preview - Terraform has the &lt;code&gt;plan&lt;/code&gt;, Pulumi has the &lt;code&gt;preview&lt;/code&gt;, and Bicep/ARM has the &lt;code&gt;what-if&lt;/code&gt;. From the perspective of the integration workflow we are not thinking about running those commands locally but as part of the workflow. And this time we are not interested in results being available as part of the workflow output, we are looking for adding them as more context to the pull request. To be able to do that we first must capture the results. A good method is writing the results to an environment variable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

jobs:
  ...
  preview:
    needs: preflight-validation
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Azure Login
      uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    - name: Prepare preview
      run: |
        echo &#39;DEPLOYMENT_WHAT_IF&amp;lt;&amp;lt;EOF&#39; &amp;gt;&amp;gt; $GITHUB_ENV
        az deployment group what-if \
          --resource-group rg-devops-practices-sample-application-sandbox \
          --template-file applications/sample-application/application.bicep \
          --result-format ResourceIdOnly &amp;gt;&amp;gt; $GITHUB_ENV
        echo &#39;EOF&#39; &amp;gt;&amp;gt; $GITHUB_ENV
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we have the results, we can add them to the pull request. My preferred approach is to create a comment. GitHub provides us with &lt;a href=&quot;https://github.com/marketplace/actions/github-script&quot;&gt;script action&lt;/a&gt; which allows us to use a pre-authenticated GitHub API client. The issue number and all other necessary information will be available through the &lt;code&gt;context&lt;/code&gt; object (if we are using the right trigger).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

jobs:
  ...
  preview:
    needs: preflight-validation
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    steps:
    ...
    - name:  Create preview comment
      uses: actions/github-script@v6
      with:
        script: |
          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: process.env.DEPLOYMENT_WHAT_IF
          })
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a result of this job, we will get a nice comment describing in a more human-readable form the changes which deploying the template would cause at this very moment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiit_5llqfOkylYVa88OyroI9BcvUZkAzMC4m0gnR-Wm2U6nojBvcCLEuuwKGlCsNUHXSDEVqsJpHyNwbFBlTBaxkJIKWqQGt35ZzSjUB0Ra9q8monAIKQb2zK-tRoAQ7nlBonrby0lrIkvNzDVQVBlR6lmP73MUwiwYZw3P2dMB32wKCX5g9VeB1yP/s1600/continuous-integration-preview-comment.png&quot; alt=&quot;Continuous Integration (Sample Application) workflow - preview comment on the pull request&quot;&gt;&lt;/p&gt;
&lt;p&gt;We may want even more. Changes are not the only valuable context we may be interested in. The second important information is how deploying the changes will impact the costs.&lt;/p&gt;
&lt;h2 id=&quot;cost-estimation-of-infrastructure-changes&quot;&gt;Cost Estimation of Infrastructure Changes&lt;/h2&gt;
&lt;p&gt;ThoughtWorks has been recommending &lt;em&gt;run cost as an architecture fitness function&lt;/em&gt; since 2019, and there is more than one infrastructure cost estimation tool available to us. The two which are worth mentioning are &lt;a href=&quot;https://github.com/infracost/infracost&quot;&gt;Infracost&lt;/a&gt; (for Terraform) and &lt;a href=&quot;https://github.com/TheCloudTheory/arm-estimator&quot;&gt;Azure Cost Estimator&lt;/a&gt; (for Bicep/ARM and recently also Terraform). As I&#39;m using Bicep in this article, I&#39;m going to focus on Azure Cost Estimator.&lt;/p&gt;
&lt;p&gt;Azure Cost Estimator is still a young tool, yet it&#39;s already quite powerful. At the moment of writing this, it supports ~86 resource types. What is very important, it&#39;s capable of generating usage base consumption for some resources if you provide usage patterns through metadata in the template. The only tricky part can be integrating it into the workflow. The project repository provides a reusable workflow, but this may not be desired (or even allowed) method in many organizations. This is why I&#39;ll walk you through the integration step by step.&lt;/p&gt;
&lt;p&gt;The first step is getting the binaries and installing them. If you are using self-hosted runners this can be part of runner setup. You can also download and install the binaries from some central location as part of the workflow itself. Below I&#39;m doing exactly that from the official project releases.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

jobs:
  ...
  cost-estimation:
    needs: preflight-validation
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Download Azure Cost Estimator
      id: download-ace
      uses: robinraju/release-downloader@v1.7
      with:
        repository: &quot;TheCloudTheory/arm-estimator&quot;
        tag: &quot;1.2&quot;
        fileName: &quot;ace-linux-x64.zip&quot;
    - name: Install Azure Cost Estimator
      run: |
        unzip ace-linux-x64.zip
        chmod +x ./azure-cost-estimator
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the binaries in place, we can use the same pattern as in the case of preview to run the tool, grab the results into an environment variable, and create a comment.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;...

jobs:
  ...
  cost-estimation:
    needs: preflight-validation
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    steps:
    ...
    - name: Azure Login
      uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    - name: Prepare cost estimation
      run: |
        echo &#39;COST_ESTIMATION&amp;lt;&amp;lt;EOF&#39; &amp;gt;&amp;gt; $GITHUB_ENV
        azure-cost-estimator applications/sample-application/application.bicep \
          ${{ secrets.AZURE_SUBSCRIPTION_ID } \
          rg-devops-practices-sample-application-sandbox \
          --stdout --disableDetailedMetrics &amp;gt;&amp;gt; $GITHUB_ENV
        echo &#39;EOF&#39; &amp;gt;&amp;gt; $GITHUB_ENV
    - name:  Create pull request comment
      uses: actions/github-script@v6
      with:
        script: |
          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: process.env.COST_ESTIMATION
          })
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will give us a lot of additional, valuable context in the pull request.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirPC55CNPopsNgwKn3xgqqRYPpS1XCNUBwapHuf3xePXbfFKct_kegFO_hfuk7x-EvaGHQRE94gLpFdA5N7LnmYlWx_dAvr_An-ns6h_r-ENR7iM9OEEUhOZ048IahuDxlYO89Z0U2hdjzOPp903CgsfQ-1O2cp2xhIv0t-zmXDEQoMS8ZNoGRMa4C/s1600/continuous-integration-cost-estimation-comment.png&quot; alt=&quot;Continuous Integration (Sample Application) workflow - cost estimation comment on the pull request&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-beginning-of-the-implementation-journey&quot;&gt;The Beginning of the Implementation Journey&lt;/h2&gt;
&lt;p&gt;This post describes just the beginning of the DevOps practices implementation journey.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcJbRiWRqsqCfK6dqzkSzN1UL_Aj3yvCPknat2esTFI3v_WkyXeSCASur9kP0tGR8HCQvaQ95UDPNQLnFQTv8u4Cllvr-rXrBZ_SRauXlJYUXhqgNeefZDiX06lJEXlNKYRn7fOkW_ij9a2iVo_Ucx4SHGADMxQgy2OkpdVrI3Z4fs7Lc8Iic560Z8/s1600/devops-pipeline-tools.png&quot; alt=&quot;DevOps Pipeline With Tools for Create and Verify Stages&quot;&gt;&lt;/p&gt;
&lt;p&gt;It gives a hint about the entire technology ecosystem, but the remaining practices have many interesting aspects to dive into. I do intend to continue walking through them with proposed implementations, just to make your journey easier. I&#39;ve also created a &lt;a href=&quot;https://github.com/tpeczek/demo-devops-practices-for-azure-infrastructure&quot;&gt;repository&lt;/a&gt; that contains samples with different parts of implementation (available in different branches with results in different closed pull requests). You can review it to find all the relevant information. You can also create a fork and have your own playground.&lt;/p&gt;</description><link>http://www.tpeczek.com/2023/06/devops-practices-for-azure.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjG80EZCZkHLsjeIOGWfPG0YAa2_XFNYevuEwBBoUk9wob-LpV4WaEB6F2YQuSNUX8NXS73IYFUDYtLS3j3pSTLliUyMHEUmJQBDmbe8jVDGKOdEbihOirH_T261JWH98wU1jXPcZSsauk8UzejSKJrvvttXXvNC4xz8xOlSiVI59uu1s-2jsbL4wMe/s72-c/devops-pipeline.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-7194716743767456138</guid><pubDate>Wed, 15 Mar 2023 10:56:00 +0000</pubDate><atom:updated>2023-03-15T14:13:04.429+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure functions</category><category domain="http://www.blogger.com/atom/ns#">isolated</category><category domain="http://www.blogger.com/atom/ns#">performance</category><category domain="http://www.blogger.com/atom/ns#">server timing</category><title>Server Timing API Meets Isolated Worker Process Azure Functions - Custom Middleware and Dependency Injection</title><description>&lt;p&gt;Server Timing API &lt;a href=&quot;https://www.tpeczek.com/2017/06/feeding-server-timing-api-from-aspnet.html&quot;&gt;has piqued my interest back in 2017&lt;/a&gt;. I&#39;ve always been a promoter of a data-driven approach to non-functional requirements. Also, I always warned the teams I worked with that it&#39;s very easy to not see the forest for the trees. Server Timing API was bringing a convenient way to communicate backend performance information to developer tools in the browser. It enabled access to back-end and front-end performance data in one place and within the context of actual interaction with the application. I&#39;ve experimented with the technology together with a couple of teams where a culture of fronted and backend engineers working close together was high. The results were great, which pushed me to create a small &lt;a href=&quot;https://www.nuget.org/packages/Lib.AspNetCore.ServerTiming&quot;&gt;library&lt;/a&gt; to simplify the onboarding of Server Timing API in ASP.NET Core applications. I&#39;ve been using that library with multiple teams through the years and judging by the downloads number I wasn&#39;t the only one. There were even some contributions to the library.&lt;/p&gt;
&lt;p&gt;Some time ago, &lt;a href=&quot;https://github.com/tpeczek/Lib.AspNetCore.ServerTiming/issues/25&quot;&gt;an issue&lt;/a&gt; was raised asking if the library could also support Azure Functions using isolated worker process mode. I couldn&#39;t think of a good reason why not, it was a great idea. Of course, I couldn&#39;t add the support directly to the existing library. Yes, the isolated worker process mode of Azure Functions shares a lot of concepts with ASP.NET Core, but the technicalities are different. So, I&#39;ve decided to create a separate &lt;a href=&quot;https://www.nuget.org/packages/Lib.Azure.Functions.Worker.ServerTiming&quot;&gt;library&lt;/a&gt;. While doing so, I&#39;ve also decided to put some notes around those concepts into a blog post in hope that someone might find them useful in the future.&lt;/p&gt;
&lt;p&gt;So, first things first, what is isolated worker process mode and why we are talking about it?&lt;/p&gt;
&lt;h2 id=&quot;azure-functions-execution-modes&quot;&gt;Azure Functions Execution Modes&lt;/h2&gt;
&lt;p&gt;There are two execution modes in Azure Functions: in-process and isolated worker process. The in-process mode means that the function code is running in the same process as the host. This is the approach that has been taken for .NET functions from the beginning (while functions in other languages were running in a separate process since version 2). This enabled Azure Functions to provide unique benefits for .NET functions (like rich bindings and direct access to SDKs) but at a price. The .NET functions could only use the same .NET version as the host. Dependency conflicts were also common. This is why Azure Functions has fully embraced the isolated worker process mode for .NET functions in version 4 and now developers have a choice of which mode they want to use. Sometimes this choice is simple (if you want to use non-LTS versions of .NET, the isolated worker process is your only option), sometimes it is more nuanced (for example isolated worker process functions have slightly longer cold start). You can take a look at the full list of differences &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-in-process-differences?WT.mc_id=DT-MVP-5002979&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When considering simplifying the onboarding of Server Timing API, the isolated worker process mode is the only option as it supports a crucial feature - custom middleware registration.&lt;/p&gt;
&lt;h2 id=&quot;custom-middleware&quot;&gt;Custom Middleware&lt;/h2&gt;
&lt;p&gt;The ability to register a custom middleware is crucial for enabling capabilities like Server Timing API because it allows for injecting logic into the invocation pipeline.&lt;/p&gt;
&lt;p&gt;In isolated worker process Azure Functions, the invocation pipeline is represented by &lt;code&gt;FunctionExecutionDelegate&lt;/code&gt;. Although it would be possible to work with &lt;code&gt;FunctionExecutionDelegate&lt;/code&gt; directly (by wrapping it with parent invocations), Azure Functions provides a convenient extension method &lt;code&gt;UseMiddleware()&lt;/code&gt; which enables registering inline or factory-based middleware. What is missing in comparison to ASP.NET Core is the convention-based middleware. This might be surprising at first as the convention-based approach is probably the most popular one in ASP.NET Core. So, for those of you who are not familiar with the factory-based approach, it requires the middleware class to implement a specific interface. In the case of Azure Functions, it&#39;s &lt;code&gt;IFunctionsWorkerMiddleware&lt;/code&gt; (in ASP.NET Core it&#39;s &lt;code&gt;IMiddleware&lt;/code&gt;). The factory-based middleware is prepared to be registered with different lifetimes, so the &lt;code&gt;Invoke&lt;/code&gt; method takes not only the context but also the delegate representing the next middleware in the pipeline as a parameter. Similarly to ASP.NET Core, we are being given the option to run code before and after functions execute, by wrapping it around the call to the next middleware delegate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class ServerTimingMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        // Pre-function execution

        await next(context);

        // Post-function execution
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The aforementioned &lt;code&gt;UseMiddleware()&lt;/code&gt; extension method should be called inside the &lt;code&gt;ConfigureFunctionsWorkerDefaults&lt;/code&gt; method as part of the host preparation steps. This method registers the middleware as a singleton (so it has the same lifetime as convention-based middleware in ASP.NET Core). It can be registered with different lifetimes, but it has to be done manually which includes wrapping invocation of &lt;code&gt;FunctionExecutionDelegate&lt;/code&gt;. For the ones interested I recommend checking the &lt;a href=&quot;https://github.com/Azure/azure-functions-dotnet-worker/blob/7c6f442b687f0b8db4427d01b35068855f18b754/src/DotNetWorker.Core/Hosting/WorkerMiddlewareWorkerApplicationBuilderExtensions.cs#L96&quot;&gt;&lt;code&gt;UseMiddleware()&lt;/code&gt; source code&lt;/a&gt; for inspiration.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(workerApplication =&amp;gt;
    {
        // Register middleware with the worker
        workerApplication.UseMiddleware();
    })
    .Build();

host.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All the valuable information about the invoked function and the invocation itself can be accessed through &lt;code&gt;FunctionContext&lt;/code&gt; class. There are also some extension methods available for it, which make it easier to work with that class. One such extension method is &lt;code&gt;GetHttpResponseData()&lt;/code&gt; which will return an instance of &lt;code&gt;HttpResponseData&lt;/code&gt; if the function has been invoked by an HTTP trigger. This is where the HTTP response can be modified, for example by adding headers related to Server Timing API.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class ServerTimingMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        // Pre-function execution

        await next(context);

        // Post-function execution
        HttpResponseData? response = context.GetHttpResponseData();
        if (response is not null)
        {
            response.Headers.Add(
                &quot;Server-Timing&quot;,
                &quot;cache;dur=300;desc=\&quot;Cache\&quot;,sql;dur=900;desc=\&quot;Sql Server\&quot;,fs;dur=600;desc=\&quot;FileSystem\&quot;,cpu;dur=1230;desc=\&quot;Total CPU\&quot;&quot;
            );
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make this functional, the values for the header need to be gathered during the invocation, which means that there needs to be a shared service between the function and the middleware. It&#39;s time to bring the dependency injection into the picture.&lt;/p&gt;
&lt;h2 id=&quot;dependency-injection&quot;&gt;Dependency Injection&lt;/h2&gt;
&lt;p&gt;The support for dependency injection in isolated worker process Azure Functions is exactly what you can expect if you have been working with modern .NET. It&#39;s based on &lt;code&gt;Microsoft.Extensions.DependencyInjection&lt;/code&gt; and supports all lifetimes options. The option which might require clarification is the scoped lifetime. In Azure Functions, it matches a function execution lifetime, which is exactly what is needed for gathering values in the context of a single invocation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;var host = new HostBuilder()
    ...
    .ConfigureServices(s =&amp;gt;
    {
        s.AddScoped();
    })
    .Build();

host.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Functions that are using dependency injection must be implemented as instance methods. When using instance methods, each invocation will create a new instance of the function class. That means that all parameters passed into the constructor of the function class are scoped to that invocation. This makes usage of constructor-based dependency injection safe for services with scoped lifetime.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class ServerTimingFunctions
{
    private readonly IServerTiming _serverTiming;

    public ServerTimingFunctions(IServerTiming serverTiming)
    {
        _serverTiming = serverTiming;
    }

    [Function(&quot;basic&quot;)]
    public HttpResponseData Basic([HttpTrigger(AuthorizationLevel.Anonymous, &quot;get&quot;)] HttpRequestData request)
    {

        var response = request.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add(&quot;Content-Type&quot;, &quot;text/plain; charset=utf-8&quot;);

        _serverTiming.Metrics.Add(new ServerTimingMetric(&quot;cache&quot;, 300, &quot;Cache&quot;));
        _serverTiming.Metrics.Add(new ServerTimingMetric(&quot;sql&quot;, 900, &quot;Sql Server&quot;));
        _serverTiming.Metrics.Add(new ServerTimingMetric(&quot;fs&quot;, 600, &quot;FileSystem&quot;));
        _serverTiming.Metrics.Add(new ServerTimingMetric(&quot;cpu&quot;, 1230, &quot;Total CPU&quot;));

        response.WriteString(&quot;-- Demo.Azure.Functions.Worker.ServerTiming --&quot;);

        return response;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above statement is not true for the middleware. As I&#39;ve already mentioned, the &lt;code&gt;UseMiddleware()&lt;/code&gt; method registers the middleware as a singleton. So, even though middleware is being resolved for every invocation separately, it is always the same instance. This means that constructor-based dependency injection is safe only for services with a singleton lifetime. To properly use a service with scoped or transient lifetime we need to use the service locator approach. An invocation-scoped service locator is available for us under &lt;code&gt;FunctionContext.InstanceServices&lt;/code&gt; property.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class ServerTimingMiddleware : IFunctionsWorkerMiddleware
{
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        ...

        // Post-function execution
        InvocationResult invocationResult = context.GetInvocationResult();

        HttpResponseData? response = invocationResult.Value as HttpResponseData;
        if (response is not null)
        {
            IServerTiming serverTiming = context.InstanceServices.GetRequiredService();
            response.Headers.Add(&quot;Server-Timing&quot;, String.Join(&quot;,&quot;, serverTiming.Metrics));
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;it-works-and-you-can-use-it-&quot;&gt;It Works! (And You Can Use It)&lt;/h2&gt;
&lt;p&gt;This way, by combining support for middleware and dependency injection, I&#39;ve established the core functionality of my &lt;a href=&quot;https://www.nuget.org/packages/Lib.Azure.Functions.Worker.ServerTiming&quot;&gt;small library&lt;/a&gt;. It&#39;s out there on NuGet, so if you want to use Server Timing to communicate performance information to your Azure Functions based API consumers you are welcome to use it. If you want to dig a little bit into the code (or maybe you have some suggestions or improvements in mind) it lives in the same &lt;a href=&quot;https://github.com/tpeczek/Lib.AspNetCore.ServerTiming&quot;&gt;repository&lt;/a&gt; as the ASP.NET Core one.&lt;/p&gt;</description><link>http://www.tpeczek.com/2023/03/server-timing-api-meets-isolated-worker.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-7005159588109239934</guid><pubDate>Tue, 20 Dec 2022 14:17:00 +0000</pubDate><atom:updated>2022-12-20T15:24:28.734+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">.net</category><category domain="http://www.blogger.com/atom/ns#">aks</category><category domain="http://www.blogger.com/atom/ns#">azure</category><category domain="http://www.blogger.com/atom/ns#">kubernetes</category><category domain="http://www.blogger.com/atom/ns#">wasi</category><category domain="http://www.blogger.com/atom/ns#">wasm</category><category domain="http://www.blogger.com/atom/ns#">webassembly</category><title>Experimenting With .NET &amp; WebAssembly - Running .NET Based Spin Application On WASI Node Pool in AKS</title><description>&lt;p&gt;I&#39;m quite an enthusiast of &lt;a href=&quot;https://webassembly.org/&quot;&gt;WebAssembly&lt;/a&gt; beyond the browser. It&#39;s already made its way into edge computing with &lt;em&gt;WasmEdge&lt;/em&gt;, &lt;em&gt;Cloudflare Workers&lt;/em&gt;, or &lt;em&gt;EdgeWorkers&lt;/em&gt;. It&#39;s also made its way into cloud computing with dedicated clouds like &lt;em&gt;wasmCloud&lt;/em&gt; or &lt;em&gt;Fermyon Cloud&lt;/em&gt;. So it shouldn&#39;t be a surprise that large cloud vendors are starting to experiment with bringing WASM to their platforms as well. In the case of Azure (my cloud of choice), it&#39;s running WASM workloads on &lt;a href=&quot;https://wasi.dev/&quot;&gt;WASI&lt;/a&gt; node pools in Azure Kubernetes Service. This is great because since Steve Sanderson showed an experimental &lt;a href=&quot;https://github.com/SteveSandersonMS/dotnet-wasi-sdk&quot;&gt;WASI SDK for .NET Core&lt;/a&gt; back in March, I was looking for a good context to play with it too.&lt;/p&gt;
&lt;p&gt;I took my first look at WASM/WASI node pools for AKS a couple of months ago. Back then the feature was based on &lt;a href=&quot;https://krustlet.dev/&quot;&gt;Krustlet&lt;/a&gt; but I&#39;ve quickly learned that the team is moving away from this approach and the feature doesn&#39;t work with the current version of the AKS control plane (it&#39;s a preview, it has that right). I&#39;ve decided to wait. Time has passed, Deis Labs has evolved its &lt;a href=&quot;https://deislabs.io/posts/helpful-webassembly-resources/&quot;&gt;tooling for running WebAssembly in Kubernetes&lt;/a&gt; from Krustlet to ContainerD shims, and the WASM/WASI node pools for AKS feature has embraced it. I&#39;ve decided to take a look at it again.&lt;/p&gt;
&lt;p&gt;The current implementation of WASM/WASI node pools provides support for two ContainerD shims: &lt;a href=&quot;https://developer.fermyon.com/spin/index&quot;&gt;Spin&lt;/a&gt; and &lt;a href=&quot;https://github.com/deislabs/spiderlightning#spiderlightning-or-slight&quot;&gt;SpiderLightning&lt;/a&gt;. Both, Spin and Slight (alternative name for SpiderLightning) provide structure and interfaces for building distributed event-driven applications built from WebAssembly components. After inspecting both of them, I&#39;ve decided to go with Spin for two reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spin is a framework for building applications in &lt;a href=&quot;https://developer.fermyon.com/cloud/index&quot;&gt;Fermyon Cloud&lt;/a&gt;. That meant a potentially stronger ecosystem and community. Also whatever I would learn, would have a broader application (not only WASM/WASI node pools for AKS).&lt;/li&gt;
&lt;li&gt;Spin has (an alpha but still) a &lt;a href=&quot;https://github.com/fermyon/spin-dotnet-sdk&quot;&gt;.NET SDK&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;your-scientists-were-so-preoccupied-with-whether-they-could-they-didn-t-stop-to-think-if-they-should&quot;&gt;Your Scientists Were So Preoccupied With Whether They Could, They Didn&#39;t Stop to Think if They Should&lt;/h2&gt;
&lt;p&gt;When Steve Sanderson revealed the experimental WASI SDK for .NET Core, he showed that you can use it to run an ASP.NET Core server in a browser. He also clearly stated you absolutely shouldn&#39;t do that. Thinking about compiling .NET to WebAssembly and running it in AKS can make one wonder if it is the same case. After all, we can just run .NET in a container. Well, I believe it makes sense. WebAssembly apps have several advantages over containers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WebAssembly apps are smaller than containers. In general, size is an Achilles&#39; heel of .NET, but, even for the sample application, I&#39;ve used here the WASM version is about ten times smaller than a container based on &lt;code&gt;dotnet/runtime:7.0&lt;/code&gt; (18.81 MB vs 190 MB).&lt;/li&gt;
&lt;li&gt;WebAssembly apps start faster and execute faster than containers. This is something I haven&#39;t measured myself yet, but this &lt;a href=&quot;https://arxiv.org/ftp/arxiv/papers/2010/2010.07115.pdf&quot;&gt;paper&lt;/a&gt; seems to make quite a strong case for it.&lt;/li&gt;
&lt;li&gt;WebAssembly apps are more secure than containers. This one is a killer aspect for me. Containers are not secure by default and significant effort has to be put to secure them. WebAssembly sandbox is secure by default.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is why I believe exploring this truly makes sense.&lt;/p&gt;
&lt;p&gt;But before we go further I want to highlight one thing - almost everything I&#39;m using in this post is currently either an alpha or in preview. It&#39;s early and subject to change.&lt;/p&gt;
&lt;h2 id=&quot;configuring-azure-cli-and-azure-subscription-to-support-wasi-node-pools&quot;&gt;Configuring Azure CLI and Azure Subscription to Support WASI Node Pools&lt;/h2&gt;
&lt;p&gt;Working with preview features in Azure requires some preparation. The first step is registering the feature in your subscription&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;az feature register \
    --namespace Microsoft.ContainerService \
    --name WasmNodePoolPreview
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The registration takes some time, you can query the features list to see if it has been completed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;az feature list \
    --query &quot;[?contains(name, &#39;Microsoft.ContainerService/WasmNodePoolPreview&#39;)].{Name:name,State:properties.state}&quot; \
    -o table
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once it&#39;s completed, the resource provider for AKS must be refreshed to pick it up.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;az provider register \
    --namespace Microsoft.ContainerService
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The subscription part is now ready, but to be able to use the feature you also have to add the preview extension to Azure CLI (WASM/WASI node pools can&#39;t be created from the Azure Portal).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;az extension add \
    --name aks-preview \
    --upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is everything we need to start having fun with WASM/WASI node pools.&lt;/p&gt;
&lt;h2 id=&quot;creating-an-aks-cluster&quot;&gt;Creating an AKS Cluster&lt;/h2&gt;
&lt;p&gt;A WASM/WASI node pool can&#39;t be used for a system node pool. This means that before we create one, we have to create a cluster with a system node pool. Something like on the diagram below should be enough.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg70L9o7hHu4KD-igajo07MFScYZNlxvog2cMflfqOEFmIy5QRWDG3nBu9V6yCBjw1oIgH86zPgSLyammbRaLgA1qa0jClEzKyzpMGAyd-YJWI76fzjUx1bVFvPukI8JJulQsNKup-EWKXT9_MpJ6rVU5cRBvo0ZHQkZ-3ZBwVB5vi85JxDsUh87SJa/s1600/aks-cluster.png&quot; alt=&quot;AKS Cluster&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you are familiar with spinning up an AKS cluster you can jump directly to the next section.&lt;/p&gt;
&lt;p&gt;If you are looking for something to copy and paste, the below commands will create a resource group, container registry, and cluster with a single node in the system node pool.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;az group create \
    -l ${LOCATION} \
    -g ${RESOURCE_GROUP}

az acr create \
    -n ${CONTAINER_REGISTRY} \
    -g ${RESOURCE_GROUP} \
    --sku Basic

az aks create \
    -n ${AKS_CLUSTER} \
    -g ${RESOURCE_GROUP} \
    -c 1 \
    --generate-ssh-keys \
    --attach-acr ${CONTAINER_REGISTRY}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;adding-a-wasm-wasi-node-pool-to-the-aks-cluster&quot;&gt;Adding a WASM/WASI Node Pool to the AKS Cluster&lt;/h2&gt;
&lt;p&gt;A WASM/WASI node pool can be added to the cluster as any other node pool, with &lt;code&gt;az aks nodepool add&lt;/code&gt; command. The part which makes it special is the &lt;code&gt;workload-runtime&lt;/code&gt; parameter which takes a value of &lt;code&gt;WasmWasi&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;az aks nodepool add \
    -n ${WASI_NODE_POLL} \
    -g ${RESOURCE_GROUP} \
    -c 1 \
    --cluster-name ${AKS_CLUSTER} \
    --workload-runtime WasmWasi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The updated diagram representing the deployment looks like this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh9in7S1O-f-hpWRB1LpIruuZwsFzo9jbJP_PhJ__Ts1B75KQ82V5-JmryM2WRHQrKGuFkTb1L_qBTqaWUHgVc5Ia2N6o1GGRmGXxmYW7A6Aj1RJuMLC40wLMJEas7eWFrmazbch7gMWnxLItdzrgdwGINVz4C-XkcGs0qZaClelojHAuQdpPfGyPX5/s1600/aks-cluster-with-a-wasi-node-pool.png&quot; alt=&quot;AKS Cluster With a WASI Node Pool&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can inspect the WASM/WASI node pool by running &lt;code&gt;kubectl get nodes&lt;/code&gt; and &lt;code&gt;kubectl describe node&lt;/code&gt; commands.&lt;/p&gt;
&lt;p&gt;With the infrastructure in place, it&#39;s time to build a Spin application.&lt;/p&gt;
&lt;h2 id=&quot;building-a-spin-application-with-net-7&quot;&gt;Building a Spin Application With .NET 7&lt;/h2&gt;
&lt;p&gt;A Spin application has a pretty straightforward structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Spin application manifest (&lt;code&gt;spin.toml&lt;/code&gt; file).&lt;/li&gt;
&lt;li&gt;One or more WebAssembly components.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The WebAssembly components are nothing else than event handlers, while the application manifest defines where there are located and maps them to triggers. The application mentions two triggers: HTTP and Redis. In the case of HTTP, you map components directly to routes.&lt;/p&gt;
&lt;p&gt;So, first we need a component that will serve as a handler. In the introduction, I&#39;ve written that one of the reasons why I have chosen Spin was the availability of .NET SDK. Sadly, when I tried to build an application using it, the application failed to start. The reason for that was that Spin SDK has too many features. Among other things it allows for making outbound HTTP requests which require &lt;code&gt;wasi-outbound-http::request&lt;/code&gt; module,  which is not present in WASM/WASI node pool (which makes sense as it&#39;s experimental and predicted to die once WASI networking APIs are stable).&lt;/p&gt;
&lt;p&gt;Luckily, a Spin application supports fallback to &lt;a href=&quot;https://github.com/deislabs/wagi/blob/main/docs/README.md&quot;&gt;WAGI&lt;/a&gt;. WAGI stands for WebAssembly Gateway Interface and is an implementation of &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc3875&quot;&gt;CGI&lt;/a&gt; (now that&#39;s a blast from the past). It enables writing the WASM component as a &quot;command line&quot; application that handles HTTP requests by reading its properties from environment variables and writing responses to the standard output. This means we should start by creating a new .NET console application.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;dotnet new console -o Demo.Wasm.Spin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next we need to add a reference to &lt;code&gt;Wasi&lt;/code&gt;.Sdk` package.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;dotnet add package Wasi.Sdk --prerelease
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s time for the code. The bare required minimum for WAGI is outputting a &lt;code&gt;Content-Type&lt;/code&gt; header and an empty line that separates headers from body. If you want to include a body, it goes after that empty line.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;using System.Runtime.InteropServices;

Console.WriteLine(&quot;Content-Type: text/plain&quot;);
Console.WriteLine();
Console.WriteLine(&quot;-- Demo.Wasm.Spin --&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the component ready, it&#39;s time for the application manifest. The one below defines an application using the HTTP trigger and mapping the component to a top-level wildcard route (so it will catch all requests). The &lt;code&gt;executor&lt;/code&gt; is how the fallback to WAGI is specified.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-toml&quot;&gt;spin_version = &quot;1&quot;
authors = [&quot;Tomasz Peczek &amp;lt;tpeczek@gmail.com&amp;gt;&quot;]
description = &quot;Basic Spin application with .NET 7&quot;
name = &quot;spin-with-dotnet-7&quot;
trigger = { type = &quot;http&quot;, base = &quot;/&quot; }
version = &quot;1.0.0&quot;

[[component]]
id = &quot;demo-wasm-spin&quot;
source = &quot;Demo.Wasm.Spin/bin/Release/net7.0/Demo.Wasm.Spin.wasm&quot;
[component.trigger]
route = &quot;/...&quot;
executor = { type = &quot;wagi&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last missing part is a &lt;code&gt;Dockerfile&lt;/code&gt; which will allow us to build an image for deployment.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-Dockerfile&quot;&gt;FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build

WORKDIR /src
COPY . .
RUN dotnet build -c Release

FROM scratch
COPY --from=build /src/bin/Release/net7.0/Demo.Wasm.Spin.wasm ./bin/Release/net7.0/Demo.Wasm.Spin.wasm
COPY --from=build /src/spin.toml .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the image on WASM/WASI node pool it needs to be built and pushed to the container registry.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-ps&quot;&gt;az acr login -n ${CONTAINER_REGISTRY}
docker build . -t ${CONTAINER_REGISTRY}.azurecr.io/spin-with-dotnet-7:latest
docker push ${CONTAINER_REGISTRY}.azurecr.io/spin-with-dotnet-7:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;running-a-spin-application-in-wasm-wasi-node-pool&quot;&gt;Running a Spin Application in WASM/WASI Node Pool&lt;/h2&gt;
&lt;p&gt;To run the Spin application we need to create proper resources in our AKS cluster. First is &lt;code&gt;RuntimeClass&lt;/code&gt; which serves as a selection mechanism, so the Pods run on the WASM/WASI node pool. There are two node selectors related to WASM/WASI node pool &lt;code&gt;kubernetes.azure.com/wasmtime-spin-v1&lt;/code&gt; and &lt;code&gt;kubernetes.azure.com/wasmtime-slight-v1&lt;/code&gt;, with &lt;code&gt;spin&lt;/code&gt; and &lt;code&gt;slight&lt;/code&gt; being their respective handlers. In our case, we only care about creating a &lt;code&gt;RuntimeClass&lt;/code&gt; for &lt;code&gt;kubernetes.azure.com/wasmtime-spin-v1&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: &quot;wasmtime-spin-v1&quot;
handler: &quot;spin&quot;
scheduling:
  nodeSelector:
    &quot;kubernetes.azure.com/wasmtime-spin-v1&quot;: &quot;true&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the &lt;code&gt;RuntimeClass&lt;/code&gt; in place, we can define a &lt;code&gt;Deployment&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: spin-with-dotnet-7
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spin-with-dotnet-7
  template:
    metadata:
      labels:
        app: spin-with-dotnet-7
    spec:
      runtimeClassName: wasmtime-spin-v1
      containers:
        - name: spin-with-dotnet-7
          image: crdotnetwasi.azurecr.io/spin-with-dotnet-7:latest
          command: [&quot;/&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Last part is exposing our Spin application to the world. As this is just a demo I&#39;ve decided to expose it directly as a &lt;code&gt;Service&lt;/code&gt; of type &lt;code&gt;LoadBalancer&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;apiVersion: v1
kind: Service
metadata:
  name: spin-with-dotnet-7
spec:
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  selector:
    app: spin-with-dotnet-7
  type: LoadBalancer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can run &lt;code&gt;kubectl apply&lt;/code&gt; and after a moment &lt;code&gt;kubectl get svc&lt;/code&gt; to retrieve the IP address of the &lt;code&gt;Service&lt;/code&gt;. You can paste that address into a browser and voilà.&lt;/p&gt;
&lt;h2 id=&quot;that-was-fun-&quot;&gt;That Was Fun!&lt;/h2&gt;
&lt;p&gt;Yes, that was really fun. All the stuff used here is still early bits, but it already shows possibilities. I intend to observe this space closely and possibly revisit it whenever some updates happen.&lt;/p&gt;
&lt;p&gt;If you want to play with a ready-to-use demo, it&#39;s available on &lt;a href=&quot;https://github.com/tpeczek/demo-dotnet-on-aks-wasi-node-pool&quot;&gt;GitHub&lt;/a&gt; with a workflow ready to deploy it to Azure.&lt;/p&gt;</description><link>http://www.tpeczek.com/2022/12/experimenting-with-net-webassembly.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg70L9o7hHu4KD-igajo07MFScYZNlxvog2cMflfqOEFmIy5QRWDG3nBu9V6yCBjw1oIgH86zPgSLyammbRaLgA1qa0jClEzKyzpMGAyd-YJWI76fzjUx1bVFvPukI8JJulQsNKup-EWKXT9_MpJ6rVU5cRBvo0ZHQkZ-3ZBwVB5vi85JxDsUh87SJa/s72-c/aks-cluster.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-390007151575047369</guid><pubDate>Tue, 25 Oct 2022 12:24:00 +0000</pubDate><atom:updated>2022-10-25T14:24:05.211+02:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">asp.net core</category><category domain="http://www.blogger.com/atom/ns#">blazor</category><category domain="http://www.blogger.com/atom/ns#">microfrontends</category><category domain="http://www.blogger.com/atom/ns#">web components</category><category domain="http://www.blogger.com/atom/ns#">webassembly</category><title>Micro Frontends in Action With ASP.NET Core - Universal Rendering With Blazor WebAssembly Based Web Components</title><description>&lt;p&gt;In the last two posts of this series on implementing the &lt;a href=&quot;https://www.manning.com/books/micro-frontends-in-action&quot;&gt;Micro Frontends in Action&lt;/a&gt; &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction&quot;&gt;samples&lt;/a&gt; in ASP.NET Core, I&#39;ve focused on Blazor WebAssembly based Web Components as a way to achieve client-side composition. As a result, we have well-encapsulated frontend parts which can communicate with each other and the page. But there is a problem with the client-side rendered fragments, they appear after a delay. While the page loads, the user sees an empty placeholder. This is for sure a bad user experience, but it has even more serious consequences, those fragments may not be visible to search engine crawlers. In the case of something like a buy button, it is very important. So, how to deal with this problem? A possible answer is &lt;em&gt;universal rendering&lt;/em&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/06/micro-frontends-in-action-with-aspnet.html&quot;&gt;Server-Side Routing via YARP in Azure Container Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/07/micro-frontends-in-action-with-aspnet.html&quot;&gt;Composition via YARP Transforms and Server-Side Includes (SSI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/08/micro-frontends-in-action-with-aspnet.html&quot;&gt;Composition via Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/09/micro-frontends-in-action-with-aspnet.html&quot;&gt;Communication Patterns for Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Universal Rendering With Blazor WebAssembly Based Web Components&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-is-universal-rendering-&quot;&gt;What Is Universal Rendering?&lt;/h2&gt;
&lt;p&gt;Universal rendering is about combining server-side and client-side rendering in a way that enables having a single codebase for both purposes. The typical approach is to handle the initial HTML rendering on the server with help of the server-side composition and then, when the page is loaded in the browser, seamlessly rerender the fragments on the client side. The initial rendering should only generate the static markup, while the rerender brings the full functionality. When done properly, this allows for a fast &lt;em&gt;First Contentful Paint&lt;/em&gt; while maintaining encapsulation.&lt;/p&gt;
&lt;p&gt;The biggest challenge is usually the single codebase, which in this case means rendering Blazor WebAssembly based Web Components on the server.&lt;/p&gt;
&lt;h2 id=&quot;server-side-rendering-for-blazor-webassembly-based-web-components&quot;&gt;Server-Side Rendering for Blazor WebAssembly Based Web Components&lt;/h2&gt;
&lt;p&gt;There is no standard approach to rendering Web Components on the server. Usually, that requires some creative solutions. But Blazor WebAssembly based Web Components are different because on the server they are Razor components and ASP.NET Core provides support for &lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/blazor/components/prerendering-and-integration?pivots=webassembly&amp;amp;WT.mc_id=DT-MVP-5002979&quot;&gt;prerendering Razor components&lt;/a&gt;. This support comes in form of a &lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/component-tag-helper?WT.mc_id=DT-MVP-5002979&quot;&gt;Component Tag Helper&lt;/a&gt;. But, before we get to it, we need to modify the Checkout service so it can return the rendered HTML. This is where the choice of &lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly?view=aspnetcore-6.0&amp;amp;WT.mc_id=DT-MVP-5002979#hosted-deployment-with-aspnet-core&quot;&gt;hosted deployment with ASP.NET Core&lt;/a&gt; will be beneficial. We can modify the hosting application to support Blazor WebAssembly and controllers with views.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

...

app.UseBlazorFrameworkFiles();
app.UseStaticFiles();

app.UseRouting();

app.MapControllerRoute(
    name: &quot;checkout-fragments&quot;,
    pattern: &quot;fragment/buy/{sku}/{edition}&quot;,
    defaults: new { controller = &quot;Fragments&quot;, action = &quot;Buy&quot; }
);

app.Run();

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The controller for the defined route doesn&#39;t need any sophisticated logic, it only needs to pass the parameters to the view. For simplicity, I&#39;ve decided to go with a dictionary as a model.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class FragmentsController : Controller
{
    public IActionResult Buy(string sku, string edition)
    {
        IDictionary&amp;lt;string, string&amp;gt; model = new Dictionar&amp;lt;string, string&amp;gt;
        {
            { &quot;Sku&quot;, sku },
            { &quot;Edition&quot;, edition }
        };

        return View(&quot;Buy&quot;, model);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only remaining thing is the view which will be using the Component Tag Helper. In general, two pieces of information should be provided to this tag helper: the type of the component and the render mode. There are multiple render modes that render different markers to be used for later bootstrapping, but here we want to use the &lt;code&gt;Static&lt;/code&gt; mode which renders only static HTML.&lt;/p&gt;
&lt;p&gt;In addition to the component type and render mode, the Component Tag Helper also enables providing values for any component parameters with a &lt;code&gt;param-{ParameterName}&lt;/code&gt; syntax. This is how we will pass the values from the model.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-html&quot;&gt;@using Demo.AspNetCore.MicroFrontendsInAction.Checkout.Frontend.Components
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model IDictionary&amp;lt;string, string&amp;gt;

&amp;lt;component type=&quot;typeof(BuyButton)&quot; render-mode=&quot;Static&quot; param-Sku=&quot;@(Model[&quot;Sku&quot;])&quot; param-Edition=&quot;@(Model[&quot;Edition&quot;])&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we start the Checkout service and use a browser to navigate to the controller route, we will see an exception complaining about the absence of &lt;code&gt;IBroadcastChannelService&lt;/code&gt;. At runtime Razor components are classes and ASP.NET Core will need to satisfy the dependencies while creating an instance. Sadly there is no support for &lt;a href=&quot;https://github.com/dotnet/razor-compiler/issues/357&quot;&gt;optional dependencies&lt;/a&gt;. The options are either a workaround based on injecting &lt;code&gt;IServiceProvider&lt;/code&gt; or making sure that the needed dependency is registered. I believe the latter to be more elegant.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddBroadcastChannel();
builder.Services.AddControllersWithViews();

var app = builder.Build();

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After this change, navigating to the controller route will display HTML, but in the case of the &lt;code&gt;BuyButton&lt;/code&gt;, it is not exactly what we want. The &lt;code&gt;BuyButton&lt;/code&gt; component contains the markup for a popup which is displayed upon clicking the button. The issue is, that the popup is hidden only with CSS. This is fine for the Web Component scenario (where the styles are already loaded when the component is being rendered) but not desired for this one. This is why I&#39;ve decided to put a condition around the popup markup.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-razor&quot;&gt;...

&amp;lt;button type=&quot;button&quot; @ref=&quot;_buttonElement&quot; @onclick=&quot;OnButtonClick&quot;&amp;gt;
    buy for @(String.IsNullOrWhiteSpace(Sku) || String.IsNullOrWhiteSpace(Edition)  ? &quot;???&quot; : _prices[Sku][Edition])
&amp;lt;/button&amp;gt;
@if (_confirmationVisible)
{
    &amp;lt;div class=&quot;confirmation confirmation-visible&quot;&amp;gt;
        ...
    &amp;lt;/div&amp;gt;
}

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the HTML returned by the controller contains only the button markup.&lt;/p&gt;
&lt;h2 id=&quot;combining-server-side-and-client-side-rendering&quot;&gt;Combining Server-Side and Client-Side Rendering&lt;/h2&gt;
&lt;p&gt;The Checkout service is now able to provide static HTML representing the &lt;code&gt;BuyButton&lt;/code&gt; fragment, based on a single codebase. In the case of micro frontends that&#39;s not everything that is needed for universal rendering. The static HTML needs to be composed into the page before it&#39;s served. In this series, I&#39;ve explored a single server-side composition technique based on &lt;a href=&quot;https://www.tpeczek.com/2022/08/micro-frontends-in-action-with-aspnet.html#:~:text=YARP%20Transforms%20and%20Server%2DSide%20Includes%20(SSI&quot;&gt;YARP Transforms and Server-Side Includes&lt;/a&gt;), so I&#39;ve decided to reuse it. First, I&#39;ve copied the code for the body transform from the &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/tree/main/05-composition-via-yarp-and-ssi/Demo.AspNetCore.MicroFrontendsInAction.Proxy/Transforms/Ssi&quot;&gt;previous sample&lt;/a&gt;. Then, I modified the routing in the proxy to transform the request coming to the Decide service. The same as previously, I&#39;ve created a dedicated route for static content so it doesn&#39;t go through the transform unnecessarily.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

var routes = new[]
{
    ...
    new RouteConfig {
        RouteId = Constants.ROOT_ROUTE_ID,
        ClusterId = Constants.DECIDE_CLUSTER_ID,
        Match = new RouteMatch { Path = &quot;/&quot; },
        Metadata = SsiTransformProvider.SsiEnabledMetadata
    },
    (new RouteConfig {
        RouteId = Constants.DECIDE_ROUTE_ID + &quot;-static&quot;,
        ClusterId = Constants.DECIDE_CLUSTER_ID,
        Match = new RouteMatch { Path = Constants.DECIDE_ROUTE_PREFIX + &quot;/static/{**catch-all}&quot; }
    }).WithTransformPathRemovePrefix(Constants.DECIDE_ROUTE_PREFIX),
    (new RouteConfig {
        RouteId = Constants.DECIDE_ROUTE_ID,
        ClusterId = Constants.DECIDE_CLUSTER_ID,
        Match = new RouteMatch { Path = Constants.DECIDE_ROUTE_PREFIX + &quot;/{**catch-all}&quot; },
        Metadata = SsiTransformProvider.SsiEnabledMetadata
    }).WithTransformPathRemovePrefix(Constants.DECIDE_ROUTE_PREFIX),
    ...
};

...

builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I could modify the markup returned by the Decide service by placing the SSI directives inside the tag representing the Custom Element.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-html&quot;&gt;&amp;lt;html&amp;gt;
  ...
  &amp;lt;body class=&quot;decide_layout&quot;&amp;gt;
    ...
    &amp;lt;div class=&quot;decide_details&quot;&amp;gt;
      &amp;lt;checkout-buy sku=&quot;porsche&quot; edition=&quot;standard&quot;&amp;gt;
        &amp;lt;!--#include virtual=&quot;/checkout/fragment/buy/porsche/standard&quot; --&amp;gt;
      &amp;lt;/checkout-buy&amp;gt;
    &amp;lt;/div&amp;gt;
    ...
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way the proxy can inject the static HTML into the markup while serving the initial response and once the JavaScript for Web Components is loaded they will be rerendered. We have achieved universal rendering.&lt;/p&gt;
&lt;h2 id=&quot;what-about-progressive-enhancements-&quot;&gt;What About Progressive Enhancements?&lt;/h2&gt;
&lt;p&gt;You might have noticed that there is a problem hiding in this solution. It&#39;s deceiving the users. The page looks like it&#39;s fully loaded but it&#39;s not interactive. There is a delay (until the JavaScript is loaded) before clicking the &lt;code&gt;BuyButton&lt;/code&gt; has any effect. This is where progressive enhancements come into play.&lt;/p&gt;
&lt;p&gt;I will not go into this subject further here, but one possible approach could be wrapping the button inside a form when the Checkout service is rendering static HTML.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-html&quot;&gt;@using Demo.AspNetCore.MicroFrontendsInAction.Checkout.Frontend.Components
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model IDictionary&amp;lt;string, string&amp;gt;

&amp;lt;form asp-controller=&quot;Checkout&quot; asp-action=&quot;Buy&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;sku&quot; valeu=&quot;@(Model[&quot;Sku&quot;])&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;edition&quot; valeu=&quot;@(Model[&quot;Edition&quot;])&quot;&amp;gt;
    &amp;lt;component type=&quot;typeof(BuyButton)&quot; render-mode=&quot;Static&quot; param-Sku=&quot;@(Model[&quot;Sku&quot;])&quot; param-Edition=&quot;@(Model[&quot;Edition&quot;])&quot; /&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Of course, that&#39;s not all the needed changes. The button would have to be rendered with &lt;code&gt;submit&lt;/code&gt; type and the Checkout service needs to handle the POST request, redirect back to the product page, and manage the cart in the background.&lt;/p&gt;
&lt;p&gt;If you are interested in doing that exercise, the sample code with universal rendering that you can use as a starter is available on &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/tree/main/16-universal-rendering-with-blazor-webassembly-based-web-components&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</description><link>http://www.tpeczek.com/2022/10/micro-frontends-in-action-with-aspnet.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-2336112181316150606</guid><pubDate>Tue, 11 Oct 2022 12:30:00 +0000</pubDate><atom:updated>2022-10-11T14:30:34.477+02:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">azure databricks</category><category domain="http://www.blogger.com/atom/ns#">github</category><category domain="http://www.blogger.com/atom/ns#">ops</category><title>Commits Promotion Between GitHub and Azure Databricks</title><description>&lt;p&gt;One of the projects I&#39;m currently working on is utilizing Azure Databricks for its machine learning component. The machine learning engineers working on the project wanted to use &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/databricks/dev-tools/index-ide?WT.mc_id=DT-MVP-5002979&quot;&gt;external IDEs&lt;/a&gt; for the development. Unfortunately, using external IDEs doesn&#39;t remove all needs for developing or testing directly in Azure Databricks. As we wanted our GitHub repository to be the only source of truth, we had to establish a commits promotion approach that would enable that.&lt;/p&gt;
&lt;p&gt;Azure Databricks has support for &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/databricks/repos/?WT.mc_id=DT-MVP-5002979&quot;&gt;Git integration&lt;/a&gt;, so we&#39;ve decided to start by using it to integrate Azure Databricks with GitHub.&lt;/p&gt;
&lt;h2 id=&quot;configuring-github-credentials-in-azure-databricks&quot;&gt;Configuring GitHub Credentials in Azure Databricks&lt;/h2&gt;
&lt;p&gt;The first step in setting up Git integration with Azure Databricks is credentials configuration. This is something that every engineer needs to do independently, to enable syncing workspace with a specific branch. It requires the following actions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Login to GitHub, click the profile picture and go to &lt;code&gt;Settings&lt;/code&gt; and then &lt;code&gt;Developer settings&lt;/code&gt; at the bottom.&lt;/li&gt;
&lt;li&gt;On the &lt;code&gt;Settings / Developer settings&lt;/code&gt; switch to &lt;code&gt;Personal access tokens&lt;/code&gt; and click &lt;code&gt;Generate new token&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fill in the form:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provide a recognizable &lt;code&gt;Note&lt;/code&gt; for the token.&lt;/li&gt;
&lt;li&gt;Set the &lt;code&gt;Expiration&lt;/code&gt; corresponding to the expected time of work on the project.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select the &lt;code&gt;repo&lt;/code&gt; scope.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjraHucsgXEWDH1QsCDxpITQ3B5Awvjm8-jffbTKgXH5tdpyFKGjzQ_LS9pDg9hjrTmlCPcODCBxk7mqrbowBbDL5OrsTYYXEyz2x-4U2WNECS2_EjeeMKezFCm3B3o83XFrvZ9aNm2-cbJpJ05ztBToi5vtpLXqddC_BNUo6rBitP6Wg5pPqqtGWOb/s1600/github-new-pat-form.png&quot; alt=&quot;GitHub - New Personal Access Token Form&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Generate token&lt;/code&gt; and copy the generated string.&lt;/li&gt;
&lt;li&gt;Launch the Azure Databricks workspace.&lt;/li&gt;
&lt;li&gt;Click the workspace name in the top right corner and then click the &lt;code&gt;User Settings&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On the &lt;code&gt;Git Integration&lt;/code&gt; tab select &lt;code&gt;GitHub&lt;/code&gt;, provide your username, paste the copied token, and click &lt;code&gt;Save&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt; &lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwMRSDVEpICBQ0ljnD7fEaxuF0tpz6I6vcOw9AFHHzdIYhUXd3IVZC_65cXr-3AOsEYlZzAl5j0XLJq8yj1mVLWVXsvOo9kZylBg6QSKOvZgs_VKW_3c-nAuE9rP4E5v9GikxK-kEgxCXMyF57iTcKQHrfV7QI_hLC_siZ9fIbXrOkEz4Xtv5RmcjK/s1600/azure-databricks-git-integration.png&quot; alt=&quot;Azure Databricks - Git Integration&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once the credentials to GitHub have been configured, the next step is the creation of an Azure Databricks Repo.&lt;/p&gt;
&lt;h2 id=&quot;creating-azure-databricks-repo-based-on-github-repository&quot;&gt;Creating Azure Databricks Repo Based on GitHub Repository&lt;/h2&gt;
&lt;p&gt;An Azure Databricks Repo is a clone of your remote Git repository (in this case GitHub repository) which can be managed through Azure Databricks UI. The creation process also happens through UI:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Launch the Azure Databricks workspace.&lt;/li&gt;
&lt;li&gt;From the left menu choose &lt;code&gt;Repos&lt;/code&gt; and then click &lt;code&gt;Add Repo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fill in the form:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check the &lt;code&gt;Create repo by cloning a Git repository&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;GitHub&lt;/code&gt; as &lt;code&gt;Git provider&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Provide the &lt;code&gt;Git repository URL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;Repository name&lt;/code&gt; will auto-populate, but you can modify it to your liking.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvUqxc8E8HUQAy3ThVk8NcUTn-52v5H_ctAGXv4GIWP3Y9oRd2pkFV3CQkrV-qAAEIEb2v6tm0l7I8Z55dDh5wnrA_lJgYHpAkuXSzAyiPA4cONuKsVrJeABnhjUp_FTUxC08sn4s75hXuzRuR1x7VgK-YAsr-yjgU5JDEtaHuP_W2Z_XHoUySlthD/s1600/azure-databricks-add-repo.png&quot; alt=&quot;Azure Databricks - Add Repo&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Submit&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And it&#39;s done. You can now select a branch next to the newly created Azure Databricks Repo. If you wish you can click the down arrow next to the repo/branch name and create a notebook, folder, or file. If the notebook you want to develop in has been already in the cloned repository, you can just select it and start developing.&lt;/p&gt;
&lt;h2 id=&quot;promoting-commits-from-azure-databricks-repo-to-github-repository&quot;&gt;Promoting Commits From Azure Databricks Repo to GitHub Repository&lt;/h2&gt;
&lt;p&gt;As I&#39;ve already mentioned, Azure Databricks Repo is managed through the UI. The Git dialog is accessible through the down arrow next to the repo/branch name or directly from the notebook through a button placed next to the name of the notebook (the label of the button is the current Git branch name). From the Git dialog, you can commit and push changes to the GitHub repository.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwh4qc_mNmb2-xDGQCKFBRAdZKrbeoPVusv-93pVma6naXnEqncO2Gg9HBmXH1lBlk0lED1RWxF4nXuIcrudh0bPi9TN0CA_erHoXiERx661O3Mg7frRTvtnht09oQYch8gtauDcF5Sv4xvnI_EsnNVyN71pRnZd2qz1dbL8ja-MZmW17WKkP5bLLv/s1600/azure-databricks-git-dialog.png&quot; alt=&quot;Azure Databricks - Git Dialog&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you are interested in other manual operations, like pulling changes or resolving merge conflicts, they are well described in the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/databricks/repos/sync-remote-repo?WT.mc_id=DT-MVP-5002979&quot;&gt;documentation&lt;/a&gt;. I&#39;m not going to describe their details here, because those are the operations we wanted to avoid by performing the majority of development in external IDEs and automating commits promotion from GitHub to Azure Databricks Repo.&lt;/p&gt;
&lt;h2 id=&quot;promoting-commits-from-github-repository-to-azure-databricks-repo&quot;&gt;Promoting Commits From GitHub Repository to Azure Databricks Repo&lt;/h2&gt;
&lt;p&gt;There are two ways to to manage Azure Databricks Repos programmatically: &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/databricks/dev-tools/api/latest/repos?WT.mc_id=DT-MVP-5002979&quot;&gt;Repos API&lt;/a&gt; and &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/databricks/dev-tools/cli/repos-cli?WT.mc_id=DT-MVP-5002979&quot;&gt;Repos CLI&lt;/a&gt;. As GitHub-hosted runners doesn&#39;t come with preinstalled Databricks CLI, we&#39;ve decided to go with Repos API and PowerShell.&lt;/p&gt;
&lt;p&gt;We wanted a GitHub Actions workflow which would run on every push and update all Azure Databricks Repos mapped to the branch to which the push has happened. After going through API endpoints we came up with following flow.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh1zizXP-x14tdFv4tEFBQfEQ9BwU-GrFB_TrNI41-RG3SqbGSo3xWCQnATFtJZN24SUFBtaDyiJo9dRedJfhCgSXbypvynQbH88jtOFIInE_Nrl0rdbYZ8DSa63fwLIb5dTT91kzk1Zvd7GrghTLn1BqTsToOCBFmn03srZiacZGC9m1ajjV4FiJ3N/s1600/commits-promotion-workflow.pn&quot; alt=&quot;GitHub Actions Workflow for Commits Promotion to Azure Databricks Repo&quot;&gt;&lt;/p&gt;
&lt;p&gt;Before we could start the implementation there was one more missing aspect - authentication.&lt;/p&gt;
&lt;p&gt;Azure Databricks can use an Azure AD service principal as an identity for an automated tool or a CI/CD process. Creation of a service principal and adding it to an Azure Databricks workspace is a multistep process, which is quite well described in the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/databricks/dev-tools/service-principals?WT.mc_id=DT-MVP-5002979#--add-an-azure-ad-service-principal-to-an-azure-databricks-workspace&quot;&gt;documentation&lt;/a&gt;. After going through it, you should be able to create the following actions secrets for your repository:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AZURE_SP_CLIENT_ID&lt;/code&gt; - Application (client) ID for the service principal.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_SP_TENANT_ID&lt;/code&gt; - Directory (tenant) ID for the service principal.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_SP_CLIENT_SECRET&lt;/code&gt; - Client secret for the service principal.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_DATABRICKS_WORKSPACE_INSTANCE_NAME&lt;/code&gt; - The Azure Databricks workspace instance name.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With help of the first three of those secrets and the Microsoft identity platform REST API, we can obtain an Azure AD access token for the service principal. The request we need to make looks like this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://login.microsoftonline.com/&amp;lt;AZURE_SP_TENANT_ID&amp;gt;/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id=&amp;lt;AZURE_SP_CLIENT_ID&amp;gt;&amp;amp;grant_type=client_credentials&amp;amp;scope=2ff814a6-3304-4ab8-85cb-cd0e6f879c1d%2F.default&amp;amp;client_secret=&amp;lt;AZURE_SP_CLIENT_SECRET&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The magical scope value (the URL-encoded &lt;code&gt;2ff814a6-3304-4ab8-85cb-cd0e6f879c1d/.default&lt;/code&gt;) is a programmatic identifier for Azure Databricks. The response to this request is a JSON object which contains the Azure AD access token in the &lt;code&gt;access_token&lt;/code&gt; field. The PowerShell script to make the request and retrieve the token can look like the one below (assuming that the secrets have been put into environment variables).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-powershell&quot;&gt;$azureAdAccessTokenUri = &quot;https://login.microsoftonline.com/$env:AZURE_SP_TENANT_ID/oauth2/v2.0/token&quot;
$azureAdAccessTokenHeaders = @{ &quot;Content-Type&quot; = &quot;application/x-www-form-urlencoded&quot; }
$azureAdAccessTokenBody = &quot;client_id=$env:AZURE_SP_CLIENT_ID&amp;amp;grant_type=client_credentials&amp;amp;scope=2ff814a6-3304-4ab8-85cb-cd0e6f879c1d%2F.default&amp;amp;client_secret=$env:AZURE_SP_CLIENT_SECRET&quot;

$azureAdAccessTokenResponse = Invoke-RestMethod -Method POST -Uri $azureAdAccessTokenUri -Headers $azureAdAccessTokenHeaders -Body $azureAdAccessTokenBody
$azureAdAccessToken = $azureAdAccessTokenResponse.access_token
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Having the token, we can start making requests against Repos API. The first request we want to make in our flow is for getting the repos.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-powershell&quot;&gt;$azureDatabricksReposUri = &quot;https://$env:AZURE_DATABRICKS_WORKSPACE_INSTANCE_NAME/api/2.0/repos&quot;
$azureDatabricksReposHeaders = @{ Authorization = &quot;Bearer $azureAdAccessToken&quot; }

$azureDatabricksReposResponse = Invoke-RestMethod -Method GET -Uri $azureDatabricksReposUri -Headers $azureDatabricksReposHeaders
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;$azureDatabricksReposHeaders&lt;/code&gt; will be used for subsequent requests as well, because we assume that the access token shouldn&#39;t expire before all repos are updated (the default expiration time is ~60 minutes). There is one more assumption here - that there are no more than twenty repos. The results from the &lt;code&gt;/repos&lt;/code&gt; endpoint are paginated (with twenty being the page size) which the above script ignores. If there are more than twenty repos, the script needs to be adjusted to handle that.&lt;/p&gt;
&lt;p&gt;Once we have all the repos we can iterate through them and update those which have matching URL (in case different repositories than the current one has also been mapped) and branch (so we don&#39;t perform unnecessary updates).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-powershell&quot;&gt;$githubRepositoryUrl = $env:GITHUB_REPOSITORY_URL.replace(&quot;git://&quot;,&quot;https://&quot;)

foreach ($azureDatabricksRepo in $azureDatabricksReposResponse.repos)
{
    if (($azureDatabricksRepo.url -eq $githubRepositoryUrl) -and ($azureDatabricksRepo.branch -eq $env:GITHUB_BRANCH_NAME))
    {
    $azureDatabricksRepoId = $azureDatabricksRepo.id;
    $azureDatabricksRepoUri  = &quot;https://$env:AZURE_DATABRICKS_WORKSPACE_INSTANCE_NAME/api/2.0/repos/$azureDatabricksRepoId&quot;
    $updateAzureDatabricksRepoBody = @{ &quot;branch&quot; = $azureDatabricksRepo.branch }

    Invoke-RestMethod -Method PATCH -Uri $azureDatabricksRepoUri -Headers $azureDatabricksReposHeaders -Body ($updateAzureDatabricksRepoBody|ConvertTo-Json)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;GITHUB_REPOSITORY_URL&lt;/code&gt; and &lt;code&gt;GITHUB_BRANCH_NAME&lt;/code&gt; are being injected into environment variables from &lt;code&gt;github&lt;/code&gt; context of the action.&lt;/p&gt;
&lt;p&gt;That&#39;s all the logic we need, you can find the complete workflow &lt;a href=&quot;https://gist.github.com/tpeczek/eba5b757fb465dadf28cc11be85c1114&quot;&gt;here&lt;/a&gt;. Sadly, at least in our case, it has thrown the following error on the first run.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;{&quot;error_code&quot;:&quot;PERMISSION_DENIED&quot;,&quot;message&quot;:&quot;Missing Git | provider credentials. Go to User Settings &amp;gt; Git Integration to | add your personal access token.&quot;}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The error does make sense. After all, from the perspective of Azure Databricks, the service principal is a user and we have never configured GitHub credentials for that user. This raised two questions.&lt;/p&gt;
&lt;p&gt;The first question was, which GitHub user should those credentials represent? This is where the concept of a &lt;em&gt;GitHub machine user&lt;/em&gt; comes into play. A GitHub machine user is a GitHub personal account, separate from the GitHub personal accounts of engineers/developers in your organization. It should be created against a dedicated email provided by your IT department and used only for automation scenarios.&lt;/p&gt;
&lt;p&gt;The second question was, how to configure the credentials. You can&#39;t launch the Azure Databricks workspace as the service principal user and do it through the UI. Luckily, Azure Databricks provides &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/databricks/dev-tools/api/latest/gitcredentials?WT.mc_id=DT-MVP-5002979&quot;&gt;Git Credentials API&lt;/a&gt; which can be used for this task. You can use Postman (or any other tool of your preference) to first make the described above request for Azure AD access token, and then make the below request to configure the credentials.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://&amp;lt;WORKSPACE_INSTANCE_NAME&amp;gt;/api/2.0/git-credentials
Content-Type: application/json

{
   &quot;personal_access_token&quot;: &quot;&amp;lt;GitHub Machine User Personal Access Token&amp;gt;&quot;,
   &quot;git_username&quot;: &quot;&amp;lt;GitHub Machine User Username&amp;gt;&quot;,
   &quot;git_provider&quot;: &quot;GitHub&quot;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After this operation, the GitHub Actions workflow started working as expected.&lt;/p&gt;
&lt;h2 id=&quot;what-this-is-not&quot;&gt;What This Is Not&lt;/h2&gt;
&lt;p&gt;This is not CI/CD for Azure Databricks. This is just a process supporting daily development in the Azure Databricks context. If you are looking for CI/CD approaches to Azure Databricks, you can take a look &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/databricks/dev-tools/index-ci-cd?WT.mc_id=DT-MVP-5002979&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</description><link>http://www.tpeczek.com/2022/10/commits-promotion-between-github-and.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjraHucsgXEWDH1QsCDxpITQ3B5Awvjm8-jffbTKgXH5tdpyFKGjzQ_LS9pDg9hjrTmlCPcODCBxk7mqrbowBbDL5OrsTYYXEyz2x-4U2WNECS2_EjeeMKezFCm3B3o83XFrvZ9aNm2-cbJpJ05ztBToi5vtpLXqddC_BNUo6rBitP6Wg5pPqqtGWOb/s72-c/github-new-pat-form.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-3822728770880711347</guid><pubDate>Mon, 12 Sep 2022 19:11:00 +0000</pubDate><atom:updated>2022-11-01T14:42:45.689+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">asp.net core</category><category domain="http://www.blogger.com/atom/ns#">blazor</category><category domain="http://www.blogger.com/atom/ns#">microfrontends</category><category domain="http://www.blogger.com/atom/ns#">web components</category><category domain="http://www.blogger.com/atom/ns#">webassembly</category><title>Micro Frontends in Action With ASP.NET Core - Communication Patterns for Blazor WebAssembly Based Web Components</title><description>&lt;p&gt;I&#39;m continuing my series on implementing the &lt;a href=&quot;https://www.manning.com/books/micro-frontends-in-action&quot;&gt;Micro Frontends in Action&lt;/a&gt; &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction&quot;&gt;samples&lt;/a&gt; in ASP.NET Core, and I&#39;m continuing the subject of Blazor WebAssembly based Web Components. In the previous post, the project has been expanded with a new service that provides its fronted fragment as a Custom Element power by Blazor WebAssembly. In this post, I will explore how Custom Elements can communicate with other frontend parts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/06/micro-frontends-in-action-with-aspnet.html&quot;&gt;Server-Side Routing via YARP in Azure Container Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/07/micro-frontends-in-action-with-aspnet.html&quot;&gt;Composition via YARP Transforms and Server-Side Includes (SSI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/08/micro-frontends-in-action-with-aspnet.html&quot;&gt;Composition via Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Communication Patterns for Blazor WebAssembly Based Web Components&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/10/micro-frontends-in-action-with-aspnet.html&quot;&gt;Universal Rendering With Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are three communication scenarios I would like to explore: passing information from page to Custom Element (parent to child), passing information from Custom Element to page (child to parent), and passing information between Custom Elements (child to child). Let&#39;s go through them one by one.&lt;/p&gt;
&lt;h2 id=&quot;page-to-custom-element&quot;&gt;Page to Custom Element&lt;/h2&gt;
&lt;p&gt;When it comes to passing information from page to Custom Element, there is a standard approach that every web developer will expect. If I want to disable a button, I set an attribute. If I want to change the text on a button, I set an attribute. In general, if I want to change the state of an element, I set an attribute. The same expectation applies to Custom Elements. How to achieve that?&lt;/p&gt;
&lt;p&gt;As mentioned in the &lt;a href=&quot;https://www.tpeczek.com/2022/08/micro-frontends-in-action-with-aspnet.html&quot;&gt;previous post&lt;/a&gt;, the ES6 class, which represents a Custom Element, can implement a set of lifecycle methods. One of these methods is &lt;code&gt;attributeChangedCallback&lt;/code&gt;. It will be invoked each time an attribute from a specified list is added, removed, or its value is changed. The list of the attributes which will result in invoking the &lt;code&gt;attributeChangedCallback&lt;/code&gt; is defined by a value returned from &lt;code&gt;observedAttributes&lt;/code&gt; static get method.&lt;/p&gt;
&lt;p&gt;So, in the case of Custom Elements implemented in JavaScript, one has to implement the &lt;code&gt;observedAttributes&lt;/code&gt; to return an array of attributes that can modify the state of the Custom Element and implement the &lt;code&gt;attributeChangedCallback&lt;/code&gt; to modify that state. Once again, you will be happy to know that all this work has already been done in the case of Blazor WebAssembly. The &lt;a href=&quot;https://github.com/aspnet/AspLabs/tree/main/src/BlazorCustomElements&quot;&gt;&lt;code&gt;Microsoft.AspNetCore.Components.CustomElements&lt;/code&gt;&lt;/a&gt; package, which wraps Blazor components as Custom Elements handles that. It provides an implementation of &lt;code&gt;observedAttributes&lt;/code&gt; which returns all the properties marked as parameters, and an implementation of &lt;code&gt;attributeChangedCallback&lt;/code&gt; which will update parameters values and give the component a chance to rerender. That makes the implementation quite simple.&lt;/p&gt;
&lt;p&gt;I&#39;ve added a new property named &lt;code&gt;Edition&lt;/code&gt; to the &lt;code&gt;BuyButton&lt;/code&gt; component, which I created in the &lt;a href=&quot;https://www.tpeczek.com/2022/08/micro-frontends-in-action-with-aspnet.html&quot;&gt;previous post&lt;/a&gt;. The new property impacts the price depending if the client has chosen a standard or platinum edition. I&#39;ve also marked the new property as a parameter.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-razor&quot;&gt;&amp;lt;button type=&quot;button&quot; @onclick=&quot;OnButtonClick&quot;&amp;gt;
    buy for @(String.IsNullOrWhiteSpace(Sku) || String.IsNullOrWhiteSpace(Edition)  ? &quot;???&quot; : _prices[Sku][Edition])
&amp;lt;/button&amp;gt;
...

@code {
    private IDictionary&amp;lt;string, Dictionary&amp;lt;string, int&amp;gt;&amp;gt; _prices = new Dictionary&amp;lt;string, Dictionary&amp;lt;string, int&amp;gt;&amp;gt;
    {
        { &quot;porsche&quot;, new Dictionary&amp;lt;string, int&amp;gt; { { &quot;standard&quot;, 66 }, { &quot;platinum&quot;, 966 } } },
        { &quot;fendt&quot;, new Dictionary&amp;lt;string, int&amp;gt; { { &quot;standard&quot;, 54 }, { &quot;platinum&quot;, 945 } }  },
        { &quot;eicher&quot;, new Dictionary&amp;lt;string, int&amp;gt; { { &quot;standard&quot;, 58 }, { &quot;platinum&quot;, 958 } }  }
    };

    [Parameter]
    public string? Sku { get; set; }

    [Parameter]
    public string? Edition { get; set; }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should be all from the component perspective. The rest should be only about using the attribute representing the property. First, I&#39;ve added it to the markup served by the Decide service with the default value. I&#39;ve also added a checkbox that allows choosing the edition.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-html&quot;&gt;&amp;lt;html&amp;gt;
    ...
    &amp;lt;body class=&quot;decide_layout&quot;&amp;gt;
        ...
        &amp;lt;div class=&quot;decide_details&quot;&amp;gt;
            &amp;lt;label class=&quot;decide_editions&quot;&amp;gt;
                &amp;lt;p&amp;gt;Material Upgrade?&amp;lt;/p&amp;gt;
                &amp;lt;input type=&quot;checkbox&quot; name=&quot;edition&quot; value=&quot;platinum&quot; /&amp;gt;
                &amp;lt;span&amp;gt;Platinum&amp;lt;br /&amp;gt;Edition&amp;lt;/span&amp;gt;
                &amp;lt;img src=&quot;https://mi-fr.org/img/porsche_platinum.svg&quot; /&amp;gt;
            &amp;lt;/label&amp;gt;
            &amp;lt;checkout-buy sku=&quot;porsche&quot; edition=&quot;standard&quot;&amp;gt;&amp;lt;/checkout-buy&amp;gt;
        &amp;lt;/div&amp;gt;
        ...
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I implemented an event handler for the &lt;code&gt;change&lt;/code&gt; event of that checkbox, where depending on its state, I would change the value of the &lt;code&gt;edition&lt;/code&gt; attribute on the custom element.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-js&quot;&gt;(function() {
    ...
    const editionsInput = document.querySelector(&quot;.decide_editions input&quot;);
    ...
    const buyButton = document.querySelector(&quot;checkout-buy&quot;);

    ...

    editionsInput.addEventListener(&quot;change&quot;, e =&amp;gt; {
        const edition = e.target.checked ? &quot;platinum&quot; : &quot;standard&quot;;
        buyButton.setAttribute(&quot;edition&quot;, edition);
        ...
    });
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It worked without any issues. Checking and unchecking the checkbox would result in nicely displaying different prices on the button.&lt;/p&gt;
&lt;h2 id=&quot;custom-element-to-page&quot;&gt;Custom Element to Page&lt;/h2&gt;
&lt;p&gt;The situation with passing information from Custom Element to the page is similar to passing information from page to Custom Element - there is an expected standard mechanism: events. If something important has occurred internally in the Custom Element and the external world should know about it, Custom Element should raise an event to which whoever is interested can subscribe.&lt;/p&gt;
&lt;p&gt;How to raise a JavaScript event from Blazor? This requires &lt;a href=&quot;https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0&amp;amp;WT.mc_id=DT-MVP-5002979&quot;&gt;calling a JavaScript function&lt;/a&gt; which will wrap a call to &lt;code&gt;dispatchEvent&lt;/code&gt;. Why can&#39;t &lt;code&gt;dispatchEvent&lt;/code&gt; be called directly? That&#39;s because Blazor requires function identifier to be relative to the global scope, while &lt;code&gt;dispatchEvent&lt;/code&gt; needs to be called on an instance of an element. This raises another challenge. Our wrapper function will require a reference to the Custom Element. Blazor supports capturing references to elements to pass them to JavaScript. The &lt;code&gt;@ref&lt;/code&gt; attribute can be included in HTML element markup, resulting in a reference being stored in the variable it is pointing to. This means that the reference to the Custom Element itself can&#39;t be passed directly, but a reference to its child element can.&lt;/p&gt;
&lt;p&gt;I&#39;ve written a wrapper function that takes the reference to the button element (but it could be any direct child of the Custom Element) as a parameter and then calls &lt;code&gt;dispatchEvent&lt;/code&gt; on its parent.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-js&quot;&gt;window.checkout = (function () {
    return {
        dispatchItemAddedEvent: function (checkoutBuyChildElement) {
            checkoutBuyChildElement.parentElement.dispatchEvent(new CustomEvent(&quot;checkout:item_added&quot;));
        }
    };
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wanted the event to be raised when the button has been clicked, so I&#39;ve modified the &lt;code&gt;OnButtonClick&lt;/code&gt; to use injected &lt;code&gt;IJSRuntime&lt;/code&gt; to call my JavaScript function. In the below code, you can also see the &lt;code&gt;@ref&lt;/code&gt; attribute in action and how I&#39;m passing that element reference to the wrapper function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-razor&quot;&gt;@using Microsoft.JSInterop

@inject IJSRuntime JS

&amp;lt;button type=&quot;button&quot; @ref=&quot;_buttonElement&quot; @onclick=&quot;OnButtonClick&quot;&amp;gt;
    buy for @(String.IsNullOrWhiteSpace(Sku) || String.IsNullOrWhiteSpace(Edition)  ? &quot;???&quot; : _prices[Sku][Edition])
&amp;lt;/button&amp;gt;
...

@code {
    private ElementReference _buttonElement;

    ...

    private async Task OnButtonClick(MouseEventArgs e)
    {
        ...

        await JS.InvokeVoidAsync(&quot;checkout.dispatchItemAddedEvent&quot;, _buttonElement);
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the whole thing to work, I had to reference the JavaScript from the Decide service markup so that the wrapper function could be called.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-html&quot;&gt;&amp;lt;html&amp;gt;
    ...
    &amp;lt;body class=&quot;decide_layout&quot;&amp;gt;
        ...
        &amp;lt;script src=&quot;/checkout/static/components.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
        &amp;lt;script src=&quot;/checkout/_content/Microsoft.AspNetCore.Components.CustomElements/BlazorCustomElements.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
        ...
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I could subscribe to the &lt;code&gt;checkout:item_added&lt;/code&gt; event and add some bells and whistles whenever it&#39;s raised.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-js&quot;&gt;(function() {
    ...
    const productElement = document.querySelector(&quot;.decide_product&quot;);
    const buyButton = document.querySelector(&quot;checkout-buy&quot;);

    ...

    buyButton.addEventListener(&quot;checkout:item_added&quot;, e =&amp;gt; {
        productElement.classList.add(&quot;decide_product--confirm&quot;);
    });

    ...
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;custom-element-to-custom-element&quot;&gt;Custom Element to Custom Element&lt;/h2&gt;
&lt;p&gt;Passing information between Custom Elements is where things get interesting. That is because there is no direct relation between Custom Elements. Let&#39;s assume that the Checkout service exposes a second Custom Element which provides a cart representation. The checkout button and mini-cart don&#39;t have to be used together. There might be a scenario where only one of them is present, or there might be scenarios where there are rendered by independent parents.&lt;/p&gt;
&lt;p&gt;Of course, everything is happening in the browser&#39;s context, so there is always an option to search through the entire DOM tree. This is an approach that should be avoided. First, it&#39;s tight coupling, as it requires Custom Element to have detailed knowledge about other Custom Element. Second, it wouldn&#39;t scale. What if there are ten different types of Custom Elements to which information should be passed? That would require ten different searches.&lt;/p&gt;
&lt;p&gt;Another option is leaving orchestration to the parent. The parent would listen to events from one Custom Element and change properties on the other. This breaks the separation of responsibilities as the parent (in our case, the Decide service) is now responsible for implementing logic that belongs to someone else (in our case, the Checkout Service).&lt;/p&gt;
&lt;p&gt;What is needed is a communication channel that will enable a publish-subscribe pattern. This will ensure proper decoupling. The classic implementation of such a channel is events based bus. The publisher raises events with bubbling enabled (by default, it&#39;s not), so subscribers can listen for those events on the &lt;code&gt;window&lt;/code&gt; object. This is an established approach, but it&#39;s not the one I&#39;ve decided to implement. An events-based bus is a little bit &quot;too public&quot; for me. In the case of multiple Custom Elements communicating, there are a lot of events on the &lt;code&gt;window&lt;/code&gt; object, and I would prefer more organization. Luckily, modern browsers provide an alternative way to implement such a channel - the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API&quot;&gt;&lt;em&gt;Broadcast Channel API&lt;/em&gt;&lt;/a&gt;. You can think about &lt;em&gt;Broadcast Channel API&lt;/em&gt; as a simple message bus that provides the capability of creating named channels. The hidden power of &lt;em&gt;Broadcast Channel API&lt;/em&gt; is that it allows communication between windows/tabs, iframes, web workers, and service workers.&lt;/p&gt;
&lt;p&gt;Using &lt;em&gt;Broadcast Channel API&lt;/em&gt; in Blazor once again requires JavaScript interop. I&#39;ve decided to use this opportunity to build a &lt;a href=&quot;https://github.com/tpeczek/Blazor.BroadcastChannel&quot;&gt;component library&lt;/a&gt; that provides easy access to it. I&#39;m not going to describe the process of creating a component library in this post, but if you are interested just let me know and I&#39;m happy to write a separate post about it. If you want to use the Broadcast Channel API, it&#39;s available on &lt;a href=&quot;https://www.nuget.org/packages/Blazor.BroadcastChannel/&quot;&gt;NuGet&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After building and publishing the component library, I referenced it in the Checkout project and registered the service it provides.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.RootComponents.RegisterAsCustomElement&amp;lt;BuyButton&amp;gt;(&quot;checkout-buy&quot;);

builder.Services.AddBroadcastChannel();

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the checkout button component, I&#39;ve injected the service. The channel can be created by calling &lt;code&gt;CreateOrJoinAsync&lt;/code&gt;, and I&#39;m doing that in &lt;code&gt;OnAfterRenderAsync&lt;/code&gt;. I&#39;ve also made the component implement &lt;code&gt;IAsyncDisposable&lt;/code&gt;, where the channel is disposed to avoid JavaScript memory leaks. The last part was calling &lt;code&gt;PostMessageAsync&lt;/code&gt; as part of &lt;code&gt;OnButtonClick&lt;/code&gt; to send the message to the channel. This completes the publisher.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-razor&quot;&gt;...

@implements IAsyncDisposable

...
@inject IBroadcastChannelService BroadcastChannelService

...

@code {
    ...
    private IBroadcastChannel? _broadcastChannel;

    ...

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _broadcastChannel = await BroadcastChannelService.CreateOrJoinAsync(&quot;checkout:item-added&quot;);
        }
    }

    private async Task OnButtonClick(MouseEventArgs e)
    {
        ...

        if (_broadcastChannel is not null)
        {
            await _broadcastChannel.PostMessageAsync(new CheckoutItem { Sku = Sku, Edition = Edition });
        }

        ...
    }

    ...

    public async ValueTask DisposeAsync()
    {
        if (_broadcastChannel is not null)
        {
            await _broadcastChannel.DisposeAsync();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The mini-cart component will be the subscriber. I&#39;ve added there the same code for injecting the service, joining the channel, and disposing of it. The main difference here is that the component will subscribe to the channel &lt;code&gt;Message&lt;/code&gt; event instead of sending anything. The &lt;code&gt;BroadcastChannelMessageEventArgs&lt;/code&gt; contains the message which has been sent in the &lt;code&gt;Data&lt;/code&gt; property as &lt;code&gt;JsonDocument&lt;/code&gt;, which can be deserialized to the desired type. In the mini-cart component, I&#39;m using the message to add items.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-razor&quot;&gt;@using System.Text.Json;

@implements IAsyncDisposable

@inject IBroadcastChannelService BroadcastChannelService

@(_items.Count == 0  ? &quot;Your cart is empty.&quot; : $&quot;You&#39;ve picked {_items.Count} tractors:&quot;)
@foreach (var item in _items)
{
    &amp;lt;img src=&quot;https://mi-fr.org/img/@(item.Sku)_@(item.Edition).svg&quot; /&amp;gt;
}

@code {
    private IList&amp;lt;CheckoutItem&amp;gt; _items = new List&amp;lt;CheckoutItem&amp;gt;();
    private IBroadcastChannel? _broadcastChannel;
    private JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _broadcastChannel = await BroadcastChannelService.CreateOrJoinAsync(&quot;checkout:item-added&quot;);
            _broadcastChannel.Message += OnMessage;
        }
    }

    private void OnMessage(object? sender, BroadcastChannelMessageEventArgs e)
    {
        _items.Add(e.Data.Deserialize&amp;lt;CheckoutItem&amp;gt;(_jsonSerializerOptions));

        StateHasChanged();
    }

    public async ValueTask DisposeAsync()
    {
        if (_broadcastChannel is not null)
        {
            await _broadcastChannel.DisposeAsync();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last thing I did in the Checkout service was exposing the mini-cart component.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.RootComponents.RegisterAsCustomElement&amp;lt;BuyButton&amp;gt;(&quot;checkout-buy&quot;);
builder.RootComponents.RegisterAsCustomElement&amp;lt;MiniCart&amp;gt;(&quot;checkout-minicart&quot;);

builder.Services.AddBroadcastChannel();

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the mini-cart could be included in the HTML owned by the Decide service.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-html&quot;&gt;&amp;lt;html&amp;gt;
  ...
  &amp;lt;body class=&quot;decide_layout&quot;&amp;gt;
    ...
    &amp;lt;div class=&quot;decide_details&quot;&amp;gt;
      &amp;lt;checkout-buy sku=&quot;porsche&quot;&amp;gt;&amp;lt;/checkout-buy&amp;gt;
    &amp;lt;/div&amp;gt;
    ...
    &amp;lt;div class=&quot;decide_summary&quot;&amp;gt;
      &amp;lt;checkout-minicart&amp;gt;&amp;lt;/checkout-minicart&amp;gt;
    &amp;lt;/div&amp;gt;
    ...
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;playing-with-the-complete-sample&quot;&gt;Playing With the Complete Sample&lt;/h2&gt;
&lt;p&gt;The complete sample is available on &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/tree/main/12-child-child-communication-with-blazor-webassembly-based-web-components&quot;&gt;GitHub&lt;/a&gt;. You can run it locally by spinning up all the services, but I&#39;ve also included a GitHub Actions &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/blob/main/.github/workflows/12-child-child-communication-with-blazor-webassembly-based-web-components.yml&quot;&gt;workflow&lt;/a&gt; that can deploy the whole solution to Azure (you just need to fork the repository and provide your own credentials).&lt;/p&gt;</description><link>http://www.tpeczek.com/2022/09/micro-frontends-in-action-with-aspnet.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-4412698974992852521</guid><pubDate>Wed, 17 Aug 2022 19:21:00 +0000</pubDate><atom:updated>2022-11-01T14:43:17.258+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">asp.net core</category><category domain="http://www.blogger.com/atom/ns#">blazor</category><category domain="http://www.blogger.com/atom/ns#">microfrontends</category><category domain="http://www.blogger.com/atom/ns#">web components</category><category domain="http://www.blogger.com/atom/ns#">webassembly</category><title>Micro Frontends in Action With ASP.NET Core - Composition via Blazor WebAssembly Based Web Components</title><description>&lt;p&gt;This is another post in my series on implementing &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction&quot;&gt;the samples&lt;/a&gt; from &lt;a href=&quot;https://www.manning.com/books/micro-frontends-in-action&quot;&gt;Micro Frontends in Action&lt;/a&gt; in ASP.NET Core:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/06/micro-frontends-in-action-with-aspnet.html&quot;&gt;Server-Side Routing via YARP in Azure Container Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/07/micro-frontends-in-action-with-aspnet.html&quot;&gt;Composition via YARP Transforms and Server-Side Includes (SSI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Composition via Blazor WebAssembly Based Web Components&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/09/micro-frontends-in-action-with-aspnet.html&quot;&gt;Communication Patterns for Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/10/micro-frontends-in-action-with-aspnet.html&quot;&gt;Universal Rendering With Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This time I&#39;m jumping from server-side composition to client-side composition. I&#39;ve used the word jumping because I haven&#39;t fully covered server-side composition yet. There is one more approach to server-side composition which I intend to cover later, but as lately I was doing some Blazor WebAssembly work I was more eager to write this one.&lt;/p&gt;
&lt;h2 id=&quot;expanding-the-project&quot;&gt;Expanding The Project&lt;/h2&gt;
&lt;p&gt;As you may remember from the &lt;a href=&quot;https://www.tpeczek.com/2022/06/micro-frontends-in-action-with-aspnet.html&quot;&gt;first post&lt;/a&gt;, the project consists of two services that are hosted in &lt;em&gt;Azure Container Apps&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgfaCy5JrPShFrxsBWEP62y4i2oJto56TXmTI8zJuZEmbiptudsSfjU2WYij2hl-YdSPKsJrqQfFarRopYabKot2fz-69JzhCn4RTfJ22zXD2Ft1_AIWDjnbwfTHfDYkOqXNr5uoK5yRwBN9GrdGXP5gZU-3z-VLdh-LuURi6wD9UA8Ozro5BXeK4S1/s1600/decide-and-inspire-frontend-layout.png&quot; alt=&quot;Decide and Inspire Frontend Layout&quot;&gt;&lt;/p&gt;
&lt;p&gt;Both services are using server-side rendering for their frontends, and the Decide service is loading Inspire service frontend fragment via Ajax. It&#39;s time to bring a new service to the picture, the Checkout service.&lt;/p&gt;
&lt;p&gt;The Checkout service is responsible for checkout flow. As this flow is more sophisticated than what Decide and Inspire services provide, the Checkout service requires client-side rendering to provide the experience of a single page application. This requirement creates a need for isolation and encapsulation of the Checkout service frontend fragment. There is a suite of technologies that can help solve that problem - Web Components.&lt;/p&gt;
&lt;h2 id=&quot;web-components&quot;&gt;Web Components&lt;/h2&gt;
&lt;p&gt;Web Components aim at enabling web developers to create reusable elements with well-encapsulated functionality. To achieve that, they bring together four different specifications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Custom Elements&lt;/em&gt;, which allows defining your own tags (custom elements) with their business logic.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Shadow DOM&lt;/em&gt;, which enables scripting and styling without collisions with other elements.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;HTML Templates&lt;/em&gt;, which allows writing markup templates.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;ES Module&lt;/em&gt;, which defines a consistent way for JavaScript inclusion and reuse.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Custom Elements is the most interesting one in this context. The way to create a Custom Element is to implement an ES6 class that extends &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement&quot;&gt;&lt;code&gt;HTMLElement&lt;/code&gt;&lt;/a&gt; and register it via &lt;code&gt;window.customElements.define&lt;/code&gt;. The class can also implement a set of lifecycle methods (&lt;code&gt;constructor&lt;/code&gt;, &lt;code&gt;connectedCallback&lt;/code&gt;, &lt;code&gt;disconnectedCallback&lt;/code&gt;, or &lt;code&gt;attributeChangedCallback&lt;/code&gt;). This allows for initializing a SPA framework (Angular, React, Vue, etc.) and instructing it to use &lt;code&gt;this&lt;/code&gt; as a root for rendering.&lt;/p&gt;
&lt;p&gt;This is exactly what is needed for the Checkout service, where the SPA framework will be Blazor WebAssembly.&lt;/p&gt;
&lt;h2 id=&quot;creating-a-blazor-webassembly-based-custom-element&quot;&gt;Creating a Blazor WebAssembly Based Custom Element&lt;/h2&gt;
&lt;p&gt;The way Blazor WebAssembly works fits nicely with Custom Elements. If you&#39;ve ever taken a look at the &lt;code&gt;Program.cs&lt;/code&gt; of a Blazor WebAssembly project, you might have noticed some calls to &lt;code&gt;builder.RootComponents.Add&lt;/code&gt;. This is because the WebAssembly to which your project gets compiled is designed to perform rendering into elements. Thanks to that a Blazor WebAssembly application can be wrapped into a Custom Element, it just requires proper initialization. You will be happy to learn, that this work has already been done. As part of &lt;a href=&quot;https://github.com/aspnet/AspLabs/tree/main/src/BlazorCustomElements&quot;&gt;AspLabs&lt;/a&gt; Steve Sanderson has prepared a package and instructions on how to make Blazor components available as Custom Elements. Let&#39;s do it.&lt;/p&gt;
&lt;p&gt;I&#39;ve started with an empty Blazor WebAssembly application &lt;a href=&quot;https://docs.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly?view=aspnetcore-6.0&amp;amp;WT.mc_id=DT-MVP-5002979#hosted-deployment-with-aspnet-core&quot;&gt;hosted in ASP.NET Core&lt;/a&gt;, to which I&#39;ve added a component that will server as a button initiating the checkout flow (the final version of that component will also contain some confirmation toast, if you&#39;re interested you can find it &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/blob/main/08-composition-via-blazor-webassembly-based-web-components/Demo.AspNetCore.MicroFrontendsInAction.Checkout/Frontend/Components/BuyButton.razor&quot;&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;button type=&quot;button&quot; @onclick=&quot;OnButtonClick&quot;&amp;gt;buy for @(String.IsNullOrWhiteSpace(Sku) ? &quot;???&quot; : _prices[Sku])&amp;lt;/button&amp;gt;
...

@code {
    // Dictionary of tractor prices
    private IDictionary&amp;lt;string, int&amp;gt; _prices = new Dictionary&amp;lt;string, int&amp;gt;
    {
        { &quot;porsche&quot;, 66 },
        { &quot;fendt&quot;, 54 },
        { &quot;eicher&quot;, 58 }
    };

    ...
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, I&#39;ve added the &lt;code&gt;Microsoft.AspNetCore.Components.CustomElements&lt;/code&gt; package to the project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Project Sdk=&quot;Microsoft.NET.Sdk.BlazorWebAssembly&quot;&amp;gt;
  ...
  &amp;lt;ItemGroup&amp;gt;
    ...
    &amp;lt;PackageReference Include=&quot;Microsoft.AspNetCore.Components.CustomElements&quot; Version=&quot;0.1.0-alpha.*&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I&#39;ve removed all &lt;code&gt;.razor&lt;/code&gt; files besides the above component and &lt;code&gt;_Imports.razor&lt;/code&gt;. After that, I modified the &lt;code&gt;Program.cs&lt;/code&gt; by removing the &lt;code&gt;builder.RootComponents.Add&lt;/code&gt; calls and adding &lt;code&gt;builder.RootComponents.RegisterAsCustomElement&lt;/code&gt; to expose my component as a &lt;code&gt;checkout-buy&lt;/code&gt; element.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.RootComponents.RegisterAsCustomElement&amp;lt;BuyButton&amp;gt;(&quot;checkout-buy&quot;);

await builder.Build().RunAsync();
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is it. After the build/publish the service will server the custom element through the &lt;code&gt;_content/Microsoft.AspNetCore.Components.CustomElements/BlazorCustomElements.js&lt;/code&gt; script, so it&#39;s time to plug it into the page served by Decide service.&lt;/p&gt;
&lt;h2 id=&quot;using-a-blazor-webassembly-based-custom-element&quot;&gt;Using a Blazor WebAssembly Based Custom Element&lt;/h2&gt;
&lt;p&gt;Before the Decide service can utilize the custom element, it is necessary to set up the routing. As described in previous posts, all services are hidden behind a &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/tree/main/08-composition-via-blazor-webassembly-based-web-components/Demo.AspNetCore.MicroFrontendsInAction.Proxy&quot;&gt;YARP-based proxy&lt;/a&gt;, which routes the incoming requests based on prefixes (to avoid conflicts). So far, both services were built in a way where the prefixes were an integral part of their implementation (they were included in actions and static content paths). With the Checkout service that would be hard to achieve, due to Blazor WebAssembly static assets.&lt;/p&gt;
&lt;p&gt;There is a way to control the base path for Blazor WebAssembly static assets (through &lt;code&gt;StaticWebAssetBasePath&lt;/code&gt; project property) but it doesn&#39;t affect the &lt;code&gt;BlazorCustomElements.js&lt;/code&gt; path. So, instead of complicating the service implementation to handle the prefix, it seems a lot better to make the prefixes a proxy concern and remove them there. YARP has an out-of-the-box capability to do so through &lt;a href=&quot;https://microsoft.github.io/reverse-proxy/articles/transforms.html#pathremoveprefix&quot;&gt;PathRemovePrefix&lt;/a&gt; transform. There is a ready-to-use extension method (&lt;code&gt;.WithTransformPathRemovePrefix&lt;/code&gt;) which allows adding that transform to a specific route.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...

var routes = new[]
{
    ...
    (new RouteConfig
    {
        RouteId = Constants.CHECKOUT_ROUTE_ID,
        ClusterId = Constants.CHECKOUT_CLUSTER_ID,
        Match = new RouteMatch { Path = Constants.CHECKOUT_ROUTE_PREFIX + &quot;/{**catch-all}&quot; }
    }).WithTransformPathRemovePrefix(Constants.CHECKOUT_ROUTE_PREFIX),
    ...
};

var clusters = new[]
{
    ...
    new ClusterConfig()
    {
        ClusterId = Constants.CHECKOUT_CLUSTER_ID,
        Destinations = new Dictionary&amp;lt;string, DestinationConfig&amp;gt;(StringComparer.OrdinalIgnoreCase)
        {
            { Constants.CHECKOUT_SERVICE_URL, new DestinationConfig() { Address = builder.Configuration[Constants.CHECKOUT_SERVICE_URL] } }
        }
    }
};

builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now the Decide service &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/tree/main/08-composition-via-blazor-webassembly-based-web-components/Demo.AspNetCore.MicroFrontendsInAction.Decide/Views/Products&quot;&gt;views&lt;/a&gt; can be modified to include the custom element. The first step is to add the static assets.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    ...
    &amp;lt;link href=&quot;/checkout/static/components.css&quot; rel=&quot;stylesheet&quot; /&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body class=&quot;decide_layout&quot;&amp;gt;
    ...
    &amp;lt;script src=&quot;/checkout/_content/Microsoft.AspNetCore.Components.CustomElements/BlazorCustomElements.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;/checkout/_framework/blazor.webassembly.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Sadly, this will not be enough for Blazor to work. When Blazor WebAssembly starts, it requests additional boot resources. They must be loaded from the Checkout service as well, which means including the prefix in URIs. This can be achieved thanks to the &lt;a href=&quot;https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/startup?view=aspnetcore-6.0&amp;amp;WT.mc_id=DT-MVP-5002979#load-boot-resources&quot;&gt;JS initializers feature&lt;/a&gt; which has been added in .NET 6. The automatic start of Blazor WebAssembly can be disabled, and it can be started manually which allows providing a function to customize the URIs.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  ...
  &amp;lt;body class=&quot;decide_layout&quot;&amp;gt;
    ...
    &amp;lt;script src=&quot;/checkout/_framework/blazor.webassembly.js&quot; autostart=&quot;false&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
      Blazor.start({
        loadBootResource: function (type, name, defaultUri, integrity) {
          return `/checkout/_framework/${name}`;
        }
      });
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally, the custom element can be used. It will be available through a tag matching the name provided as a parameter to &lt;code&gt;builder.RootComponents.RegisterAsCustomElement&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  ...
  &amp;lt;body class=&quot;decide_layout&quot;&amp;gt;
    ...
    &amp;lt;div class=&quot;decide_details&quot;&amp;gt;
      &amp;lt;checkout-buy sku=&quot;porsche&quot;&amp;gt;&amp;lt;/checkout-buy&amp;gt;
    &amp;lt;/div&amp;gt;
    ...
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;the-expanded-project&quot;&gt;The Expanded Project&lt;/h2&gt;
&lt;p&gt;As with previous samples, I&#39;ve created a GitHub Actions &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/blob/main/.github/workflows/08-composition-via-blazor-webassembly-based-web-components.yml&quot;&gt;workflow&lt;/a&gt; that deploys the &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/tree/main/08-composition-via-blazor-webassembly-based-web-components&quot;&gt;solution&lt;/a&gt; to Azure. After the deployment, if you navigate to the URL of the &lt;code&gt;ca-app-proxy&lt;/code&gt; Container App, you will see a page with following layout.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj8ygipn8iq-8qnICr-4meqt38rUL2hv9x7Fqz2jqkfPaGYzw5xbbb-u2Y7SH7A2Al9CM-lOxgdtdWEIO7dEFKJyyfW42H4feeVFCgCTdQ2_HL2-uN70b5hnKR9RM-dbiY56Foq1yTzb78tjUwQzQg1tY0ufsbe6WdwLbDjUWK8LBF5cTn8BweTx0mS/s1600/decide-inspire-and-checkout-frontend-layout.png&quot; alt=&quot;Decide, Inspire, and Checkout Frontend Layout&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can click the button rendered by the custom element and see the toast notification.&lt;/p&gt;
&lt;p&gt;This approach highlights one of the benefits of micro frontends, the freedom to choose the fronted stack. Thanks to the encapsulation of the Blazor WebAssembly app into a custom element, the Decide service can host it in its static HTML without understanding anything except how to load the scripts. If we would want to hide even that (something I haven&#39;t done here) we could create a script that encapsulates Blazor WebAssembly loading and initialization. That script could be a shared asset that loads Blazor WebAssembly from CDN (which besides the performance benefit would be also a way to go for a solution with multiple services using Blazor WebAssembly).&lt;/p&gt;</description><link>http://www.tpeczek.com/2022/08/micro-frontends-in-action-with-aspnet.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgfaCy5JrPShFrxsBWEP62y4i2oJto56TXmTI8zJuZEmbiptudsSfjU2WYij2hl-YdSPKsJrqQfFarRopYabKot2fz-69JzhCn4RTfJ22zXD2Ft1_AIWDjnbwfTHfDYkOqXNr5uoK5yRwBN9GrdGXP5gZU-3z-VLdh-LuURi6wD9UA8Ozro5BXeK4S1/s72-c/decide-and-inspire-frontend-layout.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-5699190681247385026</guid><pubDate>Thu, 28 Jul 2022 12:25:00 +0000</pubDate><atom:updated>2022-07-28T14:25:47.350+02:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">asp.net core</category><category domain="http://www.blogger.com/atom/ns#">rate limiting</category><title>Exploring Communication of Rate Limits in ASP.NET Core With Rate Limit Headers</title><description>&lt;p&gt;Rate limiting (sometimes also referred to as throttling) is a key mechanism when it comes to ensuring API responsiveness and availability. By enforcing usage quotas, it can protect an API from issues like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Denial Of Service (DOS) attacks&lt;/li&gt;
&lt;li&gt;Degraded performance due to traffic spikes&lt;/li&gt;
&lt;li&gt;Monopolization by a single consumer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Despite its importance, the typical approach to rate limiting is far from perfect when it comes to communicating usage quotas by services and (as a result) respecting those quotas by clients. It shouldn&#39;t be a surprise that various services were experimenting with different approaches to solve this problem. The common pattern in the web world is that some of such experiments start to gain traction which results in standardization efforts. This is exactly what is currently happening around communicating services usage quotas with &lt;a href=&quot;https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/&quot;&gt;&lt;em&gt;RateLimit Fields for HTTP&lt;/em&gt;&lt;/a&gt; Internet-Draft. As rate limiting will have &lt;a href=&quot;https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/&quot;&gt;built-in support&lt;/a&gt; with .NET 7, I thought it might be a good time to take a look at what this potential standard is bringing. But before that, let&#39;s recall how HTTP currently supports rate limiting (excluding custom extensions).&lt;/p&gt;
&lt;h2 id=&quot;current-http-support-for-rate-limiting&quot;&gt;Current HTTP Support for Rate Limiting&lt;/h2&gt;
&lt;p&gt;When it comes to current support for rate limiting in HTTP, it&#39;s not much. If a service detects that a client has reached the quota, instead of a regular response it may respond with &lt;em&gt;429 (Too Many Request)&lt;/em&gt; or &lt;em&gt;503 (Service Unavailable)&lt;/em&gt;. Additionally, the service may include a &lt;em&gt;Retry-After&lt;/em&gt; header in the response to indicate how long the client should wait before making another request. That&#39;s it. It means that client can be only reactive. There is no way for a client to get the information about the quota to avoid hitting it.&lt;/p&gt;
&lt;p&gt;In general, this works. That said, handling requests which are above the quota still consumes some resources on the service side. Clients would also prefer to be able to understand the quota and adjust their usage patterns instead of handling it as an exceptional situation. So as I said, it&#39;s not much.&lt;/p&gt;
&lt;h2 id=&quot;proposed-rate-limit-headers&quot;&gt;Proposed Rate Limit Headers&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/&quot;&gt;&lt;em&gt;RateLimit Fields for HTTP&lt;/em&gt;&lt;/a&gt; Internet-Draft proposes four new headers which aim at enabling a service to communicate usage quotas and policies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RateLimit-Limit&lt;/code&gt; to communicate the total quota within a time window.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RateLimit-Remaining&lt;/code&gt; to communicate the remaining quota within the current time window.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RateLimit-Reset&lt;/code&gt; to communicate the time (in seconds) remaining in the current time window.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RateLimit-Policy&lt;/code&gt; to communicate the overall quota policy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most interesting one is &lt;code&gt;RateLimit-Policy&lt;/code&gt;. It is a list of quota policy items. A quota policy item consists of a quota limit and a single required parameter &lt;code&gt;w&lt;/code&gt; which provides a time window in seconds. Custom parameters are allowed and should be treated as comments. Below you can see an example of &lt;code&gt;RateLimit-Policy&lt;/code&gt; which informs that client is allowed to make 10 requests per second, 50 requests per minute, 1000 requests per hour, and 5000 per 24 hours.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RateLimit-Policy: 10;w=1, 50;w=60, 1000;w=3600, 5000;w=86400
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The only headers which intend to be required are &lt;code&gt;RateLimit-Limit&lt;/code&gt; and &lt;code&gt;RateLimit-Reset&lt;/code&gt; (&lt;code&gt;RateLimit-Remaining&lt;/code&gt; is strongly recommended). So, how an ASP.NET Core based service can server those headers.&lt;/p&gt;
&lt;h2 id=&quot;communicating-quotas-when-using-asp-net-core-middleware&quot;&gt;Communicating Quotas When Using ASP.NET Core Middleware&lt;/h2&gt;
&lt;p&gt;As I&#39;ve already mentioned, &lt;a href=&quot;https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/&quot;&gt;built-in support for rate limiting&lt;/a&gt; comes to .NET with .NET 7. It brings generic purpose primitives for writing rate limiters as well as a few ready-to-use implementations. It also brings a middleware for ASP.NET Core. The below example shows the definition of a fixed time window policy which allows 5 requests per 10 seconds. The &lt;code&gt;OnRejected&lt;/code&gt; callback is also provided to return &lt;em&gt;429 (Too Many Request)&lt;/em&gt; status code and set the &lt;em&gt;Retry-After&lt;/em&gt; header value based on provided metadata.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;using System.Globalization;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.UseHttpsRedirection();

app.UseRateLimiter(new RateLimiterOptions
{
    OnRejected = (context, cancellationToken) =&amp;gt;
    {
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
        }

        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;

        return new ValueTask();
    }
}.AddFixedWindowLimiter(&quot;fixed-window&quot;, new FixedWindowRateLimiterOptions(
    permitLimit: 5,
    queueProcessingOrder: QueueProcessingOrder.OldestFirst,
    queueLimit: 0,
    window: TimeSpan.FromSeconds(10),
    autoReplenishment: true
)));

app.MapGet(&quot;/&quot;, context =&amp;gt; context.Response.WriteAsync(&quot;-- Demo.RateLimitHeaders.AspNetCore.RateLimitingMiddleware --&quot;))
    .RequireRateLimiting(&quot;fixed-window&quot;);

app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The question is, if and how this can be extended to return the rate limit headers? The answer is, sadly, that there seems to be no way to provide the required ones right now. All the information about rate limit policies is well hidden from public access. It would be possible to provide &lt;code&gt;RateLimit-Limit&lt;/code&gt; and &lt;code&gt;RateLimit-Policy&lt;/code&gt; as they are a direct result of provided options. It is also possible to provide &lt;code&gt;RateLimit-Remaining&lt;/code&gt;, but it requires rewriting a lot of the middleware ecosystem to get the required value. What seems completely impossible to get right now is &lt;code&gt;RateLimit-Reset&lt;/code&gt; as timers are managed centrally deep in &lt;code&gt;System.Threading.RateLimiting&lt;/code&gt; core without any access to their state. There is an option to provide your own timers, but it would mean rewriting the entire middleware stack and taking a lot of responsibility from &lt;code&gt;System.Threading.RateLimiting&lt;/code&gt;. Let&#39;s hope that things will improve.&lt;/p&gt;
&lt;h2 id=&quot;communicating-quotas-when-using-aspnetcoreratelimit-package&quot;&gt;Communicating Quotas When Using AspNetCoreRateLimit Package&lt;/h2&gt;
&lt;p&gt;That built-in support for rate limiting is something that is just coming to .NET. So far the ASP.NET Core developers were using their own implementations or non-Microsoft packages for this purpose. Arguably, the most popular rate limiting solution for ASP.NET Core is &lt;a href=&quot;https://github.com/stefanprodan/AspNetCoreRateLimit&quot;&gt;AspNetCoreRateLimit&lt;/a&gt;. The example below provides similar functionality to the one from the built-in rate limiting example.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;using AspNetCoreRateLimit;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMemoryCache();

builder.Services.Configure&amp;lt;IpRateLimitOptions&amp;gt;(options =&amp;gt;
{
    options.EnableEndpointRateLimiting = true;
    options.StackBlockedRequests = false;
    options.HttpStatusCode = 429;
    options.GeneralRules = new List&amp;lt;RateLimitRule&amp;gt;
    {
        new RateLimitRule { Endpoint = &quot;*&quot;, Period = &quot;10s&quot;, Limit = 5 }
    };
});

builder.Services.AddInMemoryRateLimiting();

builder.Services.AddSingleton&amp;lt;IRateLimitConfiguration, RateLimitConfiguration&amp;gt;();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseIpRateLimiting();

app.MapGet(&quot;/&quot;, context =&amp;gt; context.Response.WriteAsync(&quot;-- Demo.RateLimitHeaders.AspNetCore.RateLimitPackage --&quot;));

app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The AspNetCoreRateLimit has its own custom way of communicating quotas with HTTP headers. In the case of the above example, you might receive the following values in response.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X-Rate-Limit-Limit: 10s
X-Rate-Limit-Remaining: 4
X-Rate-Limit-Reset: 2022-07-24T11:30:47.2291052Z
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As you can see, they provide potentially useful information but not in a way that &lt;em&gt;RateLimit Fields for HTTP&lt;/em&gt; is going for. Luckily, AspNetCoreRateLimit is not as protective about its internal state and algorithms, the information needed can be accessed and served in a different way.&lt;/p&gt;
&lt;p&gt;The information about the current state is kept in &lt;code&gt;IRateLimitCounterStore&lt;/code&gt;. This is a dependency that could be accessed directly, but the method for generating needed identifiers lives in &lt;code&gt;ProcessingStrategy&lt;/code&gt; so it will be better to create an implementation of it dedicated for purpose of just getting the counters state.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal interface IRateLimitHeadersOnlyProcessingStrategy : IProcessingStrategy
{ }

internal class RateLimitHeadersOnlyProcessingStrategy : ProcessingStrategy, IRateLimitHeadersOnlyProcessingStrategy
{
    private readonly IRateLimitCounterStore _counterStore;
    private readonly IRateLimitConfiguration _config;

    public RateLimitHeadersOnlyProcessingStrategy(IRateLimitCounterStore counterStore, IRateLimitConfiguration config) : base(config)
    {
        _counterStore = counterStore;
        _config = config;
    }

    public override async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule,
        ICounterKeyBuilder counterKeyBuilder, RateLimitOptions rateLimitOptions, CancellationToken cancellationToken = default)
    {
        string rateLimitCounterId = BuildCounterKey(requestIdentity, rule, counterKeyBuilder, rateLimitOptions);

        RateLimitCounter? rateLimitCounter = await _counterStore.GetAsync(rateLimitCounterId, cancellationToken);
        if (rateLimitCounter.HasValue)
        {
            return new RateLimitCounter
            {
                Timestamp = rateLimitCounter.Value.Timestamp,
                Count = rateLimitCounter.Value.Count
            };
        }
        else
        {
            return new RateLimitCounter
            {
                Timestamp = DateTime.UtcNow,
                Count = _config.RateIncrementer?.Invoke() ?? 1
            };
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second thing that is needed are rules which apply to specific endpoint and identity. Those can be retrieved from specific (either based on IP or client identifier) &lt;code&gt;IRateLimitProcessor&lt;/code&gt;. The &lt;code&gt;IRateLimitProcessor&lt;/code&gt; is also a tunnel to &lt;code&gt;IProcessingStrategy&lt;/code&gt;, so it&#39;s nice we have a dedicated one. But what about the identity I&#39;ve just mentioned? The algorithm to retrieve it lies in &lt;code&gt;RateLimitMiddleware&lt;/code&gt;, so access to it will be needed. There are two options here. One is to inherit from &lt;code&gt;RateLimitMiddleware&lt;/code&gt; and the other is to create an instance of one of its implementation and use it as a dependency. The first case would require hiding the base implementation of &lt;code&gt;Invoke&lt;/code&gt; as it can&#39;t be overridden. I didn&#39;t like that, so I went with keeping an instance as a dependency approach. This led to the following code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class IpRateLimitHeadersMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RateLimitOptions _rateLimitOptions;
    private readonly IpRateLimitProcessor _ipRateLimitProcessor;
    private readonly IpRateLimitMiddleware _ipRateLimitMiddleware;

    public IpRateLimitHeadersMiddleware(RequestDelegate next,
        IRateLimitHeadersOnlyProcessingStrategy processingStrategy, IOptions&amp;lt;IpRateLimitOptions&amp;gt; options, IIpPolicyStore policyStore,
        IRateLimitConfiguration config, ILogger&amp;lt;IpRateLimitMiddleware&amp;gt; logger)
    {
        _next = next;
        _rateLimitOptions = options?.Value;
        _ipRateLimitProcessor = new IpRateLimitProcessor(options?.Value, policyStore, processingStrategy);
        _ipRateLimitMiddleware = new IpRateLimitMiddleware(next, processingStrategy, options, policyStore, config, logger);
    }

    public async Task Invoke(HttpContext context)
    {
        ClientRequestIdentity identity = await _ipRateLimitMiddleware.ResolveIdentityAsync(context);

        if (!_ipRateLimitProcessor.IsWhitelisted(identity))
        {
            var rateLimitRulesWithCounters = new Dictionary&amp;lt;RateLimitRule, RateLimitCounter&amp;gt;();

            foreach (var rateLimitRule in await _ipRateLimitProcessor.GetMatchingRulesAsync(identity, context.RequestAborted))
            {
                rateLimitRulesWithCounters.Add(
                    rateLimitRule,
                    await _ipRateLimitProcessor.ProcessRequestAsync(identity, rateLimitRule, context.RequestAborted)
                 );
            }
        }

        await _next.Invoke(context);

        return;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;rateLimitRulesWithCounters&lt;/code&gt; contains all the rules applying to the endpoint in the context of the current request. This can be used to calculate the rate limit headers values.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class IpRateLimitHeadersMiddleware
{
    private class RateLimitHeadersState
    {
        public HttpContext Context { get; set; }

        public int Limit { get; set; }

        public int Remaining { get; set; }

        public int Reset { get; set; }

        public string Policy { get; set; } = String.Empty;

        public RateLimitHeadersState(HttpContext context)
        {
            Context = context;
        }
    }

    ...

    public async Task Invoke(HttpContext context)
    {
        ...
    }

    private RateLimitHeadersState PrepareRateLimitHeaders(HttpContext context, Dictionary&amp;lt;RateLimitRule, RateLimitCounter&amp;gt; rateLimitRulesWithCounters)
    {
        RateLimitHeadersState rateLimitHeadersState = new RateLimitHeadersState(context);

        var rateLimitHeadersRuleWithCounter = rateLimitRulesWithCounters.OrderByDescending(x =&amp;gt; x.Key.PeriodTimespan).FirstOrDefault();
        var rateLimitHeadersRule = rateLimitHeadersRuleWithCounter.Key;
        var rateLimitHeadersCounter = rateLimitHeadersRuleWithCounter.Value;

        rateLimitHeadersState.Limit = (int)rateLimitHeadersRule.Limit;

        rateLimitHeadersState.Remaining = rateLimitHeadersState.Limit - (int)rateLimitHeadersCounter.Count;

        rateLimitHeadersState.Reset = (int)(
            (rateLimitHeadersCounter.Timestamp+ (rateLimitHeadersRule.PeriodTimespan ?? rateLimitHeadersRule.Period.ToTimeSpan())) - DateTime.UtcNow
            ).TotalSeconds;

        rateLimitHeadersState.Policy = String.Join(
            &quot;, &quot;,
            rateLimitRulesWithCounters.Keys.Select(rateLimitRule =&amp;gt;
                $&quot;{(int)rateLimitRule.Limit};w={(int)(rateLimitRule.PeriodTimespan ?? rateLimitRule.Period.ToTimeSpan()
            ).TotalSeconds}&quot;)
        );

        return rateLimitHeadersState;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only thing that remains is setting the headers on the response.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class IpRateLimitHeadersMiddleware
{
    ...

    public async Task Invoke(HttpContext context)
    {
        ...

        if (!_ipRateLimitProcessor.IsWhitelisted(identity))
        {
            ...

            if (rateLimitRulesWithCounters.Any() &amp;amp;&amp;amp; !_rateLimitOptions.DisableRateLimitHeaders)
            {
                context.Response.OnStarting(
                    SetRateLimitHeaders,
                    state: PrepareRateLimitHeaders(context, rateLimitRulesWithCounters)
                );
            }
        }

        ...
    }

    ...

    private Task SetRateLimitHeaders(object state)
    {
        var rateLimitHeadersState = (RateLimitHeadersState)state;

        rateLimitHeadersState.Context.Response.Headers[&quot;RateLimit-Limit&quot;] = rateLimitHeadersState.Limit.ToString(CultureInfo.InvariantCulture);
        rateLimitHeadersState.Context.Response.Headers[&quot;RateLimit-Remaining&quot;] = rateLimitHeadersState.Remaining.ToString(CultureInfo.InvariantCulture);
        rateLimitHeadersState.Context.Response.Headers[&quot;RateLimit-Reset&quot;] = rateLimitHeadersState.Reset.ToString(CultureInfo.InvariantCulture);
        rateLimitHeadersState.Context.Response.Headers[&quot;RateLimit-Policy&quot;] = rateLimitHeadersState.Policy;

        return Task.CompletedTask;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After registering the &lt;code&gt;RateLimitHeadersOnlyProcessingStrategy&lt;/code&gt; and &lt;code&gt;IpRateLimitHeadersMiddleware&lt;/code&gt; (I&#39;ve registered it after the &lt;code&gt;IpRateLimitMiddleware&lt;/code&gt;) response will contain values similar to the following ones.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RateLimit-Limit: 5
RateLimit-Remaining: 4
RateLimit-Reset: 9
RateLimit-Policy: 5;w=10
X-Rate-Limit-Limit: 10s
X-Rate-Limit-Remaining: 4
X-Rate-Limit-Reset: 2022-07-25T20:57:32.0746592Z
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The code works but certainly isn&#39;t perfect, so I&#39;ve created an &lt;a href=&quot;https://github.com/stefanprodan/AspNetCoreRateLimit/issues/352&quot;&gt;issue&lt;/a&gt; in hope that AspNetCoreRateLimit will get those headers built in.&lt;/p&gt;
&lt;h2 id=&quot;limiting-the-number-of-outbound-requests-in-httpclient&quot;&gt;Limiting the Number of Outbound Requests in HttpClient&lt;/h2&gt;
&lt;p&gt;The general rule around rate limit headers is that they should be treated as informative, so the client doesn&#39;t have to do anything specific with them. They are also described as generated at response time without any guarantee of consistency between requests. This makes perfect sense. In the simple examples above, multiple clients would be competing for the same quota so received headers values don&#39;t exactly tell how many requests a specific client can make within a given window. But, real-life scenarios are usually more specific and complex. It&#39;s very common for quotas to be per client or per IP address (this is why AspNetCoreRateLimit has concepts like request identity as a first-class citizen, the ASP.NET Core built-in middleware also enables sophisticated scenarios by using &lt;code&gt;PartitionedRateLimiter&lt;/code&gt; at its core). In a such scenario, the client might want to use rate limit headers to avoid making requests which have a high likelihood of being throttled. Let&#39;s explore that, below is a simple code that can handle &lt;em&gt;429 (Too Many Request)&lt;/em&gt; responses and utilize the &lt;em&gt;Retry-After&lt;/em&gt; header.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;HttpClient client = new();
client.BaseAddress = new(&quot;http://localhost:5262&quot;);

while (true)
{
    Console.Write(&quot;{0:hh:mm:ss}: &quot;, DateTime.UtcNow);

    int nextRequestDelay = 1;

    try
    {
        HttpResponseMessage response = await client.GetAsync(&quot;/&quot;);
        if (response.IsSuccessStatusCode)
        {
            Console.WriteLine(await response.Content.ReadAsStringAsync());
        }
        else
        {
            Console.Write($&quot;{(int)response.StatusCode}: {await response.Content.ReadAsStringAsync()}&quot;);

            string? retryAfter = response.Headers.GetValues(&quot;Retry-After&quot;).FirstOrDefault();
            if (Int32.TryParse(retryAfter, out nextRequestDelay))
            {
                Console.Write($&quot; | Retry-After: {nextRequestDelay}&quot;);
            }

            Console.WriteLine();
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }

    await Task.Delay(TimeSpan.FromSeconds(nextRequestDelay));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&#39;s assume that the service is sending all rate limit headers and that they are dedicated to the client. We can rate limit the &lt;code&gt;HttpClient&lt;/code&gt; by creating a &lt;code&gt;DelegatingHandler&lt;/code&gt; which will read the &lt;code&gt;RateLimit-Policy&lt;/code&gt; header value and instantiate a &lt;code&gt;FixedWindowRateLimiter&lt;/code&gt; based on it. The &lt;code&gt;FixedWindowRateLimiter&lt;/code&gt; will be used to rate limit the outbound requests - if a lease can&#39;t be acquired a locally created &lt;code&gt;HttpResponseMessage&lt;/code&gt; will be returned.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class RateLimitPolicyHandler : DelegatingHandler
{
    private string? _rateLimitPolicy;
    private RateLimiter? _rateLimiter;

    private static readonly Regex RATE_LIMIT_POLICY_REGEX = new Regex(@&quot;(\d+);w=(\d+)&quot;, RegexOptions.Compiled);

    public RateLimitPolicyHandler() : base(new HttpClientHandler())
    { }

    protected override async Task&amp;lt;HttpResponseMessage&amp;gt; SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (_rateLimiter is not null)
        {
            using var rateLimitLease = await _rateLimiter.WaitAsync(1, cancellationToken);
            if (rateLimitLease.IsAcquired)
            {
                return await base.SendAsync(request, cancellationToken);
            }

            var rateLimitResponse = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
            rateLimitResponse.Content = new StringContent($&quot;Service rate limit policy ({_rateLimitPolicy}) exceeded!&quot;);

            if (rateLimitLease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
            {
                rateLimitResponse.Headers.Add(&quot;Retry-After&quot;, ((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
            }

            return rateLimitResponse;
        }

        var response = await base.SendAsync(request, cancellationToken);

        if (response.Headers.Contains(&quot;RateLimit-Policy&quot;))
        {
            _rateLimitPolicy = response.Headers.GetValues(&quot;RateLimit-Policy&quot;).FirstOrDefault();

            if (_rateLimitPolicy is not null)
            {
                Match rateLimitPolicyMatch = RATE_LIMIT_POLICY_REGEX.Match(_rateLimitPolicy);

                if (rateLimitPolicyMatch.Success)
                {
                    int limit = Int32.Parse(rateLimitPolicyMatch.Groups[1].Value);
                    int window = Int32.Parse(rateLimitPolicyMatch.Groups[2].Value);

                    _rateLimiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(
                        limit,
                        QueueProcessingOrder.NewestFirst,
                        0,
                        TimeSpan.FromSeconds(window),
                        true
                    ));

                    string? rateLimitRemaining = response.Headers.GetValues(&quot;RateLimit-Remaining&quot;).FirstOrDefault();
                    if (Int32.TryParse(rateLimitRemaining, out int remaining))
                    {
                        using var rateLimitLease = await _rateLimiter.WaitAsync(limit - remaining, cancellationToken);
                    }
                }
            }
        }

        return response;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above code also uses the &lt;code&gt;RateLimit-Remaining&lt;/code&gt; header value to acquire leases for requests which are no longer available in the initial window.&lt;/p&gt;
&lt;p&gt;Now depending if the sample code is run with the &lt;code&gt;RateLimitPolicyHandler&lt;/code&gt; in the &lt;code&gt;HttpClient&lt;/code&gt; pipeline or not, the console output will be different as those 429 will be coming from a different place.&lt;/p&gt;
&lt;h2 id=&quot;opinions&quot;&gt;Opinions&lt;/h2&gt;
&lt;p&gt;The rate limit headers seem like an interesting addition for communicating services usage quotas. Properly used in the right situations might be a useful tool, it is just important not to treat them as guarantees.&lt;/p&gt;
&lt;p&gt;Serving rate limit headers from ASP.NET Core right now has its challenges. If they will become a standard and gain popularity, I think this will change.&lt;/p&gt;
&lt;p&gt;If you want to play with the samples, they are available on &lt;a href=&quot;https://github.com/tpeczek/Demo.RateLimitHeaders&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</description><link>http://www.tpeczek.com/2022/07/exploring-communication-of-rate-limits.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-310692594729019759</guid><pubDate>Tue, 05 Jul 2022 14:46:00 +0000</pubDate><atom:updated>2022-11-01T14:43:42.558+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">asp.net core</category><category domain="http://www.blogger.com/atom/ns#">microfrontends</category><category domain="http://www.blogger.com/atom/ns#">yarp</category><title>Micro Frontends in Action With ASP.NET Core - Composition via YARP Transforms and Server-Side Includes (SSI)</title><description>&lt;p&gt;I&#39;m continuing my series on implementing &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction&quot;&gt;the samples&lt;/a&gt; from &lt;a href=&quot;https://www.manning.com/books/micro-frontends-in-action&quot;&gt;Micro Frontends in Action&lt;/a&gt; in ASP.NET Core:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/06/micro-frontends-in-action-with-aspnet.html&quot;&gt;Server-Side Routing via YARP in Azure Container Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Composition via YARP Transforms and Server-Side Includes (SSI)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/08/micro-frontends-in-action-with-aspnet.html&quot;&gt;Composition via Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/09/micro-frontends-in-action-with-aspnet.html&quot;&gt;Communication Patterns for Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/10/micro-frontends-in-action-with-aspnet.html&quot;&gt;Universal Rendering With Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the previous post, I described how I&#39;ve deployed the two services which provide fragments of the frontend to the single &lt;a href=&quot;https://docs.microsoft.com/en-us/azure/container-apps/environment&quot;&gt;Container Apps environment&lt;/a&gt; and then hidden them behind a &lt;a href=&quot;https://github.com/microsoft/reverse-proxy&quot;&gt;YARP&lt;/a&gt;-based proxy. This technique (called server-side routing) solves some problems related to bad user experience (multiple domains), browser security (CORS), or search engines. That said, there are some other problems on the table.&lt;/p&gt;
&lt;p&gt;The first common problem is performance. Server-side routing does improve performance over solution where fragments are loaded directly from different domains by removing multiple DNS lookups, SSL handshakes, etc. Still, separated AJAX calls can have a very negative impact on overall page load time, especially on slower connections.&lt;/p&gt;
&lt;p&gt;The second common problem is layout shifts. When fragments are being loaded, it can often cause already visible page content to &quot;jump&quot;. This is frustrating for the end-users.&lt;/p&gt;
&lt;p&gt;A solution to both of these problems can be providing a complete page to the browser in a response to the first request.&lt;/p&gt;
&lt;h2 id=&quot;server-side-composition&quot;&gt;Server-Side Composition&lt;/h2&gt;
&lt;p&gt;Server-side composition is a technique, where the page is being fully assembled (which means requesting all the required fragments) on the server. The composition can be done either by a central service (a proxy) or can be done in a decentralized manner, where every service requests fragments it requires to build its own UI. As the current solution already has a proxy in place, I&#39;ve decided to start with a centralized approach. There are two possible mechanisms discussed in the book for this purpose: &lt;a href=&quot;https://www.w3.org/Jigsaw/Doc/User/SSI.html&quot;&gt;Server-Side Includes (SSI)&lt;/a&gt; and &lt;a href=&quot;https://www.w3.org/TR/esi-lang/&quot;&gt;Edge-Side Includes (ESI)&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;server-side-includes-ssi-&quot;&gt;Server-Side Includes (SSI)&lt;/h2&gt;
&lt;p&gt;Server-Side Includes is a quite old mechanism, it dates back to the &lt;a href=&quot;https://en.wikipedia.org/wiki/NCSA_HTTPd&quot;&gt;&lt;em&gt;NCSA HTTPd&lt;/em&gt;&lt;/a&gt; web server. It defines a set of directives (called commands or elements in some implementations) that can be placed in HTML and evaluated by the server while the page is being served. Currently, SSI is supported by &lt;em&gt;Apache&lt;/em&gt;, &lt;em&gt;Nginx&lt;/em&gt;, and &lt;em&gt;IIS&lt;/em&gt;. The subset of supported directives varies between implementations, but the most useful and always available one is &lt;code&gt;include&lt;/code&gt;, which the server replaces with a file or result of a request. All that a service needs to do, is put that directive as part of the returned markup.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  ...
  &amp;lt;body class=&quot;decide_layout&quot;&amp;gt;
    ...
    &amp;lt;aside class=&quot;decide_recos&quot;&amp;gt;
      &amp;lt;!--#include virtual=&quot;/inspire/fragment/recommendations/porsche&quot; --&amp;gt;
    &amp;lt;/aside&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;All the magic needs to happen at the proxy level, the only question is how?&lt;/p&gt;
&lt;h2 id=&quot;supporting-ssi-in-yarp-with-response-body-transform&quot;&gt;Supporting SSI in YARP With Response Body Transform&lt;/h2&gt;
&lt;p&gt;Every well-respected reverse proxy provides more capabilities than just routing and YARP is no different. One of the capabilities provided by YARP, which goes beyond routing, is &lt;a href=&quot;https://microsoft.github.io/reverse-proxy/articles/transforms.html&quot;&gt;transforms&lt;/a&gt;. Transforms allow for modifying parts of the request or response as part of the flow. Currently, there are three categories of transforms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Request&lt;/li&gt;
&lt;li&gt;Response&lt;/li&gt;
&lt;li&gt;Response Trailers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In every one of those categories, YARP provides a number of built-in transforms which allow for modifying path, query string, client certificates, and headers. There are no built-in transforms for request and response body, which probably makes sense as transforms including request and response body are slightly tricky and potentially dangerous. The first tricky part is that &lt;a href=&quot;https://microsoft.github.io/reverse-proxy/articles/direct-forwarding.html&quot;&gt;direct forwarding&lt;/a&gt; ignores any modifications to the response body which transforms could make, so the proxy service needs to be switched to a &quot;full&quot; reverse proxy experience.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;app.Run();

var builder = WebApplication.CreateBuilder(args);

...

builder.Services.AddReverseProxy();

var app = builder.Build();

app.MapReverseProxy();

app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Moving away from direct forwarding means that there is no longer a way to define path prefixes directly. The out-of-the-box approach for configuring the reverse proxy requires is through &lt;a href=&quot;https://microsoft.github.io/reverse-proxy/articles/config-files.html&quot;&gt;configuration files&lt;/a&gt;. This would mean that I would have to change the way in which the deployment workflow provides the proxy with URLs to other services. That&#39;s something I really didn&#39;t want to do. Luckily, there is a possibility of implementing configuration providers to load the configuration programmatically from any source. The documentation even contains an &lt;a href=&quot;https://microsoft.github.io/reverse-proxy/articles/config-providers.html#example&quot;&gt;example&lt;/a&gt; of an in-memory configuration provider which I&#39;ve basically copy-pasted (looking at the YARP &lt;a href=&quot;https://github.com/microsoft/reverse-proxy/issues/1713&quot;&gt;backlog&lt;/a&gt; it seems that the team has noticed its usefulness and it will be available out-of-the-box as well). This allowed me to keep the routes and clusters (this is how destinations are represented in YARP) configuration in the code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

var routes = new[]
{
    ...
    new RouteConfig
    {
        RouteId = Constants.DECIDE_ROUTE_ID,
        ClusterId = Constants.DECIDE_CLUSTER_ID,
        Match = new RouteMatch { Path = &quot;/decide/{**catch-all}&quot; }
    },
    ...
};

var clusters = new[]
{
    new ClusterConfig()
    {
        ClusterId = Constants.DECIDE_CLUSTER_ID,
        Destinations = new Dictionary&amp;lt;string, DestinationConfig&amp;gt;(StringComparer.OrdinalIgnoreCase)
        {
            { Constants.DECIDE_SERVICE_URL, new DestinationConfig() { Address = builder.Configuration[Constants.DECIDE_SERVICE_URL] } }
        }
    },
    ...
};

builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the configuration in place, it&#39;s time for the transform. There are two main ways for adding transforms. One is a callback and the other is &lt;code&gt;ITransformProvider&lt;/code&gt; implementation. The &lt;code&gt;ITransformProvider&lt;/code&gt; implementation gives more flexibility and isolation, so I&#39;ve decided to go with it. As the implementation will be a registered dependency, it gives full access to dependency injection. It also gives validation capabilities for routes and clusters.&lt;/p&gt;
&lt;p&gt;The simplest (&quot;no-op&quot;) implementation of &lt;code&gt;ITransformProvider&lt;/code&gt; can look like below.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class SsiTransformProvider : ITransformProvider
{
    public void ValidateRoute(TransformRouteValidationContext context)
    { }

    public void ValidateCluster(TransformClusterValidationContext context)
    { }

    public void Apply(TransformBuilderContext transformBuildContext)
    {
        transformBuildContext.AddResponseTransform(TransformResponse);
    }

    private ValueTask TransformResponse(ResponseTransformContext responseContext)
    {
        return default;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In order to register that &lt;code&gt;ITransformProvider&lt;/code&gt; implementation (and make &lt;code&gt;TranformResponse&lt;/code&gt; part of the flow) it is enough to call &lt;code&gt;AddTransforms&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters)
    .AddTransforms&amp;lt;SsiTransformProvider&amp;gt;();

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is where it is important to understand further specifics of transforms that are working with the request or response body. As a result of the &lt;code&gt;Apply&lt;/code&gt; method from the above implementation, the &lt;code&gt;TranformResponse&lt;/code&gt; will be executed for every flow going through YARP. This is too broad because if that transform deals with request or response body, it will come with a performance penalty. It will have to read (essentially buffer) the response from the destination. The moment the body has been read, it will also have to be written to the &lt;code&gt;HttpContext&lt;/code&gt; of the YARP response, otherwise the response will be empty. This is happening because YARP doesn&#39;t buffer the response as part of the flow (due to performance reasons), instead it attempts to read the stream which in this case is at the end.&lt;/p&gt;
&lt;p&gt;The performance penalty means that there is a need to limit the scope of impact by adding the transform only to certain routes. The routes which should be transformed need to be somehow marked. For that purpose I&#39;ve decided to include additional information in the routes through metadata. Metadata is a dictionary available on &lt;code&gt;RouteConfig&lt;/code&gt;. I&#39;ve defined a specific key and value for which the &lt;code&gt;Apply&lt;/code&gt; method will check before adding the transform. I&#39;ve also added a statically available dictionary which can be used to set the metadata.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class SsiTransformProvider : ITransformProvider
{
    private const string SSI_METADATA_FLAG = &quot;SSI&quot;;
    private const string SSI_METADATA_FLAG_ON = &quot;ON&quot;;

    public static IReadOnlyDictionary&amp;lt;string, string&amp;gt; SsiEnabledMetadata { get; } = new Dictionary&amp;lt;string, string&amp;gt;()
    {
        { SSI_METADATA_FLAG, SSI_METADATA_FLAG_ON }
    };

    ...

    public void Apply(TransformBuilderContext transformBuildContext)
    {
        if (transformBuildContext.Route.Metadata is not null
            &amp;amp;&amp;amp; transformBuildContext.Route.Metadata.ContainsKey(SSI_METADATA_FLAG)
            &amp;amp;&amp;amp; transformBuildContext.Route.Metadata[SSI_METADATA_FLAG] == SSI_METADATA_FLAG_ON)
        {
            transformBuildContext.AddResponseTransform(TransformResponse);
        }
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thanks to the in-memory configuration provider, including those metadata mean just setting one more property. While doing this I&#39;ve also increased the granularity of routes to further limit the affected scope.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;...

var routes = new[]
{
    ...
    new RouteConfig
    {
        RouteId = Constants.DECIDE_ROUTE_ID + &quot;-static&quot;,
        ClusterId = Constants.DECIDE_CLUSTER_ID,
        Match = new RouteMatch { Path = &quot;/decide/static/{**catch-all}&quot; }
    },
    new RouteConfig
    {
        RouteId = Constants.DECIDE_ROUTE_ID,
        ClusterId = Constants.DECIDE_CLUSTER_ID,
        Match = new RouteMatch { Path = &quot;/decide/{**catch-all}&quot; },
        Metadata = SsiTransformProvider.SsiEnabledMetadata
    },
    ...
};

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now it&#39;s time for the actual transformation. To focus on the logic needed to support SSI &lt;code&gt;include&lt;/code&gt; directive, let&#39;s get the response body reading and writing out of the way.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class SsiTransformProvider : ITransformProvider
{
    ...

    private ValueTask TransformResponse(ResponseTransformContext responseContext)
    {

        string proxyResponseContent = await responseContext.ProxyResponse.Content.ReadAsStringAsync();

        responseContext.SuppressResponseBody = true;

        ...

        byte[] proxyResponseContentBytes = Encoding.UTF8.GetBytes(proxyResponseContent);
        responseContext.HttpContext.Response.ContentLength = proxyResponseContentBytes.Length;
        await responseContext.HttpContext.Response.Body.WriteAsync(proxyResponseContentBytes);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In order to get the &lt;code&gt;include&lt;/code&gt; directive from the response body, I&#39;ve decided to use (I don&#39;t believe I&#39;m saying this) regex. The snippet below will grab all the directives. The group with index one of the resulting match will contain the directive name (all others than &lt;code&gt;include&lt;/code&gt; are to be ignored) while the group with index two will contain parameters for further parsing (I&#39;m doing this with another regex, but ultimately I care only for &lt;code&gt;virtual&lt;/code&gt; parameter).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class SsiTransformProvider : ITransformProvider
{
    private static readonly Regex SSI_DIRECTIVE_REGEX = new Regex(
        @&quot;&amp;lt;!--\#([a-z]+)\b([^&amp;gt;]+[^\/&amp;gt;])?--&amp;gt;&quot;,
        RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace
    );

    ...

    private ValueTask TransformResponse(ResponseTransformContext responseContext)
    {

        ...

        var directives = SSI_DIRECTIVE_REGEX.Matches(proxyResponseContent)

        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To get the content to which the &lt;code&gt;include&lt;/code&gt; directive is pointing, I will need to make an HTTP request to a specific service. The configuration provided for YARP already has an URL for that service. YARP also maintains a &lt;code&gt;HttpClient&lt;/code&gt; instance dedicated to the cluster to which the service belongs. It would be nice to reuse it. In order to do that, first I needed to identify the endpoint for the path to which the &lt;code&gt;virtual&lt;/code&gt; parameter is pointing.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;Endpoint? virtualEndpoint = null;

var endpointDataSource = context.RequestServices.GetService&amp;lt;EndpointDataSource&amp;gt;();
if (endpointDataSource is not null)
{
    var virtualPath = new PathString(directive.Parameters[VIRTUAL_PARAMETER]);
    foreach (Endpoint possibleVirtualEndpoint in endpointDataSource.Endpoints)
    {
        var routeEndpoint = possibleVirtualEndpoint as RouteEndpoint;
        if (routeEndpoint is not null)
        {
            var routeTemplateMatcher = new TemplateMatcher(new RouteTemplate(routeEndpoint.RoutePattern), _emptyRouteValueDictionary);
            if (routeTemplateMatcher.TryMatch(virtualPath, _emptyRouteValueDictionary))
            {
                virtualEndpoint = possibleVirtualEndpoint;
                break;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The endpoint also has a metadata collection. In this collection, YARP keeps the route model, which includes the cluster model.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;ClusterModel? cluster = null;

foreach (var endpointMetadata in virtualEndpoint.Metadata)
{
    var proxyRoute = endpointMetadata as RouteModel;
    if (proxyRoute is not null)
    {
        cluster = proxyRoute.Cluster?.Model;
        break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cluster model contains the configured destinations (with URLs) and that mentioned &lt;code&gt;HttpClient&lt;/code&gt; instance. All that remains is to build the request URI, make the request, and read the content.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;string virtualUri = cluster.Config.Destinations.FirstOrDefault().Value.Address + parameters[&quot;virtual&quot;];

HttpResponseMessage response = await cluster.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, virtualUri), CancellationToken.None);

string directiveOutputContent = await response.Content.ReadAsStringAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the content has been read, the directive can be replaced in the body.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;proxyResponseContent = proxyResponseContent.Substring(0, directives[directiveIndex].Index)
    + directiveOutputContent
    + proxyResponseContent.Substring(directives[directiveIndex].Index + directives[directiveIndex].Length);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;this-is-just-a-proof-of-concept&quot;&gt;This Is Just a Proof of Concept&lt;/h2&gt;
&lt;p&gt;Yes, this code (even the little more polished version available in the &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/tree/main/05_06.01-composition-via-yarp-and-ssi&quot;&gt;repository&lt;/a&gt;) is just a POC. It&#39;s missing constraints, error checking, support for multiple destinations in a cluster, support for other parameters of the &lt;code&gt;include&lt;/code&gt; directive, and much more.&lt;/p&gt;
&lt;p&gt;There should be also further performance considerations. The approach that the sample code takes (buffer the body, request content for &lt;code&gt;include&lt;/code&gt; directives in parallel, and then build the final response body) is typical for SSI, but an approach that streams the body whenever possible could be considered. This is for example how ESI (which is a more modern mechanism) is sometimes implemented.&lt;/p&gt;
&lt;p&gt;The only goal of this post (and related sample) is to show some YARP capabilities which can be used for achieving server-side composition at its level.&lt;/p&gt;</description><link>http://www.tpeczek.com/2022/07/micro-frontends-in-action-with-aspnet.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-8133906837345607575</guid><pubDate>Tue, 28 Jun 2022 13:35:00 +0000</pubDate><atom:updated>2022-11-01T14:44:22.980+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">asp.net core</category><category domain="http://www.blogger.com/atom/ns#">azure container apps</category><category domain="http://www.blogger.com/atom/ns#">microfrontends</category><category domain="http://www.blogger.com/atom/ns#">yarp</category><title>Micro Frontends in Action With ASP.NET Core - Server-Side Routing via YARP in Azure Container Apps</title><description>&lt;p&gt;Recently, I&#39;ve been reading &lt;a href=&quot;https://www.manning.com/books/micro-frontends-in-action&quot;&gt;Micro Frontends in Action&lt;/a&gt; and while doing it I&#39;ve decided that I want to implement &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction&quot;&gt;the samples&lt;/a&gt; in ASP.NET Core and host them on Azure. Then I had a thought, that maybe I can use this as an opportunity to play with some technologies I didn&#39;t have a chance to play with yet, like &lt;em&gt;YARP&lt;/em&gt; or &lt;em&gt;Azure Container Apps&lt;/em&gt;. Once I did that, the next thought was that maybe I should write down some of this and maybe someone will find it useful. So here we are, possibly starting a new series around micro frontends techniques:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server-Side Routing via YARP in Azure Container Apps&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/07/micro-frontends-in-action-with-aspnet.html&quot;&gt;Composition via YARP Transforms and Server-Side Includes (SSI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/08/micro-frontends-in-action-with-aspnet.html&quot;&gt;Composition via Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/09/micro-frontends-in-action-with-aspnet.html&quot;&gt;Communication Patterns for Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tpeczek.com/2022/10/micro-frontends-in-action-with-aspnet.html&quot;&gt;Universal Rendering With Blazor WebAssembly Based Web Components&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;micro-frontends-in-action&quot;&gt;Micro Frontends in Action&lt;/h2&gt;
&lt;p&gt;One of the praises for &lt;a href=&quot;https://www.manning.com/books/micro-frontends-in-action&quot;&gt;Micro Frontends in Action&lt;/a&gt; says that it&#39;s an excellent starting point to understanding how to introduce micro frontends in your projects. I&#39;m about one-third through it and I&#39;m willing to agree with that statement. The book starts with very simple techniques like page transition via links, composition via iframe, and composition via Ajax. Then it moves to server-side composition, advanced techniques for client-side composition, communication patterns, and more (I don&#39;t know, I haven&#39;t gotten there yet). Up to this point, it has been interesting and engaging (this is my opinion and just to be clear - nobody is paying me to read the book and provide an opinion about it).&lt;/p&gt;
&lt;p&gt;The book contains samples for every discussed technique and it was just too tempting not to implement them with technologies I like. That doesn&#39;t mean I will implement every single one of those samples. I will also certainly not describe every single one of those implementations. I&#39;m doing it for fun.&lt;/p&gt;
&lt;p&gt;I also recommend you read the book if you are interested in the subject - a blog post describing some specific implementation will not provide you with information about benefits, drawbacks, and when to use a certain technique.&lt;/p&gt;
&lt;h2 id=&quot;the-landscape-so-far&quot;&gt;The Landscape So Far&lt;/h2&gt;
&lt;p&gt;The technique I&#39;m going to describe in this post is server-side routing. Prior to introducing that technique, the project consists of two services: &lt;em&gt;&lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/tree/main/03-composition-via-ajax/Demo.AspNetCore.MicroFrontendsInAction.Decide&quot;&gt;Decide&lt;/a&gt;&lt;/em&gt; and &lt;em&gt;&lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/tree/main/03-composition-via-ajax/Demo.AspNetCore.MicroFrontendsInAction.Inspire&quot;&gt;Inspire&lt;/a&gt;&lt;/em&gt;. The Decide service is loading frontend fragments provided by Inspire service via Ajax request.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPDiYpsJfWonHLVcUrCANbXxSCJ83Clz2RL-zgf2ybeLUNXAEBr2aPXAMA88twe12XsW6BRVYMW9AGt2ddJO4vYf1ehdWpuLMwluxM3kU_rSO_altIVJg3NNjs96N3dX3CCY1xPRr939itMzjHlBGwxXE_5743xH3Fyclkjh7HPXqoKKHwKmdY7iTl/s1600/composition-via-ajax-frontend-layout.png&quot; alt=&quot;Composition via Ajax Frontend Layout&quot;&gt;&lt;/p&gt;
&lt;p&gt;Under the hood, both services are simple ASP.NET Core MVC applications that serve views based on static HTML from &lt;a href=&quot;https://github.com/naltatis/micro-frontends-in-action-code&quot;&gt;original samples&lt;/a&gt;, where the URLs are generated based on configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@{string decideServiceUrl = Configuration[&quot;DECIDE_SERVICE_URL&quot;];}
&amp;lt;link href=&quot;@(Context.Request.Scheme + &quot;://&quot; + Context.Request.Host + &quot;/static/fragment.css&quot;)&quot; rel=&quot;stylesheet&quot; /&amp;gt;
&amp;lt;div class=&quot;inspire_fragment&quot;&amp;gt;
  &amp;lt;h2 class=&quot;inspire_headline&quot;&amp;gt;Recommendations&amp;lt;/h2&amp;gt;
  &amp;lt;div class=&quot;inspire_recommendations&quot;&amp;gt;
    &amp;lt;a href=&quot;@(decideServiceUrl + &quot;/product/fendt&quot;)&quot;&amp;gt;&amp;lt;img src=&quot;https://mi-fr.org/img/fendt.svg&quot; /&amp;gt;&amp;lt;/a&amp;gt;
    &amp;lt;a href=&quot;@(decideServiceUrl + &quot;/product/eicher&quot;)&quot;&amp;gt;&amp;lt;img src=&quot;https://mi-fr.org/img/eicher.svg&quot; /&amp;gt;&amp;lt;/a&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Inspire service additionally defines a &lt;a href=&quot;https://docs.microsoft.com/en-us/aspnet/core/security/cors#cors-with-named-policy-and-middleware&quot;&gt;CORS policy&lt;/a&gt; to enable requesting the fragments from different domain via Ajax.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;var builder = WebApplication.CreateBuilder(args);

string decideServiceCorsPolicyName = &quot;decide-service-cors-policy&quot;;

builder.Services.AddCors(options =&amp;gt;
{
    options.AddPolicy(name: decideServiceCorsPolicyName, policy =&amp;gt;
    {
        policy.WithOrigins(builder.Configuration[&quot;DECIDE_SERVICE_URL&quot;]);
        policy.AllowAnyHeader();
        policy.WithMethods(&quot;GET&quot;);
    });
});

...

app.UseRouting();

app.UseCors(decideServiceCorsPolicyName);

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both services are containerized and deployed as two Container Apps within a single &lt;a href=&quot;https://docs.microsoft.com/en-us/azure/container-apps/environment&quot;&gt;Container Apps environment&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;az containerapp create \
  -n ${DECIDE_CONTAINERAPP} \
  -i ${CONTAINER_REGISTRY}.azurecr.io/decide:latest \
  -g ${RESOURCE_GROUP} \
  --environment ${CONTAINERAPPS_ENVIRONMENT} \
  --ingress external \
  --target-port 3001 \
  --min-replicas 1 \
  --registry-server ${CONTAINER_REGISTRY}.azurecr.io

az containerapp create \
  -n ${INSPIRE_CONTAINERAPP} \
  -i ${CONTAINER_REGISTRY}.azurecr.io/inspire:latest \
  -g ${RESOURCE_GROUP} \
  --environment ${CONTAINERAPPS_ENVIRONMENT} \
  --ingress external \
  --target-port 3002 \
  --min-replicas 1 \
  --registry-server ${CONTAINER_REGISTRY}.azurecr.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in the following Container Apps solution.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgEMWsYgxIpmZpDDqiSfy6eD7z_yKklf3V4qC0yTw7GOiMcIVOnqGdK0vxVfaXC02UaHaMqYVg32ACrgBYHBUTOJoOc3KRJtTG2n9bK4qrC-ogH5d1fk2re7ZsaZhD1UUV2c2D1FymnHZlhA8DKdf_mQqnq54VxIFOK3rWL-ERiySl-UuvEiZyC5wz4/s1600/composition-via-ajax-container-apps-solution.png&quot; alt=&quot;Composition via Ajax Container Apps Solution&quot;&gt;&lt;/p&gt;
&lt;p&gt;I&#39;ve created a GitHub Actions &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/blob/main/.github/workflows/03-composition-via-ajax.yml&quot;&gt;workflow&lt;/a&gt; that performs all the necessary steps (creating Azure resources, building and pushing containers images, and deploying Container Apps) to set up the entire solution from scratch. There is one tricky step in that workflow. Both services must know each other URLs but those are available only after Container Apps are created. To solve this I&#39;m first getting the ingress information for every app (with &lt;code&gt;az containerapp ingress show&lt;/code&gt;) and then write them to environment variables (using the &lt;a href=&quot;https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings&quot;&gt;multiline strings&lt;/a&gt; approach).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;jobs:
  ..
  deploy-to-container-apps:
  ..
  steps:
    ..
    - name: Get Services Ingress
      run: |
        echo &#39;DECIDE_CONTAINERAPP_INGRESS_JSON&amp;lt;&amp;lt;EOF&#39; &amp;gt;&amp;gt; $GITHUB_ENV
        az containerapp ingress show -n ${DECIDE_CONTAINERAPP} -g ${RESOURCE_GROUP} &amp;gt;&amp;gt; $GITHUB_ENV
        echo &#39;EOF&#39; &amp;gt;&amp;gt; $GITHUB_ENV
        echo &#39;INSPIRE_CONTAINERAPP_INGRESS_JSON&amp;lt;&amp;lt;EOF&#39; &amp;gt;&amp;gt; $GITHUB_ENV
        az containerapp ingress show -n ${INSPIRE_CONTAINERAPP} -g ${RESOURCE_GROUP} &amp;gt;&amp;gt; $GITHUB_ENV
        echo &#39;EOF&#39; &amp;gt;&amp;gt; $GITHUB_ENV
    ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, I&#39;m using the &lt;a href=&quot;https://docs.github.com/en/actions/learn-github-actions/expressions#fromjson&quot;&gt;&lt;code&gt;fromJson&lt;/code&gt;&lt;/a&gt; expression to get FQDN from ingress information and update the Container Apps.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;jobs:
  ..
  deploy-to-container-apps:
  ..
  steps:
    ..
    - name: Configure Services URLs
      run: |
        az containerapp update -n ${DECIDE_CONTAINERAPP} -g ${RESOURCE_GROUP} --set-env-vars INSPIRE_SERVICE_URL=https://${{ fromJSON(env.INSPIRE_CONTAINERAPP_INGRESS_JSON).fqdn }}
        az containerapp update -n ${INSPIRE_CONTAINERAPP} -g ${RESOURCE_GROUP} --set-env-vars DECIDE_SERVICE_URL=https://${{ fromJSON(env.DECIDE_CONTAINERAPP_INGRESS_JSON).fqdn }}
    ..
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;the-challenge&quot;&gt;The Challenge&lt;/h2&gt;
&lt;p&gt;There is a problem with this solution. There are two services and each of them is available to public requests under a different domain. This has several drawbacks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bad user experience (requests flying to different domains).&lt;/li&gt;
&lt;li&gt;Internal infrastructure of the solution is exposed publicly.&lt;/li&gt;
&lt;li&gt;Performance (two DNS lookups, two SSL handshakes, etc.).&lt;/li&gt;
&lt;li&gt;Indexing by search engines.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To solve this a central service (a proxy), where all requests will arrive, is needed.&lt;/p&gt;
&lt;h2 id=&quot;introducing-yarp-as-a-solution&quot;&gt;Introducing YARP as a Solution&lt;/h2&gt;
&lt;p&gt;For about a year, the ASP.NET Core stack have its own reverse proxy - &lt;a href=&quot;https://github.com/microsoft/reverse-proxy&quot;&gt;YARP&lt;/a&gt;. It provides a lot of routing features like headers-based routing, session affinity, load balancing, or destination health checks. In this scenario I&#39;m going to use &lt;a href=&quot;https://microsoft.github.io/reverse-proxy/articles/direct-forwarding.html&quot;&gt;direct forwarding&lt;/a&gt; to simply forward requests to specific services based on a path. I&#39;ve also decided to set up the rules from code instead of using configuration (it seemed simpler as I still could provide service URLs through environment variables). For this purpose, I&#39;ve created a &lt;code&gt;MapForwarder&lt;/code&gt; extension method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public static void MapForwarder(this IEndpointRouteBuilder endpoints, string pattern, string serviceUrl)
{
    var forwarder = endpoints.ServiceProvider.GetRequiredService&amp;lt;IHttpForwarder&amp;gt;();
    var requestConfig = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromMilliseconds(500) };

    endpoints.Map(pattern, async httpContext =&amp;gt;
    {
        var error = await forwarder.SendAsync(httpContext, serviceUrl, HttpClient, requestConfig, HttpTransformer.Default);

        if (error != ForwarderError.None)
        {
            var errorFeature = httpContext.GetForwarderErrorFeature();
            var exception = errorFeature?.Exception;
        }
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;ve followed the approach of defining service-specific path prefixes as well as specific prefixes for important pages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpForwarder();

var app = builder.Build();

string decideServiceUrl = app.Configuration[&quot;DECIDE_SERVICE_URL&quot;];
string inspireServiceUrl = app.Configuration[&quot;INSPIRE_SERVICE_URL&quot;];

app.UseRouting();
app.UseEndpoints(endpoints =&amp;gt;
{
    // Per service prefixes
    endpoints.MapForwarder(&quot;/&quot;, decideServiceUrl);

    // Per service prefixes
    endpoints.MapForwarder(&quot;/decide/{**catch-all}&quot;, decideServiceUrl);
    endpoints.MapForwarder(&quot;/inspire/{**catch-all}&quot;, inspireServiceUrl);

    // Per page prefixes
    endpoints.MapForwarder(&quot;/product/{**catch-all}&quot;, decideServiceUrl);
    endpoints.MapForwarder(&quot;/recommendations/{**catch-all}&quot;, inspireServiceUrl);
});

app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As it will be now the proxy responsibility to know the addresses of both services, they no longer need to be able to point to each other. All the paths can now be relative, as long as they follow the established rules.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;&amp;lt;link href=&quot;/inspire/static/fragment.css&quot; rel=&quot;stylesheet&quot; /&amp;gt;
&amp;lt;div class=&quot;inspire_fragment&quot;&amp;gt;
  &amp;lt;h2 class=&quot;inspire_headline&quot;&amp;gt;Recommendations&amp;lt;/h2&amp;gt;
  &amp;lt;div class=&quot;inspire_recommendations&quot;&amp;gt;
    &amp;lt;a href=&quot;/product/fendt&quot;&amp;gt;&amp;lt;img src=&quot;https://mi-fr.org/img/fendt.svg&quot; /&amp;gt;&amp;lt;/a&amp;gt;
    &amp;lt;a href=&quot;/product/eicher&quot;&amp;gt;&amp;lt;img src=&quot;https://mi-fr.org/img/eicher.svg&quot; /&amp;gt;&amp;lt;/a&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is also no need for CORS anymore, as from a browser perspective there are no cross-origin requests. So I&#39;ve removed that code from the Inspire service.&lt;/p&gt;
&lt;p&gt;Now it&#39;s time to hide the services from the public. For starters let&#39;s change their ingress to &lt;code&gt;internal&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;az containerapp create \
  -n ${DECIDE_CONTAINERAPP} \
  -i ${CONTAINER_REGISTRY}.azurecr.io/decide:latest \
  -g ${RESOURCE_GROUP} \
  --environment ${CONTAINERAPPS_ENVIRONMENT} \
  --ingress internal \
  --target-port 3001 \
  --min-replicas 1 \
  --registry-server ${CONTAINER_REGISTRY}.azurecr.io

az containerapp create \
  -n ${INSPIRE_CONTAINERAPP} \
  -i ${CONTAINER_REGISTRY}.azurecr.io/inspire:latest \
  -g ${RESOURCE_GROUP} \
  --environment ${CONTAINERAPPS_ENVIRONMENT} \
  --ingress internal \
  --target-port 3002 \
  --min-replicas 1 \
  --registry-server ${CONTAINER_REGISTRY}.azurecr.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way the services are now accessible only from within the Container Apps environment. The proxy can be deployed to the same Container Apps environment and expose the desired traffic outside.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;az containerapp create \
  -n ${PROXY_CONTAINERAPP} \
  -i ${CONTAINER_REGISTRY}.azurecr.io/proxy:latest \
  -g ${RESOURCE_GROUP} \
  --environment ${CONTAINERAPPS_ENVIRONMENT} \
  --ingress external \
  --target-port 3000 \
  --min-replicas 1 \
  --registry-server ${CONTAINER_REGISTRY}.azurecr.io \
  --env-vars INSPIRE_SERVICE_URL=https://${INSPIRE_CONTAINERAPP_FQDN} DECIDE_SERVICE_URL=https://${DECIDE_CONTAINERAPP_FQDN}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After this change, the Container Apps solution looks like in the below diagram.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoR0qmRQhGKeUJtiJQPWGCSAHPYJsvJ0rCXasadWnR8FqlkZlYUvpp2xPzS_mj--2mIRQzSA96prSTcInUc-7dFPWzcpWuXOCxG9n06sQuSdRktYF-Qmw7xOsvuIkiYM2ON9jrqK9RfB6pGiTvbmerSGHzRLHtH6s9OsFc2wLXNNiJ-CoOrHQVNCgd/s1600/server-side-routing-via-yarp-container-apps-solution.png&quot; alt=&quot;Server-Side Routing via YARP Container Apps Solution&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;that-s-all-folks&quot;&gt;That&#39;s All Folks&lt;/h2&gt;
&lt;p&gt;At least for this post. You can find the final solution &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/tree/main/04-server-side-routing-via-yarp&quot;&gt;here&lt;/a&gt; and its GitHub Actions workflow &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.MicroFrontendsInAction/blob/main/.github/workflows/04-server-side-routing-via-yarp.yml&quot;&gt;here&lt;/a&gt;. The services have become a little bit strange in the process (they server static HTML in an unnecessarily complicated way) but they are not the focus here, the server-side routing technique is.&lt;/p&gt;
&lt;p&gt;I don&#39;t know if or when I&#39;m going to play with another technique, but if I will it&#39;s probably going to be something around server-side composition.&lt;/p&gt;</description><link>http://www.tpeczek.com/2022/06/micro-frontends-in-action-with-aspnet.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPDiYpsJfWonHLVcUrCANbXxSCJ83Clz2RL-zgf2ybeLUNXAEBr2aPXAMA88twe12XsW6BRVYMW9AGt2ddJO4vYf1ehdWpuLMwluxM3kU_rSO_altIVJg3NNjs96N3dX3CCY1xPRr939itMzjHlBGwxXE_5743xH3Fyclkjh7HPXqoKKHwKmdY7iTl/s72-c/composition-via-ajax-frontend-layout.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-3077980356761970106</guid><pubDate>Mon, 06 Dec 2021 14:00:00 +0000</pubDate><atom:updated>2021-12-06T15:00:46.822+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">asp.net core</category><category domain="http://www.blogger.com/atom/ns#">async-streaming</category><category domain="http://www.blogger.com/atom/ns#">blazor</category><category domain="http://www.blogger.com/atom/ns#">json</category><title>ASP.NET Core 6 and IAsyncEnumerable - Receiving Async Streamed JSON in Blazor WebAssembly</title><description>&lt;p&gt;Back in July, I&#39;ve shared my &lt;a href=&quot;https://www.tpeczek.com/2021/07/aspnet-core-6-and-iasyncenumerable.html&quot;&gt;experiments around new JSON async streaming capabilities in ASP.NET Core 6&lt;/a&gt;. Last week I&#39;ve received a question about utilizing these capabilities in the Blazor WebAssembly application. The person asking the question has adopted the &lt;code&gt;DeserializeAsyncEnumerable&lt;/code&gt; based client code, but it didn&#39;t seem to work properly. All the results were always displayed at once. As I didn&#39;t have a Blazor WebAssembly sample as part of my &lt;a href=&quot;https://github.com/tpeczek/Demo.Ndjson.AsyncStreams&quot;&gt;streaming JSON objects demo&lt;/a&gt;, I&#39;ve decided I&#39;ll add one and figure out the answer to the question along the way.&lt;/p&gt;
&lt;h2 id=&quot;blazor-webassembly-and-iasyncenumerable&quot;&gt;Blazor WebAssembly and IAsyncEnumerable&lt;/h2&gt;
&lt;p&gt;Before I focus on the problem of results not being received in an async stream manner, I think it is worth discussing the way of working with &lt;code&gt;IAsyncEnumerable&lt;/code&gt; in Blazor WebAssembly. What&#39;s the challenge here? The first one is that &lt;code&gt;await foreach&lt;/code&gt; can&#39;t be used in the page markup, only in the code block. So the markup must use a synchronous loop.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-html&quot;&gt;&amp;lt;table&amp;gt;
    &amp;lt;thead&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;th&amp;gt;Date&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;Temp. (C)&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;Temp. (F)&amp;lt;/th&amp;gt;
            &amp;lt;th&amp;gt;Summary&amp;lt;/th&amp;gt;
        &amp;lt;/tr&amp;gt;
    &amp;lt;/thead&amp;gt;
    &amp;lt;tbody&amp;gt;
        @foreach (WeatherForecast weatherForecast in weatherForecasts)
        {
            &amp;lt;tr&amp;gt;
                &amp;lt;td&amp;gt;@weatherForecast.DateFormatted&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;@weatherForecast.TemperatureC&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;@weatherForecast.TemperatureF&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;@weatherForecast.Summary&amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
        }
    &amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That brings us to the second challenge. If the &lt;code&gt;await foreach&lt;/code&gt; can be used only in the code block, how the streamed results can be rendered as they come? Here the solution comes in form of the &lt;a href=&quot;https://docs.microsoft.com/en-us/aspnet/core/blazor/components/rendering#when-to-call-statehaschanged&quot;&gt;StateHasChanged&lt;/a&gt; method. Calling this method will trigger a render. With this knowledge, we can adopt the &lt;code&gt;DeserializeAsyncEnumerable&lt;/code&gt; based code from my previous post.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;@code {

    private List&amp;lt;WeatherForecast&amp;gt; weatherForecasts = new List&amp;lt;WeatherForecast&amp;gt;();

    private async Task StreamWeatherForecastsJson()
    {
        weatherForecasts = new List&amp;lt;WeatherForecast&amp;gt;();

        StateHasChanged();

        using HttpResponseMessage response = await Http.GetAsync(&quot;api/WeatherForecasts/negotiate-stream&quot;, HttpCompletionOption.ResponseHeadersRead);

        response.EnsureSuccessStatusCode();

        using Stream responseStream = await response.Content.ReadAsStreamAsync();

        await foreach (WeatherForecast weatherForecast in JsonSerializer.DeserializeAsyncEnumerable&amp;lt;WeatherForecast&amp;gt;(
            responseStream,
            new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true,
                DefaultBufferSize = 128
            }))
        {
            weatherForecasts.Add(weatherForecast);

            StateHasChanged();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running that code put me in the exact same spot where the person asking the question was. All the results were rendered at once after the entire wait time. What to do, when you have no idea what might be wrong and where? Dump what you can to the console ;). No, I&#39;m serious. Debugging through &lt;code&gt;console.log&lt;/code&gt; is in fact quite useful in many situations and I&#39;m not ashamed of using it here. I&#39;ve decided that the diagnostic version will perform direct response stream reading.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;@code {

    ...

    private async Task StreamWeatherForecastsJson()
    {
        Console.WriteLine($&quot;-- {nameof(StreamWeatherForecastsJson)} --&quot;);
        Console.WriteLine($&quot;[{DateTime.UtcNow:hh:mm:ss.fff}] Requesting weather forecasts . . .&quot;);

        using HttpResponseMessage response = await Http.GetAsync(&quot;api/WeatherForecasts/negotiate-stream&quot;, HttpCompletionOption.ResponseHeadersRead);

        response.EnsureSuccessStatusCode();

        Console.WriteLine($&quot;[{DateTime.UtcNow:hh:mm:ss.fff}] Receving weather forecasts . . .&quot;);

        using Stream responseStream = await response.Content.ReadAsStreamAsync();

        Console.WriteLine($&quot;[{DateTime.UtcNow:hh:mm:ss.fff}] Weather forecasts stream obtained . . .&quot;);

        while (true)
        {
            byte[] buffer = ArrayPool&amp;lt;byte&amp;gt;.Shared.Rent(128);
            int bytesRead = await responseStream.ReadAsync(buffer);

            Console.WriteLine($&quot;[{DateTime.UtcNow:hh:mm:ss.fff}] ({bytesRead}/{buffer.Length}) {Encoding.UTF8.GetString(buffer[0..bytesRead])}&quot;);

            ArrayPool&amp;lt;byte&amp;gt;.Shared.Return(buffer);

            if (bytesRead == 0)
            {
                break;
            }
        }

        Console.WriteLine($&quot;[{DateTime.UtcNow:hh:mm:ss.fff}] Weather forecasts has been received.&quot;);
        Console.WriteLine();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Below you can see the output from browser developer tools.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;plaintext&quot;&gt;-- StreamWeatherForecastsJson --
[08:04:01.183] Requesting weather forecasts . . .
[08:04:01.436] Receving weather forecasts . . .
[08:04:02.420] Weather forecasts stream obtained . . .
[08:04:02.426] (128/128) [{&quot;dateFormatted&quot;:&quot;06.12.2021&quot;,&quot;temperatureC&quot;:28,&quot;temperatureF&quot;:82,&quot;summary&quot;:&quot;Hot&quot;},{&quot;dateFormatted&quot;:&quot;07.12.2021&quot;,&quot;temperatureC&quot;
[08:04:02.429] (128/128) :36,&quot;temperatureF&quot;:96,&quot;summary&quot;:&quot;Scorching&quot;},{&quot;dateFormatted&quot;:&quot;08.12.2021&quot;,&quot;temperatureC&quot;:-7,&quot;temperatureF&quot;:20,&quot;summary&quot;:&quot;Mild&quot;}
[08:04:02.431] (128/128) ,{&quot;dateFormatted&quot;:&quot;09.12.2021&quot;,&quot;temperatureC&quot;:-6,&quot;temperatureF&quot;:22,&quot;summary&quot;:&quot;Hot&quot;},{&quot;dateFormatted&quot;:&quot;10.12.2021&quot;,&quot;temperatureC&quot;
[08:04:02.433] (128/128) :40,&quot;temperatureF&quot;:103,&quot;summary&quot;:&quot;Cool&quot;},{&quot;dateFormatted&quot;:&quot;11.12.2021&quot;,&quot;temperatureC&quot;:44,&quot;temperatureF&quot;:111,&quot;summary&quot;:&quot;Swelterin
[08:04:02.434] (128/128) g&quot;},{&quot;dateFormatted&quot;:&quot;12.12.2021&quot;,&quot;temperatureC&quot;:-3,&quot;temperatureF&quot;:27,&quot;summary&quot;:&quot;Balmy&quot;},{&quot;dateFormatted&quot;:&quot;13.12.2021&quot;,&quot;temperat
[08:04:02.435] (128/128) ureC&quot;:1,&quot;temperatureF&quot;:33,&quot;summary&quot;:&quot;Sweltering&quot;},{&quot;dateFormatted&quot;:&quot;14.12.2021&quot;,&quot;temperatureC&quot;:3,&quot;temperatureF&quot;:37,&quot;summary&quot;:&quot;Ho
[08:04:02.437] (88/128) t&quot;},{&quot;dateFormatted&quot;:&quot;15.12.2021&quot;,&quot;temperatureC&quot;:19,&quot;temperatureF&quot;:66,&quot;summary&quot;:&quot;Mild&quot;}]tureC&quot;:3,&quot;temperatureF&quot;:37,&quot;summary&quot;:&quot;Ho
[08:04:02.438] (0/128) t&quot;},{&quot;dateFormatted&quot;:&quot;15.12.2021&quot;,&quot;temperatureC&quot;:19,&quot;temperatureF&quot;:66,&quot;summary&quot;:&quot;Mild&quot;}]tureC&quot;:3,&quot;temperatureF&quot;:37,&quot;summary&quot;:&quot;Ho
[08:04:02.439] Weather forecasts has been received.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So the call which is blocking the whole thing seems to be &lt;code&gt;ReadAsStreamAsync&lt;/code&gt;, when it returns the entire response is already available. All I knew at this point was that Blazor WebAssembly is using a special &lt;code&gt;HttpMessageHandler&lt;/code&gt;. I needed to dig deeper.&lt;/p&gt;
&lt;h2 id=&quot;digging-into-browserhttphandler&quot;&gt;Digging Into BrowserHttpHandler&lt;/h2&gt;
&lt;p&gt;There is a number of things that have dedicated implementations for Blazor WebAssembly. The &lt;code&gt;HttpClient&lt;/code&gt; stack is one of those things. Well, there is no access to native sockets in the browser, so the HTTP calls must be performed based on browser-provided APIs. The &lt;a href=&quot;https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs&quot;&gt;&lt;code&gt;BrowserHttpHandler&lt;/code&gt;&lt;/a&gt; is implemented on top of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API&quot;&gt;Fetch API&lt;/a&gt;. Inspecting its code shows that it can provide response content in one of two forms.&lt;/p&gt;
&lt;p&gt;The first one is &lt;a href=&quot;https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs#L394&quot;&gt;&lt;code&gt;BrowserHttpContent&lt;/code&gt;&lt;/a&gt;, which is based on &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer&quot;&gt;&lt;code&gt;arrayBuffer&lt;/code&gt;&lt;/a&gt; method. This means, that it will always read the response stream to its completion, before making the content available.&lt;/p&gt;
&lt;p&gt;The second one is &lt;code&gt;StreamContent&lt;/code&gt; wrapping &lt;a href=&quot;https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs#L461&quot;&gt;&lt;code&gt;WasmHttpReadStream&lt;/code&gt;&lt;/a&gt;, which is based on &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#consuming_a_fetch_as_a_stream&quot;&gt;readable streams&lt;/a&gt;. This one allows for reading response as it comes.&lt;/p&gt;
&lt;p&gt;How does &lt;code&gt;BrowserHttpHandler&lt;/code&gt; decide which one to use? In order for &lt;code&gt;WasmHttpReadStream&lt;/code&gt; to be used, &lt;a href=&quot;https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs#L281&quot;&gt;two conditions&lt;/a&gt; must be met - the browser must support readable streams and the &lt;code&gt;WebAssemblyEnableStreamingResponse&lt;/code&gt; option must be enabled on the request. Now we are getting somewhere. Further search for &lt;code&gt;WebAssemblyEnableStreamingResponse&lt;/code&gt; will reveal a &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserresponsestreamingenabled&quot;&gt;&lt;code&gt;SetBrowserResponseStreamingEnabled&lt;/code&gt;&lt;/a&gt; extension method. Let&#39;s see what happens if it&#39;s used.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;@code {

    ...

    private async Task StreamWeatherForecastsJson()
    {
        Console.WriteLine($&quot;-- {nameof(StreamWeatherForecastsJson)} --&quot;);
        Console.WriteLine($&quot;[{DateTime.UtcNow:hh:mm:ss.fff}] Requesting weather forecasts . . .&quot;);

        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, &quot;api/WeatherForecasts/negotiate-stream&quot;);
        request.SetBrowserResponseStreamingEnabled(true);

        using HttpResponseMessage response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

        response.EnsureSuccessStatusCode();

        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives the desired output.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;plaintext&quot;&gt;-- StreamWeatherForecastsJson --
[08:53:14.722] Requesting weather forecasts . . .
[08:53:15.002] Receving weather forecasts . . .
[08:53:15.009] Weather forecasts stream obtained . . .
[08:53:15.018] (84/128) [{&quot;dateFormatted&quot;:&quot;06.12.2021&quot;,&quot;temperatureC&quot;:31,&quot;temperatureF&quot;:87,&quot;summary&quot;:&quot;Cool&quot;}
[08:53:15.057] (84/128) ,{&quot;dateFormatted&quot;:&quot;07.12.2021&quot;,&quot;temperatureC&quot;:18,&quot;temperatureF&quot;:64,&quot;summary&quot;:&quot;Cool&quot;}
[08:53:15.166] (86/128) ,{&quot;dateFormatted&quot;:&quot;08.12.2021&quot;,&quot;temperatureC&quot;:10,&quot;temperatureF&quot;:49,&quot;summary&quot;:&quot;Chilly&quot;}
[08:53:15.276] (84/128) ,{&quot;dateFormatted&quot;:&quot;09.12.2021&quot;,&quot;temperatureC&quot;:33,&quot;temperatureF&quot;:91,&quot;summary&quot;:&quot;Mild&quot;}
[08:53:15.386] (88/128) ,{&quot;dateFormatted&quot;:&quot;10.12.2021&quot;,&quot;temperatureC&quot;:-14,&quot;temperatureF&quot;:7,&quot;summary&quot;:&quot;Freezing&quot;}
[08:53:15.492] (84/128) ,{&quot;dateFormatted&quot;:&quot;11.12.2021&quot;,&quot;temperatureC&quot;:12,&quot;temperatureF&quot;:53,&quot;summary&quot;:&quot;Warm&quot;}
[08:53:15.600] (86/128) ,{&quot;dateFormatted&quot;:&quot;12.12.2021&quot;,&quot;temperatureC&quot;:6,&quot;temperatureF&quot;:42,&quot;summary&quot;:&quot;Bracing&quot;}
[08:53:15.710] (85/128) ,{&quot;dateFormatted&quot;:&quot;13.12.2021&quot;,&quot;temperatureC&quot;:48,&quot;temperatureF&quot;:118,&quot;summary&quot;:&quot;Mild&quot;}
[08:53:15.818] (89/128) ,{&quot;dateFormatted&quot;:&quot;14.12.2021&quot;,&quot;temperatureC&quot;:13,&quot;temperatureF&quot;:55,&quot;summary&quot;:&quot;Scorching&quot;}
[08:53:15.931] (88/128) ,{&quot;dateFormatted&quot;:&quot;15.12.2021&quot;,&quot;temperatureC&quot;:44,&quot;temperatureF&quot;:111,&quot;summary&quot;:&quot;Chilly&quot;}]
[08:53:15.943] (0/128) 
[08:53:15.946] Weather forecasts has been received.
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;corrected-implementation&quot;&gt;Corrected Implementation&lt;/h2&gt;
&lt;p&gt;This means, that in order to have stream-based access to the response body (regardless if it&#39;s JSON or something else), one needs to explicitly enable it on the request. So the code which is able to receive async streamed JSON and properly deserialize it to &lt;code&gt;IAsyncEnumerable&lt;/code&gt; should look like this.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;@code {

    ...

    private async Task StreamWeatherForecastsJson()
    {
        weatherForecasts = new List&amp;lt;WeatherForecast&amp;gt;();

        StateHasChanged();

        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, &quot;api/WeatherForecasts/negotiate-stream&quot;);
        request.SetBrowserResponseStreamingEnabled(true);

        using HttpResponseMessage response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

        response.EnsureSuccessStatusCode();

        using Stream responseStream = await response.Content.ReadAsStreamAsync();

        await foreach (WeatherForecast weatherForecast in JsonSerializer.DeserializeAsyncEnumerable&amp;lt;WeatherForecast&amp;gt;(
            responseStream,
            new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true,
                DefaultBufferSize = 128
            }))
        {
            weatherForecasts.Add(weatherForecast);

            StateHasChanged();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works exactly as expected - the results are being rendered as they are being returned from the backend (with precision resulting from the size of the buffer).&lt;/p&gt;
&lt;h2 id=&quot;lesson-learned&quot;&gt;Lesson Learned&lt;/h2&gt;
&lt;p&gt;Despite the fact that there is no dedicated TFM for Blazor WebAssembly, and it uses the &quot;runs everywhere&quot; one (&lt;code&gt;net5.0&lt;/code&gt;, &lt;code&gt;net6.0&lt;/code&gt;, etc.) there are platform differences. You will notice the APIs which aren&#39;t supported very quickly because they will throw &lt;code&gt;PlatformNotSupportedException&lt;/code&gt;. But there are also more sneaky ones, ones which behavior is different. Blazor WebAssembly needs to be approached with this thought in the back of your mind. This will allow you to properly handle situations when things are not working the way you&#39;ve expected.&lt;/p&gt;</description><link>http://www.tpeczek.com/2021/12/aspnet-core-6-and-iasyncenumerable.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-2338776660855107578</guid><pubDate>Tue, 30 Nov 2021 13:24:00 +0000</pubDate><atom:updated>2021-11-30T14:24:18.990+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">asp.net core</category><category domain="http://www.blogger.com/atom/ns#">cosmos db</category><category domain="http://www.blogger.com/atom/ns#">web api</category><title>Leveraging Azure Cosmos DB Partial Document Update With JSON Patch in an ASP.NET Core Web API</title><description>&lt;p&gt;A couple of weeks ago the Cosmos DB team has announced support for &lt;a href=&quot;https://docs.microsoft.com/en-us/azure/cosmos-db/partial-document-update&quot;&gt;patching documents&lt;/a&gt;. This is quite a useful and long-awaited feature, as up to this point the only way to change the stored document was to completely replace it. This feature opens up new scenarios. For example, if you are providing a Web API on top of Cosmos DB, you can now directly implement support for the &lt;em&gt;PATCH&lt;/em&gt; request method. I happen to have a small demo of such &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.WebApi&quot;&gt;Web API&lt;/a&gt;, so I&#39;ve decided to refresh it and play with leveraging this new capability.&lt;/p&gt;
&lt;h2 id=&quot;adding-patch-requests-support-to-an-asp-net-core-web-api&quot;&gt;Adding PATCH Requests Support to an ASP.NET Core Web API&lt;/h2&gt;
&lt;p&gt;An important part of handling a &lt;em&gt;PATCH&lt;/em&gt; request is deciding on the request body format. The standard approach for that, in the case of JSON-based APIs, is &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6902&quot;&gt;&lt;em&gt;JSON Patch&lt;/em&gt;&lt;/a&gt;. In fact, Cosmos DB is also using &lt;em&gt;JSON Patch&lt;/em&gt;, so it should be easier to leverage it this way.&lt;/p&gt;
&lt;p&gt;ASP.NET Core provides &lt;a href=&quot;https://docs.microsoft.com/en-us/aspnet/core/web-api/jsonpatch&quot;&gt;support for &lt;em&gt;JSON Patch&lt;/em&gt;&lt;/a&gt;, but I&#39;ve decided not to go with it. Why? It&#39;s designed around applying operations to an instance of an object and as a result, has internal assumptions about the supported list of operations. In the case of Cosmos DB, the supported operations are different, and that &quot;applying&quot; capability is not needed (it was a great way to implement &lt;em&gt;PATCH&lt;/em&gt; when Cosmos DB provided on replace option). I&#39;ve figured out it will be better to start fresh, with a lightweight model.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class JsonPatchOperation
{
    [Required]
    public string Op { get; set; }

    [Required]
    public string Path { get; set; }

    public object Value { get; set; }
}

public class JsonPatch : List&amp;lt;JsonPatchOperation&amp;gt;
{ }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This class is quite generic and should allow for handling any request which body is compliant with &lt;em&gt;JSON Patch&lt;/em&gt; structure. The first concretization I want to introduce is the list of available operations. Cosmos DB currently supports five operations: &lt;em&gt;Add&lt;/em&gt;, &lt;em&gt;Set&lt;/em&gt;, &lt;em&gt;Remove&lt;/em&gt;, &lt;em&gt;Replace&lt;/em&gt;, and &lt;em&gt;Increment&lt;/em&gt;. In this demo (at least for now) I&#39;m skipping the &lt;em&gt;Increment&lt;/em&gt; because it would require a little bit different handling than others.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public enum JsonPatchOperationType
{
    Add,
    Set,
    Remove,
    Replace,,
    Invalid
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, in the above enumeration I&#39;ve also added the &lt;code&gt;Invalid&lt;/code&gt; value. This will give me a way to represent the operations I don&#39;t intend to support through a specific value instead of e.g. throwing an exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class JsonPatchOperation
{
    private string _op;
    private JsonPatchOperationType _operationType;

    [Required]
    public string Op
    {
        get { return _op; }

        set
        {
            JsonPatchOperationType operationType;
            if (!Enum.TryParse(value, ignoreCase: true, result: out operationType))
            {
                operationType = JsonPatchOperationType.Invalid;
            }

            _operationType = operationType;

            _op = value;
        }
    }

    public JsonPatchOperationType OperationType =&amp;gt; _operationType;

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Having not supported operations represented through a specific value also allows me to implement &lt;code&gt;IValidatableObject&lt;/code&gt; which checks for them. This way, if the class is used as an action model, making a request with an unsupported operation will trigger a &lt;em&gt;400 Bad Request&lt;/em&gt; response.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class JsonPatchOperation : IValidatableObject
{
    ...

    public IEnumerable&amp;lt;ValidationResult&amp;gt; Validate(ValidationContext validationContext)
    {
        if (OperationType == JsonPatchOperationType.Invalid)
        {
            yield return new ValidationResult($&quot;Not supported operation: {Op}.&quot;, new[] { nameof(Op) });
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now all is needed is an action that will support the &lt;em&gt;PATCH&lt;/em&gt; method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;[Route(&quot;api/[controller]&quot;)]
[ApiController]
public class CharactersController : Controller
{
    ...

    [HttpPatch(&quot;{id}&quot;)]
    public async Task&amp;lt;ActionResult&amp;lt;Character&amp;gt;&amp;gt; Patch(string id, JsonPatch update)
    {
        ...
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The next step will be proxying the deserialized &lt;em&gt;JSON Patch&lt;/em&gt; to Cosmos DB .NET SDK.&lt;/p&gt;
&lt;h2 id=&quot;utilizing-cosmos-db-partial-updates&quot;&gt;Utilizing Cosmos DB Partial Updates&lt;/h2&gt;
&lt;p&gt;The Cosmos DB .NET SDK exposes partial document updates through the &lt;code&gt;PatchItemAsync&lt;/code&gt; method on a container. This method expects a collection of &lt;code&gt;PatchOperation&lt;/code&gt; instances. Instances representing specific operations can be created through static methods on &lt;code&gt;PatchOperation&lt;/code&gt; which names correspond to operations names. So the conversion from &lt;code&gt;JsonPatch&lt;/code&gt; to &lt;code&gt;PatchOperation&lt;/code&gt; collection requires calling the appropriate method for every &lt;code&gt;JsonPatchOperation&lt;/code&gt;. This is something a simple extensions method should handle.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public static class JsonPatchExtensions
{
    public static IReadOnlyList&amp;lt;PatchOperation&amp;gt; ToCosmosPatchOperations(this JsonPatch jsonPatchOperations)
    {

        List&amp;lt;PatchOperation&amp;gt; cosmosPatchOperations = new List&amp;lt;PatchOperation&amp;gt;(jsonPatchOperations.Count);
        foreach (JsonPatchOperation jsonPatchOperation in jsonPatchOperations)
        {
            switch (jsonPatchOperation.OperationType)
            {
                case JsonPatchOperationType.Add:
                    cosmosPatchOperations.Add(PatchOperation.Add(jsonPatchOperation.Path, jsonPatchOperation.Value));
                    break;
                case JsonPatchOperationType.Remove:
                    cosmosPatchOperations.Add(PatchOperation.Remove(jsonPatchOperation.Path));
                    break;
                case JsonPatchOperationType.Replace:
                    cosmosPatchOperations.Add(PatchOperation.Replace(jsonPatchOperation.Path, jsonPatchOperation.Value));
                    break;
                case JsonPatchOperationType.Set:
                    System.Int32 test = 25;
                    cosmosPatchOperations.Add(PatchOperation.Set(jsonPatchOperation.Path, jsonPatchOperation.Value));
                    break;
            }
        }

        return cosmosPatchOperations;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Making the proper call in our action.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;[Route(&quot;api/[controller]&quot;)]
[ApiController]
public class CharactersController : Controller
{
    ...

    [HttpPatch(&quot;{id}&quot;)]
    public async Task&amp;lt;ActionResult&amp;lt;Character&amp;gt;&amp;gt; Patch(string id, JsonPatch update)
    {
        ...

        ItemResponse&amp;lt;Character&amp;gt; characterItemResponse = await _starWarsCosmosClient.Characters.PatchItemAsync&amp;lt;Character&amp;gt;(
            id,
            PartitionKey.None,
            update.ToCosmosPatchOperations());

        return characterItemResponse.Resource;
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we have some testable code. In order to test I&#39;ve decided to attempt two operations: &lt;em&gt;Set&lt;/em&gt; on &lt;code&gt;/height&lt;/code&gt; and &lt;em&gt;Add&lt;/em&gt; on &lt;code&gt;/weight&lt;/code&gt;. This is represented by below &lt;em&gt;JSON Patch&lt;/em&gt; body.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-js&quot;&gt;[
  {
    &quot;op&quot;: &quot;set&quot;,
    &quot;path&quot;: &quot;/height&quot;,
    &quot;value&quot;: 195
  },
  {
    &quot;op&quot;: &quot;add&quot;,
    &quot;path&quot;: &quot;/weight&quot;,
    &quot;value&quot;: 90
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&#39;ve hit F5, crated the request in Postman, and clicked Send. What I&#39;ve received was a &lt;em&gt;500 Internal Server Error&lt;/em&gt; with a weird &lt;code&gt;Newtonsoft.Json&lt;/code&gt; deserialization exception. A quick look into Cosmos DB Explorer revealed that the document now looks like this.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-js&quot;&gt;{
    ...
    &quot;height&quot;: {
        &quot;valueKind&quot;: 4
    },
    &quot;weight&quot;: {
        &quot;valueKind&quot;: 4
    },
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not what I was expecting. What happened? The fact that &lt;code&gt;PatchItemAsync&lt;/code&gt; worked without an exception suggested that this must be how Cosmos DB .NET SDK interpreted the &lt;code&gt;JsonPatchOperation.Value&lt;/code&gt;. Through debugging I&#39;ve quickly discovered that what &lt;code&gt;JsonPatchOperation.Value&lt;/code&gt; actually holds is &lt;code&gt;System.Text.Json.JsonElement&lt;/code&gt;. The Cosmos DB .NET SDK has no other way of dealing with that than serializing public properties - regardless of how smart the implementation is. This is because Cosmos DB .NET SDK (at least for now) is based on &lt;code&gt;Newtonsoft.Json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So, this is going to be a little bit harder. Conversion from &lt;code&gt;JsonPatch&lt;/code&gt; to &lt;code&gt;PatchOperation&lt;/code&gt; collection will require deserializing the value along the way. As this is still part of deserializing the request, I&#39;ve figured out it will be best to put it into &lt;code&gt;JsonPatchOperation&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class JsonPatchOperation : IValidatableObject
{
    ...

    public T GetValue&amp;lt;T&amp;gt;()
    {
        return ((JsonElement)Value).Deserialize&amp;lt;T&amp;gt;();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will need to be called with the right type parameter, so a mapping between paths and types needs to be obtained. I&#39;ve decided to make this information available through &lt;code&gt;JsonPatch&lt;/code&gt; by making it generic and spicing with reflection.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class JsonPatch&amp;lt;T&amp;gt; : List&amp;lt;JsonPatchOperation&amp;gt;
{
    private static readonly IDictionary&amp;lt;string, Type&amp;gt; _pathsTypes;

    static JsonPatch()
    {
        _pathsTypes = typeof(T).GetProperties().ToDictionary(p =&amp;gt; $&quot;/{Char.ToLowerInvariant(p.Name[0]) + p.Name[1..]}&quot;, p =&amp;gt; p.PropertyType);
    }

    public Type GetTypeForPath(string path)
    {
        return _pathsTypes[path];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A disclaimer is in order. This simple code will work only in this simple case. I&#39;m looking only at top-level properties because my document only has top-level properties. But, in general, a path can be targeting a nested property e.g. &lt;code&gt;/mather/familyName&lt;/code&gt;. This code will have to get more complicated to handle such a case.&lt;/p&gt;
&lt;p&gt;To make the conversion from &lt;code&gt;JsonPatch&lt;/code&gt; to &lt;code&gt;PatchOperation&lt;/code&gt; work correctly, sadly, more reflection is needed as those generic calls have to be made with the right parameters at the runtime. The needed pieces can live together with the conversion extension method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public static class JsonPatchExtensions
{
    private static MethodInfo _createAddPatchOperationMethodInfo = typeof(JsonPatchExtensions)
        .GetMethod(nameof(JsonPatchExtensions.CreateAddPatchOperation), BindingFlags.NonPublic | BindingFlags.Static);
    private static MethodInfo _createReplacePatchOperationMethodInfo = typeof(JsonPatchExtensions)
        .GetMethod(nameof(JsonPatchExtensions.CreateReplacePatchOperation), BindingFlags.NonPublic | BindingFlags.Static);
    private static MethodInfo _createSetPatchOperationMethodInfo = typeof(JsonPatchExtensions)
        .GetMethod(nameof(JsonPatchExtensions.CreateSetPatchOperation), BindingFlags.NonPublic | BindingFlags.Static);

    ...

    private static PatchOperation CreateAddPatchOperation&amp;lt;T&amp;gt;(JsonPatchOperation jsonPatchOperation)
    {
        return PatchOperation.Add(jsonPatchOperation.Path, jsonPatchOperation.GetValue&amp;lt;T&amp;gt;());
    }

    private static PatchOperation CreateReplacePatchOperation&amp;lt;T&amp;gt;(JsonPatchOperation jsonPatchOperation)
    {
        return PatchOperation.Replace(jsonPatchOperation.Path, jsonPatchOperation.GetValue&amp;lt;T&amp;gt;());
    }

    private static PatchOperation CreateSetPatchOperation&amp;lt;T&amp;gt;(JsonPatchOperation jsonPatchOperation)
    {
        return PatchOperation.Set(jsonPatchOperation.Path, jsonPatchOperation.GetValue&amp;lt;T&amp;gt;());
    }

    private static PatchOperation CreatePatchOperation&amp;lt;T&amp;gt;(
        MethodInfo createSpecificPatchOperationMethodInfo,
        JsonPatch&amp;lt;T&amp;gt; jsonPatchOperations,
        JsonPatchOperation jsonPatchOperation)
    {
        Type jsonPatchOperationValueType = jsonPatchOperations.GetTypeForPath(jsonPatchOperation.Path);

        MethodInfo createSpecificPatchOperationWithValueTypeMethodInfo =
            createSpecificPatchOperationMethodInfo.MakeGenericMethod(jsonPatchOperationValueType);

        return (PatchOperation)createSpecificPatchOperationWithValueTypeMethodInfo.Invoke(null, new object[] { jsonPatchOperation });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code above has been structured to isolate the reflection related part and cache the well-known things. Of course, this is a subject of personal preference, but I hope it&#39;s readable.&lt;/p&gt;
&lt;p&gt;The last remaining thing is modifying the extension method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public static class JsonPatchExtensions
{
    ...

    public static IReadOnlyList&amp;lt;PatchOperation&amp;gt; ToCosmosPatchOperations&amp;lt;T&amp;gt;(this JsonPatch jsonPatchOperations)
    {
        List&amp;lt;PatchOperation&amp;gt; cosmosPatchOperations = new List&amp;lt;PatchOperation&amp;gt;(jsonPatchOperations.Count);
        foreach (JsonPatchOperation jsonPatchOperation in jsonPatchOperations)
        {
            switch (jsonPatchOperation.OperationType)
            {
                case JsonPatchOperationType.Add:
                    cosmosPatchOperations.Add(CreatePatchOperation(_createAddPatchOperationMethodInfo, jsonPatchOperations, jsonPatchOperation));
                    break;
                case JsonPatchOperationType.Remove:
                    cosmosPatchOperations.Add(PatchOperation.Remove(jsonPatchOperation.Path));
                    break;
                case JsonPatchOperationType.Replace:
                    cosmosPatchOperations.Add(CreatePatchOperation(_createReplacePatchOperationMethodInfo, jsonPatchOperations, jsonPatchOperation));
                    break;
                case JsonPatchOperationType.Set:
                    cosmosPatchOperations.Add(CreatePatchOperation(_createSetPatchOperationMethodInfo, jsonPatchOperations, jsonPatchOperation));
                    break;
            }
        }

        return cosmosPatchOperations;
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adjusting the action code, building the demo, running, going to Postman, sending a request, and it works!&lt;/p&gt;
&lt;h2 id=&quot;what-s-missing-&quot;&gt;What’s Missing?&lt;/h2&gt;
&lt;p&gt;This is demoware, so there is always something missing. I&#39;ve already mentioned that parts of the code are handling only simple cases. But there are two additional subjects which require more attention.&lt;/p&gt;
&lt;p&gt;One is validation. This code is not validating if paths provided for operations represent valid properties. It&#39;s also not validating if values can be converted to the right types and if the operations result in an invalid state of the entity. First, two things should be achievable at the &lt;code&gt;JsonPatch&lt;/code&gt; level. The last one will require additional code in action.&lt;/p&gt;
&lt;p&gt;The other subject is performance around that reflection code. It can be improved by caching the generic methods for specific types, but as the solution grows it might not be enough. It is worth thinking about another option here.&lt;/p&gt;
&lt;p&gt;I might tackle those two subjects, but I don&#39;t know if and when. You can keep an eye on the &lt;a href=&quot;https://github.com/tpeczek/Demo.AspNetCore.WebApi&quot;&gt;demo project&lt;/a&gt; because if I do, the code will certainly end there.&lt;/p&gt;</description><link>http://www.tpeczek.com/2021/11/leveraging-azure-cosmos-db-partial.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-7365737872932202828.post-4981240213044006715</guid><pubDate>Mon, 08 Nov 2021 20:28:00 +0000</pubDate><atom:updated>2021-11-08T21:28:18.626+01:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">asp.net core</category><category domain="http://www.blogger.com/atom/ns#">server-sent events</category><category domain="http://www.blogger.com/atom/ns#">sse</category><title>Server-Sent Events and ASP.NET Core - Disconnecting a Client</title><description>&lt;p&gt;One of the common requests for my &lt;a href=&quot;https://github.com/tpeczek/Lib.AspNetCore.ServerSentEvents&quot;&gt;Server-Sent Events library&lt;/a&gt; is the ability to disconnect clients from a server. My usual answer was that this is best to be implemented as a logical operation where the server sends an event with a specific type and clients to react to it by closing the connection. I was avoiding putting disconnect functionality into the library because it&#39;s not fully defined by the protocol.&lt;/p&gt;
&lt;h2 id=&quot;disconnecting-a-client-in-server-sent-events&quot;&gt;Disconnecting a Client in Server-Sent Events&lt;/h2&gt;
&lt;p&gt;Disconnecting a client from a server is a little bit tricky when it comes to Server-Sent Events. The reason for that is automatic reconnect. If the server simply closes the connection, the client will wait a defined period of time and reconnect. The only way to prevent the client from reconnecting is to respond with a &lt;em&gt;204 No Content&lt;/em&gt; status code. So complete flow of disconnecting a client should look like on the diagram below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1vDwX0KrD5WI3mvE_hc1Lk-CyXoQEj0hXby0twqX5W1bBvz6RRcRlEyXC9-ZjPH0Hogr7QZ7CgProw88xYtF9_p3nzMuSX6cyjXqZuXQdHODrJ3WTsHrYcCVDJptbp4WNuD5MIr96uVI/s0/SSE+Disconnect+Flow.png&quot; alt=&quot;Server-Sent Events Client Disconnect Flow Diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;There is just one challenge here - the server needs to be able to tell that it is the same client trying to reconnect.&lt;/p&gt;
&lt;h2 id=&quot;identifying-clients-in-server-sent-events&quot;&gt;Identifying Clients in Server-Sent Events&lt;/h2&gt;
&lt;p&gt;Server-Sent Events doesn&#39;t provide any specific way of identifying clients. No dedicated session mechanism. This is a place where choices and opinions are starting, a place where a library should try to stay generic. I&#39;ve given a lot of thought to whether I want it to include any specific implementation with the library and I&#39;ve decided to provide only the contract and a no-op implementation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public interface IServerSentEventsClientIdProvider
{
    Guid AcquireClientId(HttpContext context);

    void ReleaseClientId(Guid clientId, HttpContext context);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives consumers full freedom (and responsibility) to implement this aspect in whichever way they prefer. For example, below is a simple cookie-based implementation. In the case of a production scenario, you probably want to think a little bit more about cookie options, if the value should be protected, etc. Also, remember that cookies bear legal requirements.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;internal class CookieBasedServerSentEventsClientIdProvider : IServerSentEventsClientIdProvider
{
    private const string COOKIE_NAME = &quot;.ServerSentEvents.Guid&quot;;

    public Guid AcquireClientId(HttpContext context)
    {
        Guid clientId;

        string cookieValue = context.Request.Cookies[COOKIE_NAME];
        if (String.IsNullOrWhiteSpace(cookieValue) || !Guid.TryParse(cookieValue, out clientId))
        {
            clientId = Guid.NewGuid();

            context.Response.Cookies.Append(COOKIE_NAME, clientId.ToString());
        }

        return clientId;
    }

    public void ReleaseClientId(Guid clientId, HttpContext context)
    {
        context.Response.Cookies.Delete(COOKIE_NAME);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;tracking-disconnected-server-sent-events-clients&quot;&gt;Tracking Disconnected Server-Sent Events Clients&lt;/h2&gt;
&lt;p&gt;Being able to identify a client is only the first step. The second required thing is tracking the disconnected clients, so when they attempt to reconnect the server can respond with &lt;em&gt;204 No Content&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public interface IServerSentEventsNoReconnectClientsIdsStore
{
    Task AddClientId(Guid clientId);

    Task&amp;lt;bool&amp;gt; ContainsClientId(Guid clientId);

    Task RemoveClientId(Guid clientId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This part doesn&#39;t seem to be complicated until you consider scaling out. When there are multiple instances behind a load balancer, there is a possibility that reconnect attempt will reach a different instance than the one to which the client has been previously connected. This is why I&#39;ve decided to include two implementations of the above store in the library. One is simply keeping the identifiers in memory, while the second is backed by distributed cache.&lt;/p&gt;
&lt;h2 id=&quot;putting-things-together&quot;&gt;Putting Things Together&lt;/h2&gt;
&lt;p&gt;The high-level flow which library currently performs to handle Server-Sent Events requests looks like below.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class ServerSentEventsMiddleware&amp;lt;TServerSentEventsService&amp;gt; ...
{
    ...

    public async Task Invoke(HttpContext context, IPolicyEvaluator policyEvaluator)
    {
        if (CheckAcceptHeader(context.Request.Headers))
        {
            if (!await AuthorizeAsync(context, policyEvaluator))
            {
                return;
            }

            ...

            await context.Response.AcceptAsync(_serverSentEventsOptions.OnPrepareAccept);

            ServerSentEventsClient client = new ServerSentEventsClient(clientId, context.User, context.Response, _clientDisconnectServicesAvailable);

            ...

            await ConnectClientAsync(context.Request, client);

            await context.RequestAborted.WaitAsync();

            await DisconnectClientAsync(context.Request, client);
        }
        else
        {
            await _next(context);
        }
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To enable the disconnect capability, this flow needs to be adjusted to acquire the client identifier as soon as possible and prevent connection (by responding with 204) if the identifier represents a client which shouldn&#39;t be allowed to reconnect.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class ServerSentEventsMiddleware&amp;lt;TServerSentEventsService&amp;gt; ...
{
    ...

    public async Task Invoke(HttpContext context, IPolicyEvaluator policyEvaluator)
    {
        if (CheckAcceptHeader(context.Request.Headers))
        {
            if (!await AuthorizeAsync(context, policyEvaluator))
            {
                return;
            }

            Guid clientId = _serverSentEventsClientIdProvider.AcquireClientId(context);

            if (await PreventReconnectAsync(clientId, context))
            {
                return;
            }

            ...

            await context.Response.AcceptAsync(_serverSentEventsOptions.OnPrepareAccept);

            ...

            await DisconnectClientAsync(context.Request, client);
        }
        else
        {
            await _next(context);
        }
    }

    ...

    private async Task PreventReconnectAsync(Guid clientId, HttpContext context)
    {
        if (!await _serverSentEventsNoReconnectClientsIdsStore.ContainsClientIdAsync(clientId))
        {
            return false;
        }

        response.StatusCode = StatusCodes.Status204NoContent;

        _serverSentEventsClientIdProvider.ReleaseClientId(clientId, context);

        await _serverSentEventsNoReconnectClientsIdsStore.RemoveClientIdAsync(clientId);

        return true;
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also see that as part of reconnecting prevention I&#39;m releasing the client identifier and removing it from the &quot;no reconnect&quot; identifiers store. The goal is to allow any underlying implementations to clear any data and make sure that a stale identifier doesn&#39;t &quot;stick&quot; with the client.&lt;/p&gt;
&lt;p&gt;One more thing which requires adjustment are operations performed when a client is being disconnected. If the client is being disconnected by a server, its identifier should be added to the store. If the disconnect happens on the client-side, the client identifier should be released to avoid staleness.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-cs&quot;&gt;public class ServerSentEventsMiddleware&amp;lt;TServerSentEventsService&amp;gt; ...
{
    ...

    private async Task DisconnectClientAsync(HttpRequest request, ServerSentEventsClient client)
    {
        ...

        if (client.PreventReconnect)
        {
            await _serverSentEventsNoReconnectClientsIdsStore.AddClientIdAsync(client.Id);
        }
        else
        {
            _serverSentEventsClientIdProvider.ReleaseClientId(client.Id, request.HttpContext);
        }

        ...
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;coming-soon-to-a-nuget-feed-near-you&quot;&gt;Coming Soon to a NuGet Feed Near You&lt;/h2&gt;
&lt;p&gt;The code is ready and I&#39;m going to push it out with the next release of &lt;a href=&quot;https://www.nuget.org/packages/Lib.AspNetCore.ServerSentEvents&quot;&gt;Lib.AspNetCore.ServerSentEvents&lt;/a&gt;. So, if you have been waiting for disconnect capabilities in the library they soon will be there. That said, it still leaves some decisions and implementation to consumers. By sharing parts of my thought process and some implementation details I wanted to make clear why it is like this.&lt;/p&gt;</description><link>http://www.tpeczek.com/2021/11/server-sent-events-and-aspnet-core.html</link><author>noreply@blogger.com (Tomasz Pęczek)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1vDwX0KrD5WI3mvE_hc1Lk-CyXoQEj0hXby0twqX5W1bBvz6RRcRlEyXC9-ZjPH0Hogr7QZ7CgProw88xYtF9_p3nzMuSX6cyjXqZuXQdHODrJ3WTsHrYcCVDJptbp4WNuD5MIr96uVI/s72-c/SSE+Disconnect+Flow.png" height="72" width="72"/><thr:total>0</thr:total></item></channel></rss>