<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Deyan Petrov</title>
    <description>The latest articles on DEV Community by Deyan Petrov (@deyanp).</description>
    <link>https://dev.to/deyanp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F599133%2Fcb7b411a-7206-41ae-b05e-2c3c17af680f.jpeg</url>
      <title>DEV Community: Deyan Petrov</title>
      <link>https://dev.to/deyanp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deyanp"/>
    <language>en</language>
    <item>
      <title>Claude and Kusto or MCP and CQRS+</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Sat, 28 Mar 2026 00:25:01 +0000</pubDate>
      <link>https://dev.to/deyanp/claude-and-kusto-or-mcp-and-cqrs-4n2j</link>
      <guid>https://dev.to/deyanp/claude-and-kusto-or-mcp-and-cqrs-4n2j</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; Connecting Claude with Azure Data Explorer (Kusto) gives you lightweight natural language query/reporting capabilities. The Remote MCP Server gets exposed as another protocol on the existing CQRS+ based microservices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The thirst for data and analytics in a company is insatiable, and many non-technical users try to learn SQL or specific dashboarding tools just to be able to answer their own data-related questions without waiting for a specialist to find time. Ideally though the business users would like to ask those questions in natural language, the same way how they are already shooting questions to an AI/LLM. &lt;/p&gt;

&lt;p&gt;This post is all about satisfying data-hungry non-technical users by connecting Claude Desktop with a database called Azure Data Explorer by the means of a Remote MCP Server: &lt;/p&gt;

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

&lt;p&gt;Diagram 1: Claude Desktop + Kusto Remote MCP Server + Azure Data Explorer (Kusto)&lt;/p&gt;

&lt;p&gt;Additionally, the post discusses the architectural decision where to place this so-called &lt;strong&gt;MCP Tool&lt;/strong&gt; within a CQRS+ Microservice Architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude
&lt;/h2&gt;

&lt;p&gt;Claude has turned nowadays (2026) into one of the best AI tools for coders (Claude Code) and non-coders (Claude Desktop/Cowork). Even though many organizations are still held up in the ecosystems of their existing vendors (e.g. Copilot in the Microsoft Office 365/Azure world or Amazon Q in the AWS world) more and more companies  reach out to the better and more ergonomic Claude. For example, Claude Desktop has this amazing split-screen view where query remains on the left-hand side, and a visualization ("Interactive Artefact") is displayed on the right-hand side. &lt;/p&gt;

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

&lt;h2&gt;
  
  
  Azure Data Explorer (aka Kusto or ADX)
&lt;/h2&gt;

&lt;p&gt;Kusto/ADX is an amazing, lean and inexpensive cloud-hosted (only Azure) &lt;a href="https://en.wikipedia.org/wiki/Data_orientation" rel="noopener noreferrer"&gt;column store database&lt;/a&gt;, designed for very fast reads, that nobody knows of. It reminds me a bit of ClickHouse, however it does not use SQL, but KQL or Kusto Query Language, which looks very attractive for functional developers like me (F#) as it is similar to code pipelines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transactions
| where CreatedOn &amp;gt;= last3_start and CreatedOn &amp;lt;= last3_end
| project TransactionId, Amount, Type
| order by CreatedOn desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kusto is the engine powering all Microsoft Azure logging systems and even serves as the core of many other products like Azure AI Foundry, Microsoft Sentinel, Azure Application Insights, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Natural Language Query
&lt;/h2&gt;

&lt;p&gt;Of course it is possible to write SQL or in case of Kusto KQL to get data, however that requires getting acquainted with some quirky keywords and symbols. Asking questions in natural language and getting the answers also in natural language + visualization is what the non-technical people actually want. LLMs like Claude are amazing at generating code as well as SQL/KQL queries based on English questions, but to copy and paste those queries into a database query editor is so 2024-ish ;)&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Server
&lt;/h2&gt;

&lt;p&gt;MCP Servers come to the rescue, as they make a connection between the AI Tool and many systems like your database (e.g. Kusto). MCP Servers come in 2 kinds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local MCP Server (aka stdio) - requires installing some software/program (e.g. python app) on your laptop, and the AI Tool is invoking this program locally. Your local config/permissions/logged in account are being used&lt;/li&gt;
&lt;li&gt;Remote MCP Server (over HTTP) -  comes ready-to-use out-of-the-box, after some initial authentication/authorization via oAuth. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ideally the database vendor would offer an integrated Remote MCP Server. As a matter of fact I did have a chat some time ago with a Product Manager in the ADX/Kusto team (as part of a support ticket discussion, I have no connections to MS) who told me that they are working on such, however it seems that one would be more focused on bigger/enterprise products like Azure AI Foundry, instead of the relatively low-level Kusto. &lt;/p&gt;

&lt;p&gt;Alternatively, such a connector can be built in a way similar to building a REST Web Api. The MCP Protocol is based on JSON RPC and is relatively straightforward calling functions/methods over HTTP(s), the only complication is related to oAuth.&lt;/p&gt;

&lt;h2&gt;
  
  
  CQRS+, Microservices, and how MCP fits in
&lt;/h2&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/deyanp/cqrs-5276"&gt;older post&lt;/a&gt; I explain CQRS and CQRS+ in the context of microservices. In a nutshell, a software system is split into "services" based on functional areas or domains with their own context/language/terminology (aka bounded contexts), and those are subdivided into physical "microservices" (= standalone running processes) based on technical aspects like command vs query handling vs event publishing vs event handling, etc. So the question that pops up immediately is where does MCP fit in this picture?&lt;/p&gt;

&lt;p&gt;The first idea that comes to mind is to create a separate MCP microservice, responsible for implementing the MCP protocol. &lt;/p&gt;

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

&lt;p&gt;Diagram 2: Xyz Service and its microservices, including a standalone MCP microservice&lt;/p&gt;

&lt;p&gt;However, MCP is nothing more than another protocol for exposing the same command-handling or query-handling logic that you already have in your existing microservices. So the better choice seems to be that MCP co-exists with REST within the same microservices. This means that in the example below where a Remote MCP Server for Kusto is created for the sake of querying only the server would be co-hosted in the same microservice responsible for Query Handling within the Reporting service/bounded context:&lt;/p&gt;

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

&lt;p&gt;Diagram 3: Xyz Service and its microservices, with a MCP server integrated into the existing microservices&lt;/p&gt;

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

&lt;p&gt;The sample below is using .NET and F#, my favorite combo, but can be obviously written in any tech stack. Actually Microsoft's Local MCP Server for &lt;a href="https://github.com/microsoft/fabric-rti-mcp" rel="noopener noreferrer"&gt;Fabric RTI&lt;/a&gt; written in Python "served as inspiration" to Claude Code with which I wrote 75% of the below code ;)&lt;/p&gt;

&lt;p&gt;This is what the &lt;code&gt;main&lt;/code&gt; function looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// Application entry point: Key Vault decryption, host builder composition, and startup.&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nc"&gt;Program&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Threading&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Hosting&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;AzureKeyVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Environment&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HostBuilder&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Wiring&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nc"&gt;McpDI&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;MCP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DependencyInjection&lt;/span&gt;

&lt;span class="c1"&gt;// this has to be first, otherwise modules initialize with env vars which have not been decrypted yet!&lt;/span&gt;
&lt;span class="nn"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overwriteEnvironmentVariablesFromKVRef&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RunSynchronously&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;webApis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;health&lt;/span&gt;

      &lt;span class="nn"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register&lt;/span&gt;
      &lt;span class="nn"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authorize&lt;/span&gt;
      &lt;span class="nn"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;
      &lt;span class="nn"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wellKnownProtectedResource&lt;/span&gt;
      &lt;span class="nn"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wellKnownAuthServer&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mcpTools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;McpTools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executeKustoQuery&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EntryPoint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;createDefaultBuilder&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt; &lt;span class="nn"&gt;BackgroundServiceExceptionBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StopHost&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;McpDI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureMcpServices&lt;/span&gt; &lt;span class="n"&gt;mcpTools&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;configureWebHost&lt;/span&gt; &lt;span class="n"&gt;webApis&lt;/span&gt; &lt;span class="nn"&gt;McpDI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requireBearerToken&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mapMcpEndpoints&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;tokenSource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;printfn&lt;/span&gt; &lt;span class="s2"&gt;"MCP Server running at %s"&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;

    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RunSynchronously&lt;/span&gt;

    &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual definition of the MCP Server methods/functions called &lt;strong&gt;&lt;em&gt;tools&lt;/em&gt;&lt;/strong&gt; is in &lt;code&gt;Api.Wiring.fs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nc"&gt;McpTools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;executeKustoQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;McpServerToolDef&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"execute_kusto_query"&lt;/span&gt;
          &lt;span class="nc"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="s2"&gt;"Executes a KQL query against the configured Azure Data Explorer (Kusto) cluster and database. Returns query results as a JSON array. The query runs under the authenticated user's identity."&lt;/span&gt;
          &lt;span class="nc"&gt;ReadOnly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
          &lt;span class="nc"&gt;Destructive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;
          &lt;span class="nc"&gt;ExecuteOperation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="nn"&gt;McpDI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;McpTools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executeKustoQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How MCP and REST co-exist in the same Microservice
&lt;/h2&gt;

&lt;p&gt;The key insight is that MCP is literally just another set of routes alongside REST — like GraphQL or gRPC. Rather than a single monolithic setup function, the codebase separates concerns into composable pipeline stages.                                                                  &lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Register MCP tools in the DI container (&lt;code&gt;Mcp.Hosting.fs&lt;/code&gt;):
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureMcpServices&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcpTools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;McpServerToolDef&lt;/span&gt; &lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;
  &lt;span class="p"&gt;=&lt;/span&gt;                                                                                                 
      &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="p"&gt;_&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;                                                
          &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddHttpContextAccessor&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;                                               

          &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
              &lt;span class="n"&gt;mcpTools&lt;/span&gt;                                                                              
              &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;toolDef&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;                                                        
                  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                     
                      &lt;span class="nc"&gt;McpServerToolCreateOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                          &lt;span class="nc"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toolDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="nc"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toolDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="nc"&gt;ReadOnly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Nullable&lt;/span&gt; &lt;span class="n"&gt;toolDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="nc"&gt;Destructive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Nullable&lt;/span&gt; &lt;span class="n"&gt;toolDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Destructive&lt;/span&gt;                                
                      &lt;span class="p"&gt;)&lt;/span&gt;

                  &lt;span class="nn"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toolDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ExecuteOperation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;                       
              &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ofList&lt;/span&gt;                                                                       

          &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddMcpServer&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WithHttpTransport&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WithTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Configure the web host generically (&lt;code&gt;Hosting.HostBuilder.fs&lt;/code&gt;). This function knows nothing about MCP — it just wires up REST endpoints, a middleware hook, and an additional-endpoints hook:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureWebHost&lt;/span&gt;                                                                           
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webApiDefs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;WebApiDef&lt;/span&gt; &lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                  
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configureMiddleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IApplicationBuilder&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configureAdditionalEndpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IEndpointRouteBuilder&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                 
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IHostBuilder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                              
      &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureWebHostDefaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;webBuilder&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;                                         
          &lt;span class="n"&gt;webBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;                                                   
              &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UseRouting&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;                                                            

              &lt;span class="n"&gt;configureMiddleware&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;                                                               

              &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UseEndpoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;endpoints&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;                                                     
                  &lt;span class="n"&gt;webApiDefs&lt;/span&gt;
                  &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;webApiDef&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                      &lt;span class="n"&gt;endpoints&lt;/span&gt;
                          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MapMethods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webApiDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="n"&gt;webApiDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="o"&gt;],&lt;/span&gt;
  &lt;span class="n"&gt;webApiDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ExecuteOperation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                       
                          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AllowAnonymous&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
                      &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                    

                  &lt;span class="n"&gt;configureAdditionalEndpoints&lt;/span&gt; &lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                           
              &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 — Map MCP endpoints via a one-liner that plugs into the &lt;code&gt;configureAdditionalEndpoints&lt;/code&gt; hook:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mapMcpEndpoints&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IEndpointRouteBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                       
      &lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MapGroup&lt;/span&gt;&lt;span class="o"&gt;($&lt;/span&gt;&lt;span class="s2"&gt;"%s{basePath}/mcp"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="nc"&gt;MapMcp&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;                                 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Composition in &lt;code&gt;Program.fs&lt;/code&gt; — everything comes together as a pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                                     
      &lt;span class="n"&gt;createDefaultBuilder&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt; &lt;span class="nn"&gt;BackgroundServiceExceptionBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StopHost&lt;/span&gt;                         
      &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;McpDI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureMcpServices&lt;/span&gt; &lt;span class="n"&gt;mcpTools&lt;/span&gt;                                                        
      &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;configureWebHost&lt;/span&gt; &lt;span class="n"&gt;webApis&lt;/span&gt; &lt;span class="nn"&gt;McpDI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requireBearerToken&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mapMcpEndpoints&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                        
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The single host now serves:                                                                       &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /health&lt;/code&gt; — REST health check
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /.well-known/*, /oauth/*&lt;/code&gt; — REST OAuth endpoints
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /mcp&lt;/code&gt; — MCP JSON-RPC transport
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice that configureWebHost has no dependency on MCP at all. MCP plugs in through the same &lt;code&gt;configureAdditionalEndpoints&lt;/code&gt; hook that any other protocol (GraphQL, gRPC, SignalR) would use. Adding MCP to an existing service is just two pipeline stages — one for DI, one for routing — not a new deployment.  &lt;/p&gt;

&lt;h2&gt;
  
  
  The OAuth 2.1 Proxy — The Hard Part
&lt;/h2&gt;

&lt;p&gt;I mentioned above that the only complication with building a Remote MCP Server is "related to oAuth." Let me unpack that because it turned out to be a bigger than expected engineering challenge in this project.                                    &lt;/p&gt;

&lt;p&gt;The problem is a three-way mismatch between what Claude expects, what Azure Entra ID supports, and what is actually needed:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Claude expects Dynamic Client Registration (DCR) per RFC 7591 — it wants to call a /oauth/register endpoint and get back a client_id and client_secret. Azure Entra ID does not support DCR. App registrations are created in the Azure Portal or via scripts, not at runtime.
&lt;/li&gt;
&lt;li&gt;Claude authenticates to the MCP server — so the OAuth scope it requests is scoped to the MCP server itself. But what is needed is the resulting token to be scoped to Kusto, not to the MCP server, because the user's own token will be passed directly to Azure Data Explorer.&lt;/li&gt;
&lt;li&gt;Claude sends a dummy client_secret — the one it received during DCR. But Entra ID needs the real client_secret from the app registration.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The solution is an OAuth Proxy — our MCP server impersonates an OAuth authorization server by implementing five endpoints that intercept, rewrite, and forward Claude's OAuth requests to Entra ID.                                                &lt;/p&gt;

&lt;h3&gt;
  
  
  Discovery: "I am your OAuth server"
&lt;/h3&gt;

&lt;p&gt;When Claude first connects and gets a 401, it looks for .well-known metadata. 2 standard RFC endpoints are served that point Claude back at the Kusto MCP Server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;wellKnownAuthServer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                   
  &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                                                                                                                
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
          &lt;span class="o"&gt;{|&lt;/span&gt; &lt;span class="n"&gt;issuer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt; &lt;span class="s2"&gt;"https://login.microsoftonline.com/%s/v2.0"&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;                               
             &lt;span class="n"&gt;authorization_endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt; &lt;span class="s2"&gt;"%s/oauth/authorize"&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;                                       
             &lt;span class="n"&gt;token_endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt; &lt;span class="s2"&gt;"%s/oauth/token"&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;                                                 
             &lt;span class="n"&gt;registration_endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt; &lt;span class="s2"&gt;"%s/oauth/register"&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;                                         
             &lt;span class="n"&gt;scopes_supported&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                                          
              &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;ADX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"/.default"&lt;/span&gt;                                                            
                 &lt;span class="s2"&gt;"openid"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="s2"&gt;"profile"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="s2"&gt;"offline_access"&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;                                                                
             &lt;span class="n"&gt;response_types_supported&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="s2"&gt;"code"&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;                                                                     
             &lt;span class="n"&gt;grant_types_supported&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="s2"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="s2"&gt;"refresh_token"&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;                                         
             &lt;span class="n"&gt;code_challenge_methods_supported&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="s2"&gt;"S256"&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt; &lt;span class="o"&gt;|}&lt;/span&gt;                                                          

      &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;                                                                                     
      &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WriteAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;                                                   
  &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude now thinks our server is the authorization server itself — all OAuth requests will come to us.                      &lt;/p&gt;

&lt;h3&gt;
  
  
  Mock DCR: "Sure, here's your client_id"
&lt;/h3&gt;

&lt;p&gt;Claude calls POST /oauth/register expecting DCR. The Kusto MCP Server accepts its request, echoes back its redirect_uris (per RFC 7591), and returns the pre-registered Entra ID client_id along with a dummy client_secret that Claude will use later — but which will be thrown away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ReadFromJsonAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JsonElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nc"&gt;AsTask&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;

      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;redirectUris&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                                                 
          &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TryGetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"redirect_uris"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;                                                                
          &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uris&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;uris&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EnumerateArray&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ofSeq&lt;/span&gt;                       
          &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;_&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;[||]&lt;/span&gt;                                                                                                    

      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;dummySecret&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NewGuid&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"N"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                     

      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
          &lt;span class="o"&gt;{|&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clientId&lt;/span&gt;
             &lt;span class="n"&gt;client_secret&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dummySecret&lt;/span&gt;                                                                                 
             &lt;span class="n"&gt;grant_types&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="s2"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="s2"&gt;"refresh_token"&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
             &lt;span class="n"&gt;redirect_uris&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redirectUris&lt;/span&gt; &lt;span class="o"&gt;|}&lt;/span&gt;                                                                             

      &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;
      &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WriteAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;                                                     
  &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scope Rewriting: "You think you're authenticating to me, but you're actually authenticating to Kusto"
&lt;/h3&gt;

&lt;p&gt;When Claude calls GET /oauth/authorize, the call is intercepted and the scope parameter gets rewritten before redirecting to Entra ID. Claude asked for a scope targeting our MCP server, but that is replaced with the Kusto cluster scope. This is the key trick — the  access token that comes back will have the Kusto cluster as its audience, not the MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;authorize&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Query&lt;/span&gt;

      &lt;span class="c1"&gt;// The magic: rewrite scope to target Kusto&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                                                        
          &lt;span class="n"&gt;sprintf&lt;/span&gt; &lt;span class="s2"&gt;"%s/.default openid profile offline_access"&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;ADX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;                               

      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;entraAuthorizeUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                                            
          &lt;span class="n"&gt;sprintf&lt;/span&gt; &lt;span class="s2"&gt;"https://login.microsoftonline.com/%s/oauth2/v2.0/authorize"&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;                        

      &lt;span class="c1"&gt;// Pass through PKCE (code_challenge), state, and other params                                                     &lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;queryParts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clientId&lt;/span&gt;                                                                          
            &lt;span class="s2"&gt;"response_type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"code"&lt;/span&gt;                                                                                    
            &lt;span class="s2"&gt;"redirect_uri"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.[&lt;/span&gt;&lt;span class="s2"&gt;"redirect_uri"&lt;/span&gt;&lt;span class="o"&gt;].&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;                                                            
            &lt;span class="s2"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;                                                                                             
            &lt;span class="s2"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.[&lt;/span&gt;&lt;span class="s2"&gt;"state"&lt;/span&gt;&lt;span class="o"&gt;].&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;                                                                          
            &lt;span class="s2"&gt;"response_mode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"query"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;                                                                                 
          &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.[&lt;/span&gt;&lt;span class="s2"&gt;"code_challenge"&lt;/span&gt;&lt;span class="o"&gt;].&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="bp"&gt;[]&lt;/span&gt;                                        
             &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"code_challenge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.[&lt;/span&gt;&lt;span class="s2"&gt;"code_challenge"&lt;/span&gt;&lt;span class="o"&gt;].&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
                    &lt;span class="s2"&gt;"code_challenge_method"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"S256"&lt;/span&gt; &lt;span class="o"&gt;])&lt;/span&gt;                                                                   
          &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt; &lt;span class="s2"&gt;"%s=%s"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EscapeDataString&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EscapeDataString&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;                
          &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concat&lt;/span&gt; &lt;span class="s2"&gt;"&amp;amp;"&lt;/span&gt;                                                                                           

      &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sprintf&lt;/span&gt; &lt;span class="s2"&gt;"%s?%s"&lt;/span&gt; &lt;span class="n"&gt;entraAuthorizeUrl&lt;/span&gt; &lt;span class="n"&gt;queryParts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                
  &lt;span class="p"&gt;}&lt;/span&gt;      

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user now sees the familiar Microsoft login screen. After authenticating, Entra ID redirects back to Claude's callback with an authorization code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Credential Injection: "Let me fix that secret for you"
&lt;/h3&gt;

&lt;p&gt;Claude exchanges the auth code by calling POST /oauth/token — but it sends the dummy client_secret from the mock DCR. The MCP Server strips Claude's credentials, injects the real ones from our Entra ID app registration, and forwards the request to Entra ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                                      
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callOverHttp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;IDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;                                     
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                                             
  &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                                                                                                                
      &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ReadFormAsync&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;                                                         
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;entraTokenUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                                                
          &lt;span class="n"&gt;sprintf&lt;/span&gt; &lt;span class="s2"&gt;"https://login.microsoftonline.com/%s/oauth2/v2.0/token"&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;

      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;formData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;IDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;                                       

      &lt;span class="c1"&gt;// Copy everything EXCEPT client credentials                                                                       &lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"client_secret"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"client_id"&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;                                                   
              &lt;span class="n"&gt;formData&lt;/span&gt;&lt;span class="o"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;                                                                 

      &lt;span class="c1"&gt;// Inject real credentials                                                                                         &lt;/span&gt;
      &lt;span class="n"&gt;formData&lt;/span&gt;&lt;span class="o"&gt;.[&lt;/span&gt;&lt;span class="s2"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clientId&lt;/span&gt;                                                                 
      &lt;span class="n"&gt;formData&lt;/span&gt;&lt;span class="o"&gt;.[&lt;/span&gt;&lt;span class="s2"&gt;"client_secret"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;OAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clientSecret&lt;/span&gt;                                                           

      &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;responseBody&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;callOverHttp&lt;/span&gt; &lt;span class="n"&gt;entraTokenUrl&lt;/span&gt; &lt;span class="n"&gt;formData&lt;/span&gt;

      &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;statusCode&lt;/span&gt;                                                                            
      &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ContentType&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;
      &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;                                                       
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Entra ID returns an access_token scoped to the Kusto cluster. Claude stores it and sends it as a Bearer token on every subsequent MCP request. Our MCP server then passes that same token directly to Azure Data Explorer — it never sees or stores the user's credentials.       &lt;/p&gt;

&lt;h3&gt;
  
  
  Per-User Token Passthrough
&lt;/h3&gt;

&lt;p&gt;Some tutorials and examples for MCP servers that connect to databases use a shared service principal — a single identity with broad access that executes all queries on behalf of all users. This is the easy path, but it has serious drawbacks:   &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every user sees the same data regardless of their actual permissions, &lt;/li&gt;
&lt;li&gt;audit logs show a generic service account instead of the real person, and&lt;/li&gt;
&lt;li&gt;a bug or prompt injection in the LLM could expose data the user should never have access to.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here a different approach is taken. The user's own OAuth token — the one they obtained by logging into Microsoft Entra ID through the OAuth proxy flow described above — is passed directly to Azure Data Explorer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;executeKustoQuery&lt;/span&gt;                                                                                                    
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exn&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                                                   
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getBearerToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;unit&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                                                        
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                                                    
  &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                                                                                                                
      &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nn"&gt;QueryValidation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validate&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt; &lt;span class="s2"&gt;"Query rejected: %s"&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;                                                             
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Ok&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;                                                                                                          
          &lt;span class="k"&gt;try&lt;/span&gt;
              &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;userToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getBearerToken&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt;                                                                          

              &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;csb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                                                                                  
                  &lt;span class="nc"&gt;KustoConnectionStringBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WithAadUserTokenAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                         

              &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;KustoClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CreateCslQueryProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                
              &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executeQuery&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JsonObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FromSeconds&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nc"&gt;L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="bp"&gt;[]&lt;/span&gt;                         
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nn"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                   
          &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;                                                                                                   
              &lt;span class="n"&gt;logError&lt;/span&gt; &lt;span class="s2"&gt;"Error executing Kusto query"&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;                                                                  
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Error executing query"&lt;/span&gt;                                                                           
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical line is &lt;code&gt;.WithAadUserTokenAuthentication(userToken)&lt;/code&gt;. This is not a shared service principal — it is the actual user's token. Azure Data Explorer sees the real caller. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database roles apply per-user. If a user only has viewer access to the Sales database, they cannot query Engineering. This is enforced directly by Kusto.&lt;/li&gt;
&lt;li&gt;Row-level security policies work. If the company has RLS policies that restrict sales reps to seeing only their own region's data, those policies apply. The LLM cannot bypass them because the token identifies the actual user.
&lt;/li&gt;
&lt;li&gt;Audit logs show who really queried. When compliance or security wants to know who ran a particular query, the answer is "Alice from Finance" — not "mcp-server-service-principal."
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is that tokens expire (typically after one hour), so Claude needs to handle token refresh. The OAuth proxy handles refresh_token grants through the same credential-injection pattern as the initial token exchange, so this is transparent to the user.                                                                                              &lt;/p&gt;

&lt;h3&gt;
  
  
  The Full Flow
&lt;/h3&gt;

&lt;p&gt;Here is the complete OAuth dance visualized:                                                                               &lt;/p&gt;

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

&lt;p&gt;
  
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
      participant C as Claude Desktop
      participant M as MCP Server
      participant E as Entra ID
      participant K as Kusto                                                                                                 

      C-&amp;gt;&amp;gt;M: POST /mcp                                                                                                       
      M--&amp;gt;&amp;gt;C: 401 (WWW-Authenticate → /.well-known/*)                                                                     
      C-&amp;gt;&amp;gt;M: GET /.well-known/oauth-*                                                                                        
      M--&amp;gt;&amp;gt;C: {endpoints point to self}
      C-&amp;gt;&amp;gt;M: POST /oauth/register                                                                                            
      M--&amp;gt;&amp;gt;C: 201 {client_id, dummy secret}                                                                               
      C-&amp;gt;&amp;gt;M: GET /oauth/authorize                                                                                            
      M-&amp;gt;&amp;gt;E: 302 (scope rewritten to kusto/.default)                                                                      
      E--&amp;gt;&amp;gt;C: 302 + auth code (user logs in)                                                                                 
      C-&amp;gt;&amp;gt;M: POST /oauth/token (dummy secret)                                                                                
      M-&amp;gt;&amp;gt;E: POST /token (real secret injected)                                                                              
      E--&amp;gt;&amp;gt;M: access_token (scoped to Kusto)                                                                                 
      M--&amp;gt;&amp;gt;C: access_token                                                                                                   
      C-&amp;gt;&amp;gt;M: POST /mcp (Bearer token)                                                                                        
      M-&amp;gt;&amp;gt;K: KQL query (user's own token)                                                                                    
      K--&amp;gt;&amp;gt;M: results                                                                                                        
      M--&amp;gt;&amp;gt;C: query results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;This flow is reusable for any scenario where you need Claude to authenticate against an identity provider that does not support DCR, while obtaining tokens scoped to a backend service different from the MCP server itself.   &lt;/p&gt;

&lt;h2&gt;
  
  
  Security — Multi-Layer Defense
&lt;/h2&gt;

&lt;p&gt;When an LLM generates database queries, you have to assume it will occasionally generate something you do not want executed. The solution is defense in depth — multiple independent validation layers, each catching different classes of problems.                                                                                                                  &lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Token Pre-Validation (Middleware)
&lt;/h3&gt;

&lt;p&gt;Before any MCP tool sees the request, middleware validates the JWT token's structure, expiration, and issuer. This is not cryptographic verification — Azure Data Explorer does that later. This is an early-rejection layer that avoids wasting Kusto resources on obviously invalid tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;validateBearerToken&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedTenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                                           

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;                                                                                              
      &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="s2"&gt;"Malformed token"&lt;/span&gt;                                                                                          
  &lt;span class="k"&gt;else&lt;/span&gt;                                                                                                                   
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;payloadBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Base64UrlEncoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DecodeBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="o"&gt;.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;                                                       
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                                
      &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;JsonDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RootElement&lt;/span&gt;                                                                                         

      &lt;span class="c1"&gt;// Check expiration (with 5-minute clock skew)                                                                     &lt;/span&gt;
      &lt;span class="c1"&gt;// Check issuer contains expected tenant ID                                                                        &lt;/span&gt;
      &lt;span class="c1"&gt;// → Ok() or Error "reason"  &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expired token? Rejected at the door. Wrong tenant? Never reaches Kusto. No network call needed.                            &lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Whitelist-Based Query Validation
&lt;/h3&gt;

&lt;p&gt;This is the most important layer for LLM-generated queries. Instead of blacklisting dangerous patterns (a losing game), the MCP Server whitelists exactly which KQL operators and plugins are allowed.&lt;/p&gt;

&lt;p&gt;Only 44 safe, read-only tabular operators are permitted (where, project, summarize, join, extend, render, etc.). For evaluate plugins, only 17 safe ones are allowed — critically blocking python, r, sql_request, http_request, and anything else that could reach outside the Kusto cluster.                                                                           &lt;/p&gt;

&lt;p&gt;Several classes of queries are blocked outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;blockedSourcePatterns&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nc"&gt;Regex&lt;/span&gt;&lt;span class="o"&gt;(@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;externaldata&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RegexOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IgnoreCase&lt;/span&gt; &lt;span class="o"&gt;|||&lt;/span&gt; &lt;span class="nn"&gt;RegexOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Compiled&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"externaldata is not allowed"&lt;/span&gt;
      &lt;span class="nc"&gt;Regex&lt;/span&gt;&lt;span class="o"&gt;(@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;external_table&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;s*&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;("&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RegexOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IgnoreCase&lt;/span&gt; &lt;span class="o"&gt;|||&lt;/span&gt; &lt;span class="nn"&gt;RegexOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Compiled&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
      &lt;span class="s2"&gt;"external_table() is not allowed"&lt;/span&gt;
      &lt;span class="nc"&gt;Regex&lt;/span&gt;&lt;span class="o"&gt;(@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;cluster&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;s*&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;("&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RegexOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IgnoreCase&lt;/span&gt; &lt;span class="o"&gt;|||&lt;/span&gt; &lt;span class="nn"&gt;RegexOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Compiled&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
      &lt;span class="s2"&gt;"cross-cluster queries are not allowed"&lt;/span&gt;
      &lt;span class="nc"&gt;Regex&lt;/span&gt;&lt;span class="o"&gt;(@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;database&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;s*&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;("&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;RegexOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IgnoreCase&lt;/span&gt; &lt;span class="o"&gt;|||&lt;/span&gt; &lt;span class="nn"&gt;RegexOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Compiled&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
      &lt;span class="s2"&gt;"cross-database queries are not allowed"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And any statement starting with . (a Kusto management command) is rejected immediately. When a query is rejected, the tool returns the validation error directly to Claude as text — no query ever reaches Kusto. Claude then typically understands what went wrong and rewrites the query using only allowed operators.                                                       &lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Azure Data Explorer Server-Side Enforcement
&lt;/h3&gt;

&lt;p&gt;Even after passing both previous layers, Kusto enforces its own security: full cryptographic token validation, database role enforcement, row-level security policies, and audit logging with the real user's identity.&lt;/p&gt;

&lt;p&gt;No single layer is sufficient on its own. The middleware catches bad tokens cheaply. The query validation catches dangerous queries before they leave our server. And Kusto enforces the actual data access permissions. A failure in any one layer is caught by the others.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Functional Dependency Injection
&lt;/h2&gt;

&lt;p&gt;To the surprise of e.g. C# developers this F# project has no IoC container, no constructor injection, no interfaces. Dependencies are wired through plain functions and partial application.                                            &lt;/p&gt;

&lt;p&gt;Each module in &lt;code&gt;DependencyInjection.fs&lt;/code&gt; creates partially applied functions where dependencies are "baked in" at startup. For example, the &lt;code&gt;executeKustoQuery&lt;/code&gt; implementation accepts 4 parameters — &lt;code&gt;log&lt;/code&gt;, &lt;code&gt;getAuthorizationHeader&lt;/code&gt;, &lt;code&gt;createAdxClient&lt;/code&gt;, and query — but the DI module partially applies the first 3, leaving only query: &lt;code&gt;string -&amp;gt; Task&amp;lt;string&amp;gt;&lt;/code&gt;, exactly what the MCP framework needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nc"&gt;McpTools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;getAuthorizationHeader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getAuthorizationHeader&lt;/span&gt; &lt;span class="n"&gt;accessor&lt;/span&gt;

      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;createAdxClient&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ICslQueryProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                         
          &lt;span class="nc"&gt;KustoConnectionStringBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;ADX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serviceUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WithAadUserTokenAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                            
          &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;KustoClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CreateCslQueryProvider&lt;/span&gt;                                           

      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;executeKustoQuery&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                                        
          &lt;span class="nn"&gt;MCP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;McpTools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executeKustoQuery&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;getAuthorizationHeader&lt;/span&gt; &lt;span class="n"&gt;createAdxClient&lt;/span&gt;
  &lt;span class="n"&gt;query&lt;/span&gt;                                                                                             
          &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartAsTask&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the functional equivalent of constructor injection, but without the ceremony. Testing benefits similarly: you call the underlying function with fake loggers, test URLs, and stub token providers without requiring mocking frameworks.                                                   &lt;/p&gt;

&lt;p&gt;A notable detail: &lt;code&gt;getAuthorizationHeader&lt;/code&gt; is passed as a function (&lt;code&gt;unit -&amp;gt; string&lt;/code&gt;) rather than invoked eagerly. It captures &lt;code&gt;HttpContextAccessor&lt;/code&gt;, reading the current request's Authorization  header only at query execution time — after validation passes. Similarly, &lt;code&gt;createAdxClient&lt;/code&gt; is a function (&lt;code&gt;string -&amp;gt; ICslQueryProvider&lt;/code&gt;), not a pre-built client — each query gets a fresh client authenticated with the current user's token. This approach avoids threading infrastructure types through domain logic.&lt;/p&gt;

&lt;p&gt;The same pattern scales to the OAuth handlers — each one partially applies config (&lt;code&gt;tenant ID, client ID, secrets, scopes&lt;/code&gt;) and leaves just &lt;code&gt;HttpContext -&amp;gt; Async&amp;lt;unit&amp;gt;&lt;/code&gt;.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Local Development with Cloudflare Tunnel
&lt;/h2&gt;

&lt;p&gt;Remote MCP servers require public URLs for OAuth callbacks. During local development, &lt;code&gt;https://localhost:5001&lt;/code&gt; is not reachable from the internet. While ngrok was traditionally used, ngrok's free tier now shows a browser interstitial page that breaks OAuth redirects.              &lt;/p&gt;

&lt;p&gt;Cloudflare Tunnel offers a superior alternative: free, no signup required, and no interstitial blocking. The project includes two scripts that automate the setup.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cloudflared_tunnel.sh&lt;/code&gt; is a reusable helper that starts a tunnel and returns the public URL. It auto-installs &lt;code&gt;cloudflared&lt;/code&gt; if needed (via Homebrew on macOS, direct download on Linux), handles HTTPS local targets with &lt;code&gt;--no-tls-verify&lt;/code&gt;, and outputs the tunnel URL and PID on stdout for the caller to parse and clean up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;./cloudflared_tunnel.sh &lt;span class="nt"&gt;--url&lt;/span&gt; https://localhost:5001&lt;span class="si"&gt;)&lt;/span&gt;                                    
  &lt;span class="nv"&gt;TUNNEL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;CLOUDFLARED_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;                                                       
  &lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s2"&gt;"kill &lt;/span&gt;&lt;span class="nv"&gt;$CLOUDFLARED_PID&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null"&lt;/span&gt; EXIT    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dotnet_run.sh&lt;/code&gt; orchestrates local startup. With the &lt;code&gt;--mcp&lt;/code&gt; flag, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Loads environment variables from launchSettings.json
&lt;/li&gt;
&lt;li&gt;Reads the listen URL from the LocalDev launch profile&lt;/li&gt;
&lt;li&gt;Starts a Cloudflare Tunnel via cloudflared_tunnel.sh and exports MCP_BASE_URL with the tunnel
address&lt;/li&gt;
&lt;li&gt;Launches the .NET server
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  ./dotnet_run.sh &lt;span class="nt"&gt;--mcp&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
  &lt;span class="c"&gt;# Loading environment variables from launchSettings.json...                                       &lt;/span&gt;
  &lt;span class="c"&gt;# Starting cloudflared tunnel to https://localhost:5001...&lt;/span&gt;
  &lt;span class="c"&gt;# cloudflared tunnel: https://random-words.trycloudflare.com -&amp;gt; https://localhost:5001            &lt;/span&gt;
  &lt;span class="c"&gt;# MCP endpoint: https://random-words.trycloudflare.com/mcp &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;--mcp&lt;/code&gt;, it simply loads the env vars and runs the server — useful when working on REST-only features.                                                                                        &lt;/p&gt;

&lt;p&gt;The script prints the MCP endpoint URL to add as a connector in Claude Desktop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  MCP endpoint: https://random-words.trycloudflare.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server differentiates between listen address (&lt;code&gt;MCP_LISTEN_URL&lt;/code&gt; on localhost, defaulting to &lt;code&gt;https://localhost:5001&lt;/code&gt;) and public address (&lt;code&gt;MCP_BASE_URL&lt;/code&gt; via tunnel, defaulting to &lt;code&gt;MCP_LISTEN_URL&lt;/code&gt; if unset). OAuth endpoints advertise the tunnel URL in &lt;code&gt;.well-known&lt;/code&gt; metadata while actually listening locally. Claude communicates with the tunnel URL, which routes to localhost, which redirects to Entra ID, completing the OAuth flow.&lt;/p&gt;

&lt;p&gt;Tunnel URLs regenerate on restart — this is acceptable since Claude rediscovers endpoints via &lt;code&gt;.well-known&lt;/code&gt; on each connection.  &lt;/p&gt;

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

&lt;p&gt;What started as a simple idea — "let business users ask questions about their data in natural language" — turned into an   interesting exercise in protocol bridging, security layering, and architectural decisions.&lt;/p&gt;

&lt;p&gt;The key takeaways:                                                                                                         &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;MCP is just another protocol. It sits alongside REST in the same microservice, shares the same middleware and authentication, and does not deserve its own deployment boundary. In a CQRS+ architecture, MCP tools belong in the microservice that already handles the relevant queries or commands.                                                        &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The OAuth proxy pattern is reusable. If you need Claude (or any RFC-compliant OAuth client) to authenticate against Azure Entra ID — or any identity provider that does not support Dynamic Client Registration — the proxy approach of mock DCR + scope rewriting + credential injection works generically. Swap out the Kusto scope for a Microsoft Graph scope, and you have a different integration with the same plumbing.                                                                     &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Per-user token passthrough is worth the effort. Passing the user's own token to the database instead of using a shared service principal means that existing access controls, row-level security, and audit logging work without any additional code. The database already knows how to enforce permissions — let it. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Whitelist, do not blacklist, LLM-generated queries. When an LLM is writing your database queries, you cannot anticipate every dangerous pattern it might produce. Whitelisting allowed operators is a safer default — anything not explicitly permitted is rejected.                                                                                                     &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;F# is a natural fit for this kind of work. Pipeline-style KQL queries, pipeline-style F# code, partial application for dependency injection, discriminated unions for validation results — the language aligns well with both the problem domain and the implementation patterns.                                                                                           &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete source code is available on &lt;a href="https://github.com/deyanp/kusto-remote-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;If you are building something similar for a different database or identity provider, the OAuth proxy and query validation patterns should translate directly — only the query language and connection builder change. &lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/deyanp/cqrs-5276"&gt;CQRS+&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/fabric-rti-mcp" rel="noopener noreferrer"&gt;https://github.com/microsoft/fabric-rti-mcp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>microservices</category>
      <category>azure</category>
      <category>fsharp</category>
    </item>
    <item>
      <title>Azure Devops Pipeline with SonarQube Community in Docker and Playwright</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Sat, 08 Feb 2025 12:10:16 +0000</pubDate>
      <link>https://dev.to/deyanp/azure-devops-pipeline-with-sonarqube-community-in-docker-and-playwright-35jg</link>
      <guid>https://dev.to/deyanp/azure-devops-pipeline-with-sonarqube-community-in-docker-and-playwright-35jg</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; Scanning projects with SonarQube Community and retrieving a couple of metrics &lt;strong&gt;and reports&lt;/strong&gt; can be done easily with the below scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;SonarQube is one of the most well-known static code analysis tools which is basically scanning source code for security issues. It does come with a "self-managed" Community version, which unfortunately does not contain out of the box any easy option to generate reports with the scan results. The "self-managed" Enterprise Edition, or the cloud Enterprise Edition do support reporting, however they come with the (IMHO pretty funny for 2025) "Talk to sales" label ;)&lt;br&gt;
So the challenge I set for myself is how to set up (as a prototype, to get comfortable with SonarQube) &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;an automated AzureDevops pipeline&lt;/li&gt;
&lt;li&gt;a manual bash script &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;which not only scan some typescript projects, but also scrape a couple of reports/screenshots, and all this without maintaining a fixed, always running server instance of SonarQube ..&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important Note&lt;/strong&gt;: The (makefile.sh)[&lt;a href="https://github.com/gitricko/sonarless/blob/main/makefile.sh" rel="noopener noreferrer"&gt;https://github.com/gitricko/sonarless/blob/main/makefile.sh&lt;/a&gt;] file from (Sonarless)[&lt;a href="https://github.com/gitricko/sonarless" rel="noopener noreferrer"&gt;https://github.com/gitricko/sonarless&lt;/a&gt;] served as a basis for the below script/pipeline.&lt;/p&gt;
&lt;h2&gt;
  
  
  Run SonarQube Server in Docker
&lt;/h2&gt;

&lt;p&gt;The first step is to run SonarQube server in Docker. The official SonarQube Docker images can be found in [Docker Hub)[&lt;a href="https://hub.docker.com/_/sonarqube" rel="noopener noreferrer"&gt;https://hub.docker.com/_/sonarqube&lt;/a&gt;]. Also a dedicated docker network is created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DOCKER_NETWORK_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sonarqube"&lt;/span&gt;
&lt;span class="nv"&gt;DOCKER_SONAR_SERVER_INSTANCE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sonar-server"&lt;/span&gt;
&lt;span class="nv"&gt;DOCKER_SONAR_SERVER_INSTANCE_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"9234"&lt;/span&gt;
&lt;span class="nv"&gt;DOCKER_SONAR_SERVER_IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sonarqube:latest"&lt;/span&gt;

docker network create &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DOCKER_NETWORK_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DOCKER_SONAR_SERVER_INSTANCE_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DOCKER_SONAR_SERVER_INSTANCE_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:9000"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DOCKER_NETWORK_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DOCKER_SONAR_SERVER_IMAGE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the server has been started, but we need to await some time (&amp;lt; 1 minute) for the instance to really start. This is done by first trying to make an HTTP GET to the root URL, and then to the system status endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'User-Agent: Mozilla/6.0'&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_SONAR_SERVER_INSTANCE_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_SONAR_SERVER_INSTANCE_PORT&lt;/span&gt;&lt;span class="s2"&gt;/api/system/status"&lt;/span&gt; 2&amp;gt;/dev/null | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.status'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above commands are wrapped in loops until successful result is received. &lt;/p&gt;

&lt;h2&gt;
  
  
  Reset admin password
&lt;/h2&gt;

&lt;p&gt;Resetting the default admin password for the SonarQube server instance running in Docker seems to be important as otherwise the automated calls to the Web API or most importantly Web UI would require changing that password ...&lt;br&gt;
The new password must "be at least 12 characters long","must contain at least one uppercase character" and "must contain at least one special character".&lt;br&gt;
The password change is performed by invoking the /users/change_password operation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERNAME&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$OLD_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"login=&lt;/span&gt;&lt;span class="nv"&gt;$USERNAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;previousPassword=&lt;/span&gt;&lt;span class="nv"&gt;$OLD_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;password=&lt;/span&gt;&lt;span class="nv"&gt;$PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"http://localhost:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DOCKER_SONAR_SERVER_INSTANCE_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/users/change_password"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create Project
&lt;/h2&gt;

&lt;p&gt;Then a default project must be created. This is done by calling the /projects/create and /users/set_homepage APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CREDENTIALS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_SONAR_SERVER_INSTANCE_PORT&lt;/span&gt;&lt;span class="s2"&gt;/api/projects/create?name=&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_NAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;project=&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CREDENTIALS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_SONAR_SERVER_INSTANCE_PORT&lt;/span&gt;&lt;span class="s2"&gt;/api/users/set_homepage?type=PROJECT&amp;amp;component=&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start the Scanner using NPX
&lt;/h2&gt;

&lt;p&gt;Before the scanner can be started, a token needs to be created so that the SonarQube scanner can connect to the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SONAR_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CREDENTIALS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_SONAR_SERVER_INSTANCE_PORT&lt;/span&gt;&lt;span class="s2"&gt;/api/user_tokens/generate?name=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s%N&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .token&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then running the scanner is just an npx command away, but note that it has to be run in the source code project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; sonarqube-scanner &lt;span class="nt"&gt;-Dsonar&lt;/span&gt;.host.url&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_SONAR_SERVER_INSTANCE_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-Dsonar&lt;/span&gt;.token&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SONAR_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-Dsonar&lt;/span&gt;.projectKey&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Get some metrics via Web API
&lt;/h2&gt;

&lt;p&gt;After the scan has finished, we can collect a couple of numeric metrics from the standard/official Web API of the SonarQube (without any funky scraping):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CREDENTIALS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DOCKER_SONAR_SERVER_INSTANCE_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/measures/component?component=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;metricKeys=bugs,vulnerabilities,code_smells,quality_gate_details,violations,duplicated_lines_density,ncloc,coverage,reliability_rating,security_rating,security_review_rating,sqale_rating,security_hotspots,open_issues"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$SONAR_METRICS_PATH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROJECT_NAME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROJECT_NAME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"qualifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TRK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"measures"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"coverage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reliability_rating"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"code_smells"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"duplicated_lines_density"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"security_rating"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"violations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quality_gate_details"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;level&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;OK&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;conditions&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[],&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ignoredConditions&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:false}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"security_hotspots"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"security_review_rating"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqale_rating"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bugs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ncloc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vulnerabilities"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open_issues"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bestValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above is anonymized sample, so the real one has &lt;code&gt;PROJECT_NAME&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;12345&lt;/code&gt; replaced by real values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture screenshots as reports via Web UI with Playwright
&lt;/h2&gt;

&lt;p&gt;In addition, a simple Playwright script is executed against the SonarQube server instance running in Docker to capture some screenshots as reports from the UI. &lt;/p&gt;

&lt;p&gt;For that purpose the following script is executed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tsc save_mhtml.ts
node save_mhtml.js &lt;span class="nt"&gt;--projectName&lt;/span&gt; &lt;span class="nv"&gt;$PROJECT_NAME&lt;/span&gt; &lt;span class="nt"&gt;--outputFolder&lt;/span&gt; &lt;span class="nv"&gt;$SONAR_OUTPUT_FOLDER&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The playwright script itself goes through the following steps:&lt;/p&gt;

&lt;p&gt;Start a browser, etc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slowMo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Set headless to false to show the browser window + slowMo to 5000 for example&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to SonarQube Server Web UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:9234/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log in with username and password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[name="login"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[name="password"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Log in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click through some really nasty buttons which just stay in the way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;clickButtonIfVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;I understand the risk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;clickButtonIfVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Later&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;clickButtonIfVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Got it&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;clickButtonIfVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Dismiss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally scrape some web pages and store them as *.mhtml report files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gotoPageAndMakeMhtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`http://localhost:9234/dashboard?branch=main&amp;amp;id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;codeScope=overall`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outputFolder&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_overview.mhtml`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gotoPageAndMakeMhtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`http://localhost:9234/project/issues?id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;issueStatuses=OPEN%2CCONFIRMED`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outputFolder&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_issues.mhtml`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gotoPageAndMakeMhtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`http://localhost:9234/security_hotspots?id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outputFolder&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_security_hotspots.mhtml`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check if there are security vulnerabilities
&lt;/h2&gt;

&lt;p&gt;Finally, the script can check some of the metric values captured before, and fail if a threshold has been exceeded, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;vulnerabilities&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="nv"&gt;$SONAR_METRICS_PATH&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.component.measures[] | select(.metric == "vulnerabilities") | .value'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$vulnerabilities&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Vulnerabilities found: &lt;/span&gt;&lt;span class="nv"&gt;$vulnerabilities&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No vulnerabilities found"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are of course a few &lt;code&gt;waitForLoadState&lt;/code&gt;s and &lt;code&gt;waitForTimeout&lt;/code&gt;s scattered in between, so the script does take about 1 minute in total.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure DevOps Pipeline, and the final scripts
&lt;/h2&gt;

&lt;p&gt;The steps described above are wrapped up in 2 scripts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bash script for manual scanning a project from the terminal&lt;/li&gt;
&lt;li&gt;Azure DevOps Pipeline for automatic scanning of a project, e.g. on a nightly schedule&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Azure DevOps pipeline performs some additional steps, but that depends on what is already pre-installed or not on your VM image, e.g.:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Docker&lt;/li&gt;
&lt;li&gt;Install jq&lt;/li&gt;
&lt;li&gt;Install NodeJS&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The scripts and pipelines can be found at &lt;a href="https://github.com/deyanp/azuredevops-sonarqube" rel="noopener noreferrer"&gt;https://github.com/deyanp/azuredevops-sonarqube&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gitricko/sonarless" rel="noopener noreferrer"&gt;https://github.com/gitricko/sonarless&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sonarqube</category>
      <category>azuredevops</category>
    </item>
    <item>
      <title>Running and Approving Azure DevOps Pipelines from the terminal</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Sat, 08 Feb 2025 00:31:50 +0000</pubDate>
      <link>https://dev.to/deyanp/running-and-approving-azure-devops-pipelines-from-the-terminal-3bpc</link>
      <guid>https://dev.to/deyanp/running-and-approving-azure-devops-pipelines-from-the-terminal-3bpc</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; This bash script will allow you to start and approve (multiple) Azure DevOps pipelines from the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you are using Azure DevOps Pipelines (similar to Github Actions) you might be going to the browser, navigating to the list of pipelines, selecting one, and clicking on the "Run pipeline" + "Run" buttons. If you want to start 10 pipelines (e.g. for 10 microservices) at once, you need to do the above clicking 10x2 times.&lt;/p&gt;

&lt;p&gt;Once the builds have reached a stage guarded by an approval, you have to go into the build, click the "Review" + "Approve" buttons ... which would be another 10x2 clicks. &lt;/p&gt;

&lt;p&gt;Wouldn't it be better to trigger the above from the terminal/CLI? You are happy to find the &lt;code&gt;az pipelines run&lt;/code&gt; az cli extension command, however things become very tricky when it comes to the approvals ...&lt;/p&gt;

&lt;p&gt;The rest of this post is referring to a pipeline with 3 Stages - Build, Deploy to test and Deploy to prod&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The &lt;code&gt;az rest .. --resource ..&lt;/code&gt; command
&lt;/h2&gt;

&lt;p&gt;Before we get to solving the approval problem, there is one very useful command that will help us immensely - &lt;code&gt;az rest&lt;/code&gt;. This command allows us to invoke various Azure DevOps (and probably other) APIs without any personal access tokens (PATs) or similar. The important attribute to pass is &lt;code&gt;--resource "https://management.core.windows.net/"&lt;/code&gt;, which reuses our Azure DevOps session. &lt;/p&gt;

&lt;p&gt;For example, a much faster alternative to &lt;code&gt;az pipelines run&lt;/code&gt; is the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az rest &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"https://dev.azure.com/&lt;/span&gt;&lt;span class="nv"&gt;$org&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$project&lt;/span&gt;&lt;span class="s2"&gt;/_apis/pipelines?api-version=7.1"&lt;/span&gt; &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="s2"&gt;"https://management.core.windows.net/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty neat solution to the generally very nasty authentication topic, which usually requires some awkward token handling!&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource Model and Relations, or how to find the Approval
&lt;/h2&gt;

&lt;p&gt;In order to implement the approval action, one has to first understand how to get to it, which requires navigation through a couple of Azure DevOps resources, namely: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pipeline&lt;/li&gt;
&lt;li&gt;Last Build&lt;/li&gt;
&lt;li&gt;Build Timeline and 3 specific records &lt;/li&gt;
&lt;li&gt;Approval&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Pipeline
&lt;/h3&gt;

&lt;p&gt;So first we find the &lt;code&gt;pipelineId&lt;/code&gt; by pipeline name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;pipelineId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az rest &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$adoBaseUrl&lt;/span&gt;&lt;span class="s2"&gt;/pipelines?api-version=7.1"&lt;/span&gt; &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="nv"&gt;$resource&lt;/span&gt; | jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;".value.[] | select( .name == &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$pipeline&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ).id"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;adoBaseUrl=https://dev.azure.com/$org/$project/_apis/&lt;/code&gt; and &lt;code&gt;resource=https://management.core.windows.net/&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Last Build
&lt;/h3&gt;

&lt;p&gt;Next we find the last build for this pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;build&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az rest &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$adoBaseUrl&lt;/span&gt;&lt;span class="s2"&gt;/build/builds?api-version=7.1&amp;amp;definitions=&lt;/span&gt;&lt;span class="nv"&gt;$pipelineId&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;queryOrder=startTimeDescending&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;top=1"&lt;/span&gt; &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="nv"&gt;$resource&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and from it we extract the timeline url:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;timelineUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$build&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.value[0]._links.timeline.href'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build Timeline
&lt;/h3&gt;

&lt;p&gt;Getting the contents of the pipeline is easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;timeline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az rest &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$timelineUrl&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="nv"&gt;$resource&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;however it contains tons of records - depending on the pipeline definition, each one having parent record. We need to find the record corresponding to the approval stage, and for that we need to traverse (at least in my particular case) 3 records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deploy Stage record&lt;/li&gt;
&lt;li&gt;Checkpoint record&lt;/li&gt;
&lt;li&gt;Checkpoint.Approval record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is done in the following way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;deployStageRecordId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$timeline&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;".records[] | select( .identifier == &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$deployStageRecordIdentifier&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; and .type == &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Stage&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ).id"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;checkpointRecordId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$timeline&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;".records[] | select( .parentId == &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$deployStageRecordId&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; and .type == &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Checkpoint&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ).id"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;approvalId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$timeline&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;".records[] | select( .parentId == &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$checkpointRecordId&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; and .type == &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Checkpoint.Approval&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ).id"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last one gives us the &lt;code&gt;approvalId&lt;/code&gt;!&lt;/p&gt;

&lt;h3&gt;
  
  
  Approval
&lt;/h3&gt;

&lt;p&gt;Once we have the &lt;code&gt;approvalId&lt;/code&gt; we can get for example its &lt;code&gt;status&lt;/code&gt;, so that we approve only if the &lt;code&gt;status&lt;/code&gt; is &lt;code&gt;pending&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;approvalStatus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az rest &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$adoBaseUrl&lt;/span&gt;&lt;span class="s2"&gt;/pipelines/approvals/&lt;/span&gt;&lt;span class="nv"&gt;$approvalId&lt;/span&gt;&lt;span class="s2"&gt;?api-version=7.1"&lt;/span&gt; &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="nv"&gt;$resource&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;".status"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Finally, approve!
&lt;/h2&gt;

&lt;p&gt;Once the approvalId is found, the following POST command must be issued to Review/Approve it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az rest &lt;span class="nt"&gt;--method&lt;/span&gt; PATCH &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$adoBaseUrl&lt;/span&gt;&lt;span class="s2"&gt;/pipelines/approvals?api-version=7.1"&lt;/span&gt; &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="nv"&gt;$resource&lt;/span&gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"[ { &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;approvalId&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$approvalId&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;comment&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;cli&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;approved&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; } ]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full script can be found at &lt;a href="https://github.com/deyanp/adopipelines/blob/main/pipeplines.sh" rel="noopener noreferrer"&gt;https://github.com/deyanp/adopipelines/blob/main/pipeplines.sh&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/77522387/approving-pipeline-stage-azure-devops-via-api?noredirect=1&amp;amp;lq=1" rel="noopener noreferrer"&gt;https://stackoverflow.com/questions/77522387/approving-pipeline-stage-azure-devops-via-api?noredirect=1&amp;amp;lq=1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/71237839/approving-azure-pipeline-using-api" rel="noopener noreferrer"&gt;https://stackoverflow.com/questions/71237839/approving-azure-pipeline-using-api&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/rest/api/azure/devops/pipelines/?view=azure-devops-rest-7.1" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/rest/api/azure/devops/pipelines/?view=azure-devops-rest-7.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/rest/api/azure/devops/approvalsandchecks/?view=azure-devops-rest-7.1" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/rest/api/azure/devops/approvalsandchecks/?view=azure-devops-rest-7.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>devops</category>
    </item>
    <item>
      <title>F# App Stub for AKS hosting v2 (without Azure WebJobs SDK fluff)</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Mon, 25 Mar 2024 18:43:43 +0000</pubDate>
      <link>https://dev.to/deyanp/f-app-stub-for-aks-hosting-v2-without-azure-webjobs-sdk-fluff-486n</link>
      <guid>https://dev.to/deyanp/f-app-stub-for-aks-hosting-v2-without-azure-webjobs-sdk-fluff-486n</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; Use a standard .NET 8+ host for your microservices running for example in AKS without any Azure WebJobs SDK syntactic sugar or similar fluff/magic&lt;sup id="fnref1"&gt;1&lt;/sup&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;We started originally now almost 5 years ago with .NET/F# Azure Functions apps, and we had pretty bad experiences with that setup, the biggest issues being low node app density,  high costs and too much bloat from MS frameworks. &lt;/p&gt;

&lt;p&gt;Soon after that we migrated to AKS (managed Kubernetes on Azure similar to EKS and GKE), but to keep migration efforts reasonable we re-used the WebJobs SDK, which is a building stone for the Azure Functions SDK. More than 2 years ago I wrote an &lt;a href="https://dev.to/deyanp/f-app-stub-for-aks-hosting-with-webjobs-but-without-azure-functions-fluff-43lj"&gt;article about the app stub&lt;/a&gt; we were using. &lt;/p&gt;

&lt;p&gt;Everything was running relatively fine until recently when we had to &lt;a href="https://dev.to/deyanp/isolated-local-dev-env-using-k3d-multi-tenant-azure-services-docker-containers-and-mirrord-1k2m"&gt;improve our local development experience&lt;/a&gt;, and we needed a way to change the low-level workings of some of our code related to Azure Event Hubs and similar. That was the trigger for re-visiting the usage of WebJobs SDK and actually migrating away from, which is the topic of this article. &lt;/p&gt;

&lt;p&gt;When it comes to what Azure WebJobs SDK triggers we were using, I believe we are nothing special and like many others we have the following in place:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pub-Sub message bus, in our case Azure Event Hubs by means of &lt;code&gt;EventHubTrigger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Queues, in our case Azure Storage Queues by means of &lt;code&gt;QueueTrigger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Time-triggered jobs by means of &lt;code&gt;TimerTrigger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Websockets, in our case Azure SignalR by means of &lt;code&gt;SignalRTrigger&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Additionally, we use various other IHostedService/BackgroundService processes, which get started upon app start and are running in the background, doing some other work - e.g. listening to notifications from the database (change streams), aggregating something every x seconds, etc.&lt;/p&gt;

&lt;p&gt;All of the above is using configuration based on simple environment variables, some of them pointing to a secrets store (in our case Azure Key Vault). There is of course some logging and telemetry sent to a cloud service (in our case Azure Application Insights), as well as some web/REST/HTTP API (in our case using barebone &lt;a href="https://dotnet.microsoft.com/en-us/apps/aspnet/apis"&gt;ASP.NET Core SDK&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;All &lt;code&gt;XyzTriggers&lt;/code&gt; above have been migrated away from the Azure WebJobs SDK to pretty small, sweet and (almost) fully in our control implementations using directly the Azure SDK, which will be explain in the sections below. But before we do that let's look at how the app stub looks like now.&lt;/p&gt;

&lt;h2&gt;
  
  
  App Stub v2
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.fs&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;TestService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;XyzHandling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Program&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Threading&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Hosting&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Hosting&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;AzureKeyVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Environment&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;TestService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;XyzHandling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Wiring&lt;/span&gt;

&lt;span class="nn"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overwriteEnvironmentVariablesFromKVRef&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RunSynchronously&lt;/span&gt;

&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EntryPoint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createDefaultBuilder&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt; &lt;span class="nn"&gt;BackgroundServiceExceptionBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StopHost&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureLogging&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureAppInsights&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureEventHubProcessors&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;EventHubProcessors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventHubProcessor1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nn"&gt;EventHubProcessors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventHubProcessor2&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureQueueProcessors&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt;
            &lt;span class="o"&gt;([&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="nn"&gt;QueueProcessors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queueProcessor1&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;QueueProcessors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queueProcessor2&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choose&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureBackgroundServices&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;BackgroundServices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventHubPublisherTest&lt;/span&gt;
              &lt;span class="nn"&gt;BackgroundServices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queuePublisherTest&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureTimers&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;Timers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timerProcessor1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nn"&gt;Timers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timerProcessor2&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureStartup&lt;/span&gt; &lt;span class="nn"&gt;Startup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startupFunctions&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureWebHost&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkHealth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nn"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkReadiness&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nn"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getById&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;tokenSource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RunSynchronously&lt;/span&gt;

    &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;// return an integer exit code&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The above code spawns in total 9 IHosted/Background services (incl. the standard web host)&lt;/li&gt;
&lt;li&gt;Event Hub Processors listen to &amp;amp; process Azure Event Hub events&lt;/li&gt;
&lt;li&gt;Queue Processors listen to &amp;amp; process Azure Storage queue messages&lt;/li&gt;
&lt;li&gt;Timer Processors run stuff every x seconds, minutes, hours etc.&lt;/li&gt;
&lt;li&gt;There are even some generic Background Services started for writing to an event hub and a queue&lt;/li&gt;
&lt;li&gt;One can see at a glance all the running processes and even web api endpoints from the &lt;code&gt;Program.fs&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trigger Implementations
&lt;/h2&gt;

&lt;p&gt;The trigger implementations below are all based on &lt;code&gt;BackgroundService/IHostedService&lt;/code&gt;, which means a background task is spawned and is running the whole time the host itself is running. &lt;/p&gt;

&lt;p&gt;Compared to the original implementations in Azure Webjobs SDK the below ones may have less functionality (no auto-scaling for Azure Functions or similar) but at the same time are very lean and much easier to understand/maintain. &lt;/p&gt;

&lt;p&gt;The source code of a working sample application will be eventually made available &lt;a href="https://github.com/deyanp/FSharpAKSAppStubv2"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Event Hub Processor
&lt;/h3&gt;

&lt;p&gt;Old code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// Api.Wiring.fs, handle the event&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;WebJobs&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HandleXyzEvent"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="o"&gt;_.&lt;/span&gt;&lt;span class="nc"&gt;HandleXyzEvent&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventHubTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="nc"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Xyz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connectionStringKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="nc"&gt;ConsumerGroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumerGroup&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;enqueuedTimeUtc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sequenceNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ILogger&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="c1"&gt;// handle the event&lt;/span&gt;

&lt;span class="c1"&gt;// Program.fs, configure the host with web jobs&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureWebJobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureWebJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddAzureStorageCoreServices&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
          &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddEventHubs&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is some magic going on, because in Program.fs you do not say what you actually want to listen to, you just say "I want to enable Event Hubs Handling", and then you decorate some method of a class with attributes, which indicate that you want to listen to an event hub&lt;/li&gt;
&lt;li&gt;The EventHubTrigger insists on getting a key to a connection string environment variable, and fetch the value when it decides ...&lt;/li&gt;
&lt;li&gt;You seem to need to specify also a FunctionName in addition to the method name&lt;/li&gt;
&lt;li&gt;Any configuration settings for the EventHubTrigger are hidden away, you need to know what environment variables to configure, which are picked "automatigally" by the framework ;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;New code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// Api.Functions.fs, handle the event&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;processEvent&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;partitionId&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="c1"&gt;// handle the event&lt;/span&gt;

        &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retn&lt;/span&gt;

&lt;span class="c1"&gt;// Api.Wiring.fs, instantiate the processor&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;eventHubProcessor1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;EventHubProcessorDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;
            &lt;span class="s2"&gt;"EventHubProcessor1"&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkpointStorageConnectionString&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventHubConnectionString&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumerGroup&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventBatchMaximumCount&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assignedPartitionIds&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultStartingPosition&lt;/span&gt;
            &lt;span class="nn"&gt;EventHubHandlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processEvent&lt;/span&gt;

&lt;span class="c1"&gt;// Program.fs, configure the host with the processors&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureEventHubProcessors&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;EventHubProcessors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventHubProcessor1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nn"&gt;EventHubProcessors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventHubProcessor2&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Event Hub Processor Implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;AzureEventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventProcessing&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tasks&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Generic&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Threading&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Messaging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Consumer&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Blobs&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Messaging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventHubs&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Messaging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Primitives&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;Framework&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;AzureEventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogEvents&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StructuredLog&lt;/span&gt;

&lt;span class="c1"&gt;// https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/eventhub/Azure.Messaging.EventHubs/samples/Sample08_CustomEventProcessor.md&lt;/span&gt;
&lt;span class="c1"&gt;// Event Processor which considers assigned partitions&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;AssignablePartitionProcessor&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;storageClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BlobContainerClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;assignedPartitions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="bp"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;eventBatchMaximumCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;consumerGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;clientOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventProcessorOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;processEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;EventData&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="n"&gt;processError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;inherit&lt;/span&gt;
        &lt;span class="nc"&gt;PluggableCheckpointStoreEventProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorPartition&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;BlobCheckpointStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storageClient&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;eventBatchMaximumCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;consumerGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;clientOptions&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Workaround, see https://github.com/dotnet/fsharp/issues/12448 ...&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BaseListPartitionIdsAsync&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventHubConnection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="bp"&gt;[]&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ListPartitionIdsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ListPartitionIdsAsync&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventHubConnection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="bp"&gt;[]&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;assignedPartitions&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;assignedPartitions&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assignedPartitions&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FromResult&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BaseListPartitionIdsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Workaround, see https://github.com/dotnet/fsharp/issues/12448 ...&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BaseListOwnershipAsync&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorPartitionOwnership&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ListOwnershipAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ListOwnershipAsync&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorPartitionOwnership&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;assignedPartitions&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;assignedPartitions&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;assignedPartitions&lt;/span&gt;
            &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;partition&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="nc"&gt;EventProcessorPartitionOwnership&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nc"&gt;FullyQualifiedNamespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FullyQualifiedNamespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nc"&gt;EventHubName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventHubName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nc"&gt;ConsumerGroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConsumerGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nc"&gt;PartitionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nc"&gt;OwnerIdentifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nc"&gt;LastModifiedTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt;
                &lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FromResult&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BaseListOwnershipAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Workaround, see https://github.com/dotnet/fsharp/issues/12448 ...&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BaseClaimOwnershipAsync&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;desiredOwnership&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorPartitionOwnership&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorPartitionOwnership&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClaimOwnershipAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desiredOwnership&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClaimOwnershipAsync&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;desiredOwnership&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorPartitionOwnership&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorPartitionOwnership&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="c1"&gt;// Warning: if the match is removed, and only the code in the Some part is left =&amp;gt; High CPU utilization if no assignedPartitions defined!&lt;/span&gt;
        &lt;span class="c1"&gt;// for more info see https://github.com/Azure/azure-sdk-for-net/issues/39603&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;assignedPartitions&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="p"&gt;_&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;desiredOwnership&lt;/span&gt;
            &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;ownership&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ownership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LastModifiedTime&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nn"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;desiredOwnership&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FromResult&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BaseClaimOwnershipAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desiredOwnership&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Workaround, see https://github.com/dotnet/fsharp/issues/12448 ...&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BaseUpdateCheckpointAsync&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;partitionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sequenceNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Nullable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UpdateCheckpointAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partitionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sequenceNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// used in the OnProcessingEventBatchAsync member, calculate once&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventHubFullName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;Subscribing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createEventHubFullPath&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FullyQualifiedNamespace&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventHubName&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConsumerGroup&lt;/span&gt;

    &lt;span class="c1"&gt;// https://learn.microsoft.com/en-us/dotnet/api/azure.messaging.eventhubs.eventprocessorclient.onprocessingeventbatchasync?view=azure-dotnet#remarks&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OnProcessingEventBatchAsync&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
            &lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventProcessorPartition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isNull&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
                    &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
                        &lt;span class="n"&gt;events&lt;/span&gt;
                        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                            &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="nn"&gt;Subscribing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkEventEnDequeueTime&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventHubFullName&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt;

                                &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;processEvent&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PartitionId&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt;
                            &lt;span class="o"&gt;})&lt;/span&gt;
                        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;
                        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;
                        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartAsTask&lt;/span&gt;
                        &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;

                    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lastEvent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;

                    &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
                        &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BaseUpdateCheckpointAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PartitionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;lastEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;lastEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SequenceNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="c1"&gt;// It is very important that you always guard against exceptions in&lt;/span&gt;
                &lt;span class="c1"&gt;// your handler code; the processor does not have enough&lt;/span&gt;
                &lt;span class="c1"&gt;// understanding of your code to determine the correct action to take.&lt;/span&gt;
                &lt;span class="c1"&gt;// Any exceptions from your handlers go uncaught by the processor and&lt;/span&gt;
                &lt;span class="c1"&gt;// will NOT be redirected to the error handler.&lt;/span&gt;
                &lt;span class="c1"&gt;//&lt;/span&gt;
                &lt;span class="c1"&gt;// In this case, the partition processing task will fault and be restarted&lt;/span&gt;
                &lt;span class="c1"&gt;// from the last recorded checkpoint.&lt;/span&gt;

                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="s2"&gt;"OnProcessingEventBatchAsync: Exception while processing events: {ex}"&lt;/span&gt;
                    &lt;span class="n"&gt;ex&lt;/span&gt;
                    &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

        &lt;span class="c1"&gt;// bubble up, which will kill the background service and result in Health Check alert&lt;/span&gt;
        &lt;span class="c1"&gt;// alternative is to log and "swallow" the exception here, but then the processor will go in infinite loop ...&lt;/span&gt;
        &lt;span class="c1"&gt;// NOT a good idea, raising exception here invokes OnProcessingErrorAsync, which causes the host to get restarted automatically, and the health check does not detect this ..&lt;/span&gt;
        &lt;span class="c1"&gt;// ex.Reraise()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// https://learn.microsoft.com/en-us/dotnet/api/azure.messaging.eventhubs.eventprocessorclient.onprocessingerrorasync?view=azure-dotnet#remarks&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OnProcessingErrorAsync&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventProcessorPartition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;operationDescription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;partitionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;partition&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ofObj&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;_.&lt;/span&gt;&lt;span class="nc"&gt;PartitionId&lt;/span&gt;

                &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
                    &lt;span class="n"&gt;processError&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;partitionId&lt;/span&gt; &lt;span class="n"&gt;operationDescription&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
                    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartAsTask&lt;/span&gt;

            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;wex&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="c1"&gt;// It is very important that you always guard against exceptions&lt;/span&gt;
                &lt;span class="c1"&gt;// in your handler code; the processor does not have enough&lt;/span&gt;
                &lt;span class="c1"&gt;// understanding of your code to determine the correct action to&lt;/span&gt;
                &lt;span class="c1"&gt;// take. Any exceptions from your handlers go uncaught by the&lt;/span&gt;
                &lt;span class="c1"&gt;// processor and will NOT be handled in any way.&lt;/span&gt;
                &lt;span class="c1"&gt;//&lt;/span&gt;
                &lt;span class="c1"&gt;// In this case, unhandled exceptions will not impact the processor&lt;/span&gt;
                &lt;span class="c1"&gt;// operation but will go unobserved, hiding potential application problems.&lt;/span&gt;

                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="s2"&gt;"OnProcessingErrorAsync: Exception occurred while processing events: {wex}. Original exception: {ex}."&lt;/span&gt;
                    &lt;span class="n"&gt;wex&lt;/span&gt;
                    &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;wex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

        &lt;span class="c1"&gt;// do! this.StopProcessingAsync(cancellationToken)&lt;/span&gt;

        &lt;span class="c1"&gt;// bubble up, which will kill the background service and result in Health Check alert&lt;/span&gt;
        &lt;span class="c1"&gt;// alternative is to log and "swallow" the exception here, but then the processor will go in infinite loop ...&lt;/span&gt;
        &lt;span class="c1"&gt;// NOT a good idea, the host gets restarted automatically, and the health check does not detect this ..&lt;/span&gt;
        &lt;span class="c1"&gt;// ex.Reraise()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// Starts the Event Processor&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;startConsumeEvents&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpointStorageConnectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpointBlobContainerName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventHubConnectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumerGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventBatchMaximumCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assignedPartitionIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="bp"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultStartingPosition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventPosition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;EventData&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ManualResetEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;blobContainerClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                &lt;span class="nc"&gt;BlobContainerClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpointStorageConnectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkpointBlobContainerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// automatically create container if it does not exist&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;blobContainerClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CreateIfNotExistsAsync&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;

            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EventProcessorOptions&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// TODO: Customize some of them?&lt;/span&gt;

            &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DefaultStartingPosition&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;defaultStartingPosition&lt;/span&gt;

            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                &lt;span class="nc"&gt;AssignablePartitionProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;blobContainerClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;assignedPartitionIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;eventBatchMaximumCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;consumerGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;eventHubConnectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;processEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;processError&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"processor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Info&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorStarted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorStarted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="s2"&gt;"Starting with config:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;EventHubNamespace/Name/ConsumerGroup = {eventHubNamespace}/{eventHubName}/{consumerGroup}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;BlobContainerClient.Uri = {blobContainerClientUri}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;AssignedPartitionIds = {assignedPartitionIds}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;EventBatchMaximumCount = {eventBatchMaximumCount}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.PrefetchCount = {prefetchCount}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.PrefetchSizeInBytes = {prefetchSizeInBytes}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.MaximumWaitTime = {maximumWaitTime}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.TrackLastEnqueuedEventProperties = {trackLastEnqueuedEventProperties}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.DefaultStartingPosition = {defaultStartingPosition}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.LoadBalancingStrategy = {loadBalancingStrategy}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.LoadBalancingUpdateInterval = {loadBalancingUpdateInterval}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.PartitionOwnershipExpirationInterval = {partitionOwnershipExpirationInterval}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.RetryOptions.Mode = {retryOptionsMode}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.RetryOptions.Delay = {retryOptionsDelay}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.RetryOptions.MaximumDelay = {retryOptionsMaximumDelay}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.RetryOptions.MaximumRetries = {retryOptionsMaximumRetries}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.RetryOptions.TryTimeout = {retryOptionsTryTimeout}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.RetryOptions.CustomRetryPolicy = {retryOptionsCustomRetryPolicy}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    "&lt;/span&gt;
                &lt;span class="p"&gt;[|&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FullyQualifiedNamespace&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt; &lt;span class="s2"&gt;".servicebus.windows.net"&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventHubName&lt;/span&gt;
                    &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConsumerGroup&lt;/span&gt;
                    &lt;span class="n"&gt;blobContainerClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;
                    &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;"%A{assignedPartitionIds}"&lt;/span&gt;
                    &lt;span class="n"&gt;eventBatchMaximumCount&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PrefetchCount&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PrefetchSizeInBytes&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaximumWaitTime&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TrackLastEnqueuedEventProperties&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DefaultStartingPosition&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LoadBalancingStrategy&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LoadBalancingUpdateInterval&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PartitionOwnershipExpirationInterval&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;RetryOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Mode&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;RetryOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Delay&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;RetryOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaximumDelay&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;RetryOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaximumRetries&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;RetryOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TryTimeout&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;RetryOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CustomRetryPolicy&lt;/span&gt;
                &lt;span class="p"&gt;|]&lt;/span&gt;

            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartProcessingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;

            &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// Stops the Event Processor&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;stopConsumeEvents&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ManualResetEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"processor"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;:?&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;AssignablePartitionProcessor&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StopProcessingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;

            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Info&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorStopped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventProcessorStopped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="s2"&gt;"Event Hub Processor was stopped"&lt;/span&gt;
                &lt;span class="o"&gt;[||]&lt;/span&gt;

            &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Reset&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You define a function, then instantiate processor(s) with the function and a bunch of configuration values, which you can fetch by yourself, and then you tell the HostBuilder to configure your processor(s) - pretty straightforward, no reflection, no magic&lt;/li&gt;
&lt;li&gt;The implementation of the EventHubProcessor is using &lt;a href="https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/eventhub/Azure.Messaging.EventHubs.Processor/samples"&gt;EventProcessorClient&lt;/a&gt; in the background, which does everything required, including the same checkpointing in blob storage as done by WebJobs SDK. The whole implementation is less than 350 LOCs ..&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Queue Processor
&lt;/h3&gt;

&lt;p&gt;Old code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;WebJobs&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"RetryHandleXyzEvent"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="o"&gt;_.&lt;/span&gt;&lt;span class="nc"&gt;RetryHandleXyzEvent&lt;/span&gt;
        &lt;span class="o"&gt;([&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"xyz-events-retry-queue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StorageQueueConnectionStringKey"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="c1"&gt;// handle the message&lt;/span&gt;

&lt;span class="c1"&gt;// Program.fs, configure the host with web jobs&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureWebJobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureWebJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddAzureStorageCoreServices&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
          &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddAzureStorageQueues&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Api.Functions.fs, handle the event&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;processMessage&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;QueueMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="c1"&gt;// handle the queue message&lt;/span&gt;

        &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retn&lt;/span&gt;

&lt;span class="c1"&gt;// Api.Wiring.fs, instantiate the processor&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;queueProcessor1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;QueueProcessorDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;
            &lt;span class="s2"&gt;"QueueProcessor1"&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queueStorageConnectionString&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queueName&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messageBatchMaximumCount&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxPollingInterval&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxDequeueCount&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultBackOffIntervalMs&lt;/span&gt;
            &lt;span class="nn"&gt;QueueHandlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processMessage&lt;/span&gt;

&lt;span class="c1"&gt;// Program.fs, configure the host with the processors&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureQueueProcessors&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;QueueProcessors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queueProcessor1&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Queue Processor Implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;AzureStorageQueues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueProcessing&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Threading&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queues&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Models&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;AzureStorageQueues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BasicOperations&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;AzureStorageQueues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogEvents&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StructuredLog&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ExceptionHandling&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;doProcessMessage&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxDequeueCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;QueueClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;poisonQueueClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;QueueClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;QueueMessage&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;QueueMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DequeueCount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxDequeueCount&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="c1"&gt;// message has been retried too many times =&amp;gt; move it the the poison queue&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
                &lt;span class="n"&gt;poisonQueueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SendMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;
                &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;

            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="s2"&gt;"Message dequeue count = {messageDequeueCount} &amp;gt; maxDequeueCount = {maxDequeueCount} =&amp;gt; message moved to poison queue {poisonQueueUri}"&lt;/span&gt;
                &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DequeueCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;maxDequeueCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;poisonQueueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
                &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DeleteMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MessageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PopReceipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;
                &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;

            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="s2"&gt;"Message deleted from queue {queueUri}"&lt;/span&gt;
                &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="c1"&gt;// normal processing&lt;/span&gt;
            &lt;span class="c1"&gt;// NOTE: try-with is used instead of Async.Catch to catch also cases when processMessage throws exception outside of an Async block ...&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;
                &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;processMessage&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;

                &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
                    &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DeleteMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MessageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PopReceipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;
                    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;

                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="s2"&gt;"Deleted message with id = {messageId} and dequeue count = {messageDequeueCount} in queue {queueUri} after successful processing"&lt;/span&gt;
                    &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MessageId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DequeueCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="c1"&gt;// message remains in the queue and will become again visible after visibilityTimeout&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="s2"&gt;"Queue message processing failed. Retrying {remainingDequeueCount} more times, then the message will be moved to poison queue. Queue: {queueUri}; Message Body: {messageBody}"&lt;/span&gt;
                    &lt;span class="n"&gt;ex&lt;/span&gt;
                    &lt;span class="p"&gt;[|&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int64&lt;/span&gt; &lt;span class="n"&gt;maxDequeueCount&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DequeueCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;
                        &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;QueueMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toString&lt;/span&gt;
                    &lt;span class="p"&gt;|]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Catch&lt;/span&gt;
    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Choice1Of2&lt;/span&gt; &lt;span class="p"&gt;_&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Choice2Of2&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="c1"&gt;// log the message body&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="s2"&gt;"Error occurred while performing auxiliary queue message processing (e.g. DeleteMessageAsync). Message Body: {messageBody}"&lt;/span&gt;
                &lt;span class="n"&gt;ex&lt;/span&gt;
                &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;QueueMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toString&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
            &lt;span class="c1"&gt;// and propagate up the exception&lt;/span&gt;
            &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Reraise&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;/// Starts an infinite loop for receiving queue messages&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;consumeMessages&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storageConnectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queueName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;poisonQueueName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messageBatchMaximumCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxPollingInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxDequeueCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultBackOffIntervalMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;QueueMessage&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ManualResetEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QueueClientOptions&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// TODO: Configure options?&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QueueClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storageConnectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queueName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CreateIfNotExistsAsync&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;

            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;poisonQueueClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                &lt;span class="nc"&gt;QueueClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storageConnectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poisonQueueName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;poisonQueueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CreateIfNotExistsAsync&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;

            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mutable&lt;/span&gt; &lt;span class="n"&gt;backoffTimeMs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaultBackOffIntervalMs&lt;/span&gt;

            &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;

            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Info&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueProcessorStarting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueProcessorStarting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="s2"&gt;"Starting with config:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Uri = {queueClientUri}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;MessageBatchMaximumCount = {messageBatchMaximumCount}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;VisibilityTimeout = {visibilityTimeout}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;MaxPollingInterval = {maxPollingInterval}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;MaxDequeueCount = {maxDequeueCount}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.MessageEncoding = {messageEncoding}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.Retry.Mode = {retryMode}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.Retry.Delay = {retryDelay}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.Retry.MaxDelay = {retryMaxDelay}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.Retry.MaxRetries = {retryMaxRetries}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.Retry.NetworkTimeout = {retryNetworkTimeout}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;PoisonQueueName = {poisonQueueName}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    "&lt;/span&gt;
                &lt;span class="p"&gt;[|&lt;/span&gt;
                    &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;
                    &lt;span class="n"&gt;messageBatchMaximumCount&lt;/span&gt;
                    &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;
                    &lt;span class="n"&gt;maxPollingInterval&lt;/span&gt;
                    &lt;span class="n"&gt;maxDequeueCount&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MessageEncoding&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Mode&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Delay&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaxDelay&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaxRetries&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NetworkTimeout&lt;/span&gt;
                    &lt;span class="n"&gt;poisonQueueName&lt;/span&gt;
                &lt;span class="p"&gt;|]&lt;/span&gt;

            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsCancellationRequested&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt;
                    &lt;span class="c1"&gt;// any exception in this block will be caught and logged&lt;/span&gt;
                    &lt;span class="c1"&gt;// because if propagated up they will stop the queue processor/background service =&amp;gt; health check alert,&lt;/span&gt;
                    &lt;span class="c1"&gt;// but no automatic restart is currently possible ...&lt;/span&gt;

                    &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                        &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ReceiveMessagesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="n"&gt;maxMessages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messageBatchMaximumCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;

                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HasValue&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
                        &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
                            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;
                            &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                                &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                    &lt;span class="c1"&gt;// central check for endequeTime&lt;/span&gt;
                                    &lt;span class="c1"&gt;// TODO: Enable this once a proper config/solution is found for the invisibility period, which is *not* exposed as a QueueMessage property (only InsertedOn and NextVisibleOn, but VisibleOn is needed ). See fore more info https://github.com/Azure/azure-sdk-for-net/issues/40147&lt;/span&gt;
                                    &lt;span class="c1"&gt;// do checkMessageEnDequeueTime log queueClient.Name backoffTimeMs msg&lt;/span&gt;

                                    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
                                        &lt;span class="n"&gt;doProcessMessage&lt;/span&gt;
                                            &lt;span class="n"&gt;log&lt;/span&gt;
                                            &lt;span class="n"&gt;name&lt;/span&gt;
                                            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;
                                            &lt;span class="n"&gt;maxDequeueCount&lt;/span&gt;
                                            &lt;span class="n"&gt;queueClient&lt;/span&gt;
                                            &lt;span class="n"&gt;poisonQueueClient&lt;/span&gt;
                                            &lt;span class="n"&gt;processMessage&lt;/span&gt;
                                            &lt;span class="n"&gt;msg&lt;/span&gt;
                                &lt;span class="o"&gt;})&lt;/span&gt;
                            &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;
                            &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;

                        &lt;span class="n"&gt;backoffTimeMs&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;defaultBackOffIntervalMs&lt;/span&gt;

                        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WaitingForQueueMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WaitingForQueueMessages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="s2"&gt;"{messageCount} messages successfully processed from queue {queueUri}. Waiting for {backoffTimeMs} milliseconds before checking again ..."&lt;/span&gt;
                            &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;backoffTimeMs&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt;
                        &lt;span class="n"&gt;backoffTimeMs&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nn"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoffTimeMs&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxPollingInterval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TotalMilliseconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WaitingForQueueMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WaitingForQueueMessages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="s2"&gt;"No messages found in queue {queueUri}. Waiting for {backoffTimeMs} milliseconds before checking again ..."&lt;/span&gt;
                            &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;backoffTimeMs&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

                    &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoffTimeMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;QueueMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="s2"&gt;"Exception while processing messages: {ex}"&lt;/span&gt;
                        &lt;span class="n"&gt;ex&lt;/span&gt;
                        &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The implementation is based on the default &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/azure.storage.queues.queueclient.receivemessagesasync?view=azure-dotnet"&gt;QueueClient.ReceiveMessagesAsync&lt;/a&gt; approach of handling queue messages with the Azure SDK, invoked in an infinite loop with some Thread.Sleep sprinkled in it ... The whole implementation is about 200 LOCs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Timer Processor
&lt;/h3&gt;

&lt;p&gt;Old code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;WebJobs&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"DoSomethingRegularly"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ExpireCustomerDocuments&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TimerTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%DoSomethingRegularlyCrontab%"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimerInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ILogger&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="c1"&gt;// do something&lt;/span&gt;

&lt;span class="c1"&gt;// Program.fs, configure the host with web jobs&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureWebJobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureWebJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddAzureStorageCoreServices&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
          &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddTimers&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some exotic placeholder format of the crontab placeholder ..&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;New code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Api.Functions.fs, handle the event&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;processTimer1&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toProcessOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="c1"&gt;// do something&lt;/span&gt;

        &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retn&lt;/span&gt;
&lt;span class="c1"&gt;// Api.Wiring.fs, instantiate the processor&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;timerProcessor1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;TimerProcessorDef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;
            &lt;span class="s2"&gt;"TimerProcessor1"&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt;
            &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Timers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timerProcessorQueueStorageConnectionString&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="s2"&gt;"* * * * *"&lt;/span&gt;
            &lt;span class="nn"&gt;TimerHandlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processTimer1&lt;/span&gt;

&lt;span class="c1"&gt;// Program.fs, configure the host with the processors&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureTimers&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nn"&gt;Timers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timerProcessor1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nn"&gt;Timers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timerProcessor2&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timer Processor Implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;AzureStorageQueues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerProcessing&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Threading&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queues&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;NCrontab&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;Framework&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;AzureStorageQueues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogEvents&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StructuredLog&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;TimerMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;ToProcessOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;maxTimeoutPeriod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FromDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// could be up to 7 (messages are deleted after 7 days from the queue), but calculating possible downtimes/system recovery&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;calculateNextCheckOn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toProcessOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;toProcessOn&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxTimeoutPeriod&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;toProcessOn&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;maxTimeoutPeriod&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;createAndSendNextTimerMessage&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;QueueClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CrontabSchedule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;toProcessOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetNextOccurrence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;ToProcessOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toProcessOn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;encodedMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialize&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base64Encode&lt;/span&gt; &lt;span class="nn"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTF8&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;nextCheckOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calculateNextCheckOn&lt;/span&gt; &lt;span class="n"&gt;toProcessOn&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nextCheckOn&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt; &lt;span class="c1"&gt;// message should become visible always slightly after ToProcessOn because sending the msg to the queues takes some ms&lt;/span&gt;

        &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
            &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SendMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encodedMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FromSeconds&lt;/span&gt;&lt;span class="o"&gt;(-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// -1 second indicates "infinite" message TTL (i.e. 7 days)&lt;/span&gt;
            &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;
            &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;

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

&lt;span class="c1"&gt;/// Starts an infinite loop for receiving queue timer messages&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;consumeMessages&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;QueueClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CrontabSchedule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;processMessage&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messageBatchMaximumCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mutable&lt;/span&gt; &lt;span class="n"&gt;firstRun&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsCancellationRequested&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;
                &lt;span class="c1"&gt;// any exception in this block will be caught and logged&lt;/span&gt;
                &lt;span class="c1"&gt;// because if propagated up they will stop the timer processor/background service =&amp;gt; health check alert,&lt;/span&gt;
                &lt;span class="c1"&gt;// but no automatic restart is currently possible ...&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                    &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ReceiveMessagesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;maxMessages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messageBatchMaximumCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;

                &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;nextCheckOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HasValue&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tryHead&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isSome&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="c1"&gt;// the message became visible, so it needs to be processed or rescheduled&lt;/span&gt;
                        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="c1"&gt;// we expect/process only 1 timer message per timer queue!&lt;/span&gt;

                            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;decodedMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                                &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base64Decode&lt;/span&gt; &lt;span class="nn"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTF8&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deserialize&lt;/span&gt;

                            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                &lt;span class="s2"&gt;"Timer message received in queue {queueUri} with ToProcessOn = {toProcessOn}."&lt;/span&gt;
                                &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;decodedMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToProcessOn&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toStringIso&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

                            &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;toProcessOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                                &lt;span class="c1"&gt;// if ToProcessOn in the past =&amp;gt; ready to process!&lt;/span&gt;
                                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decodedMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToProcessOn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
                                    &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                        &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;processMessage&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="n"&gt;decodedMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToProcessOn&lt;/span&gt;

                                        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                            &lt;span class="s2"&gt;"Timer message in queue {queueUri} successfully processed."&lt;/span&gt;
                                            &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

                                        &lt;span class="c1"&gt;// delete this (all) message(s)&lt;/span&gt;
                                        &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClearMessagesAsync&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;

                                        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                            &lt;span class="s2"&gt;"Deleted timer message in queue {queueUri}."&lt;/span&gt;
                                            &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

                                        &lt;span class="c1"&gt;// schedule next execution in a new message&lt;/span&gt;
                                        &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;newTimerMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createAndSendNextTimerMessage&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt; &lt;span class="n"&gt;crontab&lt;/span&gt;

                                        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                            &lt;span class="s2"&gt;"New timer message with ToProcessOn = {toProcessOn} sent to queue {queueUri}."&lt;/span&gt;
                                            &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;newTimerMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToProcessOn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

                                        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;newTimerMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToProcessOn&lt;/span&gt;
                                    &lt;span class="p"&gt;}&lt;/span&gt;
                                &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="c1"&gt;// message visible before ToProcessOn .. make invisible again&lt;/span&gt;
                                    &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;nextCheckOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                                            &lt;span class="n"&gt;calculateNextCheckOn&lt;/span&gt; &lt;span class="n"&gt;decodedMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToProcessOn&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt; &lt;span class="c1"&gt;// message should become visible always slightly after ToProcessOn because sending the msg to the queues takes some ms&lt;/span&gt;

                                        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nextCheckOn&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt;

                                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Zero&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
                                            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
                                                &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UpdateMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                                                    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MessageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PopReceipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                    &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;
                                                &lt;span class="p"&gt;)&lt;/span&gt;
                                                &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;
                                                &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ignore&lt;/span&gt;

                                            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                                                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                 &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageBeingProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                                &lt;span class="s2"&gt;"Timer message in queue {queueUri} with ToProcessOn = {toProcessOn} became visible before ToProcessOn. The message's invisibility was extended."&lt;/span&gt;
                                                &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;decodedMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToProcessOn&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toStringIso&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

                                        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decodedMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToProcessOn&lt;/span&gt;
                                    &lt;span class="p"&gt;}&lt;/span&gt;

                            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;calculateNextCheckOn&lt;/span&gt; &lt;span class="n"&gt;toProcessOn&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;firstRun&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
                        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WaitingForTimerMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WaitingForTimerMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="s2"&gt;"No visible message in queue {queueUri}, but first run, so await schedule ..."&lt;/span&gt;
                            &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

                        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;toProcessOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetNextOccurrence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="n"&gt;calculateNextCheckOn&lt;/span&gt; &lt;span class="n"&gt;toProcessOn&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retn&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt;
                        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WaitingForTimerMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WaitingForTimerMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="s2"&gt;"No visible message in queue {queueUri} after sleep but there should have been one, maybe delayed, so check frequently ..."&lt;/span&gt;
                            &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

                        &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retn&lt;/span&gt;

                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sleepTimeSpan&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextCheckOn&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nn"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// add 1 second to make 100% sure sleep is over only after the message has already become visible&lt;/span&gt;

                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WaitingForTimerMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WaitingForTimerMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="s2"&gt;"Sleeping {sleepTimeSpan} before checking again in queue {queueUri}"&lt;/span&gt;
                    &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;sleepTimeSpan&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sleepTimeSpan&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Zero&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
                    &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sleepTimeSpan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerMessageProcessingError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="s2"&gt;"Exception while processing messages: {ex}"&lt;/span&gt;
                    &lt;span class="n"&gt;ex&lt;/span&gt;
                    &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

            &lt;span class="n"&gt;firstRun&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// Creates timer queue messages if missing and starts an infinite loop for receiving queue timer messages&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;createAndConsumeMessages&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontab&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CrontabSchedule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storageConnectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queueName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ManualResetEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QueueClientOptions&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// TODO: Configure options?&lt;/span&gt;

            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QueueClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storageConnectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queueName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CreateIfNotExistsAsync&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isNull&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="c1"&gt;// queue was just created&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;timerMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createAndSendNextTimerMessage&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt; &lt;span class="n"&gt;crontab&lt;/span&gt;

                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Info&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerProcessorStarting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerProcessorStarting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="s2"&gt;"New queue {queueUri} was created and a new timer message with ToProcessOn = {toProcessOn} was sent to it."&lt;/span&gt;
                    &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;timerMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToProcessOn&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toStringIso&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="c1"&gt;// queue was already existing&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetPropertiesAsync&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HasValue&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ApproximateMessagesCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="c1"&gt;// ApproximateMessagesCount, even though not exact, is guaranteed to have a value &amp;gt; 0 if there are messages. Additionally "Approximate messages count will give you an approximate count of total messages in a queue and will include both visible and invisible messages."&lt;/span&gt;
                    &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;timerMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createAndSendNextTimerMessage&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt; &lt;span class="n"&gt;crontab&lt;/span&gt;

                    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Info&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerProcessorStarting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerProcessorStarting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="s2"&gt;"Queue {queueUri} exists, but no timer message found in it. Created timer message with ToProcessOn = {toProcessOn}."&lt;/span&gt;
                        &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;timerMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToProcessOn&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toStringIso&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt;
                    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Info&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerProcessorStarting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerProcessorStarting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="s2"&gt;"Existing queue {queueUri} with timer message found."&lt;/span&gt;
                        &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;

            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;messageBatchMaximumCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;// 1 queue per timer processor, with 1 message per queue only&lt;/span&gt;

            &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;

            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Info&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerProcessorStarting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nn"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimerProcessorStarting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="s2"&gt;"Starting with config:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Uri = {queueClientUri}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;MessageBatchMaximumCount = {messageBatchMaximumCount}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;VisibilityTimeout = {visibilityTimeout}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.MessageEncoding = {messageEncoding}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.Retry.Mode = {retryMode}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.Retry.Delay = {retryDelay}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.Retry.MaxDelay = {retryMaxDelay}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.Retry.MaxRetries = {retryMaxRetries}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;Options.Retry.NetworkTimeout = {retryNetworkTimeout}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;\
                    "&lt;/span&gt;
                &lt;span class="p"&gt;[|&lt;/span&gt;
                    &lt;span class="n"&gt;queueClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;
                    &lt;span class="n"&gt;messageBatchMaximumCount&lt;/span&gt;
                    &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MessageEncoding&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Mode&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Delay&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaxDelay&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaxRetries&lt;/span&gt;
                    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NetworkTimeout&lt;/span&gt;
                &lt;span class="p"&gt;|]&lt;/span&gt;

            &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
                &lt;span class="n"&gt;consumeMessages&lt;/span&gt;
                    &lt;span class="n"&gt;log&lt;/span&gt;
                    &lt;span class="n"&gt;name&lt;/span&gt;
                    &lt;span class="n"&gt;queueClient&lt;/span&gt;
                    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;
                    &lt;span class="n"&gt;crontab&lt;/span&gt;
                    &lt;span class="n"&gt;processMessage&lt;/span&gt;
                    &lt;span class="n"&gt;messageBatchMaximumCount&lt;/span&gt;
                    &lt;span class="n"&gt;visibilityTimeout&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The implementation is based on an Azure Storage Queue with a single message inside, which is made "invisible" for a certain period of time, which allows for surviving a process crash, having multiple instances running etc. The whole implementation is about 250 LOCs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  SignalR
&lt;/h3&gt;

&lt;p&gt;Old code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;WebJobs&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;   
    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HandleAndPushToClient"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="o"&gt;_.&lt;/span&gt;&lt;span class="nc"&gt;HandleAndPushToClient&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventHubTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="nc"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Xyz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connectionStringKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="nc"&gt;ConsumerGroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                                  &lt;span class="nn"&gt;DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;EventHubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Xyz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consumerGroupForWebSocketNotification&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nc"&gt;EventData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;enqueuedTimeUtc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sequenceNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SignalR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HubName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;SignalR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hubName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="nc"&gt;ConnectionStringSetting&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AzureSignalRConnectionString"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;signalRMessages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nc"&gt;IAsyncCollector&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SignalRMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ILogger&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="c1"&gt;// transform some internal event to external&lt;/span&gt;
        &lt;span class="c1"&gt;// publish to SignalR using signalRMessages.AddAsync&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The output triggers are generally a killer feature, which is really killing you - instead of invoking a very simple Azure SDK client method, you deal with IAsyncCollector and SignalRTrigger magic, which is completely unnecessary. Go figure out how to send message to a specific user or to all ... &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;New code (same as for EventHubProcessor above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;serviceManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;SignalRClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getServiceManager&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;SignalR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;hub&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;SignalRClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getHubContext&lt;/span&gt; &lt;span class="n"&gt;serviceManager&lt;/span&gt; &lt;span class="nn"&gt;EnvVars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;SignalR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;azureSignalRHubName&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RunSynchronously&lt;/span&gt; &lt;span class="c1"&gt;// TODO: Find a way to get rid of this&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sendToUser&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;SignalRClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sendToUser&lt;/span&gt; &lt;span class="n"&gt;hub&lt;/span&gt;

&lt;span class="c1"&gt;// Api.Functions.fs, handle the event&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;processEvent&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;sendToUser&lt;/span&gt; &lt;span class="n"&gt;partitionId&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;event&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// handle the event&lt;/span&gt;
        &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;sendToUser&lt;/span&gt; &lt;span class="s2"&gt;"SomeTarget"&lt;/span&gt; &lt;span class="s2"&gt;"Some Message"&lt;/span&gt; &lt;span class="s2"&gt;"SomeUserId"&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Azure SDK for sending SignalR Messages is very very straightforward, when you use it directly ...&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Removing a layer of indirection has always generated great satisfaction in me. Not only does it make the whole application easier to understand, but you gain also a lot more control, and get to know the inner workings of the technology, without someone deciding something for you, or translating stuff like configuration for you. &lt;/p&gt;

&lt;p&gt;MS seems to always try to make things easier for the developer (patronizing him?) by providing a magical and abstract framework which achieves exactly the opposite. My recommendation to MS would be to try make everything look like a stupid console application instead, with full control of the client developer, who is just using a bunch of simple "helper" functions (or class methods in OOP) from MS, and nothing more. &lt;/p&gt;

&lt;p&gt;Hopefully someone can save some time doing something similar based on the ideas and code in this article!&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Funny enough in the meantime MS seems to be trying to do &lt;a href="https://github.com/dotnet/aspnetcore/issues/53219#issuecomment-2002608324"&gt;something similar&lt;/a&gt;, by integrating WebJobs SDK into .NET 9's HostBuilder. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>fsharp</category>
      <category>azure</category>
    </item>
    <item>
      <title>Isolated Local Dev Env using k3d, multi-tenant Azure services, docker containers, and mirrord</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Sun, 12 Nov 2023 01:00:53 +0000</pubDate>
      <link>https://dev.to/deyanp/isolated-local-dev-env-using-k3d-multi-tenant-azure-services-docker-containers-and-mirrord-1k2m</link>
      <guid>https://dev.to/deyanp/isolated-local-dev-env-using-k3d-multi-tenant-azure-services-docker-containers-and-mirrord-1k2m</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; Use k3d, multi-tenant Azure Services, local docker containers and mirrord for an amazing local development experience. &lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Being a part of a small team (&amp;lt; 10) of full-stack developers with end-to-end responsibility (from requirements till running systems) I have been primarily focusing in the past couple of years more or less on having stable test and production environments, utilizing Kubernetes (AKS) and having a set of microservices whereby individual microservices (pods) can be deployed on a daily basis in a controlled way. &lt;/p&gt;

&lt;p&gt;Our tech stack consists of .NET/F#, Kubernetes (AKS&lt;sup id="fnref1"&gt;1&lt;/sup&gt; on Azure), MongoDB (Atlas on Azure) as main OLTP database and Azure Data Explorer&lt;sup id="fnref2"&gt;2&lt;/sup&gt; as a DWH, and many additional Azure services like Key Vault, Event Hubs&lt;sup id="fnref3"&gt;3&lt;/sup&gt;, Queues&lt;sup id="fnref4"&gt;4&lt;/sup&gt;, Blob Storage&lt;sup id="fnref5"&gt;5&lt;/sup&gt;, Azure SignalR for websockets, etc. &lt;/p&gt;

&lt;p&gt;But what about the development environment? Well, we have shared Azure services for it - e.g. Key Vault, Event Hubs, Queues and Blob Storage, as well as the DWH, but we do run MongoDB locally as a docker container, and we do run the applications themselves locally, usually started using JetBrains Rider's Run/Debug configuration. And this was working relatively fine as usually a developer needs to open 1 microservice, implement some additional API, write automated tests, then manually test it via UI out of which only those pages are invoked which call the microservice in question. &lt;/p&gt;

&lt;p&gt;The above under-invested setup had its issues and we needed something better ..&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: Even though this article demonstrates a very specific implementation of a local dev environment based on a perhaps unique combination of technologies which probably nobody else uses, the underlying principles/approaches may be portable also to other tech stacks/setups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Statement
&lt;/h2&gt;

&lt;p&gt;The problem statement is straightforward: how to set up a development environment for every developer with a "complete" isolation from the other team members, and how to do this in a cost-efficient way.&lt;/p&gt;

&lt;p&gt;First, when we started developing more and more our web UIs and native mobile apps, the need arose more and more frequently to be able to perform some manual local testing of end-to-end business processes, which requires firing up a number of microservices, pub-sub of multiple events, and even querying eventually-consistent Read Models.&lt;/p&gt;

&lt;p&gt;Second, we do have some integrated authentication and other Traefik middleware functionality in Kubernetes, which cannot be tested locally unless run in a Kubernetes cluster. &lt;/p&gt;

&lt;p&gt;And last but not least, some if not most of the Azure services which we use do not have good developer experience, i.e. there are no easy-to-run docker images or similar. &lt;/p&gt;

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

&lt;p&gt;The solution consists of several approaches: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;install an emulator on the developer machine as a docker container or&lt;/li&gt;
&lt;li&gt;run the service in the cloud (Azure in our case), but in a special/custom "multi-tenant mode". The latter means that many developers can use the Azure services but in a complete isolation from each other.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The sections below contain detailed explanations of the technologies/components and approaches involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  k3d
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://k3d.io/" rel="noopener noreferrer"&gt;k3d&lt;/a&gt; (wrapper around &lt;a href="https://k3s.io/" rel="noopener noreferrer"&gt;k3s&lt;/a&gt;) is an excellent and very lightweight Kubernetes running as a docker container. We are speaking about setup time of 20-30 seconds, and a couple of minutes to compile and deploy all our microservices to it. Cluster start/stop is supported, but full cluster deletion and re-creation is almost as easy ;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;k3d registry create &lt;span class="nv"&gt;$registryName&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 5050
k3d cluster create &lt;span class="nv"&gt;$clusterName&lt;/span&gt; &lt;span class="nt"&gt;--registry-use&lt;/span&gt; &lt;span class="nv"&gt;$registryName&lt;/span&gt;:5050 &lt;span class="nt"&gt;--k3s-arg&lt;/span&gt; &lt;span class="s2"&gt;"--disable=traefik@server:*"&lt;/span&gt; &lt;span class="nt"&gt;--subnet&lt;/span&gt; &lt;span class="s1"&gt;'172.18.0.0/16'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local docker registry is useful for pushing to it all locally compiled docker images, to be pulled by Kubernetes upon deployment/pod creation. &lt;/p&gt;

&lt;p&gt;K3d comes with traefik out-of-the-box, but as you see above it is disabled in this case, as we need a more customized version of it.&lt;/p&gt;

&lt;p&gt;Traefik and all other pods we need get deployed by simply &lt;code&gt;kubectl apply -f&lt;/code&gt; a bunch of yamls (we are not using Helm). &lt;/p&gt;

&lt;p&gt;There is one &lt;a href="https://github.com/k3d-io/k3d/issues/926" rel="noopener noreferrer"&gt;nasty problem&lt;/a&gt; with k3d whereby after the developer laptop goes to sleep and gets woken up the DNS mapping &lt;code&gt;host.k3d.internal&lt;/code&gt; does not work anymore. There is a workaround though, namely to specify subnet e.g. &lt;code&gt;172.18.0.0/16&lt;/code&gt; upon creation and use &lt;code&gt;172.18.0.1&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;Stopping the cluster is done with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;k3d cluster stop cluster-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;starting with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;k3d cluster start cluster-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and deletion with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;k3d cluster delete cluster-name
k3d registry delete registry-name.localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple and fast!&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-tenant Azure services
&lt;/h2&gt;

&lt;p&gt;As mentioned above, ideally all cloud services used should have emulators which can be run locally in docker containers. Unfortunately it seems that Microsoft does not care consistently about developer UX and does not provide emulators running in docker containers for all its services, especially for such central ones like Azure Event Hubs in our case (our messaging backbone). &lt;/p&gt;

&lt;p&gt;The other problem with emulators in general is that there is no 100% guarantee that they would behave the same as the "real" cloud service. &lt;/p&gt;

&lt;p&gt;And last but not least, some cloud (in particular Azure) services are integrated with each other - e.g. Azure Data Explorer ingests data from Azure Event Hubs, something which would be generally missing from the emulators. &lt;/p&gt;

&lt;h3&gt;
  
  
  Per-Developer Azure Event Hubs Partitions
&lt;/h3&gt;

&lt;p&gt;The general idea for making Azure Events Hubs "multi-tenant" is simple - write a small wrapper around Azure SDK for Event Hubs so that publishing to event hubs (~topics) and subscribing to them via consumer groups is limited to a specific partition. Every Azure Event Hub in the standard SKU can have up to 32 partitions, which means there can be up to 32 isolated clients (1 partition each) - be it local developer environments, shared environments or CI/CD pipelines. &lt;/p&gt;

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

&lt;p&gt;Every environment has an environment variable e.g. &lt;code&gt;LOCAL_DEV_EVENTHUBS_ASSIGNEDPARTITIONIDS=0,1&lt;/code&gt; which specifies which partitions should be used. If the env var is missing then all available partitions are used, which is the case for the shared test and production environments (using their own dedicated sets of Azure Event Hubs).&lt;/p&gt;

&lt;p&gt;How "easy" is it to create a wrapper over the standard SDK? Well, that was relatively easy, based on a &lt;a href="https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/eventhub/Azure.Messaging.EventHubs/samples/Sample08_CustomEventProcessor.md#static-partition-assignment" rel="noopener noreferrer"&gt;nice sample from Microsoft&lt;/a&gt;. Unfortunately it required &lt;a href="https://github.com/Azure/azure-webjobs-sdk" rel="noopener noreferrer"&gt;migration away from the WebJobs SDK&lt;/a&gt; which was being used so far, and which did not give enough control to the underlying implementation.&lt;sup id="fnref6"&gt;6&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-Developer Azure Storage Accounts (incl. Blob Containers and Storage Queues)
&lt;/h3&gt;

&lt;p&gt;When it comes to Azure Storage Accounts - that was easy, every developer got her/his own storage account (1 per every developer) where the naming uses a suffix, e.g. &lt;code&gt;company1stacc4xy&lt;/code&gt; (xy being the developer initials). All blob containers and storage queues created inside are used exclusively be the developer in question. Storage accounts are very cheap.&lt;/p&gt;

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

&lt;p&gt;Note: There is an open-source emulator for Azure Storage called &lt;a href="https://github.com/Azure/Azurite" rel="noopener noreferrer"&gt;Azurite&lt;/a&gt;, which can be run as a docker container, and even though it might &lt;a href="https://github.com/Azure/Azurite#support-matrix" rel="noopener noreferrer"&gt;support&lt;/a&gt; all our requirements, it was simply easier (and very cheap if not free) to provision storage accounts directly in the cloud. &lt;/p&gt;

&lt;h3&gt;
  
  
  Per-Developer Azure Data Explorer (Logical) Databases
&lt;/h3&gt;

&lt;p&gt;Azure Data Explorer (ADX) is a Column Store database, which means it stores the data in columns instead of rows, which makes it extremely fast for queries on large datasets. Of course, there are some limitations when it comes to ingestion of data (to be batched) and updates/deletion of data (not easily possible). It powers all Azure logging/monitoring technologies (Azure Monitor, Azure Application Insights) and can be relatively inexpensive for a database (dev/test SKU around 100 EUR/month, and production SKUs starting from 500 EUR/month).&lt;/p&gt;

&lt;p&gt;In the system in question the data is ingested to ADX via Event Hubs (no direct ETL from OLTP -&amp;gt; DWH!) in near real-time - by default in batches every 10 minutes, however for some tables also much shorter intervals can be configured. &lt;/p&gt;

&lt;p&gt;The challenge here was how to isolate developers from each other when it comes to ADX. &lt;/p&gt;

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

&lt;p&gt;The solution consists of 2 parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every developer gets her/his own logical database (in the same physical database cluster), with a database name having a suffix db4ab for example where "ab" are the initials of the developer. A script can re-create the database in a matter of a few minutes.&lt;/li&gt;
&lt;li&gt;Implement routing of events to an alternative database based by setting a "Database" property of every event upon publishing. The document for this built-in feature is &lt;a href="https://learn.microsoft.com/en-us/azure/data-explorer/ingest-data-event-hub-overview#route-event-data-to-an-alternate-database" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note: Microsoft offers an ADX emulator running as a docker container(&lt;a href="https://learn.microsoft.com/en-us/azure/data-explorer/kusto-emulator-install" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/data-explorer/kusto-emulator-install&lt;/a&gt;), which is perfect in general, however there is no support for the data ingestion from the Azure Event Hubs which are running in the cloud. &lt;/p&gt;

&lt;h3&gt;
  
  
  Per-Developer Azure SignalR Notification Hubs
&lt;/h3&gt;

&lt;p&gt;Azure SignalR service is WebSocket implementation which is used for realtime server &amp;lt;&amp;gt; client communication used in our cose for some realtime dashboards and UI flows.&lt;/p&gt;

&lt;p&gt;Azure SignalR works with the named &lt;strong&gt;hubs&lt;/strong&gt; to which clients connect by first requesting the hub url and access token, and then establishing a connection. So the solution in this case is to create a per-developer hub based on an environment variable which is set to a different value on every developer machine, but to the same value for the test/production environment. As the hub name/url/accessToken is fully controlled by the server app (k8s pod) there is no change required in the client application (SPA in our case).&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Docker Containers running locally
&lt;/h2&gt;

&lt;p&gt;In case there are high-quality local emulators available, then these are of course preferentially used, as then there is out-of-the-box isolation from other developers. We are running local docker containers for &lt;a href="https://hub.docker.com/_/mongo" rel="noopener noreferrer"&gt;MongoDB&lt;/a&gt; and &lt;a href="https://hub.docker.com/r/atmoz/sftp" rel="noopener noreferrer"&gt;SFTP Server&lt;/a&gt; for example.&lt;/p&gt;

&lt;p&gt;The install/run script can be as simple as a 1-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--name&lt;/span&gt; mongodb &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="nt"&gt;-v&lt;/span&gt; mongodata:/data/db &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 27017:27017 mongo &lt;span class="nt"&gt;--oplogSize&lt;/span&gt; 50 &lt;span class="nt"&gt;--replSet&lt;/span&gt; rs0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run --name sftp --restart unless-stopped -p 22:22 -d atmoz/sftp foo:pass:::upload,upload/processed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;--restart unless-stopped&lt;/code&gt; policy, which makes sure that upon restart of the dev machine the docker containers are started automatically.&lt;/p&gt;

&lt;p&gt;What is important when running local docker containers are the scripts for setting them up, including all configurations to be applied - e.g. in case of MongoDB DDL scripts for the setup all local databases used by the apps. &lt;/p&gt;

&lt;h2&gt;
  
  
  mirrord
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/metalbear-co/mirrord" rel="noopener noreferrer"&gt;mirrord&lt;/a&gt; is an amazing and &lt;strong&gt;magic&lt;/strong&gt; piece of technology, which allows you to debug a running container in a Kubernetes cluster - any cluster, including the local k3d cluster for example. More importantly, from developer UX point of view the debugging runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;within the context of the container&lt;/em&gt;, which means all environment variables for example with which the container was deployed(!), as well as inbound/outbound network connectivity&lt;/li&gt;
&lt;li&gt;within your own locally-running IDE - e.g. Jetbrains Rider(!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What happens in reality: mirrord installs a pod in the Kubernetes cluster which redirects (or duplicates) all calls to the pod to the additional local process which it starts on the local dev machine.&lt;/p&gt;

&lt;p&gt;Invoking mirrord can be done in 2 ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Terminal: 

&lt;ul&gt;
&lt;li&gt;find the running pod name:&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- start mirrord targetting the pod and at the same time your local app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;```bash
mirrord exec --target pod/test-qryh-test-xyzh-68f79c7c7b-jdp4v 
dotnet bin/Debug/net7.0/TestService.XyzHandling.dll
```
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Attach your IDE to the process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;ol&gt;
&lt;li&gt;Directly from the IDE (e.g. Jetbrains Rider) - run debug configuration + mirrord: &lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Once mirrord is enabled by clicking the round button to the left, you get a confirmation:&lt;/p&gt;

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

&lt;p&gt;and can click on the Debug button for the app in question:&lt;/p&gt;

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

&lt;p&gt;after which the app is started, and any set breakpoints will be hit:&lt;/p&gt;

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

&lt;p&gt;The best thing is - the environment variables and network connections that are made from the application being debugged are coming &lt;strong&gt;from&lt;/strong&gt; the Kubernetes cluster and going &lt;strong&gt;through&lt;/strong&gt; the cluster to any other running pod or even outside application ... pure magic ;)&lt;/p&gt;

&lt;p&gt;I need to mention also the great support of the mirrord creator I got for an &lt;a href="https://github.com/metalbear-co/mirrord/issues/1987" rel="noopener noreferrer"&gt;issue&lt;/a&gt; I had with the Rider plugin. Not only did &lt;a href="https://github.com/aviramha" rel="noopener noreferrer"&gt;Aviram Hassan&lt;/a&gt; jump on a discord call with me on the same evening I reported the issue(wow!!), but he also shared his thoughts that with a bigger team size a shared development environment (in the cloud) becomes a necessity. &lt;/p&gt;

&lt;h2&gt;
  
  
  Some Non-Functional Considerations
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Scalability: The described setup has a limitation of 32 Azure Event Hubs partitions available, which means up to 30-31 developers if every developer gets 1 partition, or 15 if everybody gets 2 partitions. There is however no problem create additional Azure Event Hub Namespaces (~ Kafka clusters) which can support another 15 developers and so on. All the other cloud services used - Azure Storage Accounts, Azure Data Explorer, Azure SignalR etc. can scale out/up with the team size ... Never thought though what happens when the team is &amp;gt; 100 developers, but this is a non-issue in the context I am in currently ;)&lt;/li&gt;
&lt;li&gt;Network Connectivity/Offline usage: Offline usage is not supported, as there are cloud services involved. I am yet to experience a case where I have to developer/run/debug something without Internet connection though - 4G/5G network coverage is pretty good everywhere (incl. in public transport) nowadays.&lt;/li&gt;
&lt;li&gt;Docker containers vs multi-tenant/per-developer cloud services: as mentioned above, this is just a tradeoff, can go more to the one or the other direction. Emulators are not the real thing or may not be available, but implementing multi-tenancy for cloud services requires substantial changes in the software layer responsible for infrastructure, which may also not be that easy. &lt;/li&gt;
&lt;li&gt;mirrord on Windows/WSL: Even though everything in the above setup should and does work on Linux, MacOS and Windows, mirrord is yet to be tested whether it works well on Windows with WSL, especially the part with Jetbrains Rider connecting to the process to debug, or spawning it using mirrord plugin ..&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;A couple of approaches were combined to create a pretty easy to setup, convenient to use and cost-efficient development environment, allowing every developer to run/debug applications in full isolation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Local Kubernetes cluster with k3d&lt;/li&gt;
&lt;li&gt;Multi-tenant Azure cloud services based on some service sub-entity like partitions, logical databases or hubs&lt;/li&gt;
&lt;li&gt;Locally running emulators in docker containers&lt;/li&gt;
&lt;li&gt;Mirrord for pod debugging&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even though there was some effort setting this up, and even though the setup sounds a bit too specific and perhaps even fragile, it is doing what it is required to do, namely allowing developers to run and debug applications in isolation from each other. &lt;/p&gt;

&lt;p&gt;Hopefully someone can save some time improving her/his development environment based on the ideas in this article!&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Similar to EKS on AWS and GKE on GCP ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Similar to ClickHouse ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Similar to AWS Kinesis or GCP Pub Sub (Lite) ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;Similar to SQS on AWS ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;Similar to S3 on AWS ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;A dedicated article about this migration will be published in the near future ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>azure</category>
    </item>
    <item>
      <title>CQRS+</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Sat, 29 Jul 2023 14:44:01 +0000</pubDate>
      <link>https://dev.to/deyanp/cqrs-5276</link>
      <guid>https://dev.to/deyanp/cqrs-5276</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; Splitting your "services" into several "microservices" based on technical responsibility simplifies development and code management and gives you deployment flexibility and isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This post is intended to demonstrate one &lt;strong&gt;concrete&lt;/strong&gt; way of implementing microservices by using CQRS but going beyond the the standard Command/Query separation. It is a follow-up to &lt;a href="https://dev.to/deyanp/tbd-current-state-last-event-as-an-alternative-to-event-sourcing-5gm5"&gt;a previous post about Current State + Last Event as an alternative to Event Sourcing&lt;/a&gt;, at the end of which I outlined this architectural pattern. &lt;br&gt;
Note that even though some examples may refer to F#/.NET/Azure, the CQRS+ approach can be used with any tech stack and programming language.&lt;/p&gt;
&lt;h2&gt;
  
  
  Problem Statement
&lt;/h2&gt;

&lt;p&gt;You have split your system into several "bounded contexts", each one consisting of a single or a few relatively encapsulated "services", e.g. Customer Registration, Order Processing, Notification Service, etc.), where every "service" contains a few (1-5) "domain entities" (e.g. Notification Service contains an entity called "Notification"). For each "service" you have created a .NET/F# solution, but now you are wondering how to organize internally the functionality inside each solution - do you need a single project (+1 test project) only?&lt;/p&gt;

&lt;p&gt;On the one hand, you have numerous requirements for every "service":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Getting some user input via the UI, wrapping it as "command" which results in the creation/update of some "entities" or simply data records, validating some business rules or maintaining some invariants along the way&lt;/li&gt;
&lt;li&gt;Displaying the existing data to the user in a UI, or returning it to an external system&lt;/li&gt;
&lt;li&gt;Handling events raised by other internal/external systems, based on which you potentially means you trigger internal commands (=&amp;gt; point 1.)&lt;/li&gt;
&lt;li&gt;Maybe you need to publish some events yourself, so other internal systems can react to them?&lt;/li&gt;
&lt;li&gt;Maybe you need to publish some events externally via webhooks, so other external systems can react to them?&lt;/li&gt;
&lt;li&gt;Write some audit trail entries for some critical entities, or calculate some balance or Read Model asynchronously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On the other hand, you have heard about "microservices" with just a "few" LOCs, and you have been wondering how the hell do the "wise guys" manage to produce such microservices, while yours always end up being fat services with thousands of LOCs (or you have a single monolith).&lt;/p&gt;

&lt;p&gt;Ideally you would modularize/split your "service" in such a way that you end up with small microservices which can be written/re-written/compiled/tested/deployed/scaled/monitored/restarted individually. &lt;/p&gt;
&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;The simple structure I follow is the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your company's overall software

&lt;ul&gt;
&lt;li&gt;Bounded Context 1

&lt;ul&gt;
&lt;li&gt;Service 1

&lt;ul&gt;
&lt;li&gt;Microservice 1&lt;/li&gt;
&lt;li&gt;Microservice 2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Diagram 1: Bounded Context/Service/Microservice Hierarchy&lt;/p&gt;

&lt;p&gt;Note: it could well be that you have only a single "service" in a bounded context.&lt;/p&gt;

&lt;p&gt;The important point is that a service is split into microservices based on &lt;strong&gt;technical responsibilities&lt;/strong&gt; - a microservice responsible for command processing, another one for query processing, a third one for event handling, and so on. &lt;/p&gt;

&lt;p&gt;So a CQRS+ solution could look like this:&lt;/p&gt;

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

&lt;p&gt;Diagram 2: Xyz Service and its microservices&lt;/p&gt;

&lt;p&gt;You would create a solution for the "service" with multiple projects (one per technical microservice), and multiple test projects (again, one per technical microservice). Your solution structure would then look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;XyzService.sln
  src/
    XyzService.CommandHandling.fsproj
    XyzService.QueryHandling.fsproj
    XyzService.EventHandling.fsproj
    XyzService.EventPublishing.fsproj
    XyzService.ExternalEventPublishing.fsproj
    XyzService.ExternalEventHandling.fsproj
    XyzService.ChangeHandling.fsproj
    XyzService.Shared.fsproj
  tests/
    XyzService.CommandHandling.Tests.fsproj
    XyzService.QueryHandling.Tests.fsproj
    XyzService.EventHandling.Tests.fsproj
    XyzService.EventPublishing.Tests.fsproj
    XyzService.ExternalEventPublishing.Tests.fsproj
    XyzService.ExternalEventHandling.Tests.fsproj
    XyzService.ChangeHandling.Tests.fsproj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, as you see from the above, a &lt;code&gt;XyzService.Shared.fsproj&lt;/code&gt; project is needed as well, containing the common Persistence, Api Dtos and even Domain Types used by several projects - e.g. CommandHandling (write) and QueryHandling (read).&lt;/p&gt;

&lt;p&gt;The deployed applications (e.g. pods in K8s) would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xyz-cmdh-aaaaaaaaaa-bbbbb
xyz-cmdh-aaaaaaaaaa-ccccc
xyz-qryh-bbbbbbbbbb-ddddd
xyz-qryh-bbbbbbbbbb-eeeee
xyz-qryh-bbbbbbbbbb-fffff
xyz-evh-cccccccccc-ggggg
xyz-evp-dddddddddd-hhhhh
xyz-extevp-eeeeeeeeee-kkkkk
xyz-extevh-ffffffffff-lllll
xyz-chgh-gggggggggg-mmmmm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that cmdh and qryh replica sets above have multiple replicas, whereas the rest could have a single replica configured. &lt;/p&gt;

&lt;p&gt;K8s is ideal for deploying such fine-grained applications, as it allows for a very high density of pods per node. .NET is not that ideal though, as the memory used per pod is usually 100Mb+, so the nodes need to have more memory (Rust/Go would be better in this regard). &lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Clarity how to break down bounded contexts/services into microservices. A "service" is more or less a bounded context or a big part of such, and a "microservice" is a part of the "service" focused on a &lt;strong&gt;particular technical responsibility&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Possibility to have small microservices of a couple of hundred LOCs =&amp;gt; easy to grasp and "cache" in your brain&lt;/li&gt;
&lt;li&gt;The microservices are standalone processes/apps/pods, can be deployed, scaled, restarted, monitored individually&lt;/li&gt;
&lt;li&gt;The place of the domain logic is clear - only in the Command Handling microservice. All the other microservices contain no business logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Drawbacks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Too much unnecessary segregation =&amp;gt; accidental complexity?

&lt;ul&gt;
&lt;li&gt;Pays off once you start looking after memory leaks, or quickly fix a bug by deploying only a small microservice without affecting the rest&lt;/li&gt;
&lt;li&gt;Is also offset by the standardization of the microservices, and the smaller context each microservice represents =&amp;gt; simplicity&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The (technical) microservices are sharing some  data model or API contract, hence several of them may need to be deployed together

&lt;ul&gt;
&lt;li&gt;Data models and API contracts are generally difficult to change (due to so many other reasons), and backwards compatibility must be highly respected anyway.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The structure and contents of each "microservice" as well as the Shared project will be described below.&lt;/p&gt;

&lt;h3&gt;
  
  
  XyzService.CommandHandling (CmdH)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CmdH&lt;/code&gt; microservice is responsible for processing all commands which arrive in the form of &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; requests. Automatic Jobs (e.g. cron jobs/Timer Triggers) are also hosted in this application, as usually these invoke some business logic.&lt;/p&gt;

&lt;p&gt;The standard command processing pipeline consists of the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deserialize request into &lt;code&gt;Api.Requests.XxxxCommandDto&lt;/code&gt; and furthemore into &lt;code&gt;XyzCommand&lt;/code&gt; record type, in the process of which the fields are validated (incl. enum value lookup, boundary checks, etc.)&lt;/li&gt;
&lt;li&gt;Fetch something from the database if needed (especially in case of executing an action on an existing entity)&lt;/li&gt;
&lt;li&gt;Invoke the domain logic for creating a new entity, or updating an existing one, considering all business rules (e.g. &lt;code&gt;Blocked&lt;/code&gt; Customer can be &lt;code&gt;Closed&lt;/code&gt; or put back to &lt;code&gt;Active&lt;/code&gt;, but cannot be set to PendingKYC for example).&lt;/li&gt;
&lt;li&gt;After the business logic has been executed and a result has been produced then the domain entity is mapped to a Persistence Dto and the latter is stored in the database, and then again mapped to an Api DTO, and the latter is serialized to JSON and returned by the API. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;CmdH&lt;/code&gt; is the beefiest project/app of all, as it contains the domain logic, e.g. validation, calculation, etc. More often than not the domain entities are defined as state machines, whereby every state transition is validated, some flags are set, etc. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;CmdH&lt;/code&gt; is focused on writing stuff to a Write Data Model (optimized for writing), always respecting entity (aggregate root) consistency boundaries. NoSQL/Document databases are a good fit for a write model and remove the need for an ORM, or any additional work splitting a domain entity across several tables (not a responsibility of &lt;code&gt;CmdH&lt;/code&gt;!).&lt;/p&gt;

&lt;h3&gt;
  
  
  XyzService.QueryHandling (QryH)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;QryH&lt;/code&gt; microservice is responsible for handling &lt;code&gt;GET&lt;/code&gt; queries for fetching REST resources. &lt;/p&gt;

&lt;p&gt;The query processing pipeline usually consists of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deserialize JSON into &lt;code&gt;Api.Requests.XyzQueryDto&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Validate some of the properties (query parameters) by trying to parse them into domain types (this is why some domain types have to be in Shared.Domain.Types!)&lt;/li&gt;
&lt;li&gt;Execute database query&lt;/li&gt;
&lt;li&gt;Map &lt;code&gt;Persistence.Dtos.XyzDto&lt;/code&gt; to &lt;code&gt;Api.Responses.XyzDto&lt;/code&gt; and return to caller&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;QryH&lt;/code&gt; does not care about domain entities in general, it is happy to serve also flattened/"joined" data of several entities, or subset of the data of an entity. It relies either on the same Data Model used for writing (in case of fetching of a single entity by id for example, or any filtering based on simple indexes) or requires its own Read Model for optimized searching/filtering by a random combination of filters, where the Read Model is usually filled asynchronously =&amp;gt; eventually consistent. Column store databases can be a pretty good fit for a dedicated high performance Read Model.&lt;/p&gt;

&lt;h3&gt;
  
  
  XyzService.EventPublishing (EvP)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;EvP&lt;/code&gt; is responsible for publishing events to a message bus, so that internal subscribers (e.g. &lt;code&gt;xyz2-evh&lt;/code&gt;, or a DWH) can receive them. &lt;/p&gt;

&lt;p&gt;The standard event publishing pipeline looks like this (in case &lt;a href="https://dev.to/deyanp/use-change-streams-instead-of-traditional-outbox-or-distributed-transactions-cdb"&gt;database Change Streams/Feed technology&lt;/a&gt; is used): &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive database change stream event, deserialize it to a &lt;code&gt;Persistence.Dtos.XyzDto&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Map the persistence dto to a &lt;code&gt;Api.Dtos.XyzDto&lt;/code&gt; (event schema is part of the public service contract!)&lt;/li&gt;
&lt;li&gt;Publish dto to a message bus topic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;EvP&lt;/code&gt; exposes also API for subscribing and unsubscribing to specific event types. Additionally, &lt;code&gt;EvP&lt;/code&gt; also allows (again via a dedicated API) for replay of events.&lt;/p&gt;

&lt;h3&gt;
  
  
  XyzService.EventHandling (EvH)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;EvH&lt;/code&gt; microservice is handling events received from other internal microservices (published by some other &lt;code&gt;xyz2-evp&lt;/code&gt;, see above), received &lt;strong&gt;usually&lt;/strong&gt; from a message bus like Kafka, Azure Event Hubs, etc. &lt;/p&gt;

&lt;p&gt;The standard event processing pipelines looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deserialize received event JSON into &lt;code&gt;Api.Dtos.XyzDto&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Handle the parsed event by e.g. creating a command and sending it to &lt;code&gt;CmdH&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;EvH&lt;/code&gt; is taking care of cases when a certain event cannot be processed, due to some internal event handling issues, or inability to connect to &lt;code&gt;CmdH&lt;/code&gt; for example. In that case &lt;code&gt;EvH&lt;/code&gt; has the choice to either stop/restart the processing of all events for the specific consumer group of the topic, or "park" the problematic event on a message queue for further retrying later on, which comes with the downside of losing strict ordering of the consumer group-based event processing. &lt;/p&gt;

&lt;h3&gt;
  
  
  XyzService.ExternalEventPublishing (ExtEvp)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ExtEvp&lt;/code&gt; is responsible for pushing events via webhooks/HTTPs to external subscribers (including retry in case the subscriber is down). &lt;/p&gt;

&lt;p&gt;The standard external event publishing pipeline looks like this (in case &lt;a href="https://dev.to/deyanp/use-change-streams-instead-of-traditional-outbox-or-distributed-transactions-cdb"&gt;database Change Streams/Feed technology&lt;/a&gt; is used): &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive database change stream event, deserialize it to &lt;code&gt;Persistence.Dtos.XyzDto&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Map the persistence dto to &lt;code&gt;Api.Dtos.XyzDto&lt;/code&gt; (event schema is part of the public service contract!)&lt;/li&gt;
&lt;li&gt;Send it to a collection of (subscribed in advance) HTTP endpoints&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;ExtEvp&lt;/code&gt; must take care of temporary unavailability of some (out of all) subscribers. Parking messages in "retry message queues" with invisibility period is usually a good approach of handling this problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  XyzService.ExternalEventHandling (ExtEvh)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ExtEvh&lt;/code&gt; is responsible for handling events received from external systems, usually via webhooks =&amp;gt; public listener endpoints.&lt;/p&gt;

&lt;p&gt;The standard external event handling pipeline looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive e.g. HTTPs request, map it to &lt;code&gt;Api.Dtos.XyzDto&lt;/code&gt; (= initial schema validation)&lt;/li&gt;
&lt;li&gt;Convert the request to an internal command to &lt;code&gt;CmdH&lt;/code&gt;, and invoke &lt;code&gt;CmdH&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Respond with "200 OK" back to the webhook call&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;ExtEvh&lt;/code&gt;'s endpoints must be secured, so that they can be invoked only by trusted external callers.&lt;/p&gt;

&lt;h3&gt;
  
  
  XyzService.ChangeHandling (ChgH)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ChgH&lt;/code&gt; is responsible for handling database Change Streams/Feed changes and calculating/creating something. It can be regarded as a more general version of &lt;code&gt;EvP&lt;/code&gt;/&lt;code&gt;ExtEvP&lt;/code&gt;, and originally it was used only for auditing or creating/calculating (e.g. diff between 2 versions of an entity) audit trail entries and writing them to the database, but some other calculation logic can be put here as well (e.g. maintaining balance entities or similar). &lt;/p&gt;

&lt;p&gt;The standard change handling pipeline looks like this (in case &lt;a href="https://dev.to/deyanp/use-change-streams-instead-of-traditional-outbox-or-distributed-transactions-cdb"&gt;database Change Streams/Feed technology&lt;/a&gt; is used): &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive database change stream event, deserialize it to &lt;code&gt;Persistence.Dtos.XyzDto&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Do some processing, e.g. calculate a diff between 2 entity states and store an audit trail entry, or trigger a command to &lt;code&gt;CmdH&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;ChgH&lt;/code&gt;, together with &lt;code&gt;Evp&lt;/code&gt; and &lt;code&gt;ExtEvp&lt;/code&gt; is used to offload &lt;code&gt;CmdH&lt;/code&gt; from any additional duties. &lt;code&gt;CmdH&lt;/code&gt; focuses on performing business logic and storing data into the write model. All the other services act in a near-real-time on the data change and perform their additional duties. &lt;/p&gt;

&lt;p&gt;It is possible though that some of the other microservices also directly subscribe to database change streams - such is the case for example when an &lt;strong&gt;in-memory&lt;/strong&gt; (= in-process) cache has to be built in &lt;code&gt;QryH&lt;/code&gt; for example. &lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Solution Files
&lt;/h3&gt;

&lt;p&gt;The solution contains the following central files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open API (Swagger) yaml file - combines &lt;strong&gt;all&lt;/strong&gt; &lt;code&gt;PUT/POST/PATCH/DELETE/GET&lt;/code&gt; operations from &lt;strong&gt;all&lt;/strong&gt; microservices in a &lt;strong&gt;single&lt;/strong&gt; API&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Additional Project Files
&lt;/h3&gt;

&lt;p&gt;Every fsproj contains the following deployment-related files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dockerfile&lt;/li&gt;
&lt;li&gt;Kubernetes deployment yaml file&lt;/li&gt;
&lt;li&gt;Azure DevOps deployment yaml file + bash script&lt;/li&gt;
&lt;li&gt;Bicep templates + bash script (in case of using Azure, similar for AWS/GCP)&lt;/li&gt;
&lt;li&gt;DB DDL/DML Scripts for setting up and upgrading the database&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Solution containing reusable code
&lt;/h3&gt;

&lt;p&gt;What if you have 10 bounded contexts with 1 service per each, and then you have to split those 10 services into microservices? What to do with some repetitive code for e.g. writing or reading messages from message bus and writing to a message queue in case something goes wrong? &lt;/p&gt;

&lt;p&gt;In case all microservices are &lt;em&gt;using the same technology&lt;/em&gt;, then some generic/reusable functionality used in all microservices of the same type (e.g. &lt;code&gt;EventHandling&lt;/code&gt;) can be extracted into a separate solution/project (e.g. &lt;code&gt;Framework.Services.EventHandling&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;You may easily end up with 2 solutions containing reusable "Framework"/helper code: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Framework&lt;/code&gt; - containing projects/code reusable across all types of microservices, e.g. string manipulation, date helpers etc&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Framework.Services&lt;/code&gt; - containing projects/code reusable across certain class of microservices, e.g. *.EventHandling&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Diagram 3: Framework, Framework.Service and Services hierarchy&lt;/p&gt;

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

&lt;p&gt;Splitting services into microservices based on CQRS+ clearly explains how to break down your code into manageable components. This approach allows you to focus on a single technical responsibility in each microservice, as well as write, compile, deploy, scale and monitor the latter individually. Starting a new service and breaking it down into microservices is then a straightforward story which can also be done by less-experienced developers.&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>cqrs</category>
    </item>
    <item>
      <title>Use Change Streams instead of Traditional Outbox or Distributed Transactions</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Mon, 26 Sep 2022 15:05:06 +0000</pubDate>
      <link>https://dev.to/deyanp/use-change-streams-instead-of-traditional-outbox-or-distributed-transactions-cdb</link>
      <guid>https://dev.to/deyanp/use-change-streams-instead-of-traditional-outbox-or-distributed-transactions-cdb</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; Use Change Streams instead of traditional outbox pattern with local database transaction, or even worse - distributed transaction across database and message bus. &lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the context of Microservice Architecture (MSA) and Event-Driven Architecture (EDA) one of the core questions is how to  save an entity's state to the database &lt;em&gt;and reliably (= with 100% guarantee)&lt;/em&gt; push corresponding event(s) to a message bus, so that other services can eventually consume the published event(s) and do their job in an eventually consistent manner.&lt;/p&gt;

&lt;p&gt;To my surprise there is still not enough awareness (e.g. judging by answers I get in interviews) of what is the most efficient solution and furthermore, experts with 20+ years of experience are still referring to older patterns, which are already superseded by newer, more efficient ones. &lt;/p&gt;

&lt;p&gt;In detail, assuming we have these 2 operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;An entity's state is written to a database table  (regardless if insert/update of last state, or insert of event in the event-sourced persistence approach)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An event message is written to a message bus,&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;the problem to solve is the following: the process could crash in between the 2 steps which leaves us with an inserted/updated entity in the database but with no event sent to other microservices, so some side-effects (e.g. sending welcome email) are not performed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This post does not deal with the topic how to create Domain Events/External Events, for more info on &lt;em&gt;one way of doing&lt;/em&gt; that please read the (older) post &lt;a href="https://dev.to/deyanp/tbd-current-state-last-event-as-an-alternative-to-event-sourcing-5gm5"&gt;Current State + Last Event as an alternative to Event Sourcing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traditional Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Outbox Pattern with a local database transaction
&lt;/h3&gt;

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

&lt;p&gt;The idea here is to take advantage of local database transactions in order to guarantee 100% that an event message is also written in addition to writing the entity, and then process all event messages in a reliable/retriable manner (again with 100% guarantee that every message will be processed at least once).&lt;/p&gt;

&lt;p&gt;The following 2 steps need to be implemented: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bundle together the insert/update of the entity's state with an insert of a "message/event" in another database table (in the same database), within a local database transaction:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRANSACTION&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;Customers&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;CustomerEvents&lt;/span&gt; &lt;span class="p"&gt;....&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt; &lt;span class="n"&gt;TRANSACTION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Then have a "Reader" process which goes over the CustomerEvents sequentially, reads every record and publishes the event onto a message bus, and marks the event as "published". In case of an error (process crashes), then start again from last not-published event. As it is possible to experience a crash in between &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;e.g. every X seconds&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;CustomerEvents&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;IsPublished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;False&lt;/span&gt; 
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;CreatedOn&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;messageBusClient.publish(event)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;CustomerEvents&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;IsPublished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;True&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;CustomerEventId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eventId&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Issues" with this approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Need to create/maintain the CustomerEvents table&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Upon updating the Customer entity it is necessary to take care also of inserting into the CustomerEvents table, and this in a local database transaction (imagine there are many microservices with many entities to store) ... Ideally one would need to only care about inserting/updating Customers table, without anything else ...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The local database transaction creates short-lived page locks, which reduce concurrency&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Distributed Transactions
&lt;/h3&gt;

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

&lt;p&gt;This approach spans a distributed transaction between 2 different technologies - database and message bus. Used to be quite popular 15-20 years ago, but since then many sources discourage from using it due to vendor lock-in, complexity, concurrency issues. &lt;/p&gt;

&lt;p&gt;Pseudo code would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="n"&gt;distributedTransactionCoordinator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartTransaction&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; 
    &lt;span class="n"&gt;databaseClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;messageBusClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;distributedTransactionCoordinator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CommitTransaction&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;_&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;distributedTransactionCoordinator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RollbackTransaction&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have not used distributed transactions in the past 15 years myself so treat the above pseudo code as a very rough approximation conveying only the general idea. &lt;/p&gt;

&lt;p&gt;"Issues" with this approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Distributed transactions are not really supported across all different technologies, such should be carefully chosen ..&lt;/li&gt;
&lt;li&gt;Distributed transactions have their own complexity and implications in terms of rollbacks, locks, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note: Explaining how distributed transaction coordinators are implemented using Two-Phase Commit (2PC) is beyond the scope of this post, as this approach is anyway not used/recommended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Solution: Change Streams/Feed/Change Data Capture
&lt;/h2&gt;

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

&lt;p&gt;Many databases are tracking the changes to table rows (or collection documents) in a separate table/collection, and are using it for "notifying" interested subscribers about these (either internal - e.g. for data replication across nodes, or external - e.g. custom application code). &lt;/p&gt;

&lt;p&gt;Some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/streamsmain.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/streamsmain.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mongodb.com/docs/manual/changeStreams/" rel="noopener noreferrer"&gt;https://www.mongodb.com/docs/manual/changeStreams/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/cosmos-db/change-feed" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/cosmos-db/change-feed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/relational-databases/track-changes/about-change-data-capture-sql-server?view=sql-server-ver16" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/sql/relational-databases/track-changes/about-change-data-capture-sql-server?view=sql-server-ver16&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/logicaldecoding.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/logicaldecoding.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://debezium.io/" rel="noopener noreferrer"&gt;https://debezium.io/&lt;/a&gt; for CDC on top of standard relational databases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One example is MongoDB Change Streams which track all changes of a database and its collections in a special system collection &lt;code&gt;local.oplog.rs&lt;/code&gt; being used both for data replication across nodes in a cluster, as well as for "watching" or polling by external clients with a pretty convenient client SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ForEachAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;printfn&lt;/span&gt; &lt;span class="s2"&gt;"%A"&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// This should not be reached as the cursor is processing forever&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;local.oplog.rs&lt;/code&gt; is a capped collection, which means after some time the oldest entries are removed automatically from it. How long/much data is stored in this collection is configurable. &lt;/p&gt;

&lt;p&gt;A sample document (= &lt;code&gt;change&lt;/code&gt; in the code above) in local.oplog.rs looks like this:&lt;/p&gt;

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

&lt;p&gt;A client application can "watch" (poll) multiple collections individually, and for example publish events on a message bus. In this way by hooking to the database change stream the 100% guaranteed publishing to a message bus can be accomplished.&lt;/p&gt;

&lt;p&gt;Why this approach is better:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;When inserting/updating/deleting the entity in the original table/collection &lt;strong&gt;nothing else needs to be done&lt;/strong&gt;. Also no 1:1 table/collection needs to be manually maintained&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The database change stream mechanism is of production quality, used also internally by the database itself, and is very stable/reliable. Same for the SDK/API for consuming it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Additional Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Message Delivery, Idempotency, Duplicate Detection
&lt;/h3&gt;

&lt;p&gt;Having in mind the 3 message delivery guarantees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At most once&lt;/li&gt;
&lt;li&gt;At least once&lt;/li&gt;
&lt;li&gt;Exactly once,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;the most practical approach is to settle on "At least once", which means that the event message will be published at least once, but it is possible that it is published 2+ times (e.g. in case the publishing process crashes, or a timeout is received when calling the message bus API). Such duplicate event messages can be handled in the following ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Idempotent processing or event message deduplication by the subscriber&lt;/li&gt;
&lt;li&gt;Event message deduplication by the Message Bus middleware&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Idempotent processing or event message deduplication by the subscriber in simple terms means that if the same event is received by a subscriber twice, the subscriber is able to detect that this event has already been processed, do nothing and return OK (as if the event was processed successfully). This can be achieved in different ways, e.g.:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A local &lt;code&gt;ProcessedEvents&lt;/code&gt; table in the subscriber's database which contains the (unique) event id of those events which were successfully processed. Upon receiving a new event first a lookup in that table is done, and if nothing found only then the event is processed, otherwise return OK directly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The event id is used as a column in another table, and there is a unique constraint on it. Upon inserting a new row to this other table the unique constraint generates an error in case of duplicate processing, which can be caught and converted to OK result&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The primary key of another table can be the same as the event id, so that again upon duplicate insert the default unique constraint generates an error in case of duplicate processing, which can be caught and converted to OK result&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Event message deduplication by the message bus middleware itself is something that I usually do not use, as I prefer not to be tightly coupled to specific middleware with exclusive functionality, but instead be able to use any message bus having just basic pub-sub (at least once) functionality ...&lt;/p&gt;

&lt;h3&gt;
  
  
  Checkpointing
&lt;/h3&gt;

&lt;p&gt;What happens if the subscriber process consuming the change stream dies/crashes? Upon restarting that process has several options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Start re-processing all change stream events from the beginning - this will result in a lot of duplicate events published, and requires idempotent/deduplicating downstream subscribers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start from the end of the change streams (only new change stream events will be processed) - in this case some events in between will never be processed/published.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start from a checkpoint which indicates last successfully processed change stream entry. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Obviously option 3 is the best but it requires a little bit of extension to the change stream "watching" code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upon startup the watcher reads the last checkpoint from a custom table and starts watching change stream events from that one onwards&lt;/li&gt;
&lt;li&gt;After processing every 1 (or N) change stream events the watcher must update the checkpoint with the last successfully processed event id/timestamp (in case of MongoDB every change stream event has a &lt;code&gt;ResumeToken&lt;/code&gt; property). If checkpoints are saved upon every processed change stream event then max 1 change stream events will be retried. In case the checkpoint is saved less frequently, e.g. after every 5th event, then max 5 events will be retried again in case of a process crash or similar. In both cases the "at least once" message delivery guarantee is still valid, and if downstream processor are implemented with this in mind there should be no problem. &lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Multiple subscribers (Competing Consumers)
&lt;/h3&gt;

&lt;p&gt;By default the change stream event handling application is a single instance/single thread application, which gets all change stream events and publishes them to a message bus as event messages. This is a very fast operation and for "normal" applications a single instance is perfectly fine, as long as it is hosted in such a way (e.g. Kubernetes) that if it dies then automatically a new instance is spawned.&lt;/p&gt;

&lt;p&gt;But what if there are so many events per second that multiple instances are required to run in parallel? &lt;/p&gt;

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

&lt;p&gt;This is something I have not needed so far, but in such a case I would probably have e.g. 3 application instances listening to the same change stream (Customers) and skipping every first, second or third change stream event. Every change stream "watcher" will have its own checkpoint. &lt;/p&gt;

&lt;h3&gt;
  
  
  Event Replay
&lt;/h3&gt;

&lt;p&gt;Sometimes there is need for replaying all events. We have 2 "&lt;strong&gt;buffers&lt;/strong&gt;" to consider: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The Change Stream database storage - in case of MongoDB is configurable but usually does not hold all changes since the creation of the database&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Message Bus - also stores messages only for a limited period of time, e.g. in case of standard Azure Event Hubs for 7 days&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the best case only change stream events or event messages are replayed which in one of the 2 buffers. &lt;/p&gt;

&lt;p&gt;If events must be replayed all the way from source table (e.g. Customers) then we have 3 options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;All changes to Customers are additionally stored in an &lt;code&gt;CustomerAuditTrailEntries&lt;/code&gt; collection, so the latter can be read sequentially and for each entry a custom change stream event can be created and published to the message bus directly. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Customers table actually does not exist, &lt;a href="https://martinfowler.com/eaaDev/EventSourcing.html" rel="noopener noreferrer"&gt;Event Sourcing&lt;/a&gt; is used and every single Customer event is inserted as a separate table row/document. Then iterating over these rows/documents will be sufficient for generating the event messages and publishing them to the message bus&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No audit trail is stored, no Event Sourcing is used, updates are done in place. In this case the best that can be done is to generate and publish to the message bus events for the &lt;strong&gt;last state&lt;/strong&gt; of each customer entity.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is worth mentioning that the Event Replay itself can also be exposed to external webhook subscribers who can trigger it over a REST API (e.g. &lt;code&gt;POST /replays with from,to,eventType and subscriberId properties&lt;/code&gt; in the request body). &lt;/p&gt;

&lt;p&gt;Furthermore, the event replay should ideally publish event message to a separate topic on the message bus, to which only interested in replay subscribers should be listening, otherwise all existing subscribers can be flooded with a lot of events. &lt;/p&gt;

&lt;p&gt;Last but not list, sometimes direct database manipulations might be required (e.g. database refactoring/migrations). Such migration should or should not trigger change stream event processing. In case of the latter, a simple filtering scheme on some entity property (e.g. &lt;code&gt;LastModifiedOn&lt;/code&gt;) should be introducing in the change stream "watching" application so that such changes can be skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Handling
&lt;/h3&gt;

&lt;p&gt;While processing the change stream events (= looping over the open cursor polling the change stream collection) an error/exception may happen in relation to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Individual document - e.g. cannot be deserialized, or publishing to message bus fails due to transient error&lt;/li&gt;
&lt;li&gt;The whole change stream - e.g. network/database outage&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Individual document processing error can be handled by simply "parking" the document into a retry queue, which is handled by another thread automatically with a small delay, and if reprocessing again fails the message can be parked in a poison queue, to be looked at manually.&lt;/p&gt;

&lt;p&gt;In case of a general error the change stream can be either restarted from the last saved checkpoint after a small delay (= catch exception, re-open cursor), or the whole process may be let to crash, which would invoke its automatic restart by e.g. Kubernetes.&lt;/p&gt;

&lt;p&gt;Remember, what we are speaking about here is just iterating over a database collection containing changes (= change stream) while keeping a clientside "index" how far down the road the watcher is. In case of a single watcher (enough in most cases) you can always stop for a while, wait, restart from last index or even from a previous position on the stream. This is the same simple and scalable approach used in many other technologies based on the append-only &lt;a href="https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying" rel="noopener noreferrer"&gt;"Log"&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://blog.insiderattack.net/atomic-microservices-transactions-with-mongodb-transactional-outbox-1c96e0522e7c" rel="noopener noreferrer"&gt;https://blog.insiderattack.net/atomic-microservices-transactions-with-mongodb-transactional-outbox-1c96e0522e7c&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/wix-engineering/event-driven-architecture-5-pitfalls-to-avoid-b3ebf885bdb1" rel="noopener noreferrer"&gt;https://medium.com/wix-engineering/event-driven-architecture-5-pitfalls-to-avoid-b3ebf885bdb1&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>Real-life performance comparison of MongoDB Atlas on Azure/GCP/AWS</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Wed, 05 Jan 2022 00:44:45 +0000</pubDate>
      <link>https://dev.to/deyanp/real-life-performance-comparison-of-mongodb-atlas-on-azure-vs-gcp-m0o</link>
      <guid>https://dev.to/deyanp/real-life-performance-comparison-of-mongodb-atlas-on-azure-vs-gcp-m0o</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; MongoDB Atlas M20-M40 on Azure, GCP and AWS perform pretty similarly, with slightly varying levels of performance (shorter durations) and stability (outliers, variability) across the different cluster/storage sizes: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;M20: AWS &amp;gt; Azure &amp;gt; GCP (sometimes Azure and AWS have higher variability)&lt;/li&gt;
&lt;li&gt;M30: AWS &amp;gt; Azure &amp;gt; GCP (higher variability for GCP)&lt;/li&gt;
&lt;li&gt;M40: AWS &amp;gt; Azure ~&amp;gt; GCP (higher variability for GCP and AWS).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The intention of this post is to compare the real-life performance of MongoDB M20-M40 running on Azure, GCP and AWS. Read further if you want to get the condensed results of otherwise a multi-day/week exercise&lt;sup id="fnref1"&gt;1&lt;/sup&gt;. &lt;br&gt;
The trigger for the load tests were sur&lt;a href="https://dev.to/deyanp/mongodb-atlas-azure-a-forced-marriage-169m"&gt;various disk-related performance issues on Azure&lt;/a&gt; and repeated statements that MongoDB on AWS and GCP is running much better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important Notes&lt;/strong&gt;: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;This post is discussing only small dedicated instances - M20-M40 with 128 and 256 GB storage only (no need for/experience with bigger ones yet)&lt;/li&gt;
&lt;li&gt;The Reader should only pay attention to the &lt;strong&gt;relative&lt;/strong&gt; aspect of the results, i.e. how different clouds compare to each other, or different instance/storage sizes improve the performance. The absolute request durations in milliseconds etc. are to be ignored, as they are only valid for the specific custom code under test.&lt;/li&gt;
&lt;li&gt;This is not a "scientific" or properly done benchmark (I am an amateur performance/load tester). If you see the approach is wrong or/and can be improved - please let me know in the comments what I can do better (appreciated).&lt;/li&gt;
&lt;li&gt;The architecture details of the software under test is out of scope for this article (I may or may not write another one on that). Same for the load test automation tooling, which is basically implemented using Azure DevOps Pipelines and a substantial number of bash scripts. &lt;/li&gt;
&lt;li&gt;Even though many load tests with different requests per second were performed, only the result of a subset of the tests - 50 requests per second - will be used in this article, as these are enough to illustrate the differences in performance of MongoDB Atlas on Azure/GCP/AWS.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;The software under test is a set of 5 microservices responsible for &lt;strong&gt;synchronously&lt;/strong&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt; processing a business operation/transaction &lt;sup id="fnref3"&gt;3&lt;/sup&gt;. The overall interaction between the services looks like this (anonymized): &lt;/p&gt;

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

&lt;p&gt;Note that all database operations are single-document ones where the document size is less than 1 KB, and fall into 3 categories:&lt;br&gt;
1) find a single document by UUID id (primary key)&lt;br&gt;
2) insert a single document&lt;br&gt;
3) replace a single document&lt;/p&gt;

&lt;p&gt;The load test infrastructure involves the following tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Azure DevOps Pipelines - for running bash scripts in sequence, responsible for both setup, running the load tests and tear down of the load test environment (both Azure and GCP)&lt;/li&gt;
&lt;li&gt;NBomber - a load test client, generating in this case 50 requests per second using &lt;a href="https://nbomber.com/docs/general-concepts#load-simulations-intro"&gt;InjectPerSec(rate = 50, during = seconds 600) Load Simulation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"Client" Kubernetes cluster (AKS, GKE or EKS) with just 1 node (4 vCPUs, 8Gb RAM) for hosting the Azure DevOps self-hosted pool with 1 agent running NBomber&lt;/li&gt;
&lt;li&gt;"Server" Kubernetes cluster (AKS, GKE or EKS) with 2 nodes (2 vCPUs, 16 GB RAM) for running the microservices (3/5 microservices configured with 2 replicas, 2/5 with 1 replica only). Traefik is running as well in the "server" cluster to act as an (HTTPs) API Gateway.&lt;/li&gt;
&lt;li&gt;Both K8s clusters are in the same region, Vnet/VPC Network, different subnets, however the client clusters uses HTTPs with the public DNS hostname of the server for making the calls (so certain traversal of the network infrastructure of the cloud provider takes place, however we are speaking about single-digit ms here). &lt;/li&gt;
&lt;/ol&gt;

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

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

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

&lt;p&gt;The MongoDB instances used are summarized in the following table:&lt;/p&gt;

&lt;p&gt;TABLE MongoDB Instances, including IOPS and $$$&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cloud&lt;/th&gt;
&lt;th&gt;Instance Type&lt;/th&gt;
&lt;th&gt;Storage Size (GB)&lt;/th&gt;
&lt;th&gt;vCPUs&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;IOPS&lt;/th&gt;
&lt;th&gt;$$$/h&lt;/th&gt;
&lt;th&gt;$$$/month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;M20&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;2&lt;sup id="fnref4"&gt;4&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0.34&lt;/td&gt;
&lt;td&gt;245&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCP&lt;/td&gt;
&lt;td&gt;M20&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3.75&lt;/td&gt;
&lt;td&gt;7680&lt;sup id="fnref5"&gt;5&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;0.33&lt;/td&gt;
&lt;td&gt;238&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;M20&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;2&lt;sup id="fnref4"&gt;4&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;238&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;M20&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;2&lt;sup id="fnref4"&gt;4&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1100&lt;/td&gt;
&lt;td&gt;0.45&lt;/td&gt;
&lt;td&gt;324&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCP&lt;/td&gt;
&lt;td&gt;M20&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3.75&lt;/td&gt;
&lt;td&gt;15000&lt;sup id="fnref5"&gt;5&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;0.45&lt;/td&gt;
&lt;td&gt;324&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;M20&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;2&lt;sup id="fnref4"&gt;4&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;0.38&lt;/td&gt;
&lt;td&gt;274&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;M30&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;576&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCP&lt;/td&gt;
&lt;td&gt;M30&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;7680&lt;sup id="fnref5"&gt;5&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;432&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;M30&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;3000&lt;sup id="fnref6"&gt;6&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;482&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;M30&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1100&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;td&gt;655&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCP&lt;/td&gt;
&lt;td&gt;M30&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;15000&lt;sup id="fnref5"&gt;5&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;td&gt;518&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;M30&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;3000&lt;sup id="fnref6"&gt;6&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;540&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;M40&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;1.46&lt;/td&gt;
&lt;td&gt;1051&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCP&lt;/td&gt;
&lt;td&gt;M40&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;7680&lt;sup id="fnref5"&gt;5&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;1.06&lt;/td&gt;
&lt;td&gt;763&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;M40&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;3000&lt;sup id="fnref6"&gt;6&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;1.23&lt;/td&gt;
&lt;td&gt;886&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Worth noting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Azure M20 has 2 CPUs vs 1 vCPU for GCP M20, however in case of Azure the vCPU are &lt;a href="https://docs.microsoft.com/en-us/azure/virtual-machines/sizes-b-series-burstable#q-how-are-credits-accumulated-and-consumed"&gt;burstable&lt;/a&gt;, whereas in GCP the single vCPU can be utilized to 100% all the time.&lt;/li&gt;
&lt;li&gt;There is a huge difference in IOPS (on paper) with GCP offering 14-30x more IOPS than Azure for the same storage size!&lt;/li&gt;
&lt;li&gt;With bigger instance sizes Azure becomes noticeably more expensive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following sections will present the load testing results from different perspectives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure vs GCP vs AWS Performance M20 with 128Gb disk
&lt;/h2&gt;

&lt;h3&gt;
  
  
  50 rps, 5 minutes duration, 3 executions
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Azure M20 is clearly faster than GCP M20, and when it comes to mean durations at the same level as AWS M20. &lt;/li&gt;
&lt;li&gt;AWS M20 has very high 99 percentile and max durations though (=&amp;gt; a lot variability)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  50 rps, 1 hour duration, 3 executions
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS is faster than Azure M20 which is faster than GCP when it comes to average durations, &lt;/li&gt;
&lt;li&gt;AWS has better 99th percentiles as well &lt;/li&gt;
&lt;li&gt;Maximal durations for Azure are worse than GCP and AWS.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Azure vs GCP vs AWS Performance M20 with 256Gb disk
&lt;/h2&gt;

&lt;h3&gt;
  
  
  50 rps, 5 minutes duration, 3 executions
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Azure and AWS are roughly at the same level, faster than GCP&lt;/li&gt;
&lt;li&gt;AWS shows max duration outliers in 2 out of the 3 executions&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1 hour duration, 3 executions
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS M20 is faster than Azure which is faster than GCP when it comes to average durations and 99th percentile&lt;/li&gt;
&lt;li&gt;All 3 have a relatively high max request duration &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Azure vs GCP vs AWS Performance M30 with 128Gb disk
&lt;/h2&gt;

&lt;h3&gt;
  
  
  50 rps, 5 minutes duration, 3 executions
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4n2Kk-jQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0nptdd8da532qr1o0y6q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4n2Kk-jQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0nptdd8da532qr1o0y6q.png" alt="Image description" width="880" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS M30 is significantly faster than Azure M30, which is faster than GCP M30&lt;/li&gt;
&lt;li&gt;For all 3 the maximal durations are more consistent (less variability) compared to M20 (especially for AWS)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  50 rps, 1 hour duration, 3 executions
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS is faster than Azure &amp;amp; GCP&lt;/li&gt;
&lt;li&gt;Azure M30 is at the same level as GCP when it comes to average durations, 99th percentiles are almost equal, but the maximal durations of GCP are the highest (could be also individual outliers only). &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Azure vs GCP Performance M30 with 256Gb disk
&lt;/h2&gt;

&lt;h3&gt;
  
  
  50 rps, 1 hour duration, 3 executions
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the 1-hour test executions Azure M30 is at the same level as GCP when it comes to average durations, 99th percentiles are almost equal, with the maximal durations for Azure better than GCP's. &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Azure vs GCP vs AWS Performance M40 with 128Gb disk
&lt;/h2&gt;

&lt;h3&gt;
  
  
  50 rps, 5 minutes duration, 3 executions
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS M40 is slightly faster than Azure M40 and GCP M40 (however Azure M40 is substantially more expensive, see table above)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  50 rps, 1 hour duration, 3 executions
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS M40 is slightly faster than Azure M40 and GCP M40&lt;/li&gt;
&lt;li&gt;GCP and AWS have higher variability (the 3rd AWS test execution resulted even in 5 timeouts out of 180k requests ..)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Azure Performance Different Instance/Storage Sizes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  50 rps, 5 minute duration
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Average durations do not improve substantially with the increase of the instance/storage size (even increase from M20 -&amp;gt; M30??)&lt;/li&gt;
&lt;li&gt;99 percentile and max durations are higher with M20/128 and more stable with M20/128, M30 and M40&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  50 rps, 1 hour duration
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Average durations do not improve substantially with the increase of the instance/storage size&lt;/li&gt;
&lt;li&gt;99 percentile and max durations improve significantly with the increase of the instance/storage size (variability decreases, less outliers)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  GCP Performance Different Instance/Storage Sizes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  50 rps, 5 minute duration
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Averages improve slightly with the instance/storage size. 2. Same is valid for 99 percentile and max durations, which are pretty stable&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  50 rps, 1 hour duration
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Averages improve slightly with the instance/storage size. 2. Same is &lt;strong&gt;not&lt;/strong&gt; valid for 99 percentile and max durations ... there are strange peaks in case of M40/128 ...&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  AWS Performance Different Instance/Storage Sizes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  50 rps, 5 minute duration
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Averages are better with M30, but almost no difference with M40 for this load&lt;/li&gt;
&lt;li&gt;High 99 percentile and max durations in case of M20, stable for M30 and M40&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  50 rps, 1 hour duration
&lt;/h3&gt;

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

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Averages are better with M30, but almost no difference with M40 for this load&lt;/li&gt;
&lt;li&gt;High 99 percentile and max durations in case of M20, stable for M30 and M40 (1 execution only has max duration outlier)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  MongoDB Metrics Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Max Disk Write Latency
&lt;/h3&gt;

&lt;p&gt;M20, 128Gb, Azure:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--grnl-hGg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3mricq8l87xcy80yi0cq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--grnl-hGg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3mricq8l87xcy80yi0cq.png" alt="Image description" width="880" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;M20, 128Gb, GCP:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xo17OcOc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3woo5es3pb1p7ojnj2gd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xo17OcOc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3woo5es3pb1p7ojnj2gd.png" alt="Image description" width="880" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;M20, 128Gb, AWS:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QlvbP1QX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/whfaq8ohvgb8y1mf3cd6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QlvbP1QX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/whfaq8ohvgb8y1mf3cd6.png" alt="Image description" width="880" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notes: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Blue line is the Max Disk Write Latency&lt;/li&gt;
&lt;li&gt;Green line is Average Disk Write Latency&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Interpretation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS has 10-20x lower Max Disk Write Latency (1-2ms stable, only 1-2 spikes within 1h up to 6ms ) compared to Azure (40-60ms, 1 spike within 1h up to 80 ms) and GCP (30-40ms, 2-3 spikes within 1h up to 50 ms). This is a &lt;strong&gt;gigantic&lt;/strong&gt; difference!&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Factors possibly affecting the results
&lt;/h2&gt;

&lt;p&gt;There are a few hypotheses as to why the results could be incorrect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hypothesis 1: Burstability of M20 on Azure might lead to good results in shorter test durations, and worse results in longer ones&lt;/li&gt;
&lt;li&gt;Hypothesis 2: There is another bottleneck in the load test infrastructure (K8s node utilization checked)&lt;/li&gt;
&lt;li&gt;Hypothesis 3: Even though all setups use Network Peering between the K8s Cluster and MongoDB Atlas cluster it could be  (due to routing issues) that the messages take another less-efficient route.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Overall AWS has a better performance compared to Azure and GCP. &lt;/p&gt;

&lt;p&gt;Strangely enough, even though the IOPS numbers for GCP are the highest, it seems to offer the lowest performance. &lt;/p&gt;

&lt;p&gt;Azure for sure has its problems with variability/outliers&lt;sup id="fnref7"&gt;7&lt;/sup&gt;, but performs relatively well. Having in mind that Azure becomes considerably more expensive with bigger instance/storage sizes, it seems that AWS and GCP are the winner in the M30/M40 range. &lt;/p&gt;

&lt;p&gt;The real interesting finding IMHO is the far superior Max Write Disk Latency of AWS ... wondering if MongoDB Atlas is using direct attached storage in AWS vs. network-attached storage in Azure/GCP ...&lt;/p&gt;

&lt;p&gt;P.S. I would appreciate any comments pointing out an obvious mistake leading to wrong results!&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Including the load test automation/setup itself ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Synchronous = Client calls via HTTPS/REST Service1, Service1 acts as an orchestrator and calls synchronously Service2, Service3 etc. and awaits (async) their responses. Some of the inter-microservice calls are done in sequence, and some in parallel.  ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;"Transaction" does not mean database transaction or distributed database transaction. No database transactions are used (no database was harmed ;) ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;Burstable, which means from 2 x 100% = 200% only 40% are provisioned, but accumulated credits allow bursting up to the full 200% from time to time ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;Half of the IOPS are read IOPS and half are write IOPS ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;Non-provisioned, the provisioned IOPS on AWS are very expensive ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;See &lt;a href="https://dev.to/deyanp/mongodb-atlas-azure-a-forced-marriage-169m"&gt;this post about various disk-related performance issues on Azure&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>mongodb</category>
      <category>azure</category>
      <category>googlecloud</category>
      <category>performance</category>
    </item>
    <item>
      <title>MongoDB Atlas &amp; Azure - a forced marriage?</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Mon, 09 Aug 2021 09:59:41 +0000</pubDate>
      <link>https://dev.to/deyanp/mongodb-atlas-azure-a-forced-marriage-169m</link>
      <guid>https://dev.to/deyanp/mongodb-atlas-azure-a-forced-marriage-169m</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; MongoDB Atlas on Azure (smaller instances with smaller storage) works but comes with a number of pitfalls you should be aware of. You'd save a lot of headaches hosting MongoDB Atlas on AWS/GCP instead. &lt;strong&gt;UPDATE 4th Nov 2024&lt;/strong&gt;: MongoDB Atlas finally implemented Azure Premium SSD v2 and &lt;strong&gt;most performance issues have been solved&lt;/strong&gt;!!!&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The intention of this post is to point out a number of issues we have experienced over the past 2 years using MongoDB Atlas on Azure, having 2 main objectives in mind &lt;sup id="fnref1"&gt;1&lt;/sup&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Shorten your/others' troubleshooting path if you happen to go the same way&lt;/li&gt;
&lt;li&gt;Gather inputs from other customers of MongoDB Atlas on Azure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But you might ask immediately - why MongoDB Atlas on Azure instead of using the native CosmosDB? The reasons for us were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bloated document size + charging based on non-compressed data 0,25$/Gb/month - during our testing with CosmosDB a simple 180 byte json document was somehow taking 981 bytes in the end storage wise&lt;/li&gt;
&lt;li&gt;Missing real atomic updates (findOneAndUpdate, supported by the MongoDB API for Cosmos DB) in the standard SQL API, in particular in stored procedures - see &lt;a href="https://feedback.azure.com/forums/263030-azure-cosmos-db/suggestions/38110195-support-for-atomic-updates-in-sql-api" rel="noopener noreferrer"&gt;this&lt;/a&gt; and &lt;a href="https://social.msdn.microsoft.com/Forums/en-US/b0a1d77d-85b4-4a62-9489-82bbff0bebe5/single-partition-stored-procedure-for-transactionalatomic-accounting-operation?forum=azurecosmosdb" rel="noopener noreferrer"&gt;this&lt;/a&gt; for more information.&lt;/li&gt;
&lt;li&gt;Partition-First is mandatory - every table must be partitioned, with the partition key usually unable to satisfy both good distribution and queries by another attribute. MongoDB allows you to start with single and &lt;em&gt;unlimited&lt;/em&gt; partitions and you can add sharding later on.&lt;/li&gt;
&lt;li&gt;Partitioning Limitations - 10k RUs and 20GB per partition only&lt;/li&gt;
&lt;li&gt;Limited database transaction support - only within a single partition.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Important Notes&lt;/strong&gt;: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;This post is discussing only small instances - M10-M30 (no need/experience yet for/with bigger ones)!&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.mongodb.com/manual/reference/write-concern/" rel="noopener noreferrer"&gt;Write Concern&lt;/a&gt; = Majority is assumed in below discussions. In our case written data has to mean "durable on at least 2 out of 3 nodes", as we cannot afford losing "written/ACK-ed" data upon node failover.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following issues will be discussed in detail below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limited IOPS for small storage sizes when setting up new MongoDB Atlas cluster on Azure&lt;/strong&gt; (UPDATE 4th Nov 2024 - &lt;strong&gt;resolved&lt;/strong&gt;!)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sudden Disk Latencies of up to 15 seconds&lt;/strong&gt;, with missing/misleading metrics (UPDATE 4th Nov 2024 - &lt;strong&gt;resolved&lt;/strong&gt;!) &lt;/li&gt;
&lt;li&gt;Burstable CPU with Missing CPU Steal Metric&lt;/li&gt;
&lt;li&gt;Oplog deletion upon storage downgrade&lt;/li&gt;
&lt;li&gt;Different Operation Processing Time with different primary nodes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU Spikes every 15 minutes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Downtime during cluster upgrade (resolved)&lt;/li&gt;
&lt;li&gt;Random node failovers (resolved)&lt;/li&gt;
&lt;li&gt;Node "stalled" (froze) for 45 seconds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sudden but long-lasting Disk Utilizition Percent/Disk Latencies&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limited IOPS for small storage sizes on new clusters (mostly resolved)
&lt;/h2&gt;

&lt;p&gt;Inadequate storage IOPS when using small storage sizes (&amp;lt; 512Gb) is the elephant in the room. &lt;/p&gt;

&lt;p&gt;When starting with MongoDB Atlas on Azure one of the first important questions I had was - is the pricing similar to that for AWS/GCP, or is it higher (I have experienced quite a few Azure services incl. basic infrastructure ones are in reality more expensive than AWS/GCP ...). At a first glance I thought it's all relatively similar:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MongoDB Atlas Pricing on Azure Netherlands (westeurope), small instances:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdqqhfbchke2raanlqtu9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdqqhfbchke2raanlqtu9.png" alt="MongoDB Atlas Pricing on Azure" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MongoDB Atlas Pricing on AWS Ireland (eu-west-1), small instances:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwup0cnxwvjelhneb34fo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwup0cnxwvjelhneb34fo.png" alt="MongoDB Atlas Pricing on AWS" width="800" height="309"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MongoDB Atlas Pricing on GCP Belgium (europe-west1), small instances:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdchcpik6fuheznum6nhi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdchcpik6fuheznum6nhi.png" alt="Image description" width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes, Azure is still the most expensive hosting for MongoDB, but the difference does not seem that big at a first glance ... This is compounded though by the fact that performance is not the same - expand the details for M20 for example and check the IOPS value in there:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: IOPS for Azure have been increased from 120 to 1280 after MongoDB Atlas implemented Azure Premium SSD v2 in Q4 2024!&lt;/p&gt;

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

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

&lt;p&gt;Did you notice the difference? Azure gives you only 1280 IOPS for starters vs. 2000+ IOPS on AWS and GCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sudden Disk Latencies of up to 15 seconds, Missing/Misleading Metrics (resolved)
&lt;/h2&gt;

&lt;p&gt;The story about low disk IOPS with small storage sizes does not end here though. The problem is made worse by having no way to see when the IOPS are throttled. This is how the Disk IOPS metric looks for the a M20 primary node:&lt;/p&gt;

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

&lt;p&gt;Doesn't the above graph give you the impression that not 120, but even 20 IOPS will be enough? Yes ... but actually not, and you may see in db logs or in the MongoDB Atlas Profiler (visual) the following or worse:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;(UPDATE 8th Oct 2021)&lt;/strong&gt; MongoDB have added Max Values for all hardware metrics (thanks @MongoDB Atlas Team!), so now you can see more clearly how the IOPS are fully utilized from time to time (for some yet&lt;br&gt;
 unknown reason):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7d3rfgc2t9bzu2v44gq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7d3rfgc2t9bzu2v44gq.png" alt="Disk Latency and Disk Utilization Percent, Max + Avg values" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnz4igr93ooq76d1taqso.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnz4igr93ooq76d1taqso.png" alt="Avg CPU and Max Cpu" width="800" height="199"&gt;&lt;/a&gt;&lt;br&gt;
Note 1: The peak coincides with the regular every 15-minutes "chef scripts" run by MongoDB Atlas, see below for more info)&lt;br&gt;
Note 2: Dev.to corrupts the resolution of the uploaded images ... otherwise you would see the higher blue-green line on the CPU metrics being &lt;strong&gt;iowait&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Even with 128Gb (500 IOPS) and normal (=low) opcounters (no peaks!) we have experienced within 1 week 2 cases of single small document insert taking up to 7-15 seconds (yes, seconds!, instead of 6-8 ms in average) when something happens to the IOPS .. &lt;/p&gt;

&lt;p&gt;After several km-long support cases based on scarce comments and internal metrics provided by MongoDB Support my conclusion is that IOPS limits are hit by the primary or secondary nodes when the write operation must be confirmed by primary + at least 1 secondary, and the secondary cannot confirm because the oplog has not been synced yet due to IOPS throttling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(UPDATE 8th Oct 2021)&lt;/strong&gt; Turns out &lt;a href="https://docs.microsoft.com/en-us/azure/virtual-machines/disk-bursting" rel="noopener noreferrer"&gt;all Azure Premium SSDs support bursting up to 3500 IOPS for up to 30 minutes&lt;/a&gt; (even confirmed by MongoDB Atlas Support Engineer!), but the $1mln question is then why, oh why, do we experience still these 7-15 seconds (not milliseconds) disk latency (insert of single small document, no db trx or anything) then??&lt;/p&gt;

&lt;p&gt;Trying to find someone @MongoDB Atlas to access the underlying Azure VM Disk Metrics and check the values of the following ones, which can throw some light if burstable credits are exhausted from time to time (but I don't really believe that this is happening in our case ..):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data Disk Used Burst IO Credits Percentage (Max)&lt;/li&gt;
&lt;li&gt;OS Disk Used Burst IO Credits Percentage (Max)&lt;/li&gt;
&lt;li&gt;Data Disk IOPS Consumed Percentage (Max)&lt;/li&gt;
&lt;li&gt;OS Disk IOPS Consumed Percentage (Max)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;(UPDATE 11th Oct 2021)&lt;/strong&gt; Even after storage upgrade to 256Gb / 1100 IOPS still getting randomly hit by single-document insert/replace operations taking 100-200x more than usual, e.g. 1600+ ms instead of 6-8ms ... Happens couple of times per day when the load is relatively low - single-digit business operations per second, every business operation = about 10 single-document read/write db operations. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(UPDATE 4th Jan 2022)&lt;/strong&gt; The last statement about the root cause of the intermittent disk latency issues is that the regular 15-minute Ansible monitoring/management scripts hit hard the OS Disk of the Standard_B2s VM (in case of M20) which causes a delayed (after about 7 minutes??) disk throttling affecting also the MongoDB process ...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(UPDATE 22th Feb 2022)&lt;/strong&gt; After many months of investigations we are back at square 1: the Ansible monitoring/management process running every 15 minutes is &lt;strong&gt;not&lt;/strong&gt; the reason for the slowdowns, root cause is not stated (=&amp;gt; unknown?), we should upgrade to M30 or change the cloud provider ...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(UPDATE 4th March 2022)&lt;/strong&gt; Problem still occurring after upgrade to M30, contrary to statement that the problem would be solved by upgrade to M30 ...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(UPDATE 31st Oct 2022)&lt;/strong&gt; Problem can NOT be resolved, issue lies with Azure Infrastructure (Disks) and not with MongoDB Atlas itself. Only options are move to AWS/GCP or wait for new generation Azure disks ...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(UPDATE 4th Nov 2024)&lt;/strong&gt; MongoDB Support upgraded our existing clusters to Azure Premium SSD v2, and we finally see much more stable operation durations! Current IOPS Comparison is also a bit better for Azure, but still lagging from AWS/GCP:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Azure&lt;/th&gt;
&lt;th&gt;AWS (eu-west1)&lt;/th&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M10 vCPUs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M10 RAM Gb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M10 Disk Size Gb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8-128&lt;/td&gt;
&lt;td&gt;10-128&lt;/td&gt;
&lt;td&gt;10-128&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M10 IOPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;640&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;600 (300 read + 300 write) - 7680 (3840 read + 3840 write)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M10 Custom IOPS?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M10 Max Connections&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1500&lt;/td&gt;
&lt;td&gt;1500&lt;/td&gt;
&lt;td&gt;1500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M10 Network Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low network performance&lt;/td&gt;
&lt;td&gt;Up to 5Gb&lt;/td&gt;
&lt;td&gt;Low to Moderate network performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M20 vCPUs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M20 RAM Gb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M20 Disk Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16-256&lt;/td&gt;
&lt;td&gt;20-256&lt;/td&gt;
&lt;td&gt;20-256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M20 IOPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1280&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;1200(600 read + 600 write) – 15000 (7680 read + 7680 write)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M20 Custom IOPS?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M20 Max Connections&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M20 Network Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moderate network performance&lt;/td&gt;
&lt;td&gt;Up to 5Gb&lt;/td&gt;
&lt;td&gt;Moderate network performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M30 vCPUs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M30 RAM Gb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M30 Disk Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;32-512&lt;/td&gt;
&lt;td&gt;40-512&lt;/td&gt;
&lt;td&gt;40-512&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M30 IOPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3200&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;2400(1200 read + 1200 write) – 15000 (7680 read + 7680 write)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M30 Custom IOPS?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Yes, 10-3600&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M30 Max Connections&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;M30 Network Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High network performance&lt;/td&gt;
&lt;td&gt;Up to 10Gb&lt;/td&gt;
&lt;td&gt;High network performance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Burstable CPU with Missing CPU Steal Metric
&lt;/h2&gt;

&lt;p&gt;M10 and M20 instances are using B-series Azure VMs (e.g. M20 is using &lt;code&gt;Standard_B2s&lt;/code&gt;). These are burstable VMs where you have for &lt;code&gt;Standard_B2s&lt;/code&gt; 40% CPU baseline performance guaranteed (40% = 2 vCPUs * 20% CPU utilization each). If you use less than 40% you accumulate credits and every credit gives you the right to burst above the 40% (e.g. to 100% = 1 vCPU fully utilized or 200% = both vCPUs fully utilized) for certain period of time until credits reach 0. &lt;/p&gt;

&lt;p&gt;There is a &lt;a href="https://docs.atlas.mongodb.com/reference/alert-resolutions/system-cpu-usage/#alert-conditions" rel="noopener noreferrer"&gt;CPU Steal % metric&lt;/a&gt; in MongoDB Atlas which should be 0 if all good, and should start increasing in case you need more CPU but you cannot get it because you are throttled to your baseline performance (= no available credits for bursting). An alert can be configured once this metric reaches certain threshold (e.g. above 0 for several minutes) ...&lt;/p&gt;

&lt;p&gt;That is all fine and good, but the problem is that MongoDB Atlas seems to have implemented the CPU Steal % metric/alert only for AWS ... so in Azure there is no way to detect and alert such an important situation ...&lt;/p&gt;

&lt;h2&gt;
  
  
  Oplog deletion upon storage downgrade
&lt;/h2&gt;

&lt;p&gt;While testing different combinations of instance size (e.g. M20, M30) and storage size (16, 64, 128Gb) we had to downgrade the storage a couple of times (e.g. from 128 back to 16Gb). Usually upgrade operations worked flawlessly - MongoDB Atlas takes one node after the other offline, replaces, syncs with primary and puts back in cluster, no data loss. In case of storage downgrade we lost all the data in the &lt;a href="https://docs.mongodb.com/manual/core/replica-set-oplog/" rel="noopener noreferrer"&gt;Oplog&lt;/a&gt;, which is of critical importance for our application built on top of &lt;a href="https://docs.mongodb.com/manual/changeStreams/" rel="noopener noreferrer"&gt;MongoDB Change Streams&lt;/a&gt; for all the async event publishing functionality. This means our K8s pods waiting for change events lost their resume tokens (saved checkpoints) as the latter were pointing to non-existent oplog positions ... &lt;/p&gt;

&lt;p&gt;According to MongoDB this happens when &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"An Azure machine is migrated to a new instance family" &lt;/li&gt;
&lt;li&gt;"A user requests a disk decrease on Azure instances"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and I have the feeling the above is again Azure-specific. My request for changing that behavior on Azure has not been honoured yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different Operation Processing Time with different primary nodes
&lt;/h2&gt;

&lt;p&gt;While testing with different instance and storage sizes I noticed that in a standard 3-node cluster I get different average business operation (1 business operation contains 2-3 single document &lt;code&gt;find&lt;/code&gt; and 7-8 &lt;code&gt;modify&lt;/code&gt; operations)  processing times depending on which node is primary:&lt;/p&gt;

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

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

&lt;p&gt;You see 15-20ms difference in average times, meaning up to 25%, which is huge! My suspicion is that this is because of some networking overhead due to putting the different nodes in different Availability Zones, however my client applications are running on 3 Kubernetes nodes split in the same way to the 3 different Azure Availability as the MongoDB nodes ... Wouldn't be surprised if this is another Azure idiosyncrasy ...&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU Spikes every 15 minutes
&lt;/h2&gt;

&lt;p&gt;This is how the CPU looks like on &lt;strong&gt;all&lt;/strong&gt; of our M20 clusters:&lt;/p&gt;

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

&lt;p&gt;What are these regular CPU spikes every 15 minutes you might ask - is there some heavy regular application activity? The answer is no, our application is not doing anything, however there is some MongoDB Atlas Monitoring cron job (aka "Chef scripts") which is doing some heavy work every 15 minutes. Remember: M20 is a burstable instance with baseline of 40%, so every 15 minutes this monitoring process is "stealing" 1-2 minutes of CPU, for which you have paid ... &lt;br&gt;
I had a couple of tickets with MongoDB Support on this topic suspecting that this is the reason for the randomly slow operation processing times, not clear if it is correlated or not (several disk latency occurrences happened at the same time, but a few also not), however it still feels like unoptimized admin intervention ...&lt;/p&gt;

&lt;h2&gt;
  
  
  Downtime during cluster upgrade (resolved)
&lt;/h2&gt;

&lt;p&gt;With MongoDB Azure we were suffering for more than 6-months  from downtimes (applications could not connect anymore to the cluster and needed to be manually restarted!) during cluster maintenances. This happened exactly at the point of time when the 3rd node was getting replaced (all fine with 1st and 2nd). &lt;br&gt;
It took tons of discussions (incl. with an Account Manager) and paying for Professional Services until this problem was fixed in the .NET Driver, but only after another &lt;strong&gt;big customer&lt;/strong&gt; complained.&lt;/p&gt;

&lt;h2&gt;
  
  
  Random node failovers
&lt;/h2&gt;

&lt;p&gt;For a week now we are experiencing random node failovers.  Activity Feed in Atlas Portal shows only this:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft0jk495duv50umecpo8q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft0jk495duv50umecpo8q.png" alt="Alt Text" width="800" height="82"&gt;&lt;/a&gt;&lt;br&gt;
The current explanation by MongoDB Support (as far as I have understood it) is that there are network issues between the nodes and a node triggers re-election. But how come one of the most stable cloud components (network) is having so many outages, and what is the resolution? No answer yet ...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(Update 8 Oct 2021)&lt;/strong&gt;: After MongoDB Atlas Support talked to Azure Support network reconfiguration was performed. Since then we have not experienced additional unexpected failovers, so (assuming this has been integrated in the setup scripts for new MongoDB Atlas clusters on Azure) the problem can be treated as resolved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(Update 22 Feb 2022)&lt;/strong&gt;: We had another such case 2 weeks ago, which caused some of our business operations to take 25 seconds instead of 70 ms (still were successful though). Turns out the network configuration performed previously was not persistent (after VM reboot gone), which I was not told at the time. Now MongoDB Support has repeated the network config but in a persistent manner, let's see ...&lt;/p&gt;

&lt;h2&gt;
  
  
  Node "stalled" (froze) for 45 seconds
&lt;/h2&gt;

&lt;p&gt;Another case from past week - the primary node just "stalled" or froze for 45 seconds, and then continued working. Of course, during that time all business operations were affected, and some of them timed out. According to MongoDB Support something happened to the node's disk - it just physically stopped working. What can be done so that this does not happen again? Nothing ... if it happens again the node can be replaced ...&lt;/p&gt;

&lt;p&gt;And another case of 3 seconds disk "stalling" or whatever  - disk queue went up, db operations got 100 slower:&lt;/p&gt;

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

&lt;p&gt;Was this due to exceeding Disk IOPS and being throttled by Azure Storage - if the metric is to be believed (and I don't) - No ..:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijrtkmckuxmijqxmp9mz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijrtkmckuxmijqxmp9mz.png" alt="image" width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I understand, that cloud VMs may go away or freeze in case the host crashes, but somehow too many strange things happen lately with our MongoDB Atlas clusters on Azure - can Azure be that unstable??&lt;/p&gt;

&lt;h2&gt;
  
  
  Sudden but long-lasting Disk Utilizition Percent/Disk Latencies
&lt;/h2&gt;

&lt;p&gt;A picture is worth a thousand words:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disk Utilization % - from all processes running on the node&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2jxn7d92pr70a5w1ibhh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2jxn7d92pr70a5w1ibhh.png" alt="Disk Utilization % - from all processes running on the node" width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

&lt;p&gt;Happens already for the 2nd time in the past few days, the solution last time was to fail over manually to another node, and wait 30-45 minutes (!??!)&lt;/p&gt;

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

&lt;p&gt;The astute reader may have already concluded from the above that the performance and stability of the M20 MongoDB Atlas instance on Azure is a joke, and this due to disk-related issues.&lt;/p&gt;

&lt;p&gt;As recommended &lt;strong&gt;multiple times&lt;/strong&gt; by MongoDB Support and Account Management, MongoDB Atlas should rather be used in conjunction with hosting on AWS/GCP where it seems to be much more stable, fast and also cheap. &lt;/p&gt;

&lt;p&gt;This is something that was not crystal-clear to me when I decided for Azure and implemented the rest of the infrastructure there. Also a quick look at the different MongoDB Atlas hosting options did not explicitly warn me that one gets much less IOPS and a bunch of additional problems on Azure. &lt;/p&gt;

&lt;p&gt;If CosmosDB didn't have some of the issues (bloated/expensive storage, lack of atomic update, enforced partitioning from the start, etc.) we would have moved to it long ago. Migration to GCP/AWS is something I will be actively investigating, however there are some goodies we are using (AKS, App Insights, Azure Data Explorer/Kusto) which need more work.&lt;/p&gt;

&lt;p&gt;I have read that Azure has a weak offering of low-level IaaS, but why it is so lagging behind AWS/GCP when it comes to IOPS for smaller disks is beyond my understanding (read about the software layer they have put on top of it, but I don't care). Or rather - I cannot wrap my head around the question why Azure is not working day and night to fix this big gap in such an important fundamental/enabling service. &lt;/p&gt;

&lt;p&gt;I wish Azure could fix this and additionally expose CPU Steal % and other metrics, so that MongoDB Atlas could level up its Azure hosting. I wish additionally that MongoDB Atlas could invest a little bit more in its Azure hosting (15 minutes heavy cron jobs can be optimized, additional metrics can be added even with the current Azure API I guess). But I have learnt the hard way that such wishes usually end up sitting in glorious Product Feedback Lists for ages ... &lt;/p&gt;

&lt;p&gt;Have you experienced similar issues like the above? Or have you found other solutions? Would be happy to get such input from other Azure MongoDB Atlas customers!&lt;/p&gt;

&lt;p&gt;P.S. Please vote for the following MongoDB feedback ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://feedback.mongodb.com/forums/924280-database/suggestions/43867758-higher-iops-for-small-disk-sizes-mongodb-atlas-on" rel="noopener noreferrer"&gt;Higher IOPS for small disk sizes (MongoDB Atlas on Azure)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://feedback.mongodb.com/forums/924280-database/suggestions/40458187-burstable-iops-for-mongodb-atlas-on-azure" rel="noopener noreferrer"&gt;Burstable IOPS for MongoDB Atlas on Azure
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://feedback.mongodb.com/forums/924145-atlas/suggestions/43964442-cpu-steal-on-azure" rel="noopener noreferrer"&gt;CPU Steal % on Azure
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://feedback.mongodb.com/forums/924145-atlas/suggestions/43965369-preserve-oplog-upon-storage-downgrade-in-azure" rel="noopener noreferrer"&gt;Preserve Oplog upon storage downgrade in Azure
&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;OK, you got me, additionally I have a secret hope that if someone from MongoDB Atlas and Azure reads this (s)he might trigger some internal improvement ... but realistically this never works that way, we all know that :( ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>mongodb</category>
      <category>azure</category>
    </item>
    <item>
      <title>F# App Stub for AKS hosting (with WebJobs but without Azure Functions fluff)</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Tue, 01 Jun 2021 17:01:58 +0000</pubDate>
      <link>https://dev.to/deyanp/f-app-stub-for-aks-hosting-with-webjobs-but-without-azure-functions-fluff-43lj</link>
      <guid>https://dev.to/deyanp/f-app-stub-for-aks-hosting-with-webjobs-but-without-azure-functions-fluff-43lj</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; Use a standard .NET 5 host which gives you full control (incl. upgradability!) and still allows for WebJobs syntactic sugar, instead of the fluffy and "magical" Azure Functions host. &lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In a &lt;a href="https://dev.to/deyanp/use-azure-kubernetes-service-aks-traefik-instead-of-azure-functions-hosting-azure-api-management-1gkg"&gt;previous post&lt;/a&gt; I listed a number of reasons why you'd better migrate away from Azure Functions and host your .NET (F# recommended!) apps in AKS. In this post I will try to give you as much code as you need to be able to start with this alternative approach in a matter of minutes instead of hours/days.&lt;/p&gt;

&lt;p&gt;I will create a fully-blown stub including all the functionality you may need - WebJobs (EventHubTrigger, QueueTrigger, TimerTrigger), Web Server, custom Background Services, App Insights integration, Console Log Formatting, so it will look as close to Azure Functions as possible, but with full control. &lt;/p&gt;

&lt;p&gt;Note: &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide"&gt;Azure Functions support for .NET 5&lt;/a&gt; (the so-called Isolated Host) has actually taken a similar approach, but there is still too much fluff/magic for my liking ..&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Approach/Principles
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Mimic Azure Functions setup - including the usage of WebJobs (used by Azure Functions themselves), Application Insights integration, even the same or slightly better console log formatting!&lt;/li&gt;
&lt;li&gt;"Close to the metal" or as basic as possible - just use Asp.Net Core, no framework like Saturn, Giraffe etc. required for just a few PUT/POST/PATCH/GET operations.&lt;/li&gt;
&lt;li&gt;Utilize standard HostBuilder functionality like running additional &lt;a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0&amp;amp;tabs=visual-studio#ihostedservice-interface"&gt;HostedServices&lt;/a&gt; or &lt;a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0&amp;amp;tabs=visual-studio#backgroundservice-base-class"&gt;BackgroundServices&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Program.fs
&lt;/h2&gt;

&lt;p&gt;If you don't have prior knowledge about HostBuilder then in a nutshell it is just a configurable way of spawning several threads/application services, e.g. a web server, some background services used by the WebJobs SDK for listening to Azure Storage Queues or Event Hubs for example, some other custom background processes of yours (e.g. for watching a MongoDB collection) or even some startup code which runs when the whole application starts (e.g. for filling an in-memory cache from a db table).&lt;/p&gt;

&lt;p&gt;The main bootstrapping code is in the main function and may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EntryPoint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CreateDefaultBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureLogging&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureAppInsights&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;configureWorker1&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;configureWorker2&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;HostBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configureWebHost&lt;/span&gt; &lt;span class="n"&gt;configureEndpoints&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;configureWebJobs&lt;/span&gt;

    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;tokenSource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;    
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AwaitTask&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RunSynchronously&lt;/span&gt;

    &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;// return an integer exit code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you see from above, we are configuring logging, App Insights integration, then 2 background workers (IHostedServices), the web server with 3 "HttpTriggers", and web jobs which actually means EventHubTriggers, QueueTriggers, TimerTriggers etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebJobs Configuration
&lt;/h2&gt;

&lt;p&gt;WebJobs configuration is extremely straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureWebJobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureWebJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddAzureStorageCoreServices&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddEventHubs&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddAzureStorageQueues&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddTimers&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additional services can be configured as well, e.g. &lt;code&gt;b.AddSignalR() |&amp;gt; ignore&lt;/code&gt;, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web Server Configuration
&lt;/h2&gt;

&lt;p&gt;Standard Kestrel configuration ... basic mapping of GET/PUT/POST/PATCH to functions, with manual injection of dependencies (e.g. logger) as a function parameter (even partial application not required):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureEndpoints&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IApplicationBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IEndpointRouteBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;hostedServices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;BackgroundService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findAll&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ApplicationServices&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;worker1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;BackgroundService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt; &lt;span class="n"&gt;hostedServices&lt;/span&gt; &lt;span class="s2"&gt;"Worker1"&lt;/span&gt;
    &lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"api/v1/background-services/worker1/status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;worker1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetProcessingStatusHttp&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;MappedHttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toHttpResponse&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
        &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;worker2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;BackgroundService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt; &lt;span class="n"&gt;hostedServices&lt;/span&gt; &lt;span class="s2"&gt;"Worker2"&lt;/span&gt;
    &lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"api/v1/background-services/worker2/status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;worker2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetProcessingStatusHttp&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;MappedHttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toHttpResponse&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
        &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;

    &lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"api/v1/webFunction1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;ApplicationServices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;()&lt;/span&gt;

        &lt;span class="nn"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;webFunction1&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartAsTask&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MappedHttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toHttpResponse&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureWebHost&lt;/span&gt; &lt;span class="n"&gt;configureEndpoints&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IHostBuilder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;HostingEnvironment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsDevelopment&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddCors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddDefaultPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;b&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AllowCredentials&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WithOrigins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8080"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WithHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"content-type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"if-match"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"etag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"content-disposition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"x-requested-with"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WithExposedHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"content-type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"if-match"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"etag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"content-disposition"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WithMethods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"PUT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"PATCH"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;

    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureWebHostDefaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UseRouting&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;HostingEnvironment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsDevelopment&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;        
                &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UseCors&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;

            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UseEndpoints&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;endpoints&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;configureEndpoints&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="n"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;

        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bonus: As you see above there are 2 status web functions which report the status of the threads started by the background services (see next section).&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Background Services Configuration
&lt;/h2&gt;

&lt;p&gt;You can do anything in a HostedService/BackgroundService, even get rid of the WebJobs SDK and simply listen to event hubs using EventHubProcessor for example by yourself (I have always regarded XxxTrigger as a non-mandatory syntactic sugar ...):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;startedEvent1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ManualResetEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureWorker1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IHostBuilder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IHostedService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; 
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;()&lt;/span&gt;
                &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;BackgroundService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;
                    &lt;span class="s2"&gt;"Worker1"&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;BackgroundServices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker1&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="n"&gt;startedEvent1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;IHostedService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureWorker2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IHostBuilder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IHostedService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; 
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;()&lt;/span&gt;
                &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;BackgroundService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Hosted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;
                    &lt;span class="s2"&gt;"Worker2"&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;BackgroundServices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker2&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="n"&gt;startedEvent1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;IHostedService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Logging Configuration, including Console Log Formatting
&lt;/h2&gt;

&lt;p&gt;To simulate the Azure Functions logger formatting I had to unfortunately implement a full-blown ConsoleFormatter ... but also made the log output a bit more-concise on the way ;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureLogging&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IHostBuilder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureLogging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;//        b.ClearProviders() |&amp;gt; ignore  // commented out because otherwise Application Insights does not collect logger.Information ...&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddConsole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FormatterName&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="s2"&gt;"CustomConsoleFormatter"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddConsoleFormatter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CustomConsoleFormatter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CustomConsoleFormatterOptions&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Excludes&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultExcludes&lt;/span&gt;
                &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SingleLinePerCategory&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Azure.WebJobs.Hosting.OptionsLoggingService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="s2"&gt;"Host.Triggers.Timer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dict&lt;/span&gt;
                &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SingleLine&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
                &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ColorBehavior&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nn"&gt;LoggerColorBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Application Insights Integration
&lt;/h2&gt;

&lt;p&gt;Application Insights works here as well, note that I am injecting a custom initializer and processor to suppress some http status codes etc.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;configureAppInsights&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;IHostBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IHostBuilder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;    
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;aiOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApplicationInsightsServiceOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EnableAdaptiveSampling&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// disable sampling (turned on by default, see https://docs.microsoft.com/en-us/azure/azure-monitor/app/sampling#brief-summary)&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddApplicationInsightsTelemetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aiOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ITelemetryInitializer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CustomTelemetryInitializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nn"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"WEBSITE_SITE_NAME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultSuppressedStatusCodes&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;

        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CustomTelemetryProcessorConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;defaultCustomTelemetryProcessorConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddApplicationInsightsTelemetryProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CustomTelemetryProcessor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Custom Functions
&lt;/h2&gt;

&lt;p&gt;And the last bit are the user-defined functions (="Azure Functions):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nc"&gt;BackgroundServices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;worker1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;startedEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ManualResetEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;startedEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
                &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Hello from worker1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nn"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;        
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;worker2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;startedEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ManualResetEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;startedEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
                &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Hello from worker2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nn"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;        

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nc"&gt;WebApi&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;webFunction1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MappedHttpResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"webFunction1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
                &lt;span class="nc"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Some response"&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;MappedHttpResponseContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Json&lt;/span&gt;
                &lt;span class="nc"&gt;Headers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;WebJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;telemetryConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TelemetryConfiguration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;telemetryClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TelemetryClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;telemetryConfiguration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HandleEventHubMessage"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="o"&gt;_.&lt;/span&gt;&lt;span class="nc"&gt;HandleEventHubMessage&lt;/span&gt;
        &lt;span class="o"&gt;([&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EventHubTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EventHubConnectionString"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ConsumerGroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"test-cg"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;    &lt;span class="c1"&gt;// path to event hub is in the connection string&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EventData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;enqueuedTimeUtc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sequenceNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogInformation&lt;/span&gt;&lt;span class="o"&gt;($&lt;/span&gt;&lt;span class="s2"&gt;"HandleEventHubMessage: {Encoding.UTF8.GetString(msg.Body.ToArray())}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartAsTask&lt;/span&gt;

    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HandleQueueMessage"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="o"&gt;_.&lt;/span&gt;&lt;span class="nc"&gt;HandleQueueMessage&lt;/span&gt;
        &lt;span class="o"&gt;([&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;QueueTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"test-queue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StorageQueueConnectionString"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogInformation&lt;/span&gt;&lt;span class="o"&gt;($&lt;/span&gt;&lt;span class="s2"&gt;"HandleQueueMessage: {msg}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartAsTask&lt;/span&gt;

    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HandleTimerEvent"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HandleTimerEvent&lt;/span&gt;
        &lt;span class="o"&gt;([&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TimerTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"0 0 0 1 * *"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RunOnStartup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimerInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HandleTimerEvent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Async&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StartAsTask&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you see, not much difference compared to "real" Azure Functions ...&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration from Azure Functions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add Program.fs if not already existing, and copy over the host builder pipeline (including the required WebJobs AddXxxx services)&lt;/li&gt;
&lt;li&gt;Configure WebJobs (no change required to corresponding XxxTriggers)&lt;/li&gt;
&lt;li&gt;Implement configureEndpoints, and remove HttpTrigger attributes from the http-based azure functions (no change required for the non-http azure functions, attributes remain)&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;With greater control comes bigger responsibility, however you can see from the above that the required code is not only relatively easy to grasp and maintain, but it offers much more! For example, background services can be injected nicely, and a lot of additional HostBuilder functionality can be used - there are tons of Q&amp;amp;As and documentations online. Last but not least, after migrating from Azure Functions v3 to the above "custom .NET host" we experienced a &lt;a href="https://github.com/Azure/Azure-Functions/issues/1810"&gt;50% drop in memory consumption of the AKS pods&lt;/a&gt; which I believe is attributable to the removed Azure Functions bloat, so that was another bonus for us. &lt;/p&gt;

&lt;p&gt;P.S. Working project can be found at &lt;a href="https://github.com/deyanp/FSharpAKSAppStub"&gt;https://github.com/deyanp/FSharpAKSAppStub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>fsharp</category>
      <category>azure</category>
    </item>
    <item>
      <title>BDD-style Testing in F# with Xunit.Gherkin, GherkinProvider and TickSpec</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Mon, 31 May 2021 13:56:24 +0000</pubDate>
      <link>https://dev.to/deyanp/bdd-like-testing-in-f-with-xunit-gherkin-gherkinprovider-and-tickspec-11d9</link>
      <guid>https://dev.to/deyanp/bdd-like-testing-in-f-with-xunit-gherkin-gherkinprovider-and-tickspec-11d9</guid>
      <description>&lt;p&gt;TLDR: Use TickSpec in F# for BDD-style tests, and utilize the classless or module/function-based approach with a Context record type being passed between the individual step functions.&lt;/p&gt;

&lt;p&gt;Note: The below requires basic knowledge of &lt;a href="https://dannorth.net/introducing-bdd/"&gt;Behavior-Driven Development&lt;/a&gt;/ &lt;a href="https://www.amazon.com/Specification-Example-Successful-Deliver-Software/dp/1617290084/ref=as_li_ss_tl?crid=13KUDOEYLQIP2&amp;amp;keywords=specification+by+example&amp;amp;qid=1584366389&amp;amp;sprefix=specification+,aps,253&amp;amp;sr=8-1&amp;amp;linkCode=sl1&amp;amp;tag=swingwiki-20&amp;amp;linkId=0a895a47f2d7fcb9928406d5c2c58029&amp;amp;language=en_US"&gt;Specification By Example&lt;/a&gt; / &lt;a href="https://cucumber.io/docs/gherkin/reference/"&gt;Gherkin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Over the years BDD has been helping me immensely to document the software systems I build "for free" while writing functional tests for them. Naturally, after moving to F#, one of the first topics for me was what is the most suitable BDD framework for F#? &lt;br&gt;
I had prior experience with SpecFlow in C#, and even though SpecFlow is an amazing tool with excellent IDE integration, sometimes it was getting in my way due to its code-generation approach, and its general "heaviness". So not only did I want to find a BDD framework in F#, but I wished it were a bit more lightweight than SpecFlow. &lt;/p&gt;

&lt;p&gt;This blog post contains a short analysis of the following 3 approaches, but starts with a plain-old Xunit just for the sake of basic introduction: &lt;/p&gt;

&lt;p&gt;1) Xunit.GherkinQuick&lt;br&gt;
2) GherkinProvider&lt;br&gt;
3) TickSpec&lt;/p&gt;

&lt;p&gt;But before we start - what are we testing? I have selected a set of very simple functions based on NCrontab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Crontab&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;NCrontab&lt;/span&gt;

&lt;span class="c1"&gt;/// Parses 5 (standard) or 6 (with seconds) component crontab expressions&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontabExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CrontabSchedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;crontabComponentCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crontabExpression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;StringSplitOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RemoveEmptyEntries&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="nc"&gt;Length&lt;/span&gt;
    &lt;span class="nn"&gt;CrontabSchedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Parse&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontabExpression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;CrontabSchedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseOptions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nc"&gt;IncludingSeconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontabComponentCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;/// For start and stop crontabs, e.g. start every day in the morning and stop every day in the evening&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;isRunning&lt;/span&gt;
    &lt;span class="n"&gt;startScheduleCrontab&lt;/span&gt;
    &lt;span class="n"&gt;stopScheduleCrontab&lt;/span&gt;
    &lt;span class="n"&gt;maxStartedDurationHours&lt;/span&gt;
    &lt;span class="n"&gt;maxStoppedDurationHours&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;startSchedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="n"&gt;startScheduleCrontab&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;stopSchedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="n"&gt;stopScheduleCrontab&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lastStart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;startSchedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetNextOccurrences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddHours&lt;/span&gt;&lt;span class="o"&gt;(-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;maxStartedDurationHours&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tryLast&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lastStop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stopSchedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetNextOccurrences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddHours&lt;/span&gt;&lt;span class="o"&gt;(-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;maxStoppedDurationHours&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tryLast&lt;/span&gt;
    &lt;span class="c1"&gt;//let nextStart = startSchedule.GetNextOccurrences(pointInTime, pointInTime.AddHours(maxStoppedDurationHours)) |&amp;gt; Seq.tryHead&lt;/span&gt;
    &lt;span class="c1"&gt;//let nextStop = stopSchedule.GetNextOccurrences(pointInTime, pointInTime.AddHours(maxStartedDurationHours)) |&amp;gt; Seq.tryHead&lt;/span&gt;

    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;lastStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastStop&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;lastStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;lastStop&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;lastStart&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lastStop&lt;/span&gt; 
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;_&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;failwithf&lt;/span&gt; &lt;span class="s2"&gt;"LastStart could not be found using startScheduleCrontab %s and maxStartedDurationHours %f"&lt;/span&gt; &lt;span class="n"&gt;startScheduleCrontab&lt;/span&gt; &lt;span class="n"&gt;maxStartedDurationHours&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;_,&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;failwithf&lt;/span&gt; &lt;span class="s2"&gt;"LastStop could not be found using stopScheduleCrontab %s and maxStoppedDurationHours %f"&lt;/span&gt; &lt;span class="n"&gt;stopScheduleCrontab&lt;/span&gt; &lt;span class="n"&gt;maxStoppedDurationHours&lt;/span&gt;

&lt;span class="c1"&gt;/// For single execution crontabs, e.g. once daily&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;getLastRunOn&lt;/span&gt;
    &lt;span class="n"&gt;runScheduleCrontab&lt;/span&gt;
    &lt;span class="n"&gt;maxRunIntervalHours&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;    
    &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;runSchedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="n"&gt;runScheduleCrontab&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lastRunOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runSchedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetNextOccurrences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddHours&lt;/span&gt;&lt;span class="o"&gt;(-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;maxRunIntervalHours&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tryHead&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;lastRunOn&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;lastRunOn&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;lastRunOn&lt;/span&gt; 
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;failwithf&lt;/span&gt; &lt;span class="s2"&gt;"LastRun could not be found using runScheduleCrontab %s and maxRunIntervalHours %f"&lt;/span&gt; &lt;span class="n"&gt;runScheduleCrontab&lt;/span&gt; &lt;span class="n"&gt;maxRunIntervalHours&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a nutshell, given a crontab expression for a job with start and stop schedules the function &lt;code&gt;isRunning&lt;/code&gt; is telling you whether the job is running for a given point in time. The function &lt;code&gt;getLastRun&lt;/code&gt; gives you the last time a single execution job has run calculated from a given point in time. &lt;/p&gt;

&lt;h1&gt;
  
  
  Plain-old Xunit
&lt;/h1&gt;

&lt;p&gt;Below is how this could be implemented without BDD-style feature file with Given/When/Thens, just with code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Xunit&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;Xunit&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;SimpleTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TypeExtensions&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;parseExamples&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[|&lt;/span&gt;
        &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="s2"&gt;"0 0 2 * *"&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
        &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="s2"&gt;"0 0 2 * * *"&lt;/span&gt;  &lt;span class="p"&gt;|]&lt;/span&gt;
    &lt;span class="p"&gt;|]&lt;/span&gt;

&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;MemberData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"parseExamples"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;``Crontab Parsing``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontabExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="c1"&gt;//When crontab expression &amp;lt;CrontabExpression&amp;gt; is parsed&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;crontabSchedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="n"&gt;crontabExpression&lt;/span&gt;

    &lt;span class="c1"&gt;//Then the CrontabSchedule has the same string representation&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontabSchedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crontabExpression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Parsed crontabSchedule.ToString() not equal to original crontab expression"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;isRunningExamples&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[|&lt;/span&gt;
        &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2020-10-21T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt; &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
        &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2020-10-21T08:00:00Z"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
    &lt;span class="p"&gt;|]&lt;/span&gt;

&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;MemberData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"isRunningExamples"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;``Running/Not Running Interval is identified successfully``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunningExpected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="c1"&gt;//When isRunning is called at &amp;lt;PointInTime&amp;gt; with the following parameters&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;isRunningActual&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
        &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isRunning&lt;/span&gt;
            &lt;span class="s2"&gt;"0 7 * * 1-5"&lt;/span&gt;
            &lt;span class="s2"&gt;"0 16 * * 1-5"&lt;/span&gt;
            &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="n"&gt;pointInTime&lt;/span&gt;

    &lt;span class="c1"&gt;//Then the isRunning result is &amp;lt;Result&amp;gt;&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunningExpected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isRunningActual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;getLastRunOnExamples&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[|&lt;/span&gt;
        &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2020-10-21T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2020-10-20T02:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
        &lt;span class="p"&gt;[|&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2020-10-21T08:00:00Z"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2020-10-21T02:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;|]&lt;/span&gt;
    &lt;span class="p"&gt;|]&lt;/span&gt;

&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;MemberData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"getLastRunOnExamples"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;``Last Run DateTime is retrieved successfully``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastRunOnExpected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="c1"&gt;//When getLastRunOn is called at &amp;lt;PointInTime&amp;gt; with the following parameters&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lastRunOnActual&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLastRunOn&lt;/span&gt;
            &lt;span class="s2"&gt;"0 0 2 * * *"&lt;/span&gt;
            &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="n"&gt;pointInTime&lt;/span&gt;

    &lt;span class="c1"&gt;//Then the getLastRunOn result is &amp;lt;Result&amp;gt;&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastRunOnExpected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastRunOnActual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The points of interest here are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Theory with MemberData is used to feed the test function with multiple inputs and expected outputs&lt;/li&gt;
&lt;li&gt;Comments are used to identify the Given/When/Then (or Arrange/Act/Assert) parts of the test, as all of these are bundled together in the same F# function&lt;/li&gt;
&lt;li&gt;The xunit test (=function) names cannot generally accommodate Given/When/Then, as then they would become too long ...&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before I start listing the BDD/Gherkin testing options, here is the feature file containing the Gherkin language-based scenarios:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kd"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Crontab Tests

  &lt;span class="kn"&gt;Scenario Outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Crontab Parsing
    &lt;span class="nf"&gt;When &lt;/span&gt;crontab expression &lt;span class="nv"&gt;&amp;lt;CrontabExpression&amp;gt;&lt;/span&gt; is parsed
    &lt;span class="nf"&gt;Then &lt;/span&gt;the CrontabSchedule has the same string representation

    &lt;span class="nn"&gt;Examples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;CrontabExpression&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;0&lt;/span&gt; &lt;span class="n"&gt;0&lt;/span&gt; &lt;span class="n"&gt;2&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt;         &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;0&lt;/span&gt; &lt;span class="n"&gt;0&lt;/span&gt; &lt;span class="n"&gt;2&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt;       &lt;span class="p"&gt;|&lt;/span&gt;

  &lt;span class="kn"&gt;Scenario Outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Running/Not Running Interval is identified successfully
    &lt;span class="nf"&gt;When &lt;/span&gt;isRunning is called at &lt;span class="nv"&gt;&amp;lt;PointInTime&amp;gt;&lt;/span&gt; with the following parameters
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Parameter&lt;/span&gt;               &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Value&lt;/span&gt;        &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;startScheduleCrontab&lt;/span&gt;    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;0&lt;/span&gt; &lt;span class="n"&gt;7&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt; &lt;span class="n"&gt;1-5&lt;/span&gt;  &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;stopScheduleCrontab&lt;/span&gt;     &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;0&lt;/span&gt; &lt;span class="n"&gt;16&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt; &lt;span class="n"&gt;1-5&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;maxStartedDurationHours&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;72&lt;/span&gt;           &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;maxStoppedDurationHours&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;72&lt;/span&gt;           &lt;span class="p"&gt;|&lt;/span&gt;
    &lt;span class="nf"&gt;Then &lt;/span&gt;the isRunning result is &lt;span class="nv"&gt;&amp;lt;Result&amp;gt;&lt;/span&gt;

    &lt;span class="nn"&gt;Examples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;PointInTime&lt;/span&gt;          &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;2020-10-21T00:00:00Z&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;false&lt;/span&gt;  &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;2020-10-21T08:00:00Z&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;   &lt;span class="p"&gt;|&lt;/span&gt;

  &lt;span class="kn"&gt;Scenario Outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Last Run DateTime is retrieved successfully
    &lt;span class="nf"&gt;When &lt;/span&gt;getLastRunOn is called at &lt;span class="nv"&gt;&amp;lt;PointInTime&amp;gt;&lt;/span&gt; with the following parameters
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Parameter&lt;/span&gt;           &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Value&lt;/span&gt;       &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;runScheduleCrontab&lt;/span&gt;  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;0&lt;/span&gt; &lt;span class="n"&gt;0&lt;/span&gt; &lt;span class="n"&gt;2&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt; &lt;span class="n"&gt;*&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;maxRunIntervalHours&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;24&lt;/span&gt;          &lt;span class="p"&gt;|&lt;/span&gt;
    &lt;span class="nf"&gt;Then &lt;/span&gt;the getLastRunOn result is &lt;span class="nv"&gt;&amp;lt;Result&amp;gt;&lt;/span&gt;

    &lt;span class="nn"&gt;Examples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;PointInTime&lt;/span&gt;          &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Result&lt;/span&gt;               &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;2020-10-21T00:00:00Z&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;2020-10-20T02:00:00Z&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;2020-10-21T08:00:00Z&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;2020-10-21T02:00:00Z&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Xunit.Gherkin.Quick
&lt;/h1&gt;

&lt;p&gt;There is a hidden gem for Xunit called &lt;a href="https://github.com/ttutisani/Xunit.Gherkin.Quick"&gt;Xunit.Gherkin.Quick&lt;/a&gt; which allows you to create standard feature files using the Gherkin language, and automate these with Xunit-based tests. &lt;/p&gt;

&lt;p&gt;As usual a nuget package reference is required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Gherkin.TypeProvider"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"0.1.10"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As usual a nuget package reference is required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PackageReference&lt;/span&gt; &lt;span class="nc"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Xunit.Gherkin.Quick"&lt;/span&gt; &lt;span class="nc"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4.1.0"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The feature file must be in some folder (e.g. in the project folder), but it does not need to be added to the fproj file in a special way (Content/EmbeddedResource).&lt;/p&gt;

&lt;p&gt;Here is how the code based on Xunit.Gherkin.Quick could look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;XunitGherkinQuick&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Gherkin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ast&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;NCrontab&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;Xunit&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Xunit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Gherkin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Quick&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;SimpleTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TypeExtensions&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nc"&gt;Table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;DataTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rows&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cells&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Cells&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ofSeq&lt;/span&gt;
            &lt;span class="n"&gt;cells&lt;/span&gt;&lt;span class="o"&gt;.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;].&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Cells&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;Xunit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Gherkin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Quick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FeatureFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./CrontabTests.feature"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;CrontabTests&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;inherit&lt;/span&gt; &lt;span class="nc"&gt;Feature&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mutable&lt;/span&gt; &lt;span class="n"&gt;ctxCrontabExpression&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mutable&lt;/span&gt; &lt;span class="n"&gt;ctxCrontabSchedule&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CrontabSchedule&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mutable&lt;/span&gt; &lt;span class="n"&gt;isRunningActual&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mutable&lt;/span&gt; &lt;span class="n"&gt;lastRunOnActual&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt;

    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"crontab expression (.+) is parsed"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``When crontab expression &amp;lt;CrontabExpression&amp;gt; is parsed``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontabExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;ctxCrontabExpression&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;crontabExpression&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt;
        &lt;span class="n"&gt;ctxCrontabSchedule&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="n"&gt;crontabExpression&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt;

    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"the CrontabSchedule has the same string representation"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``Then the CrontabSchedule has the same string representation``&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctxCrontabSchedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctxCrontabExpression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Parsed crontabSchedule.ToString() not equal to original crontab expression"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"isRunning is called at (.*) with the following parameters"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``When isRunning is called at &amp;lt;PointInTime&amp;gt; with the following parameters``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datatable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;DataTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;isRunningActual&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt; 
            &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isRunning&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"startScheduleCrontab"&lt;/span&gt; &lt;span class="n"&gt;datatable&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"stopScheduleCrontab"&lt;/span&gt; &lt;span class="n"&gt;datatable&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"maxStartedDurationHours"&lt;/span&gt; &lt;span class="n"&gt;datatable&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToDouble&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"maxStoppedDurationHours"&lt;/span&gt; &lt;span class="n"&gt;datatable&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToDouble&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt;

    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"the isRunning result is (.*)"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``Then the isRunning result is &amp;lt;Result&amp;gt;``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunningExpected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunningExpected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isRunningActual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"getLastRunOn is called at (.*) with the following parameters"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``When getLastRunOn is called at &amp;lt;PointInTime&amp;gt; with the following parameters``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datatable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;DataTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;lastRunOnActual&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;-&lt;/span&gt;
            &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLastRunOn&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"runScheduleCrontab"&lt;/span&gt; &lt;span class="n"&gt;datatable&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"maxRunIntervalHours"&lt;/span&gt; &lt;span class="n"&gt;datatable&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToDouble&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt;

    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"the getLastRunOn result is (.*)"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``Then the getLastRunOn restheult is &amp;lt;Result&amp;gt;``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastRunOnExpected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastRunOnExpected&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastRunOnActual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The points of interest here are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The feature file is referenced using a class attribute, which automatically means you must have 1 class per feature file (which turns out to be a good thing)&lt;/li&gt;
&lt;li&gt;The function names can be anything, here I have just used 1:1 the step text, because the actual mapping between a step (= line in the feature file) and a method in the implementation is done by using method-level attributes. This is a bit of a hassle, because you wonder how to name your methods ...&lt;/li&gt;
&lt;li&gt;Mutable variables are used to transfer state from Given-&amp;gt;When-&amp;gt;Then step functions&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  GherkinProvider
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/bddkickstarter/Gherkin.TypeProvider"&gt;Gherkin Provider&lt;/a&gt; is a &lt;a href="https://docs.microsoft.com/en-us/dotnet/fsharp/tutorials/type-providers/"&gt;F# Type Provider&lt;/a&gt;, which means it automatically generates types based on the feature file while you are typing in your IDE - magic! ;) I was (and still am) very excited when I first saw it, as it has the promise to bring the best of 2 worlds: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scenarios + Examples extracted into a very readable feature file&lt;/li&gt;
&lt;li&gt;Code for Given/When/Then residing in the same function!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As usual a nuget package reference is required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Gherkin.TypeProvider"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"0.1.10"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The feature file must be added to the project as Content, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt; &lt;span class="nc"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CrontabTests.feature"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below an implementation with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GherkinProvider&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;Xunit&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;FSharp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Gherkin&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;GherkinProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Validation&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Xunit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ordering&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;SimpleTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TypeExtensions&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;TestFeature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GherkinProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="o"&gt;(__&lt;/span&gt;&lt;span class="nc"&gt;SOURCE_DIRECTORY__&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"/CrontabTests.feature"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;TestFeature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CreateFeature&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;assertScenarioInSync&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nn"&gt;TestFeature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TestFeature_ScenarioBase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Visited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Steps&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Visited&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;parseExamples&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Scenarios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``Crontab Parsing``&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Examples&lt;/span&gt;
    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;CrontabExpression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;|])&lt;/span&gt;

&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;MemberData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"parseExamples"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;``Parsing of Crontabs works``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontabExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Scenarios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``Crontab Parsing``&lt;/span&gt;

    &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``0 When crontab expression &amp;lt;CrontabExpression&amp;gt; is parsed``&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;crontabSchedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="n"&gt;crontabExpression&lt;/span&gt;

    &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``1 Then the CrontabSchedule has the same string representation``&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontabSchedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crontabExpression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Parsed crontabSchedule.ToString() not equal to original crontab expression"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;assertScenarioInSync&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;isRunningExamples&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Scenarios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``Running_Not Running Interval is identified successfully``&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Examples&lt;/span&gt;
    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;PointInTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;|])&lt;/span&gt;

&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;MemberData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"isRunningExamples"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;``Running/Not Running Interval is identified successfully``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunningExpected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Scenarios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``Running_Not Running Interval is identified successfully``&lt;/span&gt;

    &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``0 When isRunning is called at &amp;lt;PointInTime&amp;gt; with the following parameters``&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``0 When isRunning is called at &amp;lt;PointInTime&amp;gt; with the following parameters``&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Argument&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;isRunningActual&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
        &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isRunning&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"startScheduleCrontab"&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"stopScheduleCrontab"&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"maxStartedDurationHours"&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToDouble&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"maxStoppedDurationHours"&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToDouble&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``1 Then the isRunning result is &amp;lt;Result&amp;gt;``&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunningExpected&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isRunningActual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;assertScenarioInSync&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;getLastRunOnExamples&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Scenarios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``Last Run DateTime is retrieved successfully``&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Examples&lt;/span&gt;
    &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;PointInTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;|])&lt;/span&gt;

&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;MemberData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"getLastRunOnExamples"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;``Last Run DateTime is retrieved successfully``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastRunOnExpected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Scenarios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``Last Run DateTime is retrieved successfully``&lt;/span&gt;

    &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``0 When getLastRunOn is called at &amp;lt;PointInTime&amp;gt; with the following parameters``&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``0 When getLastRunOn is called at &amp;lt;PointInTime&amp;gt; with the following parameters``&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Argument&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lastRunOnActual&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLastRunOn&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"runScheduleCrontab"&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"maxRunIntervalHours"&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToDouble&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToStringIso&lt;/span&gt;

    &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;``1 Then the getLastRunOn result is &amp;lt;Result&amp;gt;``&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastRunOnExpected&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastRunOnActual&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;assertScenarioInSync&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;

&lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Int32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MaxValue&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;validateFeatureVisited&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeatureValidator&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Validate&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;failwith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Points of interest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When you load the feature file it autogenerates a type with a number of neested types (line 10)&lt;/li&gt;
&lt;li&gt;The examples are passed to the functions by using standard Xunit Theory + Member Data, very low-level, no magic&lt;/li&gt;
&lt;li&gt;Because the sub-types are autogenerated, you cannot easily create functions operating on different subtypes&lt;/li&gt;
&lt;li&gt;There are strange statements inside the test functions like &lt;code&gt;scenario. \&lt;/code&gt;&lt;code&gt;1 Then the getLastRunOn result is &amp;lt;Result&amp;gt;\&lt;/code&gt;&lt;code&gt;|&amp;gt; ignore&lt;/code&gt; and there is the &lt;code&gt;validateFeatureVisited&lt;/code&gt; invoked at the end (that's why &lt;code&gt;Order(Int32.MaxValue)&lt;/code&gt; attribute[^1]) to make sure that all features are visited. I have nothing against the former, as they serve as strongly-typed comments (compare with plain-old Xunit sample at the beginning), but &lt;code&gt;validateFeatureVisited&lt;/code&gt; is a bit too much for me ...&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  TickSpec
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/fsprojects/TickSpec"&gt;TickSpec&lt;/a&gt; is the dinasour in the room (dating back to 2010!!) but remarkably straightforward (compared to SpecFlow). It does not use code-generation magic, but it has a little magic related to how steps (in feature files) are mapped to functions and how step parameters (e.g. tables) are mapped to function parameters, and how parameters are passed between Given/When/Then functions. &lt;/p&gt;

&lt;p&gt;As usual a nuget package reference is required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"TickSpec"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.0.0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The feature file must be added to the project as Content, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EmbeddedResource&lt;/span&gt; &lt;span class="nc"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CrontabTests.feature"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An implementation of automated tests for the Crontab functions looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features.fs&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TickSpec&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Diagnostics&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TickSpec&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;Xunit&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;TickSpecXunitWiring&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Version2&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;Features&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AssemblyStepDefinitionsSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Reflection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetExecutingAssembly&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;scenarios&lt;/span&gt; &lt;span class="n"&gt;resourceName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ScenariosFromEmbeddedResource&lt;/span&gt; &lt;span class="n"&gt;resourceName&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;MemberData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ofScenarios&lt;/span&gt;

    &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nc"&gt;MemberData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"scenarios"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Framework.Crontab.Tests.CrontabTests.feature"&lt;/span&gt;&lt;span class="o"&gt;)&amp;gt;]&lt;/span&gt;
    &lt;span class="k"&gt;member&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CrontabTests&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;XunitSerializableScenario&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
        &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ScenarioAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="nc"&gt;Invoke&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CrontabTests.TickSpec.fs&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;CrontabTests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TickSpec&lt;/span&gt;

&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;NCrontab&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;Xunit&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nc"&gt;TickSpec&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;
&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;SimpleTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TypeExtensions&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;ParseContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;CrontabExpression&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="nc"&gt;ParsedCrontabSchedule&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CrontabSchedule&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; 

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;``crontab expression (.*) is parsed``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crontabExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;crontabSchedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="n"&gt;crontabExpression&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;CrontabExpression&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crontabExpression&lt;/span&gt;
        &lt;span class="nc"&gt;ParsedCrontabSchedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crontabSchedule&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;``the CrontabSchedule has the same string representation``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ParseContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;ParsedCrontabSchedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToString&lt;/span&gt;&lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CrontabExpression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Parsed crontabSchedule.ToString() not equal to original crontab expression"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;``isRunning is called at (.*) with the following parameters``&lt;/span&gt; &lt;span class="n"&gt;pointInTime&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isRunning&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;VTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"startScheduleCrontab"&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;VTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"stopScheduleCrontab"&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;VTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"maxStartedDurationHours"&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rows&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;VTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"maxStoppedDurationHours"&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rows&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;``the isRunning result is (.*)``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunningExpected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunningActual&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunningExpected&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isRunningActual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;``getLastRunOn is called at (.*) with the following parameters``&lt;/span&gt; &lt;span class="n"&gt;pointInTime&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nn"&gt;Framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Crontab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLastRunOn&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;VTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"runScheduleCrontab"&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;VTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValueByKey&lt;/span&gt; &lt;span class="s2"&gt;"maxRunIntervalHours"&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rows&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pointInTime&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;``the getLastRunOn result is (.*)``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastRunOnExpected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastRunOnActual&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastRunOnExpected&lt;/span&gt; &lt;span class="p"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseUtcIso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastRunOnActual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Points of interest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;There is no need for a class + member methods, just a plain module with functions can be used!&lt;/li&gt;
&lt;li&gt;Functions are mapped to steps by &lt;strong&gt;name&lt;/strong&gt; - no need for attributes!&lt;/li&gt;
&lt;li&gt;Function arguments are automatically captured by TickSpec based on simple regular expressions&lt;/li&gt;
&lt;li&gt;Examples are handled by TickSpec after doing some Xunit wiring involving the usual suspects - Theory and MemberData, however the code is minimal and TickSpec does some function argument magic again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note the following potential pitfalls related to TickSpec function/argument mapping magic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initially we did experience some strange error messages related to arguments not mapped correctly, but as soon as we started using a Context record type per feature all these were gone. That one could look like this:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight fsharp"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Value1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt;
    &lt;span class="nc"&gt;Value2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;``some given step``&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="nc"&gt;Value1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="nc"&gt;Value2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;When&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;``some when step``&lt;/span&gt; &lt;span class="bp"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Value2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;[&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;]&lt;/span&gt; &lt;span class="n"&gt;``some then step (.*)``&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;someArgOfThisStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nn"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;someArgOfThisStep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nn"&gt;Value2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Additionally, quite a few times TickSpec was reporting missing step implementation error, and it turned out we have forgotten the &lt;code&gt;()&lt;/code&gt; of a function which has no arguments ;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end we decided to focus on using TickSpec for all our automated tests (yes, including unit tests ... [^2]) due to its elegant approach of mapping steps/examples with functions and their arguments. &lt;/p&gt;

&lt;p&gt;P.S. Project can be found at: &lt;a href="https://github.com/deyanp/FSharpBDDComparison"&gt;https://github.com/deyanp/FSharpBDDComparison&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[^1] Which requires &lt;code&gt;[&amp;lt;assembly: Xunit.TestCaseOrderer("Xunit.Extensions.Ordering.TestCaseOrderer", "Xunit.Extensions.Ordering")&amp;gt;]&lt;/code&gt; in AssemblyInfo.fs or similar&lt;br&gt;
[^2] I/we do not care too much about the question if BDD-style test automation should or should not be used for unit tests. See &lt;a href="https://www.youtube.com/watch?v=YL1q3tDgImM"&gt;Behavior Driven Development vs Unit Testing&lt;br&gt;
&lt;/a&gt; for a discussion I am not sure I agree 100% with ..&lt;/p&gt;

</description>
      <category>fsharp</category>
      <category>bdd</category>
    </item>
    <item>
      <title>Use Azure Kubernetes Service (AKS) + Traefik instead of Azure Functions hosting + Azure API Management</title>
      <dc:creator>Deyan Petrov</dc:creator>
      <pubDate>Tue, 20 Apr 2021 11:31:36 +0000</pubDate>
      <link>https://dev.to/deyanp/use-azure-kubernetes-service-aks-traefik-instead-of-azure-functions-hosting-azure-api-management-1gkg</link>
      <guid>https://dev.to/deyanp/use-azure-kubernetes-service-aks-traefik-instead-of-azure-functions-hosting-azure-api-management-1gkg</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; You'd better use AKS (incl. Traefik or similar ingress controller) instead of Azure Functions App Service/Premium/Consumption Plans + Azure API Management for hosting your microservices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: The context of this blog post are systems with a fair number of API/transaction requests per day (e.g. 50k+) with API calls (almost) every second for the majority of the day, and standard non-functional requirements like  response times must be &lt;em&gt;way below&lt;/em&gt; 1 second, top-notch security, etc. This is not addressing hobby projects with public access to everything and occasional 1-2 API calls per day and/or a single monthly peak of 1000 API calls/hour!&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Being aware of the latest serverless trend I started a project with the "fully serverless" approach in mind, which in MS/Azure world means Azure Functions framework for building our .NET Core applications, and using Azure Functions Hosting Plans on Azure. The Azure Functions were "hidden" behind Azure API Management (APIM), so that  JWT Tokens (from a web-based backoffice UI) and api keys (from B2B integrations) could be centrally validated.&lt;/p&gt;

&lt;p&gt;Fast-forward to today and we are currently using none of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Azure Functions framework&lt;/li&gt;
&lt;li&gt;Azure Functions hosting in Consumption/Premium/App Service plan&lt;/li&gt;
&lt;li&gt;APIM&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead we are pretty happy with these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Standard .NET 5 apps (using WebJobs SDK only as a syntactic sugar for some non-HTTP triggers)&lt;/li&gt;
&lt;li&gt;AKS&lt;/li&gt;
&lt;li&gt;Traefik (running in AKS)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why did we change our mind? There were numerous reasons for that, and I will try to explain each one below. But first a short overview of Azure Functions and APIM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure Functions Hosting
&lt;/h2&gt;

&lt;p&gt;An Azure Functions App can be hosted in &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-scale" rel="noopener noreferrer"&gt;multiple ways&lt;/a&gt;: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Consumption plan&lt;/li&gt;
&lt;li&gt;Premium plan&lt;/li&gt;
&lt;li&gt;Dedicated App Service plan (one of the oldest Azure offerings)&lt;/li&gt;
&lt;li&gt;App Service Environment (ASE) &lt;/li&gt;
&lt;li&gt;Azure Kubernetes Service (AKS)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When I read the list I was thinking - wow, I can't go wrong with Azure Functions, even if one hosting option turns out to be sub-optimal there are so many others! Upon a second look though 2 of the options should be removed straight away: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Consumption plan has unsurmountable cold start issues, and no VNET integration&lt;br&gt;
a. Yes, we did implement a health-check calling every single function app (50+) every 5 minutes, plus integrated this in Azure DevOps pipelines, however this keeps warm 1 &lt;em&gt;existing&lt;/em&gt; instance only, not any new instance created due to scale out&lt;br&gt;
b. Yes, we did implement WarmupTrigger in our function apps, however we still experienced cold starts&lt;br&gt;
c. Without VNET integration, you cannot effectively hide all function apps from the Internet and connect them with any other hidden infrastructure like Azure Key Vaults, Event Hubs etc, unless you start playing with source IP restrictions, and this becomes very quickly unmaintainable, latest when you start whitelisting whole Azure datacenters/regions ...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;App Service Environment (ASE) is a super expensive dinosaur which seems to be intended for enterprise customers. ASEv2 requires a monthly fee of more than EUR 1000 for "flat stamping" (whatever that means). ASEv3 (preview) reduces that to USD 280/month for merely 2 vCPUs and 8 GB of RAM of processing capacity ... Not to mention the complexity of network configurations, etc. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So you are effectively left with options 2, 3 and 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure Functions Application Framework
&lt;/h2&gt;

&lt;p&gt;Azure Functions as Application Framework with .NET Core 3.1 takes your "functions" decorated with a &lt;code&gt;FunctionName&lt;/code&gt; attribute, and runs them in a magical way &lt;sup id="fnref1"&gt;1&lt;/sup&gt; within the Azure Functions WebScript Host (provided by MS). You do not have a &lt;code&gt;Program.cs/fs&lt;/code&gt; with &lt;code&gt;main&lt;/code&gt; or similar, if you want to wire up some start up code you have to implement a class and use an assembly-level &lt;code&gt;FunctionsStartup&lt;/code&gt; attribute so that the Azure Functions runtime can find it (awkwardness to the max ;)&lt;/p&gt;

&lt;p&gt;Azure Functions with .NET 5 lets you run your code in a separate process and you have a &lt;code&gt;Program.cs/fs&lt;/code&gt; with &lt;code&gt;main&lt;/code&gt;, however all calls to your code will be passing first the Azure Functions WebScript host and then the latter will invoke your code via GRPC &lt;sup id="fnref2"&gt;2&lt;/sup&gt; ...&lt;/p&gt;

&lt;p&gt;There is no GRPC support on the horizon for Azure Functions to my knowledge, which means that your inter-service calls cannot use GRPC ... FYI, everybody is using GRPC for inter-service calls due to significant performance benefits, and also .NET Core 3.1/.NET 5.0 have very good GRPC support already.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure API Management (APIM)
&lt;/h2&gt;

&lt;p&gt;APIM is a pretty sophisticated service which wants to intimately know your APIs. You import your Open API / Swagger yamls for example, and it enumerates all operations, so it understands your APIs. Based on that APIM can also render a Web UI where you can list your APIs, test them, etc. Defining Products and Subscriptions comes on top, as well as various Policy which can be injected at different levels. &lt;/p&gt;

&lt;p&gt;APIM &lt;a href="https://azure.microsoft.com/en-us/pricing/details/api-management/" rel="noopener noreferrer"&gt;comes in several tiers&lt;/a&gt;, but note that even though most of them may support your requests/second requirements, only 2 of them have VNET support: Developer (non-production use) and Premium (at staggering EUR 2350/month). &lt;/p&gt;

&lt;p&gt;Does that sound like a real enterprise offering? Well, if this is not an enterprise offering, I don't know what is ...&lt;/p&gt;

&lt;h2&gt;
  
  
  Reasons for migrating away from Azure Functions + APIM
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Costs
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Azure Functions Premium Plan Costs&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-premium-plan?tabs=portal#available-instance-skus" rel="noopener noreferrer"&gt;There are 3 instances sizes&lt;/a&gt;:&lt;/p&gt;

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

&lt;p&gt;and the &lt;a href="https://azure.microsoft.com/en-us/pricing/details/functions/" rel="noopener noreferrer"&gt;billing is&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;which means more than 100 EUR/month for a single instance of the smallest 1 vCpu and 3.5GB memory!!&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Azure Functions App Service Plan Costs&lt;/p&gt;

&lt;p&gt;Premium v3 (VNET Integration)&lt;/p&gt;

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

&lt;p&gt;In contrast, AKS node billing is actually the &lt;a href="https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux/" rel="noopener noreferrer"&gt;standard Azure Virtual Machine billing&lt;/a&gt;, so you can get a standard VM with 2 vCPUs + 8 Gb RAM for as low 70-80 EUR/month excluding any discounts for 1-3y reservations, or spot pricing, etc.! Traefik is an open-source project with a pretty sufficient for our uses free community edition ...&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;p&gt;We experienced serious problems running 5 &lt;em&gt;different&lt;/em&gt; function apps only on the smallest SKUs of Azure Functions Premium (EP1) or App Service Plan (P1V2) ... Not only the applications were slow at processing a very low number of requests, but the memory utilization was very high (due to only 3,5Gb of RAM, much less available though).&lt;/p&gt;

&lt;p&gt;In contrast, we are easily running almost 50 apps in AKS per node (every node with 2 vCPU + 16GB RAM, costing around 90 EUR/month), with less than 20% max CPU utilization and less than 55% RAM utilization ...&lt;/p&gt;

&lt;h3&gt;
  
  
  Security
&lt;/h3&gt;

&lt;p&gt;Imagine you want to secure your fairly simple APIM + Azure Functions + some underlying Azure services "modern" architecture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3a1jro9bd0ic2lihsfl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3a1jro9bd0ic2lihsfl.png" alt="image"&gt;&lt;/a&gt;&lt;br&gt;
Diagram: "Modern" serverless architecture based on Azure Functions &lt;/p&gt;

&lt;p&gt;You are using the "serverless/consumption" versions of Azure Functions and APIM ... you will be surprised by quite a few imho unsurmountable security challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;VNET Integration&lt;/p&gt;

&lt;p&gt;I am not getting why in 2021 Azure allows you to create resources without a VNET (VPC) ... For hobby projects I understand, but any serious enterprise-grade system cannot survive without vnets ... It is also beyond my understanding why the Consumption Plan has no VNET Integration (besides internal technical limitations of Microsoft's implementation, or marketing/pricing agendas) ... I see talented &lt;a href="https://youtu.be/WQQkVHXBle8" rel="noopener noreferrer"&gt;people wasting their time trying to find Source IP Restriction-based approaches to security their Consumption Plan hosted Azure Functions&lt;/a&gt; and I am wondering why they are doing this to themselves ...&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network security for the underlying Storage Account - &lt;a href="https://github.com/Azure/Azure-Functions/issues/1349#issuecomment-787492922" rel="noopener noreferrer"&gt;some people are still not able in 2021 to secure&lt;/a&gt; the required by Azure Functions runtime storage account ... this has been a major oustanding issue for the past several years, and even though MS have finally fixed that I am not 100% sure if it has been rolled out everywhere ... &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;With Premium and App Service Plans VNET Integration exists, however with the former 1 of my function apps once lost its VNET Integration (= full downtime). The answer to my MS Support ticket (120051322002711) was to host my function app in other region in addition, and put azure front door or similar on top ... I am not saying that this incident can/will repeat for you, I just have the feeling that the VNET Integration was bolted on the Azure Functions Premium Plan more of as an afterthought rather than a mandatory underlying infrastructure principle ...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Function Apps on Consumption Plan do not have reliable &lt;code&gt;outboundIPAddresses&lt;/code&gt;/&lt;code&gt;possibleOutboundIPAddresses&lt;/code&gt;, all IP ranges of the whole Azure region must be whitelisted (&amp;gt; 127 IP ranges), however the firewall rules in Key Vault allows for max 127 rules (no, this is not a joke)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If function apps and storage accounts are in the same region, then connectivity function apps -&amp;gt; storage accounts goes via private Azure network (e.g. 10.150.*), however whitelisting of public ip ranges is only allowed in storage account firewall (or vnets, no private ip ranges – no, this is not a joke either).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It turns out that Azure API Management on Consumption Plan does not have a dedicated IP, so whitelisting of the whole Azure West Europe datacenter IP addresses must be performed, which is clashes with 127 rules max limit for firewalls in Azure. Azure Support is suggesting additionally to use the 40k json file with all Azure IP ranges, however that requires heavy investment in setting up a function app to parse the file on a weekly basis and to update the firewall rules in all ip-restricted services. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vnet Integration for APIM is only possible in Developer (no SLA, gets restarted with downtime on a monthly basis) or Premium Tier (costs EUR 2500/month). Numerous ideas in the feedback center, no result.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Slow deployment + 100% CPU
&lt;/h3&gt;

&lt;p&gt;On both App Service Linux and Windows Plans we experienced very high CPU peak every time we deployed (incl. slot swap). We had only a few (max 5) function apps hosted on the smallest (1 vCore) instance, with no application load whatsoever (no requests). Microsoft Support's answer was: "It is normal to have some CPU increase during deployment for a short period of time and it should not impact the overall availability of the functions.". Of course, it did affect the performance of the functions dramatically ... &lt;/p&gt;

&lt;p&gt;With the App Service Plan the deployment is handled by the Kudu management container where it is using the local storage for deployment which is also slower and besides this there are some additional intermediary steps in the deployment.&lt;/p&gt;

&lt;p&gt;Additionally, we experienced from time to time failed deployments with errors like "Bad Gateway 502", or sometimes 409.&lt;/p&gt;

&lt;p&gt;I think there is a general issue with the performance of function apps on the App Service Linux plan, and that may be the root cause for the deployment issues …&lt;/p&gt;

&lt;p&gt;In contrast, deploying to AKS is very fast (less than 50 seconds) and has no measurable impact on the CPU utilization of the node (2 vCPU, 16 GB RAM).&lt;/p&gt;

&lt;h3&gt;
  
  
  Inter-service (Service-to-Service) API calls
&lt;/h3&gt;

&lt;p&gt;In the context of Microservice Architecture sooner or later you will need to have synchronous (REST or GRPC) calls from one app to another. Yes, you should try not to have such, yes, you should try to make everything based on events/messages via message bus ... however, the reality is that you will definitely reach some consistency limitations which will force you to have such inter-service calls. &lt;/p&gt;

&lt;p&gt;In case you are using Azure Functions Subscription Plan you will be charged for every inter-service call ... imagine you have an orchestration service1, which is making a GET to service2, then making POST to service3 and service4 ... then you will be charged for additional 3 calls for every API call to the orchestration service.&lt;/p&gt;

&lt;p&gt;In case of Premium or App Service Plan, your inter-service calls will traverse the Azure network stack, which is much slower than having service calls on the same node as is the case for AKS. We were even forced to deploy several microservices as 1 deployment unit with in-process calls instead (using project references, which was causing a big deployment mess) ...&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Azure Functions peculiarities
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Strange limitations of length of app name&lt;/p&gt;

&lt;p&gt;Update of Azure Function's App Application Settings from the UI takes 2x longer for App Service Linux hosted function app compared to Premium Windows hosted function app.&lt;br&gt;
It seems that if we have a long function name (e.g. 59 characters) everything is slower, including updating Application Settings in the UI.&lt;br&gt;
MS Support recommends keeping function app names under 40 characters especially when working with Deployment Slots because there is a limit set in for the length of the hostname that is generated from the function name ( app_name.azurewebsites.net ), and as the name of the deployment slot is added to the host name it can get truncated and could cause issues with deployment or slot swapping.&lt;/p&gt;

&lt;p&gt;In contrast, K8s &lt;a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/names/" rel="noopener noreferrer"&gt;defines&lt;/a&gt; name limits of up to 253 characters ...  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Strange tooling (Kudu)&lt;/p&gt;

&lt;p&gt;While investigating an issue with a function app I got the request from MS Support to "please login to the kudu site" .. of the function app "... or select the functionapp-&amp;gt; platform features-&amp;gt; AdvancedTools(kudu)". Kudu seems to a very old and custom web UI for App Service ... with 100% guarantee that any knowledge gained is not transferrable to any other cloud provider or similar ;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Strange settings&lt;/p&gt;

&lt;p&gt;There are a number of "magic" settings to be considered like &lt;code&gt;Always On&lt;/code&gt; so that the app runs correctly. On an App Service plan, the functions runtime goes idle after a few minutes of inactivity, so only HTTP triggers will 'wake up' your functions.&lt;br&gt;
Whenever you are doing run from packages it is recommended to have the &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE&lt;/code&gt; setting in the app settings.&lt;br&gt;
For VNET Integration of Premium Plan-hosted function apps for example you should set &lt;code&gt;WEBSITE_VNET_ROUTE_ALL=1&lt;/code&gt; and &lt;code&gt;WEBSITE_DNS_SERVER=168.63.129.16&lt;/code&gt; ...&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Other APIM-related hassles
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;APIM setup takes extremely long time &lt;/p&gt;

&lt;p&gt;Extremely long means more than half an hour. The "serverless" tier is the only exception and takes much less, but compare the features before you decide for it (remember - no VNET Integration!)&lt;/p&gt;

&lt;p&gt;In contrast Traefik takes seconds to install in AKS/K8s cluster (we are using simple yaml deployment files, no helm charts).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Same server resources seem to be shared between the Developer Portal and API requests&lt;/p&gt;

&lt;p&gt;Can it be that someone browsing your API Web Portal has an impact on your REST API response time SLA?&lt;/p&gt;

&lt;p&gt;In contrast AKS/K8s has &lt;a href="https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits" rel="noopener noreferrer"&gt;extensive requests/limits capabilities&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CPU utilization for a prolonged time&lt;/p&gt;

&lt;p&gt;We were paying 125 EUR/month for 1 unit which should allow us 1000 rps, but even with only 20-30 requests per &lt;strong&gt;5 minutes&lt;/strong&gt; we were seeing 50% capacity utilization of our APIM instance. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa6spdc248bad68sx6lyh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa6spdc248bad68sx6lyh.png" alt="image"&gt;&lt;/a&gt;&lt;br&gt;
Diagram: 2 APIM instances with same minimal load, the one one the right showing high utilization for no reason&lt;/p&gt;

&lt;p&gt;After some useless roundtrips with the MS Support person (no, we have absolutely no load on the system currently, no complex polices, nothing ...) he indicated, that such capacity utilization is to be ignored, and might happen due to updates of a freshly set up APIM instance for a duration of a couple of days ... &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cumbersome APIM deployment scripts&lt;/p&gt;

&lt;p&gt;Creating an APIM instance and configuring it requires some heavy scripting, as you need to additionally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define Products&lt;/li&gt;
&lt;li&gt;Define Policies&lt;/li&gt;
&lt;li&gt;Define Subscriptions&lt;/li&gt;
&lt;li&gt;Import APIs from swagger yamls for example&lt;/li&gt;
&lt;li&gt;Create Revisions of the APIs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Policies alone can be defined on different levels - Product, all APIs, single API (all operations), single operation. &lt;/p&gt;

&lt;p&gt;Until recently the full functionality of APIM could only be configured with Powershell, as Azure CLI did not have the support ... &lt;/p&gt;

&lt;p&gt;The creation of the deployment scripts took us tons of time (still fragile), and their execution was also pretty lengthy. &lt;/p&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;h3&gt;
  
  
  Azure Functions Runtime/Framework issues
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;The fact that Azure Functions Team "missed" the .NET 5 launch by 4-5 months (Nov 2021 - March 2022) is pretty well known. I think MS Team got surprised that not all of their customers are "enterprise developers" lagging 1-2 years behind latest technologies due to big companies upgrading slowly ... The promise is that this will not repeat with .NET 6+ ...&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;2: Now we have 2 parallel runtimes - in-process (.NET Core 3.1) and out-of-process (.NET 5) and it seems this duality will continue for a couple of years ... Not sure how often you have to switch in-process -&amp;gt; out-of-process -&amp;gt; in-process ...&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;With the out-of-process model (.NET 5) the Azure Functions Runtime calls your functions using GRPC ... as mentioned above, not sure what is the performance impact of that vs. fully in process &lt;sup id="fnref2"&gt;2&lt;/sup&gt;. I am not sure I want to use GRPC inside my application process just because MS wants to have out-of-process now ...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Azure Functions Runtime &lt;a href="https://github.com/Azure/Azure-Functions/issues/1810" rel="noopener noreferrer"&gt;seems to consume 2 times more memory&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GRPC is not supported for inter-service communications, so a microservice based on the Azure Functions runtime cannot expose GRPC interface, which is easily possible with non-Azure Functions Runtime hosted .NET 5 app&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary and Recommendation
&lt;/h2&gt;

&lt;p&gt;Long story short, my personal recommendation is: do not waste your time with Azure Functions hosting (or even application framework) and APIM for any serious project with standard security/performance requirements, and with the goal of having competitive pricing for hosting ... Or in other words, use Azure Functions only for quick-and-dirty hobby projects, or one-off jobs (maybe some glue infrastructure code), or in case performance, security and costs are absolutely no factors. &lt;br&gt;
Additionally, value your time and better invest in learning something standard like Kubernetes where you can use your knowledge across clouds and on premise, instead of learning all Azure Functions peculiarities, which IMHO may be a result of Microsoft's trying to reuse existing (but with lots of heritage) Azure App Service/Web Script platform/framework for Azure Function ... &lt;/p&gt;

&lt;p&gt;In detail:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Migrate from Azure Functions Hosting (Consumption/Premium/App Service Plan) to AKS&lt;sup id="fnref3"&gt;3&lt;/sup&gt; for much lower costs, better security, and well-known container management&lt;/li&gt;
&lt;li&gt;Migrate from Azure API Management to Traefik or similar K8s Ingress Controller for reducing costs, simplifying management and deployment. &lt;/li&gt;
&lt;li&gt;Migrate from Azure Functions .NET Core 3 (in-process) or .NET 5 (out-of-process) runtime to standard .NET 5 application using WebJobs SDK directly for independence, faster upgrades, lower memory consumption, GRPC and many other possibilities.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If there is interest I could create another post showing how to do the above ...&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;For example the mandatory Microsoft.NET.Sdk.Functions nuget generates automatically function.json file upon compilation .. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Had an open github issue on the .NET 5 worker preview project asking what about the performance impact of out-of-process/grpc between the host and my code, however the repo got archived and my issue got dropped unanswered ... ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;I think AKS is one of the best Azure products currently, very competitive when compared to EKS or GKE etc. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>azure</category>
      <category>aks</category>
      <category>azurefunctions</category>
      <category>serverless</category>
    </item>
  </channel>
</rss>
